我所在的团队几年前开始实现一款基于 URL 拼接方式的实时合图能力,应用场景则是用在广告图千人千面的场景下,很多 App 首页都有 banner 广告位,为了最大程度提升广告的效率,广告算法推荐需要实现了一套通过不同的 sku + 不同的文案 + 不同的模版,然后生成一张广告图的技术方案。
这套技术方案抛开推荐算法不提,最为重要的是如何在用户打开 App 的瞬间能够合成出广告图,而我当时主要负责服务端广告图合成技术。于是就有了本文的技术方案- 毫秒级广告图合成。
URL 设计
对于图片服务器而言,要求业务逻辑尽量简单,最大化利用图片路径来做参数传递,当浏览器打开一个图片资源时,源服务器可以快速找到所在服务器分片并迅速定位到文件资源返回给上游。基于此我们设计了一套 URL 方式能够承载动态信息:
http://img.example.com/${appName}/${image}/${variable}/${token}/${meta}/q.jpg
- appName: 业务名
- variable: 图片信息 + 文案信息 + 模版信息
- token: 加密参数, 把 variable 加密下校验 URL 合法性
- meta: 裁剪信息 + 缩放信息 + 质量信息
技术架构
架构设计一切从性能出发,使用了 Nginx + lua + c 扩展的方式来实现服务,而并不需要 nginx 接受请求转发给其它服务进程处理,减少 IO 开销与进程切换的开销。
ngx_lua 采用 “one-coroutine-per-request” 的处理模型,对于每个用户请求,ngx_lua会唤醒一个协程用于执行用户代码处理请求,当请求处理完成这个协程会被销毁。每个协程都有一个独立的全局环境(变量空间),继承于全局共享的、只读的“comman data”。所以,被用户代码注入全局空间的任何变量都不会影响其他请求的处理,并且这些变量在请求处理完成后会被释放,这样就保证所有的用户代码都运行在一个“sandbox”(沙箱),这个沙箱与请求具有相同的生命周期。 得益于Lua协程的支持,ngx_lua在处理10000个并发请求时只需要很少的内存。根据测试,ngx_lua处理每个请求只需要2KB的内存,如果使用LuaJIT则会更少。所以ngx_lua非常适合用于实现可扩展的、高并发的服务。
lua 逻辑
Nginx 接受到请求后通过 location 匹配到对应的 lua 脚本并执行,lua 里面的逻辑主要包含参数提取,主要通过正则的方式取出来文案+图片+模版+缩放等信息,然后获取模版+获取图片信息然后调用 c 扩展实现合图,最后返回合成后的图片信息。
Nginx 缓存
这里还涉及到一个缓存优化,因为模版信息及图片获取信息依赖外部接口,因此为了减少外部请求(昂贵),借助于 Nginx 缓存可以实现基于内存级别的缓存。
当启用了 Nginx 的缓存功能时,Nginx 会将服务端的响应保存在本地磁盘上,在后续的请求中只有请求满足缓存的条件就会命中缓存,Nginx不会再将请求转发到后端的服务上。
proxy_cache file;
proxy_cache_key $1$is_args$args;
proxy_cache_min_uses 1;
proxy_cache_valid 200 1d;
proxy_no_cache $no_cache;
proxy_cache_expire $2;
合图引擎
合图引擎使用 c 实现,图形绘制能力基于 Graphics Magick 实现,大致流程如下:
CDN域名
最后通过 cdn 域名来缓解合图源站的压力,通过把域名改造为 CDN 域名能够大大降低合图的数量。只要 URL 没有发生改变,请求会先到 CDN ,如果 CDN 没有才会会源站请求合成图片,合成之后则推到 CDN 源站存储,以便后续快速获取资源。
总结
最后这套方案的实时合图图层数量小于 10 的情况下,性能 TP99 稳定在 200ms 以内,图层数越多则消耗的时间会越长,但已经能满足当下的业务诉求。
有了这套能力之后也尝试拓展更多应用场景,动态 banner 也使用了这套能力,能瞬间合成多张图片后通过 FFmpeg 合成为 GIF 或 MP4。