打造一套高度可扩展的缩略图网站接口
作者:郑凯
关于缩略图,大部分网站是这样处理的:在获得一张图片的同时,显式或隐式的生成几种不同大小的图片,以备将来使用。比方说,用户头像、上传的照片。通常这已足够。特别是那些功能相对简单(这绝不是贬义,如果不喜欢,你可以理解为“专一”)的网站。
但是需求总是在变化的,比方说在一次改版后,设计师希望用户的头像从 50px 扩大到 60px,使之更美观,你可以抱怨设计师的反复无常,继而以种种理由告诉设计师这东西没法变——需要太多的时间和精力应对每次改变。但从一个较长的时间跨度来看,不改版是不可能,你不能指望设计师从一开始就把所有需求钉死,就好像你永远也不可能写出完美的程序一样。
另外,在上一个项目中,用户头像和相册功能是不同的人来做、运行在不同的服务器上,但都是图片处理,都需要考虑临时目录、磁盘剩余空间、负载等问题。
缩略图接口就是为了解决以上两个问题
甚至我也说不好,是不是因为之前看过的 Abusing Amazon images 一直让我念念不忘(可能这个页面更直观一些),所以让总想在工作环境中尝试它。那篇文章的核心就是通过复杂的 url 来控制图片的合成、旋转之类的各种效果。这样实际上所有图片的使用都分成了两个问题:
- 图片服务器根据集成在 URL 上的各种参数来返回相应的图片。
- 图片的调用者只需要关心,输入几个参数,返回一个对应的 URL 字符串,他根本不用管 URL 的具体格式,只要保证在模板中恰当的放到
<img src="
后面
我只是想缩放图片,没有 Amazon 的那么复杂。实际上 URL 中要包含这些参数:缩放后的宽高、缩放方式(这个稍后解释)、图片的原始 URL。
比方说原始 URL 是 http://photo.example.com/dir/hash.jpg
那么缩放后的 URL(假设是宽高各 60 的)就可能是这样:
http://resize.example.com/w60/h60/style1/photo.example.com/dir/hash.jpg
缩放的服务器在接收到 URL 后,先 wget 这张图片到本地,然后用 ImageMagicK 的 convert
命令缩放,最后 readfile 该图,或者缓存里已经有了,直接 readfile,但无论如何,最终都是返回图片。
而在用户看到的大部分页面里,HTML 代码里都直接链接着 resize.example.com
而非 photo.example.com
的图片,所有的访问压力、所有需要动用的手段(反向代理、CDN 之类的)也都集中在这个图片接口的域名 resize.example.com
上
简单的原理或者说过程即是如此,但实践中遇到的各种各样的问题还有很多。
这是一个高负载的应用,对 CPU、内存、硬盘容量和速度要求都很高,但核心的部分就是 ImageMagicK convert,绝大部分的资源消耗都在这里。因此使用哪种语言、哪种 web 服务器,都是无足轻重的事情。
我虽然用的 PHP,却没有使用 MagickWand 这个扩展。最大的理由,KISS 原则,我不想在任何升级后还要等相应的扩展被编译、测试后才能使用,所说的升级包括 PHP、MagickWand、ImageMagick 这三者,他们之间的组合所可能产生的问题要比 PHP 和 ImageMagick 的两者各自的升级带来的问题要多得多。实际上 PHP 只是 URL 参与到 convert 命令参数之间的翻译工具,实际的转换我都是在用 exec("convert -q ...")
来完成。并且升级是必要的,举个例子,即使是 2008 年的 ImageMagick 6.3.2,在处理 GIF98a 动画格式时仍然有 BUG。
缩放格式
在页面中所要展示的图片,都有一个框架尺寸,即最终的不可以超过这个框架,之后可能会有林林总总的其他规矩,比方说空白部分是什么颜色、是居中还是左对齐,不是接口要考虑的了。
如果是固定百分比的缩放,可能是最简单的。但事实上我们需要各种大小各种比例的图片,举个例子,如果我们需要把图片缩成 50x50 大小,即宽高比 1:1,但是原图片是 1024x768 的,宽高比 4:3,这是该如何处理?通常有这么三种方式:
inside 可能是最常见的,取最长的边来适应。类似 Adobe Reader 中的 fit page、ACDSee 中 fit image、或者通常的播放其全屏播放时候的缩放规则。缺点是如果原图和框架的比例不符,会有空白,总的来说就是展示面积变小。
outside 取短边来适应,如果比例不符会截掉多出来的部分。保持比例,但是画面内容有损失,特别是有可能破坏人像。当你登录 flickr 后在首页看到的那几组图片就是这么缩放的。
stretch 会破坏原始比例,Windows 的桌布设置就有“拉伸”这个选项。通常 icon 级别的缩略图才需要用这个方法,比方说一个 16x16 的头像。
其实“框架”或者说“容器”的这个概念并不十分易懂。起码我跟很多人解释过这个问题,用各种方法,画图,数字,但都失败了。
以上只是在产生做图片接口这个念头后第一时间内所能想象到的问题,能做优化的地方很多。还拿上面提到的那个 URL,这里面就有很多文章可做:
http://resize.example.com/w60/h60/style1/photo.example.com/dir/hash.jpg
如果有人看懂了这个 URL 格式,至少就可以捣乱(比方说大量生成最大尺寸的缩略图),所以首先需要验证,在 url 里加入公钥,每个请求都要验证,以确保是自己网站的程序生成的 URL,我也不想让它太长,所以只用了 CRC32 的一半,这已经足够
还有很多让这个 URL 变短的方法,比方说去掉 photo.example.com
中的 .example.com
,比方说宽高用定长的 2 字节 36 进制数(即 sprintf("%02s", base_convert(789, 10, 36))
),这样还能顺便节省两个分隔符“/”。因为第一版 URL 实在过于冗长,所以我决定缩减的时候把能想到的招都用上了。
随着这套系统的增长,事实上缩放服务器服务器已经成为整个网站所有动态内容图片的最主要出口(除非有人点“下载原图”链接,那也局限在相册功能的极小一部分点击),正当我打算扩充这套系统的时候,又很幸运的了解一些关于浏览器的并发数上限的问题,简单的说,针对每个域名,同时下载数是固定死的,IE 6、IE 7、Firefox 2 都是遵从 rfc2616 的 2 个,opera、IE 8、Firefox 3 是 6 个或 8 个。如果一个页面有大量小图片(在实际环境中,网站的每个页面至少会有很多用户头像,而且每个页面都是不同的图片,不像网站模板本身的图片加载一次就够了),因此连接时间远大于传输时间。Google 虽然没有人写文章来说这事,但他们的作品是最好的例子:是否注意过 map.google.com
下的图片域名有 4 个么?mt0.google.com
/ mt1.google.com
/ mt2.google.com
/ mt3.google.com
。相比之下,所增加的 DNS 解析时间无足轻重
于是我把所有图片也分到了 4 个域名上,按 crc32(原始URL) % 4 来划分,至于后台,我们可以指到 1 - 4 台机器上,这暂时也足够了。
当 4 台 8G 4核的机器也撑不住的时候,很明显需要别的方法了。普通的轮巡机制对此不起作用,假设同一个 URL 的两次请求被指到了两台不同的机器上,那这两台机器都需要做相同的工作:下载原始图片、缩放、缓存。同时由于图片的总量巨大,squid 和 CDN 也并不是很可靠,要经常面对大量的回源请求。
必须保证同一个 URL 只在同一台服务器上被处理。我之前的方法只能并行 4 台,直到老刘研究通了 nginx,可以根据 URI 做 Sharding,这个方法已经可以保证上百台机器内仍然有效了。
其他体会
当平均 10kb 大小的图片覆盖了几百 G 硬盘的 70% - 80% 的时候,硬盘 IO 的瓶颈是巨大的。按现在的主流机器来运转这套系统,第一个瓶颈都会是磁盘而非 CPU 或内存。我们是用 sysstat 来评估磁盘的负载情况的。
整个网站在一段时间内需要的图片总量是固定的,因此建议硬盘尽可能大,尽量少删已生成过的文件。要明白,正是因为有相当数量被删除的文件要重新生成,所以 CPU 和内存占用才那么高。
尝试过关闭 atime,但变化很小,观察不到。
convert 进程所占用的内存会达到 80MB 甚至几百 MB,这很正常,你可以把 100 个整数放到一个数组里,看看会占用多少内存。同时 convert
要记得用参数 +profile "*"
,不然早晚会有一天你会奇怪为什么一个 20x20 的 jpeg 会有 30KB 大小。jpeg 质量我用的是 85,我曾偷偷改到过 75,但不到一天就有不止一个人跟我说感觉图片质量变差了。说明大家的眼睛还是很敏感的。
尽管这个工具可以非常自由的生成各类尺寸图片,但一定要跟设计师也讲明白,只在真正有必要的时候才应该增加新的尺寸规格。把函数封装好,比方说头像是 50x60 的,不要让程序员直接用 resizeImage(50, 60)
,而是用实体名,比方说 resizeImage("middleAvatar")
。
PHP 脚本开头记得用 ignore_user_abort()
,这样在生成新图片时,即使用户提前关闭浏览器,PHP 也会把工作做完,这样下次再有相同请求时可以直接 readfile 已有的图片而不必重新缩放。
在做增加新机器、新尺寸之类较大影响的改动,需要提前“预热”,即生成一份可能的图片请求列表,要尽可能的长,然后在做改动之前几天的夜里,分批用工具提前访问这些地址,以保证服务器上有一定量的缓存。我碰到的情况是,缩放服务器在从 2 台扩充到 4 台时,由于所有机器是在一个机房的内网访问,结果相册服务器被那两台新服务器扒的没响应了。