Monthly Archives: January 2020

如何构建一个实时图片处理服务

图片处理的必要性

现如今的互联网应用,有大量的场景会涉及图片的展示,如头像、微博和朋友圈的图片、即时聊天里的图片、电商网站的商品图、首页的促销轮播图。这些图片可能来源于内容编辑的 PS 软件,也可能来自于用户的高清摄像头拍摄产生,这些图片的文件大小和尺寸越来越大。而几乎每一个应用都会有Web 端、手机客户端、Pad 客户端、小程序、Mobile 版的 Web 站点,在设计针对这些不同尺寸大小的设备的应用界面时,展示的图片的控件的尺寸也是不一样的。假设每次展示图片时都加载很大的原图的话,肯定会很影响体验。一方面,原图的文件大小较大,下载到端本地需要更长的时间;另一方面,原图的尺寸很大,应用需要花更多的计算来解析图片和压缩到图片控件的像素尺寸。加载原图既浪费流量占用带宽,也浪费端的 CPU 和 GPU 资源,展示图片的延时会造成不好的用户体验。因此灵活的、高性能的图片缩略对应互联网应用来说必不可少。

常见云服务商的图片服务

云厂商提供的图片处理能力较丰富,本节只介绍和基本的缩略、裁剪、压缩、格式转换有关的功能。

阿里云OSS

图片处理功能就集成在 OSS,通过 GET 请求 URL 查询参数方式来控制输出的图片,控制参数格式为

x-oss-process = image/action1,param11_value11,param12_value12/action2,param21_value21

其中 action 为操作,目前 image 模块支持的操作包括:缩放 resize、裁剪 crop、旋转 rotate、锐化 sharpen、格式转换 format、质量调节 quality、水印 watermark。action 后紧跟多个参数名值对,使用_分隔参数名和参数值。如

https://image-demo.oss-cn-hangzhou.aliyuncs.com/example.jpg?x-oss-process=image/resize,w_500

将图片将图缩略成宽度为 500 px,高度按原比例处理。

七牛云存储

七牛图片处理包括基本处理(缩略裁剪压缩) imageView2、水印 watermark、基本信息 imageInfo、圆角处理 roundPic、瘦身imageslim等,以基本处理为例,通过在原始 URL添加

?imageView2/<mode>/w/<LongEdge>/h/<ShortEdge>
/format/<Format>/interlace/<Interlace>
/q/<Quality>/colors/<colors>
/ignore-error/<ignoreError>

即可得到想要的转换图片。官方示例

http://dn-odum9helk.qbox.me/resource/gogopher.jpg?imageView2/1/w/200/h/200

可以得到裁剪正中部分,等比缩小生成200×200缩略图。

腾讯云数据万象(Cloud Infinite,CI)

基础图片处理服务提供缩放、裁剪、旋转、格式转换、质量变换、搞死模糊、锐化、水印、EXIF处理等功能。类似的,也是通过在原 URL 后添加自定义参数方式进行转换输出,以缩放为例,接口形如为

download_url?imageMogr2/thumbnail/<imageSizeAndOffsetGeometry>

假设缩放图片宽高为原图 50%,示例如下:

http://examples-1251000004.picsh.myqcloud.com/sample.jpeg?imageMogr2/thumbnail/!50p

华为云

通过示例

https://obs.cn-north-1.myhuaweicloud.com/image-demo/example.jpg?x-image-process=image/resize,w_500,limit_0

可以看出,与阿里云 OSS 基本是一样的。

预备知识

图片的缩放模式

假设图片控件为主,尺寸保持固定,按图片内容与控件的关系,可以分为:
Fit to Frame: 图片按原貌,完全展示在图片控件中,且紧贴控件的最小边;
Fill Frame:用图片填充满图片控件。

不难分析得出,为了达到 Fit 的效果,图片必需保持宽高比例(Aspect To Fit),因此这种现实模式称为AspectFit,此模式下,图片居中于控件,图片控件部分区域(大边两侧)可能会留白。

为了达到 Fill 的效果,可以有两种方法。一种是拉伸图片(Scale To Fill),原图所有内容都会展示在控件中,但是会扭曲。效果如下方所示。

另一种方法是裁剪 (Crop),只有部分内容显示在图片框中,一定会填充满图片控件的最大边,因此还伴随着保持图形的长宽比前提下的放大或者缩小的操作,因此又可称为Aspect To Fill。效果如下方所示。

最后一种是复制平铺了,这已经是多图了,离题了。

抛开图片控件,图片缩放还有一种模式是保持宽高比的前提下,按百分比缩放,我们称为Aspect Percent Scale。

裁剪模式

裁剪图片的某个区域,需要使用偏移坐标x, y和画框大小 w, h,你可能想裁剪左上角的一块,也可能是正中心的一块,也可能是右侧的一块。为了更灵活的指定裁剪位置,引入了新的重力(Gravity)方向这个概念,重力方向包括:NorthWest、 North、 NorthEast、 West、 Center、 East、 SouthWest、 South、 SouthEast,如下图所示。

重力迫使裁剪框和原图和一个方向上对齐,我们用下面几个图例来解释,假设篮框为原图,红框为裁剪输出图。

正的偏移量,表示裁剪框对抗重力,逃离原图,负偏移量表示臣服于重力,裁剪框超重力方向移动。下图分别展示了在 g = SE 下正偏移和负偏移的裁剪。

特殊的,g = Center,指定偏移量的效果与 g = NW 类似,-x 超西移动, -y超北移动。

图片格式

有损 vs 无损图片

文件格式有可能会对图片的文件大小进行不同程度的压缩,图片的压缩分为有损压缩和无损压缩两种。

  • 有损压缩。指在压缩文件大小的过程中,损失了一部分图片的像素信息,并且这种损失是不可逆的,我们不可能从有一个有损压缩过的图片中恢复出和原来一模一样的图片。

  • 无损压缩。只在压缩文件大小的过程中,图片的像素信息没有任何丢失。我们任何时候都可以从无损压缩过的图片中恢复出原来的信息。

索引色 vs 直接色

存储1个像素的颜色所用的位数(bit)成为色阶(Color Depth)。这里所谓的颜色有两种形式,一种称作索引颜色(Index Color),一种称作直接颜色(Direct Color)。

  • 索引色。抽取出图片中的最共性的颜色,放入一个颜色数组。这个颜色在数组的索引就可以用来表代这个颜色。这个数组成为调色板,特定到单个图片。如果是8位索引色,则调色板最多有256种颜色。原来需要24位表示一个像素,现在只需要8位,图片大小得以降低。

  • 直接色。使用四个字节来代表一种颜色,这四个数字分别代表这个颜色中红色、绿色、蓝色以及透明度。现在流行的显示设备可以在这四个维度分别支持 256 种变化,所以直接色可以表示2的32次方种颜色。当然并非所有的直接色都支持这么多种,为压缩空间使用,有可能只有表达红、绿、蓝的三个数字,每个数字也可能不支持 256 种变化之多。

点阵图vs矢量图

  • 点阵图,也叫做位图,像素图。构成点阵图的最小单位是像素,位图就是由像素阵列的排列来实现其显示效果的,每个像素有自己的颜色信息,在对位图图像进行编辑操作的时候,可操作的对象是每个像素,。点阵图缩放会失真,用最近非常流行的沙画来比喻最恰当不过,当你从远处看的时候,画面细腻多彩,但是当你靠的非常近的时候,你就能看到组成画面的每粒沙子以及每个沙粒的颜色。

  • 矢量图,也叫做向量图。矢量图并不纪录画面上每一点的信息,而是纪录了元素形状及颜色的算法,当你打开一付矢量图的时候,软件对图形象对应的函数进行运算,将运算结果[图形的形状和颜色]显示给你看。无论显示画面是大还是小,画面上的对象对应的算法是不变的,所以,即使对画面进行倍数相当大的缩放,其显示效果仍然不失真。

