黄慧攀,又拍云 CTO。最早在 2001 年开始 web 开发工作;2006 年创办 yo2.cn 优博网(WordPress 博客平台);2010 年加入又拍云开始构建第一代云存储和云 CDN 服务。曾从事前端、后端和服务端等工作,目前主要从事技术架构工作。

又拍云的云处理服务包括图片和音视频处理服务,其中图片处理系统每秒处理图片数量 3000 多张,一天要处理 1 亿张图片左右。图片处理服务面向的客户主要有电商、图片社交两类,他们也是图片处理需求的大户,其他类型的客户对图片处理需求比较少。
选择适合业务场景的系统架构
我们今天不谈具体产品,只讨论系统架构,很多人关注在图片处理领域选用 GraphicsMagick、ImageMagick 或者软编码或硬编码的性能高低。这些方案都各有利弊,需要按业务场景来选择。
又拍云因为是公有云处理服务,使用场景和作图功能要求很泛,什么打水印、模糊、加特效等,因此用软编码的方式是唯一选择。
对于业务只有做缩略图功能要求的,肯定是硬编码的性能高。
又拍云图片处理集群规模及架构
图片处理集群规模
30 台 24 核、48G 内存的服务器,相当于有 30 * (24 - 1) = 690 核的处理能力。

这是我们的狗眼监控系统,对平台每个子服务都有 QPS 和平均处理耗时等关键指标的监控。上图是作图集群的 QPS 统计,处理能力、处理量都很稳定。
现行架构

前端部署了 8 台 nginx 做 7 层负载均衡,这里没用 LVS 做 4 层负载均衡,由于这个场景下 nginx 本身不存在瓶颈,因此我们简单使用了 IP 轮询方式,并在业务代码中来做容错。
Nginx 中配置 30 台 GmServer 的 upstream。这其中 28 台为普通的 GmServer,另外 2 台是 BigGmServer,这个是非常特殊的角色,Why?
因为我们提供的是公有云服务,客户传什么图进来,我们是不知道的,各种各样,甚至有几十兆的 GIF 图。但这种图所占整体的比例非常小,远低于 1%。所以我们只部署了 2 台 BigGmServer 来做这部分的处理任务。
自研的 GmServer
GmServer 最重要的策略特性是:
Server 只并发处理 CPU 核数以内的任务,如果超出了就返回 502,让 nginx 做容错处理,选择下一台处理服务器。
这个架构策略比选用哪个图片处理库重要得多。
因为在并发数超出 CPU 处理能力的时候,整体的处理性能下降比较严重,大家都在抢占 CPU,结果是大家都做不好。
GmServer 是基于 Linux + epoll + GraphicsMagick 开发的,在可控范围内做到全部内存处理图片,不走磁盘 IO。
对于 GraphicsMagick 本身还有不少处理逻辑会使用到本地磁盘 IO,我们把这部分放到 /dev/shm 上。
服务器 48G 内存,才 23 个并发任务,所以是绝对够用的。完全基于内存做处理,可以避开磁盘 IO 的损耗,取得最高性能。
另外一个大头是修复 GraphicsMagick 的 BUG 和新功能的添加。因为是公有云,所以接收的图片各种各样,甚至有些是缺损的或者是经过“变态”压缩的 gif,GraphicsMagick 不一定能够识别和处理。
我们有专人长期在为此而战斗,并且这些 BUG 修复一般都会提交给 GraphicsMagick 官方,用以给开源社区一些贡献。
比如最大的一个就是 WebP 格式的支持,是我们工程师给 GraphicsMagick 提交的。

又拍网到现在的又拍云,使用 GraphicsMagick 有 6~7 个年头,在这方面的技术和经验积累非常多。如果您在使用过程中遇到特殊无法处理的图片,可以邮件联系找我们看看是否能做到兼容处理:huipan.huang@upai.com。
任务调度逻辑详解
处理图片任务 a(普通图片)
8 台 nginx 中随机选 1 台,再 nginx 把任务随机分配给 1 个 GmServer,处理完成。
处理图片任务 b(大图片或者大 GIF)
8 台 nginx 中随机选 1 台,再 nginx 把任务随机分配给 1 个 GmServer,处理 5 秒超时返回 413。再由 nginx 把任务分配给 BigGmServer。
这个 5 秒超时是很特殊的经验值,对于小图片处理都是几十毫秒的事,2k 分辨率的也就百毫秒,所以 5 秒以上的图片就特别变态了。
为了保障绝大部分普通图片的处理服务,所以我们把这种变态图片扔到 BigGmServer 去处理,BigGmServer 配置的任务超时时间是 60 秒。
为什么要扔到 BigGmServer?请想象一下普通处理集群,被变态图片占满会出现什么效果?
因为 nginx 的 7 层负载均衡还带容错能力,1 个变态图片进来处理不了,会尝试其他机器,不一会儿整个集群都被这个变态图片给占了,所以我们有必要把任务分类处理的。

