分享图截图方案与优化

在业务中常会用到分享图片或者说截图这一功能,利用标题和固定图片分享对用户的吸引力有限,若脱离 web 利用原生手机的截屏也会存在长图无法处理,视窗区域不可控等等问题。能否根据内容生成带有用户独特的分享图方案?

主流截图方向

在解决方案上主要有2种解决思路:前端截图和服务端截图

前端截图

前端截图主要会用到 html2canvas 这个第三方库,主要原理是将 DOM 结构绘制在 canvas 上面产生图片。但是坑很多,例如:图片需要时间戳,无法利用缓存;对很多css3样式无法支持,存在比较多的兼容性;截图模糊;有背景音乐的时候,生成图的时候会出现响声,音频文件也会重复加载等等;当前业界也有利用 html2canvas 处理的,例如:阿里咸鱼的H5分享截图方案优化

服务端截图

服务端截图主要原理前端将 dom 结构或者 url 传递给后端,后端通过打开无头浏览器来现实页面,截图再返回给前端。可能会用到 PhantomJS 和 Puppeteer.js 等第三方库。

这里还要提到全民 k 歌使用的截图方案,利用 ImageMagick 工具集和开发包,ImageMagick官网。具体方案请看web实时长图实践(内部)

项目当前截图方案

项目当前使用的方案就是利用 Puppeteer.js 来进程截图操作的,具体实现方案流程图:

当前方案流程图

更多请看:html2img

流程图中的 TSW 是指TSW,内部请看TSW(内部)

当前方案经过各种机型测试,兼容性很棒(毕竟是截图,哈哈),唯一的问题就是:并发不够,1台机器(8核)每秒的并发处理能力只能在 16 左右,若出现更高的并发只能通过增加机器来实现。当前的优化目标就是在保证截图质量的情况下,提高并发量。

摒弃的方案

截图交由前端,后端提供不变部分的 base64 图片

该方案本质是前端利用 html2canvas 库对动态的元素进行截图,静态的部分通过后端返回,后端的 base64 图片是在模版上传或者 ci 流程中进行截图保存。前端只截动态部分,可以极大减少 html2canvas 出现坑的概率(本质还是无法避免),前端再将动态图片和后端静态图片进行合成。

该方案优点是摒弃了后端的截图过程,极大的提升了服务器的并发能力,缺点是前端还是可能产生存粹前端利用 html2canvas 截图产生的各种兼容性问题,并且还要利用 canvas 进行图片合成操作,操作比较繁琐。本方案为上面提到的前端截图方案。

尝试后端只截动态部分,前端与静态图片进行前端拼图片合并或绘制合并

该方案是想在当前截图方案的基础上,测试页面 dom 元素的复杂度是否会影响 Puppeteer.js 的截图速度(截图大小固定的情况下)。然而经过测试,后端只截部分耗时和截全部耗时基本没区别。(但是发现截图图片的大小会影响耗时,图片越大耗时越长)。以下是测试数据:

当前方案流程图

既然在截图大小固定的情况下,后端只截部分不会影响截图的速度,那后面的前端拼图和绘制合并也就无从谈起了。虽然发现截图大小会影响截图速度,但是动态元素的大小不同场景是不一样的,并不能动态设定动态元素的截图大小。所以该方案无优化效果。

尝试合并多请求在单tab截图,提升tab利用率

该方案也想在当前截图方案的基础上,合并多个请求,利用 Puppeteer.js 同步截图完毕后返回给前端,提升 tab 的利用率。

在 Puppeteer.js 中,可以多次用 await page.setContent(pageContent, { waitUntil: ['load'] }) API 进行截图内容设置,也可以多次用 await page.screenshot({ path: './test.png' }); API 进行截图保存。

该方案的优点是提高了 tab 的利用率,不需要频繁的打开和关闭 tab(截一张开关一次),在相同的并发请求下,可以适当的减少服务器的数量,相同服务器数量下,等同于提高了并发量。缺点是提升有上限,并且到底是合并多少个请求后在一个 tab 中操作呢?因为截图操作是同步的,所以合并几个请求就必须等所有请求都截图完毕后才能统一返回,这样多个请求同步操作下来是需要时间的,如果必须多个,那接口的返回速度将受到比较大的影响,这是难以接受的一点,此外合并后返回怎么分发数据也是个问题。

实施的方案

puppeteer 截图流程服务优化(已优化上线方案)

在探索优化方案的过程中,可以发现当前并发量的限制本质是服务器通过 puppeteer 打开无头浏览器截图的速度和 cpu 处理数量限制。通过压测统计未做任何优化下, puppeteer 截图流程中,随着并发升高,服务端截图流程时间变化:

