Skip to content

Latest commit

 

History

History
151 lines (125 loc) · 16.4 KB

arch_design_old.md

File metadata and controls

151 lines (125 loc) · 16.4 KB

#高性能图片服务器浅谈# ####Architecture Design of Image Server#### @招牌疯子

##综述## 几年前就有业界大佬提到互联网进入读图时代,图片服务早已成为一个互联网应用占比很大的部分,对图片的处理能力也相应地变成一项基本要求。需要处理海量图片的典型应用有:

  1. 图片类应用,如百度相册。
  2. 导购类应用,如Guang.com。
  3. 电商类应用,如淘宝。
  4. 云存储服务,如七牛云存储。
    除此之外几乎所有的网站都需要考虑自己图片处理的解决方案,以免在流量变大之后显得手足无措。
    本文将从作者自己设计完成的图片服务程序zimg的设计思路出发,探讨高性能图片服务器的特点、难点和应对办法。
    zimg是一款支持主要图片处理功能,并且具有很高性能表现的图片服务器程序,它使用方便并且完全开源。

##主要问题## 要想处理好图片,需要面对的三个主要问题是:大流量,高并发,海量存储。下面将逐一进行讨论。 ###大流量### 除了那些拥有自己数据中心的高帅富企业,大家都需要考虑到流量问题,因为流量就是钱,图片相对于文本来说流量增加了一个数量级,省下的每一个字节都是白花花的银子。我曾经在一篇博客[1]里看到,作者在业务里面引入PHP的imagick模块进行压缩,短短几行代码就做到了每个月为公司节省2万人民币的效果,可见凡是涉及到图片的互联网应用,都应该统筹规划,降低流量节约开支。 ###高并发### 高并发的问题在用户少时几乎不会发现,但是一旦遇到热点,比如淘宝的双十一,或者你的网站被人上传了一张爆炸性的图片,短时间内涌入大量的浏览请求,如果事先没有设计好方案,很可能导致大量的等待,更多的刷新和更多请求的死循环。总的来说,就是要把性能做得足够好。
###海量存储### 在2012年的介绍Facebook图片存储的文章[2]里提到,当时Facebook用户上传图片15亿张,总容量超过了1.5PB,虽然我们很难做成一个可以跟Facebook比肩的应用,但是从架构设计的角度来说,这样的野心还是要有的,要提前设计好海量数据的存储和拓展方案以应对将来不断增长的业务需求。

以上三个问题,其实也是相互制约和钳制的,比如要想降低流量,就需要大量的计算,导致响应时间延长,处理能力的下降;再比如为了处理更多的图片,必然要在解析和查找上消耗资源,同样也会降低处理能力。所以,图片服务虽然看起来业务简单,实际做起来也不是一件小事。
##设计方案## zimg是我针对图片处理服务器而设计的开源程序,它拥有很高的性能,也满足了图片方面最重要的处理需求,但是它才刚刚发布不久,肯定还有很多问题和设计不周的地方,希望对类似问题有兴趣的朋友加入进来一起开发完善。
###总体思路### 首先是独立图片服务,建立独立的图片服务器有很多好处,比如:

  1. CDN分流。如果你有注意的话,热门网站的图片地址都有特殊的域名,比如微博的是ww1.sinaimg.cn,人人的是fmn.xnpic.com等等,域名不同可以在CDN解析的层面就做到非常明显的优化效果。
  2. 浏览器并发连接数限制。一般来说,浏览器加载HTML资源时会建立很多的连接,并行地下载资源。不同的浏览器对同一主机的并发连接数限制是不同的,比如IE8是10个,Firefox是30个。如果把图片服务器独立出来,就不会占用掉对主站连接数的名额,一定程度上提升了网站的性能。
  3. 浏览器缓存。现在的浏览器都具有缓存功能,但是由于cookie的存在,大部分都不会缓存带有cookie的请求,导致的结果是大量的图片请求无法命中,只能重新下载。独立域名的图片服务器,可以很大程度上解决此问题。