BMP

BitMap 的缩写,是无损的、既支持索引色也支持直接色的、点阵图。这是一种比较老的图片格式。BMP 是无损的,但同时这种图片格式几乎没有对数据进行压缩,所以 BMP 格式的图片通常具有较大的文件大小。虽然同时支持索引色和直接色是一个优点,但是太大的文件格式格式导致它几乎没有用武之地,现在除了在Windows 操作系统中还比较常见之外,我们几乎看不到它。

JPEG

JPEG 是原是指一种标准有的有损压缩方法。现在口头所说的 JPEG 图片实际是指采用 JPEG 方法压缩 JFIF 格式存储的以 jpg 或者 jpeg 为后缀的图片文件。jpeg 是有损的、采用直接色的、点阵图。JPEG 的设计目标,是在不影响人眼可分辨的图片质量的前提下,尽可能的压缩文件大小。这意味着 JPEG 去掉了一部分图片的原始信息,也即是进行了有损压缩。JPEG 的图片的优点,是采用了直接色,得益于更丰富的色彩,JPEG 非常适合用来存储照片,用来表达更生动的图像效果,比如颜色渐变。JPEG 不适合用来存储企业 Logo、线框类的图。因为有损压缩会导致图片模糊,而直接色的选用,又会导致图片文件较大。

GIF

全称 Graphics Interchange Format,采用 LZW 压缩算法进行编码。是无损的、采用索引色的、点阵图。 GIF 是无损的,采用 GIF 格式保存图片不会降低图片质量。但得益于数据的压缩, GIF 格式的图片,其文件大小要远小于 BMP 格式的图片。文件小,是 GIF 格式的优点,同时,GIF 格式还具有支持动画以及透明的优点。但 GIF 格式仅支持 8 bit 的索引色,即在整个图片中,只能存在 256 种不同的颜色。 GIF 格式适用于对色彩要求不高同时需要文件体积较小的场景,比如企业Logo、线框类的图等。因其体积小的特点,现在 GIF 被广泛的应用在各类网站中。

PNG-8

PNG 全称 Portable Network Graphics,PNG-8 是 PNG 的索引色版本。PNG-8 是无损的、使用索引色的、点阵图。PNG-8 最初就是开发出来作为有专利纠纷的 GIF 的替代。除了支持 GIF 已有特性之外,PNG-8 还支持透明度。现在,除非需要动画的支持,否则我们没有理由使用 GIF 而不是 PNG-8。

PNG-24

PNG-24 是 PNG 的直接色版本。PNG-24 是无损的、使用直接色的、点阵图。无损的、使用直接色的点阵图,听起来非常像 BMP,是的,从显示效果上来看,PNG-24 跟 BMP 没有不同。PNG-24 的优点在于,它压缩了图片的数据,使得同样效果的图片,PNG-24 格式的文件大小要比 BMP 小得多。当然,PNG24 的图片还是要比 JPEG、GIF、PNG-8 大得多。虽然 PNG-24 的一个很大的目标,是替换 JPEG 的使用。但一般而言,PNG-24 的文件大小是 JPEG 的五倍之多,而显示效果则通常只能获得一点点提升。所以,只有在你不在乎图片的文件体积,而想要最好的显示效果时,才应该使用 PNG-24 格式。另外,PNG-24 跟 PNG-8 一样,是支持图片透明度的。

SVG

全称 Scalable Vector Graphics,是无损的、矢量图。SVG 跟上面这些图片格式最大的不同,是 SVG 是矢量图。这意味着 SVG 图片由直线和曲线以及绘制它们的方法组成。当你放大一个 SVG图片的时候,你看到的还是线和曲线,而不会出现像素点。这意味着 SVG 图片在放大时,不会失真,所以它非常适合用来绘制企业 Logo、Icon 等。SVG 是很多种矢量图中的一种,它的特点是使用XML来描述图片。借助于前几年 XML 技术的流行, SVG 也流行了很多。使用 XML 的优点是,任何时候你都可以把它当做一个文本文件来对待,也就是说,你可以非常方便的修改 SVG 图片,你所需要的只需要一个文本编辑器。SVG 并非只能绘制简单的Logo类的图片,它可以绘制出精致的图片的。

WebP

WebP 是谷歌开发的一种新图片格式,WebP 是同时支持有损和无损压缩的、使用直接色的、点阵图。从名字就可以看出来它是为Web而生的,什么叫为 Web 而生呢?就是说相同质量的图片,WebP 具有更小的文件体积。现在网站上充满了大量的图片,如果能够降低每一个图片的文件大小,那么将大大减少浏览器和服务器之间的数据传输量,进而降低访问延迟,提升访问体验。在无损压缩的情况下,相同质量的 WebP 图片,文件大小要比PNG 小 26%;在有损压缩的情况下,具有相同图片精度的 WebP 图片,文件大小要比JPEG 小 25%~34%;WebP 图片格式支持图片透明度,一个无损压缩的WebP 图片,如果要支持透明度只需要 22% 的格外文件大小。想象Web上的图片之多,百分之几十的提升,是非常非常大的优化。目前只有 Chrome 浏览器和 Opera 浏览器支持 WebP 格式,所以WebP的应用并不广泛。

GraphicsMagick 功能、参数介绍,以及命令行示例

GraphicsMagick 号称图像处理领域的瑞士军刀。 短小精悍的代码却提供了一个鲁棒、高效的工具和库集合,来处理图像的读取、写入和操作,支持超过 88 种图像格式,包括重要的 DPX、GIF、JPEG、JPEG-2000、PNG、PDF、PNM 和 TIFF。

通过使用 OpenMP 可是利用多线程进行图片处理,增强了通过扩展CPU提高处理能力。

GraphicsMagick 可以在绝大多数的平台上使用,Linux、Mac、Windows 都没有问题。

GraphicsMagick 支持大图片的处理,并且已经做过GB级别的图像处理实验。GraphicsMagick 能够动态的生成图片,特别适用于互联网的应用。可以用来处理调整尺寸、旋转、加亮、颜色调整、增加特效等方面。GaphicsMagick 不仅支持命令行的模式,同时也支持 C、C++、Perl、PHP、Tcl、Ruby 等的调用。事实上,GraphicsMagick 是从 ImageMagick 5.5.2 分支出来的,但是现在他变得更稳定和优秀

安装GraphicsMagick后,以命令行方式调用 gm command [options ...],其中 command 是子命令,options 是相关命令参数,子命令包括:
* convert 对单张或者多种图片进行转换,如缩放、裁剪、转变格式、质量变换
* mogrify 和convert相似,但是默认会覆盖原图
* identify 获取图片的基本信息,如格式、大小、压缩等级、尺寸等
* composite 图片层叠
* montage 拼接图片
* compare 可视化图片差异比较
* conjure 使用MSL脚本进行处理
* batch 进入批量处理模式
* time 计算另一个子命令的耗时
* benchmark 对另一个子命令进行性能测试并返回结果

示例0 安装

# yum install -y gcc libpng libjpeg libpng-devel libjpeg-devel libtiff libtiff-devel freetype freetype-devel jasper jasper-devel  libwebp-devel libwebp gd-devel giflib giflib-devel

# yum install -y GraphicsMagick
gm version
GraphicsMagick 1.3.21 2015-02-28 Q8 http://www.GraphicsMagick.org/
Copyright (C) 2002-2014 GraphicsMagick Group.
Additional copyrights and licenses apply to this software.
See http://www.GraphicsMagick.org/www/Copyright.html for details.