puppeteer 截图流程压测数据

根据表格可得趋势图:

puppeteer 截图流程压测数据
注:这里由于截图效果,省略绘制了“截图时间”和“总耗时”的最后一个数据点,具体可看上一份表格数据

从上图中可以发现,puppeteer 在截图流程中,浏览器打开时间、tab 打开时间、截图时间、tab 关闭时间、浏览器关闭时间,随着并发量的增加,时间增长变化很大。本质是:正在使用的浏览器 tab 数量会增加 cpu 的使用率,cpu 使用率增高会严重影响 puppeteer 的截图速度。

由于 puppeteer 在截图的流程中对浏览器的各项操作都需要耗费不少时间,若能减少或省略各个流程的时间,就可以极大的优化接口的处理速度,并增加 cpu 每秒的处理能力,从而就能提高并发量,即:

从:

请求到达 -> 启动 Chromium -> 打开 tab 页 -> 运行代码并截图 -> 关闭 tab 页 -> 关闭 Chromium -> 返回数据

到:

请求到达 -> 启动 Chromium -> 使用默认打开的 tab 页 -> 运行代码并截图 -> 关闭 tab 页 -> 关闭 Chromium -> 返回数据

再到:

请求到达 -> 使用已启动的 Chromium -> 打开 tab 页 -> 运行代码并截图 -> 关闭 tab 页 -> 缓存 Chromium -> 返回数据

最后:

请求到达 -> 使用已启动 Chromium -> 使用已开启 tab 页 -> 运行代码并截图 -> 缓存 tab 页 -> 缓存 Chromium -> 返回数据

带着上面的想法,我在服务运行的时候事先开启好浏览器,并提开启好固定数量的 tab 页面,并用 node-pool 把一个个 tab 变成一个个实例进行开启、使用和销毁控制,具体实现方案流程图:

优化后方案流程图

截图流程优化方案对比

在优化过程中,把 4 个优化过程,计为 4 个方案,分别为:

a)浏览器不复用,tab 不复用
b)浏览器不复用,tab 复用(浏览器默认打开的 tab)
c)浏览器复用,tab 不复用(现网方案)
d)浏览器复用,tab 复用,固定 tab 数(单核2)

对四个方案压测 30s 平均截图耗时、RPS 和失败率统计(不带限流)结果为:

优化后方案流程图
结论:最终确认采用:浏览器复用,tab 复用方案

与现网方案压测对比

为新方案加入限流(防止超高并发下服务挂掉)逻辑后,进行现网方案和新方案最终压测对比数据统计:

优化后方案流程图

总结,对比现网方案优化和提高点:

1、接口返回速度提升,截图总耗时优化到只有 puppeteer.js 中截图步骤存在耗时,单次请求都在300ms以内(qps在16左右),平均速度提高了 250ms :

优化后方案流程图

2、RPS 提升,整体提升一倍左右

优化后方案流程图

3、失败率降低(这里的失败是指服务处理不过来返回的固定 code,采用兜底方案处理)

优化后方案流程图

ci 流程截图并存储,前端支持图片合成能力(计划实践方案)

方案流程:事先把需要进行动态截图的 html 进行处理,拿到动态元素的样式和位置作为属性数组,然后隐藏(不改变布局,利用 visibility: hidden; 或者pacity: 0;等)动态数据后,后截图做为静态图片,存储静态图片和属性数组放入 redis 缓存或者数据库中,前端通过接口拿到属性数组后,先将接口数据(例如头像和用户名等)与动态属性进程处理后再与静态图片进行 canvas 绘制合成图片。

具体实践是利用 Puppeteer.js 在模版 ci 流程中拿到需截图模版的的属性(样式)和位置,根据模版 id 为 key 存储在 redis 中(当前保存的是 base64 格式的图片)。前端只要传递模版 id 即可拿到当前需要绘制模版的静态图片(base64格式)、属性数组,以及接口数据。前端封装 SDK 用于便捷调用绘制分享图,绘制结束后返回最终图片。

具体实现方案流程图:

当前方案流程图

计划实践方案存在的问题

1、文字问题:例如:多行字自适应、文字点点点“…”、特殊字体处理等等
2、二维码
3、模版代码中是否有动态操 dom 元素的逻辑
4、服务端动态根据并发来判断图片处理方式,做到自适应
5、其他暂未发现的问题

未完待续

以上便是对截图优化方案截止当前的探索和处理,若有错误,欢迎指正,更欢迎大家提出疑问与想法,感谢阅读~

待实践方案(ci 流程截图并存储,前端支持图片合成能力)实践后会来完善该篇文章,或者另详细写一篇文章说明。