图片服务器被独立出来之后,会面临两个选择,主流的方案是前端采用Nginx,中间是PHP或者自己开发的模块,后端是物理存储;比较特别一些的,还是Facebook,他们把图片的请求处理和存储合并成一体,叫做haystack,这样做的好处是,haystack只会处理与图片相关的请求,剥去了普通http服务器繁杂的功能,更加轻量高效,同时也使部署和运维难度降低。
zimg采用的是与Facebook相似的策略,将图片处理的大权收归自己所有,绝大部分事情都由自己处理,除非特别必要,最小程度地引入第三方模块。
同时由于zimg还处于初始阶段,暂时面向图片量在200TB以内的中小型服务,所以存储不支持分布式集群,分布式功能将在2.0版本中完成。
###架构设计### 为了极致的性能表现,zimg全部采用C语言开发,总体上分为三个层次,前端http处理层,中间图片处理层和后端的存储层。http处理层引入基于libevent的libevhtp库,在性能和功能之间找到了平衡点。图片处理层采用imagemagick库,imagemagick是现在公认功能最强,性能最好的图片处理函数库。存储层采用直接读写硬盘的方案,更加深入的优化将在后续进行,比如引入TFS[3]等。为了避免数据库带来的性能瓶颈,zimg不引入结构化数据库,图片的查找全部采用哈希来解决。
事实上图片服务器的设计,是一个在I/O与CPU运算之间的博弈过程,由于不同服务器硬件不同,I/O和CPU运算速度差异很大,很难一棒子定死。zimg所选择的思路是,尽量减少I/O,将压力放在CPU上,事实证明这样的思路基本没错,在硬盘性能很差的机器上效果更加明显;即使以后SSD全面普及,CPU的运算能力也会相应提升,总体来说zimg的方案也不会太失衡。
###代码层面### 虽然zimg在二进制实体上没有分模块,上面已经提到了原因,现阶段面向中小型的服务,单机部署即可,但是代码上是分离的,下面介绍主要部分的功能和实现,更详细的内容可以从github上拉下来研究。热泪欢迎fork和contribute。

main.c是程序的入口,主要功能是处理启动参数,部分参数功能如下:

-p [port] 监听端口号,默认4869
-t [thread_num] 线程数,默认4,请调整为具体服务器的CPU核心数
-k [max_keepalive_num] 最高保持连接数,默认1,不启用长连接,0为启用
-l 启用log,会带来很大的性能损失,自行斟酌是否开启
-M [memcached_ip] 启用缓存的连接IP
-m [memcached_port] 启用缓存的连接端口
-b [backlog_num] 每个线程的最大连接数,默认1024,酌情设置

zhttpd.c是解析http请求的部分,分为GET和POST两大部分,GET请求会根据请求的URL参数去寻找图片并转给图片处理层处理,最后将结果返回给用户;POST接收上传请求然后将图片存入计算好的路径中。
为了实现zimg的总体设计愿景,zhttpd承担了很大部分的任务,也有一些关键点,下面捡重点的说一下:

在zimg中图片的唯一Key值就是该图片的MD5,这样既可以隐藏路径,又能减少前端(指zimg前面的部分,可能是你的应用服务器)和zimg本身的存储压力,是避免引入结构化存储部分的关键,所以所有GET请求都是基于MD5拼接而成的。
大家设想一下,假如你的网站某个地方需要展示一张图片,这个图片原图的大小是1000*1000,但是你想要展示的地方只有300*300,你会怎么做呢?一般还是依靠CSS来进行控制,但是这样的话就会造成很多流量的浪费。为此,zimg提供了图片裁剪功能,你所需要做的就是在图片URL后面加上w=300&h=300(width和height)即可。
另一个情景是图片灰白化,比如某天遇到重大灾害,想要网站所有图片变成灰白的,那么只需在图片URL后面再加上g=1(gray)即可。
当然,依托于imagemagick所提供的完善的图片处理函数,zimg将在后续版本中逐步增加功能,比如加水印等。

在图片上传部分,其实能玩的花样很少,但是所消耗的时间最多。现在我们再假设一种情景,前端采用Nginx,图片上传功能用PHP实现,需要写的代码很少,但是性能如何呢,答案是很差。首先PHP接收到Nginx传过来的请求后,会根据http协议分离出其中的二进制文件,存储在一个临时目录里,等我们在PHP代码里使用$_FILES["upfile"][tmp_name]获取到文件然后计算MD5再存储到指定目录,有一次读文件一次写文件是多余的,其实最好的情况是我们拿到http请求中的二进制文件(最好在内存里),直接计算MD5然后存储。
So,我去参考了PHP源代码,自己实现了POST文件的解析,让http层直接和存储层连在了一起,提高了性能。
除了POST请求这个例子,zimg代码中有多处都体现了这种“减少磁盘I/O,尽量在内存中读写”和“避免内存复制”的思想,一点点的积累,最终将会带来优秀的表现。

zimg.c是调用imagemagick处理图片的部分,这里先解释一下在zimg中图片存储路径的规划方案。
上文曾经提到,现阶段zimg服务于存储量级在200TB以内的单机图片服务器,所以路径采用2级子目录的方案。由于Linux同目录下的子目录数最好不要超过2000,所以根据MD5的前六位进行哈希,1-3位转换为十六进制数后除以4,范围正好落在1024以内,以这个数作为第一级子目录;4-6位同样处理,作为第二级子目录;二级子目录下是以MD5命名的文件夹,每个MD5文件夹内存储图片的原图和其他根据需要存储的版本,假设一个图片平均占用空间200KB,一台zimg服务器支持的总容量就可以计算出来了:

1024 * 1024 * 1024 * 200KB = 200TB

这样的数量应该已经算很大了,在200T范围内可以采用加硬盘的方式来拓容,当然如果有更大的需求,请期待zimg后续版本的分布式集群存储支持。
除了路径规划,zimg另一大功能就是压缩图片。从用户角度来说,zimg返回来的图片只要看起来跟原图差不多就行了,如果我确实需要原图,也可以通过特别的参数来获得。基于这样的条件,zimg.c对于所有转换的图片都进行了压缩,压缩之后肉眼几乎无法分辨,但是体积将减少67.05%。具体的处理方式为:

图片裁剪时使用LanczosFilter滤镜;
以75%的压缩率进行压缩;
去除图片的Exif信息;
转换为JPEG格式。

经过这样的处理之后可以很大程度的减少流量,实现设计目标。

zcache.c是引入memcached缓存的部分,引入缓存是很重要的,尤其是图片量级上升之后。在zimg中缓存被作为一个很重要的功能,几乎所有zimg.c中的查找部分都会先去检查缓存是否存在。比如:
我想要a(代表某MD5)图片裁剪为100*100之后再灰白化的版本,那么过程是这样的,先去找a&w=100&h=100&g=1的缓存是否存在,不存在的话去找这个文件是否存在(这个请求所对应的文件名为 a/100*100pg),还不存在就去找这个分辨率的彩色图缓存是否存在,若依然不存在就去找彩色图文件是否存在(对应的文件名为 a/100*100p),居然也没有,那就去找原图的缓存在不在,什么,原图缓存都没有,只能打开原图文件了,然后开始裁剪,灰白化,然后返回给用户并存入缓存中。
可以看出,上面过程中只要有一个环节命中缓存,都不需要去硬盘上打开文件,而是从内存里获取,众所周知内存和硬盘的速度差距是巨大的,那么这样的设计对于热点图片抗压将会十分重要。

除了上述核心代码以外就是一些支持性的代码了,比如log部分,conf读取部分(当前版本已废除),md5计算部分等等。 ##性能测试## 上面密密麻麻说了这么多,实际效果如何还是得要靠性能测试来说话了。为了横向对比zimg的性能,我用PHP写了一个功能一模一样的后端,仅用时一下午,这充分证明了“PHP是世界上最好的语言”,也同时说明了用C语言来进行开发是多么的费劲,不过,我喜欢性能测试结果出来之后的那份成就感,这样的付出我觉得是值得的。
###测试方案### 采用Apache自带的测试程序ab对指定请求进行测试,在特定并发数下进行10w个请求的测试,结果依据该并发下每秒处理请求数来定性,对比的方案是Nginx+PHP。

注:以下测试数据单位皆为rps(request per second)。

###测试环境### 操作系统:openSUSE 12.3
CPU:Intel Xeon E3-1230 V2
内存:8GB DDR3 1333MHz
硬盘:西部数据 1TB 7200转
###软件版本### zimg:1.0.0
Nginx:1.2.9
PHP:5.3.17
###测试结果###

测试项目 zimg Nginx+PHP
静态图片 2857.80 426.56
动态裁剪图片 2799.34 58.61

总的来说测试结果符合预期,纯C写成并且专门为图片而做了大量优化的zimg表现远远优于采用PHP的方案,性能有6-48倍的提升。
###高压测试### 在测试过程中由于php-fpm的性能瓶颈,导致并发压力根本压不上去,为了充分展现zimg面对超高并发的抗压能力,我又做了另一项对比测试,即单纯的echo测试。测试方法是在逐渐升高的并发压力下完成20w个echo请求,记录每种并发压力下的处理能力。硬件环境不变,这次所要对比的是业界以性能著称的Nginx,Nginx和zimg都是接收echo请求后返回“It works!”字符串,不做任何复杂的业务,测试结果如下:

并发数 zimg Nginx
100 32765.35 33412.12
300 32991.86 32063.05
500 31364.29 30599.07
1000 28936.67 28163.63
2000 27939.02 25124.51
3000 28168.56 22053.22
4000 28463.45 21464.88
5000 27947.37 13536.93
6000 27533.83 14430.21
7000 27502.03 14623.62
8000 26505.07 13389.28
9000 27124.89 13650.01
10000 27446.23 10901.13
11000 26335.22 10585.73
12000 27068.68 10461.54
13000 26798.55 8530.11
14000 26741.93 7628.09
15000 26556.54 9832.16
16000 26815.70 8018.44
17000 27811.33 7951.21
18000 25722.97 6246.00
19000 26730.02 8134.93
20000 27678.67 6106.95

这是一份有趣的数据,其实Nginx在并发1000开始已经出现了部分失败,在并发9000以后就无法完成20w个请求,通过不断降低请求数才勉强完成了测试。而强大的zimg毫无压力地完成了20000并发以内的所有测试,没有一个失败返回。为了直观地显示测试结果请参考下图:

测试结果

由于去掉了不需要的复杂功能,zimg在http处理层面要远比Nginx轻量,同时测试数据也说明了它的高效。能有这样的成绩则完全要归功于libevhtp项目,它比libevent自带的http库要优秀得多。在我设计zimg的早期版本时,选用了libevent自带的evhttp库,然后采用线程池的方式来实现多线程处理,结果发现在高压力之下问题频出,最后无奈放弃。这个版本封存在github上的zimg_workqueue分支中,也算是一个纪念吧。 ##最后## 图片服务器的设计方案多种多样,zimg也只是提供了其中的一种思路而已,它才刚刚诞生,以后还有很长的路要走,共同学习,共同进步。
在孤独而漫长的开发过程中,经常会遇到思维枯竭毫无头绪的时候,感谢好基友@Xscape给予大量建设性指导性意见;还有@喀啦喀拉在存储路径规划问题上提供的思路。
那么就用这样一句经典而充满力量的话作为结尾吧。

We stand on the shoulders of giants.

[1]:http://hi.baidu.com/bjmayor/item/bdb066ddd08263f8cb0c39cd 此本的转载 [2]:http://www.biaodianfu.com/facebook-efficient-storage-of-billions-of-photos.html Facebook图片存储架构学习 [3]:https://github.com/alibaba/tfs TFS