高性能图片服务器–ZIMG(转)

2011年李彦宏在百度联盟峰会上就提到过互联网的读图时代已经到来1,图片服务早已成为一个互联网应用中占比很大的部分,对图片的处理能力也相应地变成企业和开发者的一项基本技能。须要处理海量图片的典型应用有:
1. 图片类应用,如百度相册。
2. 导购类应用,如Guang.com。
3. 电商类应用,如淘宝。
4. 云存储服务,如七牛云存储。
除此以外几乎全部的网站都须要考虑本身图片处理的解决方案,以避免在流量变大以后显得手足无措。
本文将从做者本身设计完成的图片服务程序zimg的设计思路出发,探讨高性能图片服务器的特色、难点和应对办法。php

主要问题

要想处理好图片,须要面对的三个主要问题是:大流量,高并发,海量存储。下面将逐一进行讨论。html

大流量

除了那些拥有本身数据中心的大型企业,中小型企业都须要考虑到流量问题,由于流量就是成本,图片相对于文原本说流量增长了一个数量级,省下的每个字节都是白花花的银子。我曾经在一篇博客2里看到,做者在业务逻辑中引入PHP的imagick模块进行压缩,短短几行代码就作到了每月为公司节省2万人民币的效果,可见凡是涉及到图片的互联网应用,都应该统筹规划,下降流量节约开支。前端

高并发

高并发的问题在用户量较低时几乎不会出现,可是一旦用户攀升,或者遇到热点事件,好比淘宝的双十一,或者网站被人上传了一张爆炸性的新闻图片,短期内将会涌入大量的浏览请求,若是架构设计得很差,又没有紧急应对方案,极可能致使大量的等待、更多的页面刷新和更多请求的死循环。总的来讲,就是要把图片服务的性能作得足够好。node

海量存储

在2012年的介绍Facebook图片存储的文章3里提到,当时Facebook用户上传图片15亿张,总容量超过了1.5PB,这样的数量级是通常企业没法承受的。虽然咱们很难作出一个能够跟Facebook比肩的应用,可是从架构设计的角度来讲,良好的拓展方案仍是要有的。咱们须要提早设计出最合适的海量图片数据存储方案和操做方便的拓容方案,以应对未来不断增加的业务需求。git

以上三个问题,其实也是相互制约和钳制的,好比要想下降流量,就须要大量的计算,致使请求处理时间延长,系统单位时间内的处理能力降低;再好比为了存储更多的图片,必然要在查找上消耗资源,一样也会下降处理能力。因此,图片服务虽然看起来业务简单,实际作起来也不是一件小事。github

设计方案

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的1.0版本,设计面向图片量在TB级别的中小型服务,物理存储暂时不支持分布式集群,分布式功能将在2.0版本中完成。浏览器

架构设计

为了极致的性能表现,zimg所有采用C语言开发,整体上分为三个层次,前端http处理层,中间图片处理层和后端的存储层。下图为zimg架构设计图:
整体架构七牛云存储

http处理层引入基于libevent的libevhtp库,libevhtp是一款专门处理基本http请求的库,它太适合zimg的业务场景了,在性能和功能之间找到了很好的平衡点。图片处理层采用imagemagick库,imagemagick是如今公认功能最强,性能最好的图片处理函数库。存储层采用memcached缓存加直接读写硬盘的方案,更加深刻的优化将在后续进行,好比引入TFS4等。为了不数据库带来的性能瓶颈,zimg不引入结构化数据库,图片的查找所有采用哈希来解决。
事实上图片服务器的设计,是一个在I/O与CPU运算之间的博弈过程,最好的策略固然是继续拆:CPU敏感的http和图片处理层部署于运算能力更强的机器上,内存敏感的cache层部署于内存更大的机器上,I/O敏感的物理存储层则放在配备SSD的机器上,但并非全部人都能负担得起这么奢侈的配置。zimg折中成本和业务需求,目前只须要部署在一台服务器上。因为不一样服务器硬件不一样,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协议(RFC1867)分离出其中的二进制文件,存储在一个临时目录里,等咱们在PHP代码里使用$_FILES["upfile"][tmp_name]获取到文件后计算MD5再存储到指定目录,在这个过程当中有一次读文件一次写文件是多余的,其实最好的状况是咱们拿到http请求中的二进制文件(最好在内存里),直接计算MD5而后存储。
因而我去阅读了PHP的源代码,本身实现了POST文件的解析,让http层直接和存储层连在了一块儿,提升了上传图片的性能。关于RFC1867的内容和PHP是如何处理的,感兴趣的读者能够去搜索了解下,这里推荐@Laruence的文章《PHP文件上传源码分析(RFC1867) 》
除了POST请求这个例子,zimg代码中有多处都体现了这种“减小磁盘I/O,尽可能在内存中读写”和“避免内存复制”的思想,一点点的积累,最终将会带来优秀的表现。

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

1024 * 1024 * 1024 * 200KB = 200TB

这样的数量应该已经算很大了,在200TB的范围内能够采用加硬盘的方式来拓容,固然若是有更大的需求,请期待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),若仍是没有,那就去查询原图的缓,原图缓存依然未命中的话,只能打开原图文件了,而后开始裁剪,灰白化,而后返回给用户并存入缓存中。
能够看出,上面过程当中若是某个环节命中缓存,就会相应地减小I/O或图片处理的运算次数。众所周知内存和硬盘的读写速度差距是巨大的,那么这样的设计对于热点图片抗压将会十分重要。

除了上述核心代码之外就是一些支持性的代码了,好比log部分,md5计算部分,util部分等。

性能测试

为了横向对比zimg的性能,我用PHP写了一个功能如出一辙的后端,仅用时一下午,这充分证实了“PHP是世界上最好的语言”,也同时说明了用C语言来进行开发是多么的辛苦,不过,我喜欢性能测试结果出来以后的那份成就感,这样的付出我以为是值得的。

测试方案

采用Apache自带的测试程序ab对指定请求进行测试,在特定并发数100的状况下进行10w个请求的测试,结果依据该并发下每秒处理请求数来定性,对比的方案是未启用缓存的zimg,启用缓存的zimg和Nginx+PHP,其中zimg端口为4868,Nginx端口为80。

测试命令分别为:

ab2 -c 100 -n 100000 http://127.0.0.1:4869/5f189d8ec57f5a5a0d3dcba47fa797e2
ab2 -c 100 -n 100000 http://127.0.0.1:80/zimg.php?md5=5f189d8ec57f5a5a0d3dcba47fa797e2
ab2 -c 100 -n 100000 http://127.0.0.1:4869/5f189d8ec57f5a5a0d3dcba47fa797e2?w=100&h=100&g=1
ab2 -c 100 -n 100000 http://127.0.0.1:80/zimg.php?md5=5f189d8ec57f5a5a0d3dcba47fa797e2&w=100&h=100&g=1

注:如下测试数据单位皆为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 zimg+memcached Nginx+PHP
静态图片 2857.80 4995.95 426.56
动态裁剪图片 2799.34 4658.35 58.61

总的来讲测试结果符合预期,纯C写成而且专门为图片而作了大量优化的zimg表现远远优于采用PHP的方案,性能有6-79倍的提高。

高压测试

在测试过程当中因为php-fpm的性能瓶颈,致使并发压力根本压不上去,为了充分展示zimg面对超高并发的抗压能力,我又作了另外一项对比测试,即单纯的echo测试。测试方法是在逐渐升高的并发压力下完成20w个echo请求,记录每种并发压力下的处理能力。硬件环境不变,此次所要对比的是业界以性能著称的Nginx,Nginx和zimg都是接收echo请求后返回简单的“It works!”页面,不作任何复杂的业务。

测试命令分别为:

ab2 -c 5000 -n 200000 http://127.0.0.1:4869/
ab2 -c 5000 -n 200000 http://127.0.0.1:80/

测试结果以下:

Concurrency 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分支中,也算是一个记念吧。

转自 IT世界(http://www.alivenode.com)

相关文章
相关标签/搜索