(任务 a、b、c 示意图)
处理图片任务 c(普通图片)
这里有个前提假设:我们的 GmServer 只能做 2 个并发任务(否则我画图得画 24 个)即目前状态下 GmServer 不会再接收任务。新来的 c 任务会返回 502,让 nginx 重新选择一台 GmServer 来处理任务。即使遍历集群也就 20ms 以内,一般下一跳就能得到处理了。
现行架构的优缺点分析
一般每周 review 一下处理的 QPS,关注一下吐出的错误码比例,能准确判断到集群是否过载,提前能做好扩容工作。
当前架构优点
服务稳定,过载时业务不会全面受到影响。
架构简单、通用。
当前架构不足
动态扩容不友好,因为架构太简单,动态扩容的实现复杂度高一些,因为需要跟 nginx 做联动。为此要配置自动发现和 nginx 动态配置 upstream 等(当然这块用 nginx + Lua 很好做)。
最近 Docker 很火,我们也有在关注这方面的技术。综合分析下来,打算做一套新的架构,把我们图片处理和音视频处理,原来两套独立的集群混合起来,把资源利用率做上去。
未来的新架构方向
Nginx 改为使用自己研发的 ServiceServer,基于消息队列实现任务排队。此前的 GmServer 转为 GmWorker,主动对接消息队列来处理任务。

这个架构的优点是:Worker 轻量级,且很容易做动态扩容,因为 Server 端不需要配置 Worker 信息,Worker 在启动时主动向 Server 申报身份和认领任务。接下去配合 Docker 的动态扩容就非常方便。
这里有个重点是
ServiceServer 可支撑图片集群、音视频处理,甚至普通 Web 请求任务。
Worker可混合部署。
Worker 不需要做服务监听,可以很方便的用 docker 横行扩展。
如果想走捷径,可以考虑 nginx Plus 版本的 7 层负载均衡支持队列功能,也能达到这个效果。但也有最大的缺点:$1900 / 单机 / 年。当然这个 nginx 的方案,做 docker 的横向扩展时,还是逃不掉跟 nginx 做联动、自动发现等事情。
运营中遇到过的坑
集群自身是没遇到过什么大的坑,只有些小逻辑 bug 的问题。但在服务上有缓存失效和业务攻击的问题,需要做自动降级和适当屏蔽。
先参考下我们的服务架构

