https://mp.weixin.qq.com/s/MSR18sqUznuiUgUM5YkvZg
我们选择了Golang做为当前版本的开发语言,架构如下:
采用多进程单协程图片处理模型。图片库主要依赖的是GraphicsMagick,和少部分ImageMagick,通过封装cgo调用实现。
Golang调用cgo会申明一个进入syscall的指令,意味着调度器会创建一个M去执行goroutine。因此当有大量并发调用,并且图片处理足够慢,比如一张像素特别大的原图,就会引发大量线程同时存在,造成不必要context switch,CPU load看上去很高,实际效率很低。
因此我们通常会通过Master进程fork出和CPU相等数量的Worker进程做图片处理,每个进程只有一个协程来处理图片,每个进程会创建一个可配置的buffer用于保存原图的blob, 这样能最大化利用单协程的利用率。
采用这种架构当时主要还为了规避GM本身的一个问题,参考我们向作者提交的issue:
https://sourceforge.net/p/graphicsmagick/mailman/graphicsmagick-help/?viewmonth=201708.
问题描述是setjmp函数和longjmp函数在某些操作系统非线程安全,作者需要一个全局锁来保证线程安全。因此多线程调用本身是低效的。
这个问题在java或者.net封装的GM也会存在。上一个版本的Lua不存在这个问题,因为Nginx本身会fork多个Worker进程进行图片处理,并且只可能存在一个正在运行的协程。事实上Linux执行这两个函数本身是线程安全的,作者可以通过build的时候来决定是不是需要加上线程安全的flag。在发表本文的时候,作者已经在最新的release中修复了这个bug。
这里的Nginx不仅仅用来做LB,因为Nginx能提供很丰富的脚本,可以省去很多开发工作量,并且当有获取原图的需求,可以通过Nginx sendfile直接从存储取回,节省不必要的系统开销。
LB算法并不是简单的RR,我们会根据每个进程的CPU消耗,以及原图像素,buffer消耗等维度动态算出各进程的负载量,如果Nginx RR到一个负载非常大的进程,可以通过返回重定向状态码让Nginx重新跳转,这里可能会出现几次网络跳转,但是因为是Loopback,网络上的消耗相对图片处理的消耗可以忽略不计。
Master进程用来管理Worker进程,当有Worker意外Crash,则会重新拉起一个Worker进程,始终保持和CPU数量一致。 Master进程的健康安全会定期Report给监控系统做告警。
二、小结
当前的图片服务架构,支撑了携程每天上亿次原图处理,平均图片处理延时控制在200毫秒以内,图片处理失败率小于万分之一,从发布至今节点没有出现宕机现象,偶尔Worker进程有性能问题和Crash也通过日志和分析工具逐一解决。
如上所述,携程图片服务架构经历了三次改版,从一开始没有设计复杂的架构,只是为了解决碰到实际问题而重构,到后来根据遇到的问题,不断调整,也说明了没有完美的架构,只有适合的架构。
当然,要提供稳定图片服务,架构是一方面,也必须有其他技术上的支持,比如图片本身质量和尺寸的优化,盗链和版权问题,端到端的实时监控和预警机制,不良内容识别,产品图片管理和编辑功能,以及海外用户图片访问加速问题。这些问题每个都能写下不少篇幅的文章,有时间再和小伙伴分享。
目前,携程图片服务已在github上开源了小部分功能,开源地址:https://github.com/ctripcorp/nephele
后续会逐步完善,欢迎PR。