Feature Support:
  Native Thread Safe       yes
  Large Files (> 32 bit)   yes
  Large Memory (> 32 bit)  yes
  BZIP                     yes
  DPS                      no
  FlashPix                 no
  FreeType                 yes
  Ghostscript (Library)    no
  JBIG                     no
  JPEG-2000                yes
  JPEG                     yes
  Little CMS               yes
  Loadable Modules         no
  OpenMP                   yes (200805)
  PNG                      yes
  TIFF                     yes
  TRIO                     no
  UMEM                     no
  WebP                     yes
  WMF                      no
  X11                      yes
  XML                      yes
  ZLIB                     yes

Host type: x86_64-unknown-linux-gnu

Configured using the command:
  ./configure 

...省略

为了支持 JPEG、PNG 等格式图片,必需先安装对应的库,Feature Supportyes的项表示已经开启的特性,可以看到 PNG 、JPEG、WebP、TIFF 格式均以支持。

示例1 获取详细图片基本信息

gm identify -verbose IMG20191209100903.jpg
Image: IMG20191209100903.jpg
  Format: JPEG (Joint Photographic Experts Group JFIF format)
  Geometry: 3120x4160
  Class: DirectClass
  Type: true color
  Depth: 8 bits-per-pixel component
  Channel Depths:
    Red:      8 bits
    Green:    8 bits
    Blue:     8 bits
  Channel Statistics:
    ...省略...
  Filesize: 2.9Mi
  Interlace: No
  Orientation: Unknown
  Background Color: white
  Border Color: #DFDFDF
  Matte Color: #BDBDBD
  Page geometry: 3120x4160+0+0
  Compose: Over
  Dispose: Undefined
  Iterations: 0
  Compression: JPEG
  JPEG-Quality: 95
  JPEG-Colorspace: 2
  JPEG-Colorspace-Name: RGB
  JPEG-Sampling-factors: 2x2,1x1,1x1
  Signature: 9992bc81f6c1b99c4504ef40144a7dd3217805d48fe4e9bf9e9af49c1211f280
  Profile-EXIF: 12966 bytes
    Make: OPPO
    Model: OPPO R7sm
    X Resolution: 72/1
    Y Resolution: 72/1
    Resolution Unit: 2
    Y Cb Cr Positioning: 1
    Exif Offset: 142
    Exposure Time: 1/20
    F Number: 220/100
    ISO Speed Ratings: 878
    Exif Version: 0220
    ...
    Flash: 16
    Focal Length: 3790/1000
    ...
    Color Space: 1
    Exif Image Width: 3120
    Exif Image Length: 4160
    Interoperability Offset: 478
    Exposure Index: 338/1
    Gain Control: 8
    ... 省略GPS信息 ...
  Tainted: False
  User Time: 0.240u
  Elapsed Time: 0m:0.240000s
  Pixels Per Second: 51.6Mi

示例2 按需获取图片信息

gm identify -format "%f\n%e\n%b\n%m\n%w\n%h\n%Q\n%q\n%#" IMG20191209100903.jpg
IMG20191209100903.jpg
jpg
2.9Mi
JPEG
3120
4160
95
8
9992bc81f6c1b99c4504ef40144a7dd3217805d48fe4e9bf9e9af49c1211f280

其中,%Q 是质量信息,%w%h是宽高,%e%m是图片后缀和图片格式,%#是图片有效像素的哈希值。值的注意的是,获取%#都是非常耗时的。

示例3 缩放图片

gm convert cockatoo.jpg -resize 120x120 thumbnail.jpg

表示把原图 cockatoo.jpg 在保持宽高比的前提下缩放到小于 120×120,生成新图片thubmnail.jpg

接口设计

通过观察以上云厂商的现有产品文档,可以看出都是提供对象存储的基础上,在原图 URL 的添加控制参数的方式进行图片实时处理和输出。支持原图格式包括 jpg、jpeg、png、bmp、webp、gif、tiff,支持处理后格式包括 jpg、png、bmp、webp,会限制可处理的原图文件大小,会限制原图宽高,gif 图会限制大小和帧数,超过限制或处理失败降级返回原图。
控制参数设计上,我们觉得阿里云的设计更规整,更容易拓展,因此参考设计为

x-process=cmd1:param1_value1,param2_value2/cmd2:param3_value3

其中 cmd 为操作,作为示例,只支持缩放 resize、格式转换 format、裁剪 crop、质量变换 quality。param_value 为参数名值对,多对参数间用,分隔,多个 cmd 直接用 / 分隔。

假设原图为1280×900 的 png,示例

/img/get?fileId=5ceba7e7b6238e31272dc506&x-process=resize:m_aspectFit,w_800,h_800/quality:q_90/format:t_jpg

表示保持宽高比缩略到大不于 800×800 的jpg,质量为原来的 90%。

各操作的参数设计如下。

缩放 resize

参数 长参名 必要 说明 默认
m mode Y 缩略模式
* aspectFit 按最大边缩略,保持宽高比,确保不超出wxh矩形区域,可能不完全填充,不放大
* aspectFill 按最小边优先缩略,填充,保持宽高比,超出矩形区域时居中裁剪,不放大
* scaleToFill 拉伸填充,填充,不保持宽高比
* aspectPercent 按百分比缩放,保持宽高比
aspectFit
w width Y 宽度,最大4096
h height Y 高度,最大4096
p percent N 百分比,只在aspectPercent,大于0

裁剪 crop

参数名 长参名 必要 说明 默认
w width Y 输出图宽
h height Y 输出图高
x x offset Y 反重力的横向偏移量 0
y y offset Y 反重力的纵向偏移量 0
g gravity N 重力向,取值
nw n ne
w c e
sw s se
nw

质量变换 quality

参数 长参名 必要 说明 默认值
q relative quality N 相对质量,只对jpg有效
假设原图jpg, 质量为90,令q=90,则输出图质量为81
Q absolute quaity N 绝对质量
假设原图为jpg, 质量为80,令Q=90,则输出图质量为90,图片大小会变大
假设原图为png,则Q取值从1到1009,其中Q/10为zlib压缩等级(0最快-100最慢),Q%10为过滤类型
png:75(压缩等级7;过滤类型5,自适应)
jpeg:原图质量系数

q和Q同时有时,优先采用q

格式转换 format

参数 长参名 必要 说明 默认
t type N 输出格式,目前仅支持格式:jpeg、jpg、png、bmp、webp、gif、tiff 原图格式

限制:
* 输出图去掉了除EXIF以外的所有profile信息
* 最大原图20M

如何构建图片处理程序

思路很直接,大致分为以下几步:

  1. 参数解析和校验
  2. 下载原图
  3. 根据 x-process 的参数值,生成对应的 GraphicsMagick 命令行
  4. fork 子进程,运行 gm 命令
  5. 输出原图

如何提升单机程序性能

缓存缩略图 + 只生成一次

短时间内(具体多长要看应用和场景)大量用户对同一张缩略的疯狂访问是必然的。因此缓存缩略图显得很有必要,尤其对缩略耗时较长的图,能有效减少请求耗时。对相同参数,多次计算产生的缩略图必然相同,因此建立参数和结果图的关系,下次请求时,就可以用参数直接找到结果图了。一种直观的方法是把参数规则后作为结果图的文件名,如

5de873920d5ce4000c92f0b6+crop-w_200,h_200,x_-10,y_-10,g_c.jpeg

表示原图id为 5de873920d5ce4000c92f0b6,缩略参数为 x-process = crop:g_c,w_200,h_200,x_-10,y_-10

笔者实际项目中,缓存命中率在5成左右。

针对某一个生产参数,首先检查文件系统内是否有缩略图,有的话直接返回。否则使用JVM内的锁,控制只有一个请求线程去生产缩略图,其他请求等待。实例代码。

if (shouldDownload) {
         // 获取锁,并标记文件需要生产
            LockingHelper.gainLockOf(outputFilePath);
            if (lock != null) {
                try {
                   // 加锁
                    lock.lock();
                    // 再次检查是否需要下载
                    if (LockingHelper.containLockOf(outputFilePath)) {
                        try {
                            // TODO 生产缩略图,保存到磁盘
                        } catch (Exception e) {
                            ...
                        } finally {
                            // 清除下载标记
                            LockingHelper.releaseLockOf(outputFilePath);
                        }
                    }
                } finally {
                   // 解锁
                    lock.unlock();
                }
            }
        }

等待的线程拿到锁后,还需要再次检查是否需要下载,双重检查的典型套路。这里的锁粒度是缩略图的输出路径(outputFilePath),即粒度细到具体控制参数。缩略到 100×100 和缩略到 200×200 是可以并发执行的,不会相互影响。

缓存原图 + 只下载一次

一张原图在产生后,会很快被多端多界面多用户拉取到,此时会并发的请求不同尺寸的缩略图,它们都需要使用到原图,因此图片处理服务只拉取一次原图,并进行缓存,能一定程度上的提升性能。
当某个实例接收到并发的多个对同一原图的缩略请求,首先检查文件系统内是否有原图,有的话进行下一步,否则获取锁进行下载,如上面所述相似,锁粒度到fileId。

写文件优化

首先介绍文件系统中文件与目录的一些基础知识:
1. 目录实际上是一个记录文件名和 inode 对应关系的映射文件
2. 单个目录下的文件越多,这个映射文件越大,需要的读取次数就越多(一般系统调用会每次读 32k 或者类似的量级)

假设有 1M 个文件,一级目录使用两个字母或数字,可以有 (26 + 10)^2 个二级目录,也就是 1296 个目录,每个目录名两个字节,加上 inode 和其他一些消耗, 10-20 字节完全够用,一次读取就能获得所有二级目录;而二级目录平均是 772 个文件,一次读取也能完成,总共两次读取,找到缓存文件,而如果把 1M 个文件放在一个目录下,如果每个记录 32 字节,需要 1000 次读取。

因此作为一个优化,最好构建一个多级的目录,用来保存原图和缩略图,尽可能的减少每一个目录里的子文件/子目录的个数,文件均匀分布,层级也不宜过深,具体如何平衡要根据自己业务特性和可利用条件决定。举例可以用 fileId 中的变化较大的四位,外加原图上传日期,构造了一个4级的路径。

20191205/ab/cd/5de873920d5ce4000c92f0b6+crop-w_200,h_200,x_-10,y_-10,g_c.jpeg

进程池

每次生产图片是都去fork gm 子进程,不仅代价大,而且在高并发的情况下,容易造成子进程数过大(处理网络请求的线程数比较大,因为fork出的子进程也会很多),系统负载飙高,上下文切换频繁。因此控制高峰期的子进程数在一定范围,可以保护系统自身。
前文提到 gm 有一个 batch 批量模式,运行在此模式下在 gm 进程,会一直读取标准输入,逐行接收命令及参数,实时进行处理。

# gm batch -feedback on

...省略...

GM> identify -format "%f\n%e\n%b\n%m\n%w\n%h\n%Q\n%q" IMG20191209100903.jpg
IMG20191209100903.jpg
jpg
2.9Mi
JPEG
3120
4160
95
8
PASS
GM> convert IMG20191209100903.jpg -resize 200x200 IMG20191209100903_200.jpg
PASS
GM> 

因此我们可以预先 fork 一批 gm 子进程,每次要运行命令时,从子进程池中挑选一个子进程,通过 pipe 这种 IPC 方式,发送命令。这又是一直常见的技术的套路:池化,一般的,最大活跃进程数、最大空闲进程数、最小空闲进程数、获取等待时间都可参数化控制。具体架构示意图如下。

获取等待时间不宜设置太大,在获取不到子进程时,可以降级直接返回原图。另一方面,要控制可接受的原图大小和尺寸,实践表明,大图处理耗时极大,瞬时多张大图容易占用完子进程,因此大图直接降级为输出原图即可。
最大活跃进程数控制在 CPU 核数的 3/4 左右,留出一些给 Web 线程。
最小空闲数设置在 1/4 的总 CPU 核数,能应对低峰期的小波的突发请求。