缓存失效时候的高可用设计
数据中心缓存有磁盘损坏或者服务器故障时,会导致大部分的缓存失效,从而导致落到作图服务的请求数增大好几倍。而作图服务是无法承受如此大压力的,所以我们允许向用户吐 503 错误码。
注意这个错误码要配置起码 1 分钟以上的缓存失效时间,以避免用户重复刷新请求。如果不设置一个让客户端缓存的时间,那这个 503 错误码吐得没什么意义了。
CDN 缓存一般不会出问题,因为它是分布式、多缓存节点的架构,服务器数量太庞大,即使有磁盘坏或者机器故障所产生的震荡都很小,不必担心。
不同租户隔离与高可用设计
但还要担心的一个是“恶意”请求,这里说的恶意是带双引号的,因为它并不是真正意义的攻击,很可能只是客户一个小变动导致。比如修改了一个缩略图版本号配置,这个操作就会导致旧版本的缩略图缓存全部失效,而要重新到作图服务处理。
如果这是个小客户,那没啥事,但如果是像花瓣网这样的大客户,就问题很严重了。平时花瓣网的图片处理量就已经占集群超过 50%,一下子翻几十倍的处理量进来,肯定会对作图服务造成影响。一个客户影响整个平台!
为此我们在 nginx 上加了针对客户的峰值控制系统,来避免这种情况而影响整个平台。
针对具体场景的优化与设计
缩略图版本号的使用确实给业务带来非常大的方便,随时可针对业务的需求切换使用不同的缩略图格式,点下鼠标马上生效。
但这也是有代价的,就是上面所说的影响。无法完美解决这个问题,要不把作图集群扩到完全无缓存都能处理的能力,要不就接受繁忙时会出现部分图片 503 错误。
而我们会考虑到成本,显然无法提供到峰值的处理能力,那势必要接受一定的错误量。但这个也是可以尽量避免,比如应用方要做缩略图版本切换时,可以考虑提前预热一段时间,灰度切换,使得这个峰值能得到一定的均衡。
图片在线服务的安全问题
另外一些缺乏经验的云厂商,也提供在线作图服务,且更方便。比如 http://a.com/b.jpg?w=100   通过参数自由配置想要的缩略图大小,试想一下,被人恶意组合url参数来刷你的作图集群,会是怎么个效果。
上面提到,如果客户有类似需求,最好自动化控制单客户占用资源,我们也是去年才提供通过 url 参数来作图的功能。 建议大家在新建这样的服务时,要多考虑周到。
Q & A
1. 前端部署了 8 台 nginx 做 7 层负载均衡,这里没用 LVS 做 4 层负载均衡。为何如此设计?
黄慧攀:这个不是性能的问题,只是开发多几行代码,或运维多一个 LVS 管理的事而已。我们是在业务代码上, {s1….s8} 随机选 1 台的。LVS 大多起负载均衡作用,但在这个场景下 nginx 本身压力很小并不存在瓶颈,因此我们简单使用了 IP 轮询方式,并在业务代码中来做容错。
2. 后端存储图片的数据库用的是什么 ?
黄慧攀:这个我们是存在云存储里面的。建议你用 Key/Value 数据库也行。
3. 后续有没有考虑将图片的处理用 GPU 做?
黄慧攀:前面说到过我们的业务需求复杂度比较高,用 GPU 会大大增加我们的研发难度和投入。目前的场景选用 CPU 的方案最适合我们。
4. 能否简单对比下 GraphicsMagick、ImageMagick 优缺点?
黄慧攀:你可以这么理解,gm 是 im 的一个基础库,im 在 gm 上做了更好的功能封装。套了层壳,性能是 gm 最好的。
5. 又拍云的图片服务是否提供 OCR 服务?
黄慧攀:暂时没这样的计划。
6. 老架构按业务区分流量和服务,胜在稳定。新架构在业务上混搭,彼此间是否有权重和流控,会不会有某类流量陡增对其他类业务产生冲击,或者说如何保障每类业务稳定?
黄慧攀:这个就看我们怎么做 Worker 部分的开发。我们每个 Worker 启动都会锁定 1 个  CPU 核上的,不会对其他 Worker 造成影响;
另外说到的任务权重,和个别业务需要突发处理的情况,我们有做考虑。先分成 2 类任务, 1、同步任务(如图片处理);2、异步任务。 我们优先处理同步任务,对每单个客户要做好全局的资源限制。
7. 我们的系统通过参数来处理图片,而业务方可能直接用链接,这样导致每次都走一遍图片处理逻辑,有什么优化方案?
黄慧攀:自己内部的服务,可以考虑增加权限及 IP 访问频率等限制即可。 如果是云服务商可以参考我上面方案。
群友:可以对参数做加密,时间戳也打进去,一方面鉴权,一方面验证请求有效期。
黄慧攀:Good idea 。其实,我还是建议用缩略图版本号。
群友:处理过的图片会存储磁盘吗?
黄慧攀:处理过的图片,不存磁盘。 但上面有 2 层缓存。
8. 上文所说 nginx 请求 gm_server 时,能否用连接池代替上面逐台尝试的做法?
黄慧攀:用连接池会导致负载不均衡的。 遍历池子的损耗很小。另外如果考虑连接池了,用上面新架构模式或许更好, 放到消息队列,Worker 来消费。
9:超时判断是在 nginx?那样虽然返回错误码了,但 gm 的转换还在进行,还是会占用资源?
黄慧攀:超时判断是 gmserver 做的,gmserver 是多进程的,自己内部直接把自己杀了。如果用 nginx 来做肯定面临你说的问题,所以非常必要自己开发这个 gmserver。
http://www.dataguru.cn/article-9063-1.html
又拍云图片处理集群架构
标签: