在某Q和某信中都有我们熟悉的公众号和结构化消息,例如
又或者这样:
这些图片都是固定的,每个用户看到的图片都是同一张。第一张,实在没有太多的点击欲望!第二张就还算凑合吧,不过哪来天天这么多福利图!
如果想让每个用户看到的消息有所不同,因人而异,我们需要依赖终端做相应的开发。
例如某某运动的消息:
这种方式就比固定图片好很多了,用户的点击欲望明显加强。非常好!除了开发周期慢一点之外。
呃,不妙,很快又要过阅兵节,然后是月饼节!老板想要做更炫酷的消息,怎么办?马上打电话给终端同学:“喂!喂?喂!3天能给我修改好模版吗?”。不过终端同学不是神,做得出来也发布不了。。。
那么,我们聪明的产品同学就想到了“动态合成图片”的高招,马上找到我们前后台开发。我们一碰面,又马上敲定可以搞起,问题就是怎么搞起了。
好啦,废话说完,该进入正题了。
消息的样式:
设计同学是毫不手软啊,真当这里是网页一样,各种设计各种字体各种效果各种进度条。
项目情况:
- 100万活跃用户
- 2小时内推送200万个消息
200万个消息也就是200万个图片,2小时内推送完成,也就是每秒大概300张图片。3ms一张图片?开什么玩笑?你以为每个服务器都有博尔特这么快吗!你要知道,这个图片并不小(630*350),要拼图,还要编码为JPG/PNG什么的。
挑战是存在的,需求是要做的,任务是要完成的。我们开始研究合成图的各种现成的方案(那些从零开始实现各种尖端算法的思路就算了),包括:
- C++图形库;
- 浏览器截图;
- Flash。
什么?浏览器截图?什么?Flash?服务器生成图片,跟浏览器和Flash什么事?
别急,我们慢慢说明。
C++图形库:
后台同学哪个不是精通C++,所以我们的后台同学就开始研究各种C++方案。列出来一大堆:Boost.GIL、CImg、CxImage、FreeImage、Magick++(ImageMagick)、GDCM、ITK、OpenCV、VIGRA、VTK。各种高级术语,吓你一跳。
首先,尝试的是专业的Boost.GIL,但发现api羞涩难懂。后又转到街知巷闻的ImageMagick,很多重构同学都是用这个库做图片压缩。于是,后台同学浴血奋战,拼出了第一个效果:
嗯,看来离目标效果不远了。虽然这给人感觉win10和win95的感觉,但要知道,win95升级4、5次就到win10了。
不过,悲剧的还不是这个丑,悲剧的是,合成一张jpg一共需要耗时300ms!
请允许我掐指一算,300ms一张,一秒3张,要达到一秒300张的目标,就需要100台机器。嗯,大老板这么有钱,应该不会介意的。
好吧,开个玩笑,后台同学彻底放弃了。
浏览器截图:
为什么要想浏览器截图?其实以前在项目中用过,只不过当时并没有这么高的速度要求。毕竟设计稿就非常适合用网页实现,如果浏览器截图的速度能达到要求,那么做这个动态图片的成本就很低了。
有很多linux命令行工具,可以对网页截图,原理是启动webkit渲染网页,然后截图。例如gnome-screenshot、wkhtmltoimage。
实际情况是让人沮丧的,截图随便需要1秒2秒的时间。
不过,这个也是能理解的,毕竟要启动webkit,网页要刷新,再截图,能不慢吗?
Flash:
笔者本身做Flash出身,所以对Flash生成图片情有独钟,既然如此,何不拿Flash测试一下呢?
经过测试,我们发现Flash不单能轻松的完美复现设计的效果,而且截图效率非常高,最终也选择了这个方案。
不过,要让Flash运行在linux服务器上,倒是要下一番功夫。
研究的内容包括:
- Flash player or Air?
- Flash和C++的通信?
- 高效压缩图片?
#Flash player or Air?
player和air只是swf运行的两种形式而已,对速度不会有影响。研究这个目的是尝试实现原来的通信架构,因为Air模式才能在flash侧运行ServerSocket。如果Flash能运行ServerSocket,那么Flash就称为服务提供者,C++需要合成图的时候,只需要连接socket,传输参数,然后接收图片即可。
不过,Adobe于2011年宣布从air 2.7开始不再支持linux版本,所以否决了Air,还是继续使用Flash player。另外,要让flash正常运行起来,还需要安装xvfb服务。
#Flash和C++的通信?
为了保证高效的通信,避免每次截图都重启Flash player,我们设计了这样的通信机制:
C++控制Flash的生命周期,定期重启Flash。Flash启动后,马上链接C++提供的socket服务。连接成功后,C++给Flash分配任务,传输相应的用户数据;Flash接收数据后,拉取用户头像、生成图片并压缩为JPG,再以二进制形式在socket中回传给C++。回传完毕后,Flash保持socket连接,等待新的任务。
#高效压缩图片?
图片动态拼接完成后,需要压缩为png或者jpg,又或者更多其他格式。当然,在当前的软件环境来看,jpg和png是唯二的选择了。
我们做了很多测试,包括:
- as3core压缩80%的jpg和无损png,也就是as3代码做编码运算
- 改进版pngencoder:https://github.com/cameron314/PNGEncoder2
- flascc(alchemy c++加速)压缩80%的jpg(as3_jpeg_wrapper)
- png8和有损png24,使用的是blooddy(https://github.com/kenkozheng/blooddy),其中也有flascc加速。
大致的情况如下:
综合文件大小和压缩时间,我们暂时选择了flascc压缩的jpg。
但清晰度方面略有欠缺,80%质量的jpg在呈现文字时,边缘会略有模糊。不过这只在大屏机器上有细微的感觉。后续可能会考虑改为blooddy压缩的有损png24,虽然文件大小和耗时都比现有方案增加1倍,但图片要清晰一些。不得不赞扬一下blooddy的作者,俄罗斯人做软件要么就不做,要做就是很牛逼的。
有一个基础数据还没列出,就是Flash拼接生成画面的时间。这个倒是快的惊人,不算加载图片的时间,只需要8ms左右。
那么最终,Flash生成一张图的时间大概就是40ms。
最后,请再允许我掐指一算。默默的打开计算器。。。
40ms*2000000/1000 = 80000s = 22.2小时
那么如果同时有10台机器,就大概可以在2小时内发送完成了。虽然100台机器搞不到,10台机器还是有办法的。
当然,实际情况还有图片传输的耗时(实际上这里更大,要传输到公众号平台),实际需要200ms一张图片,不过这些都是异步的,我们单机启动25个Flash进程,总体运行平滑。