gm 参数优化

  • -size wxh ,提示 JPEG 解码器只需返回指定大小的像素,在大尺寸原图缩小时有优化效果
  • 开启OpenMP,并使用系统变量 OMP_NUM_THREADS 或者命令参数 -limit Threads n 控制每个gm进程的线程数
  • +profile '!EXIF,*',删除结果图的 profile,可以一定程序减小结果图的文件大小,EXIF包含图片方向,实际生产中按需保留
  • -strip 删除结果图的 profile 和其他信息,可以一定程序减小结果图的文件大小
  • 其他参数

HTTP Cache Control

使用HTTP的Cache Control特性,能有效的对服务端的请求,减少下载流量,减少计算。具体就是在响应时输出一系列的Header:
1. Cache-Control: max-age=$$
2. Expires: $$
3. Last-Modified: $$
4. ETag: $$ 。

而在接收到请求时,比较两个头部:
1. If-Modified-Since: $$
2. If-None-Match: $$ 。

因此可以缓存每张缩略图的宽高、大小、修改时间、ETag、文件哈希值等元数据到类似LevelDB、RocketDB等本地缓存。

其他手段

可能借助物理硬件加速,如某些云厂商就提供了「FPGA图片转码加速服务」。

如何构建图片处理集群

集群架构如下图所示。

使用 Nginx作为负载均衡,反向代理负载到后端的图片处理实例。当请求到来时,根据请求中的?fileId=xxx使用一致性哈希的方式分发到某一个实例。

为什么使用一致性哈希这种负载均衡算法?
* 首先对于同一张原图的所有处理请求,一定会落到同一个实例,可以充分利用该实例已有的原图缓存,结果图缓存,避免重复生成,减少整个集群的磁盘消耗和计算消耗。一致性哈希负载提高缓存的命中率,减少计算。
* 使用虚拟节点的一致性哈希算法在物理节点时候不可用时,数据和请求迁移更少。在某个实例不可能用时,只有部分请求会被转移到新节点处理,只有图片需要重新生产。

在 Nginx 还开启了proxy buffer功能后,会缓冲后端节点的响应内容。开启此功能后,业务实例快速完成图片处理,把响应交给 Nginx,快速释放业务线程的占用,而 Nginx 可以慢慢的完成与客户端的文件传输。proxy buffer 对文件服务这类响应较大的业务来说必不可少,是低速公网与高速内网的差速齿轮。

如何控制磁盘消耗

通过定时删除原图和缩略,可以控制磁盘消耗。大部分的业务场景都有热点效应,用户只会查看最近产生的内容,因此可以放心大胆的删除较早前的文件。
一方面控制缓存的时长,另一方面调整定时任务的频次。具体使用怎么的缓存淘汰策略就不在这里说太多,一句话:看业务。
另一个方面是减少缓存量,比如对生成速度快、文件小的图片不缓存,每次都直接生成。

安全问题

通过 URL 方式指定参数,很容易被通过修改参数的方式攻击,可以建立一组特定的样式(style),每个样式对一组常用参数进行封装,对外只以样式名方式访问。例如建立样式 smallmediumbig 分别指代三个常用尺寸的缩略图。

small -> m_aspectFit:w_100,h_100,q_90
medium -> m_aspectFit:w_400,h_400,q_90
big -> m_aspectFit:w_1280,h_1280,q_90

/img/get?fileId=$$&style=big

一点程度上也可以减少URL变得冗长,便于管理与阅读。

另一方面是防止盗图,控制Referer。

总结

前文首先讲解了如何设计图片处理功能,然后介绍了实现以及需要优化的点,最后如何集群化。大概总结一下为了提高性能的关键点
1. 减少对服务端的请求
2. 减少生成
3. 充分利用缓存(原图、结果图、元数据、集群定向)
4. 利用可控进程池控制系统不过载
5. 利用proxy buffer避免慢客户端影响
6. gm参数调整
7. 减少目录大小。
8. 锁粒度控制