本文字数:22817字
预计阅读时间:58分钟
狐友作为搜狐的一款社交产品,在流量传播上有着旺盛的需求点。而在流量传播所需的众多载体之中,海报图片以其简单的分享形式、可定制的视觉体验、自带二维码识别导流等特点,成为了社交产品高频必备的流量载体。
作为狐友的前端开发,生成海报图片就成为了我们工作中持续不断的一个重要需求点。以下是狐友目前的产品前端服务矩阵和海报图片的产品形式。
图 1 狐友产品前端服务矩阵和海报图片的产品形式
从上图1可以看到,生成海报图片对于狐友产品矩阵来说是一个高频强需求。海报图片作为分享载体,对于各平台的分享流程对接也非常畅通和直观,例如不同于小程序卡片分享只能拘泥于微信平台,网页分享的链接形式不够直观。
而在海报图片这个重要环节,长期的主要技术手段一直是通过各客户端开发在本地设备上进行绘制,但这种方案存在如下的劣势困扰着我们:
各端无法复用:如图2, 如果要全平台都要做图片分享,那么需要各端分别开发,即使生成的图片一模一样,也要开发iOS
、Android
、H5
、小程序一共4遍,整体开发各端无法互相复用。
长图大图崩溃:客户端限于设备平台或系统限制,对于长图的生成并不友好,会出现长图因为内存或算力限制无法生成的情况,其中小程序尤为明显,在微信的框架下很容易长图生成造成程序直接崩溃。
开发效率较低:客户端本地绘制海报图片,一般需要手写原生代码效率不高。小程序端虽然使用wxml-to-canvas
(H5端使用html-to-canvas
)来绘制减轻了一些手写命令式绘制代码的负担,但这种标记语言转canvas
在实现上也存在缺陷,相比HTML+CSS
的表达力还是非常受限。所以,海报图片在代码层面开发效率比较低。
为了解决以上问题,我们开始着手调研并实践落地了一套全新的海报图片统一服务,命名为hy-ssr-img
。
图 2 狐友大前端海报生成各端开发状态
海报图片统一服务是一套基于Puppeteer
的Node.js
后端服务端(SSR
)渲染页面并截图生成海报图片的服务,这一服务解决了原有海报图片生成的三大问题:
各端无法复用 -> 4端同时复用:iOS
、Android
、H5
、小程序4端只需要开发一遍即可,效率提升400%,如图3
长图大图崩溃 -> 无惧长图渲染:不需要再担心长图渲染问题,服务端渲染图片能力理论上可以达到上万长度的渲染,针对不超过5屏的图片需求来说绰绰有余
开发效率较低 -> 开发效率最优:使用效率超高、表达力超强的标记语言HTML + CSS
渲染,开发效率和表达力都达到最优状态
图 3 海报图片统一服务各端复用
那么,海报图片统一服务是如何建立起来的呢?下面是从项目立项开始的具体设计方案。
首先,我们调研了业界现有的图片渲染方案,如下表。通过下表的调研总结可以看出各个方案各有优劣势。由于复用提效是重要需求点,我们把方案锁定了服务端渲染方向上,即入选方案为:
Node.js截图方案
服务端图层绘画方案
在对比两种方案,以及参考相关业务实践后,我们最终选用了最常见的【Node.js截图方案】。因为服务端图层渲染选用的图形库在排版表达力上远不及HTML+CSS
在浏览器上的表达力,而且系统预期的使用对象是前端开发人员,并不是产品和运营,所以服务端图层渲染方案的拖拽方式并不是必选项,反而由于拖拽渲染表达力的限制,对于实现一些排版复杂的图片非常吃力。而Node.js
截图方案则没有这方面的问题,前端开发只需要常规开发页面即可,然后交给Puppeteer
渲染后进行截图形成最终用户看到的海报图片。
表 1 项目选型方案对比
在确定了【Node.js截图方案】之后,我们对项目整体架构流程进行了设计,首先由于我们的应用的后端Java
接口服务已经非常成熟,针对参数的合法性校验及加签解签等防护措施都已建立,所以我们在开发Node.js
服务时就没有必要再造轮子,那么怎么把这些基础接口功能和Node.js
服务联系起来呢?我们决定把Node.js
服务放到内网,通过只允许后端服务直接访问的方式来达到这一目的,这样后端接口层就像一个盾牌,挡在用户和Node.js
服务之间,而我们就可以专注实现Node.js
的截图功能了。整体方案流程如下图4:
图 4 Node.js截图方案整体流程方案
整体架构流程方案确定后,我们就需要细化截图相关的详细流程方案。在经过调研之后,我们发现常见的截图开源方案在我们的使用场景中还有很多需要优化的点和不满足需求的部分,因此我们决定自研开发【仅首屏SSR
渲染】方案。针对项目特点,每个海报图片只有一张,所以不存在首屏渲染过后还要渲染第二页的场景,因此该方案只包含首屏字符串渲染即可,不需要带有常见 SSR 的客户端激活渲染的打包构建及执行流程。 去掉常见SSR
方案中的非首屏渲染逻辑后,页面就只包含了首屏必要的渲染代码(HTML + CSS
),去除一切不需要的环节(JS
执行激活),保证页面给到 Puppeteer
渲染时是最简化的状态,尽可能减少网络I/O和本地磁盘I/O
,只包含单纯的渲染过程,以加速渲染速度。以下是常见方案和自研方案的对比。
最常见的截图方案是通过请求网页地址,渲染后截图,如图5。
图 5 Node.js截图详细流程常见方案
这种方案不管是CSR
(客户端渲染)方式,还是SSR
(服务端渲染)方式,都还是有优化空间。我们可以发现,Puppeteer
请求网页的网络消耗是可以被节省下来的,如果我们直接使用在本地生成好的HTML
和CSS
,则请求网页并下载所用的时间就可以节省下来,获得更快的截图速度。
另外页面的动态渲染通过SSR
来进行,这样完全不需要客户端JS
的存在,直接还可以省去加载客户端JS
的时间,获得更优的渲染速度,这种优化后的Node.js
截图详细流程方案如图6所示。
图 6 优化后的Node.js截图详细流程方案
该方案还有技术细节需要设计,即如何完成SSR渲染首屏的工作,由于我们的技术栈是Vue
,所以选用vue-server-renderer
为基础库来完成这一过程,如图7。
图 7 仅首屏SSR渲染方案技术流程
整体架构流程方案确定之后,我们就要进行更细化的业务技术流程设计,这里有很多细节需要考虑:
后端接口层如何和Node.js
截图服务进行通信?
截图服务是一个高耗时服务,如何防止长链接堆积?
如何设计海报图片缓存层?如何设计提供海报预渲染?
其他流程细节...
为了解决这些问题,我们设计了如下的流程,如图8。
图 8 Node.js截图服务业务技术流程
可以看到整个流程设计由接口同步请求和截图异步任务两大块组成。我们把Node.js
截图服务当做一个纯渲染图片的服务,用户发起请求给后端服务时会携带渲染页面所需要的动态参数,后端服务则负责参数校验等工作,并且下发给用户一个异步任务ID。然后后端服务会请求截图服务,截图服务收到请求后并不会立刻截图,而是直接返回后端服务,并开启异步任务去进行耗时的截图服务,这样就可以防止长链接堆积造成服务不可用的情况发生。之前下发给用户侧的异步任务ID
就是异步截图任务完成后通知后端服务的凭证,这样当截图服务完成后就可以通知后端服务截图完成状态(截图成功或者失败),而用户侧则可通过轮询后端服务接口得知截图是否完成并使用海报图片了,当然可以设置一个超时时间来完成整个截图服务的交互闭环。
对于海报图片的缓存层也是要考虑的,因为很多场景下用户请求的海报图片是一模一样的,比如我们的热榜海报会在固定时间生成一次,那么缓存层可以有效缓解截图耗时操作,并且为预生成海报图片提供了基础。比如我们会在特定时间通过代码自动预渲染一批海报,当第一个用户来访问时就不需要等待耗时的截图服务,直接返回渲染好的海报图片即可。那么命中缓存的条件是什么呢?对于长的一模一样的海报图片当然希望只生成一次后都走缓存层。我们的设计中决定“图片长相”的因素有两个:
1.海报地址:对应不同的海报样式
2.海报参数:对应同一个海报样式中的不同数据
所以,我们通过hash(“海报地址”+“海报参数”)的方式得到缓存命中的key
,以此来控制命中缓存。另外截图服务生成海报图片后会直接上传到CDN
进行存储,用户侧加载海报图片的速度和稳定性也得到了相应的保障。
在业务技术流程方案确定后,就需要为此搭建一套工程化开发环境,来支持项目业务的具体开发。我们基于公司现有基础设施以及技术栈,确定了以下主要技术选型:
接口服务框架:Express
页面渲染框架:Vue
支持SSR
渲染:vue-server-renderer
支持浏览器截图:Puppeteer
支持页面开发环境 :webpack
图片存储:OSS
(公司内部对象存储服务)
服务端日志:Logstash
+ Kafka
+ Elasticsearch
+ Kibana
(公司内部基础日志服务链)
部署:DomeOS
(公司内部云服务平台)
如图9,展示了通过以上技术栈及基础设施组建的整个工程化方案。
图 9 Node.js截图服务工程化方案
这里需要说明是,一个海报图片对应一个页面,一个页面会有两个入口文件:一个CSR
(客户端渲染)用于开发页面时使用,一个SSR
(服务端渲染)用于截图时Puppeteer
渲染页面使用。这么设计的原因是,CSR
渲染使用webpack-dev-server
是现成的开源方案,对于开发时热更新等支持不需要自定义开发,开箱即用,如果开发时也使用SSR
进行就需要进行针对性的改造,由于开发体验上并没有区别,我们就选用了更高效地搭建方式。而截图时页面渲染方式就是上文提到的【仅首屏 SSR
渲染】。
从用户发起请求到海报图片返回,这整个过程的耗时需要进行“压榨”,以获得更好的用户体验。以下是我们在开发过程中实践和验证的相关重点优化和部分效果收益。从第一版基础开发到最后优化完成的版本,截图服务总用时从1300ms+降低到了600ms+(注:测试数据均取相同开发机10次执行结果的平均值,渲染相同的海报图片,图片为超长图且内容丰富,长宽为:4967×750)。表2为各关键优化点明细。
表 2 项目关键优化实践
为了方便使用海报图片的开发人员,我们还配套开发了海报生成系统在线文档。开发人员可以通过该系统查看现有的海报图片以及相关参数字段,并可以通过右方的编辑器更改字段的值并实时得到新的渲染图进行预览和下载。如图10,开发人员可以在这个playground
里进行所见即所得的预览及操作。
图 10 海报在线文档预览系统
海报图片服务作为一个后端服务,日志采集分析和监控是必不可少的,我们可以通过日志得到以下信息:
海报图片生成的访问情况
海报图片生成的性能优劣
海报图片生成失败的原因
海报图片生成异常的报警
所以,我们封装了3种log日志用于海报图片服务:
access:记录每次请求的请求日志
debug:记录生成图片的关键节点信息的日志
error:记录生成图片失败及原因的错误日志
日志处理完成后,我们接入了公司现有日志基建服务来完成后续的日志采集、存储和分析等功能,如图11所示,通过这一套日志流程,我们就可以更加放心地上线并时刻关注我们服务的运行情况,轻松做到快速排查和分析问题。
图 11 海报系统日志服务
海报生成是一个耗时任务,其绘制速度依赖于服务器的CPU
和内存。经过线上数据评估,截图服务qps
最大支持60即可,经使用JMeter
并发测试:
当使用单实例(CPU
1核 + 内存 2G)单进程部署项目时,并发为1时生成5000像素的图片需要耗时约2s,并发数超过1时,后面的请求得等待前面请求生成完图片,才会生成,生成图片时间会随并发数成倍增加。
当使用单实例(CPU
1核 + 内存 2G)多进程部署项目时,并发为1时生成5000像素的图片需要耗时约2s,并发数超过1时,当并发数少于进程数时,会同时生成图片,并发为5时,图片返回时间约为9s,此时CPU
占用会超过40%,内存占用20%,虽然CPU和内存在并发数量为5的时候占用率不高,但是生成图片的速度会大幅下降,所以需要增大单实例CPU
和内存的大小。
当使用单实例(CPU
4核 + 内存 6G)多进程部署时,并发为1时生成5000像素的图片耗时约1s,并发数为5的时候,图片返回时间约为3s,当并发为10的时候,图片返回时间约为8s。此时CPU
占用会超过20%,内存占用10%。
所以为了保证图片的生成速度和稳定性,使并发量少的时候图片生成速度尽可能快,并发量大的时候图片生成速度在可接受时间内,采用了多实例加多进程的部署方式,如图12,项目部署于DomeOS
平台,部署了5台 (CPU
4核 + 内存 6G) 实例,每个实例通过pm2
启动4个进程。通过负载均衡可以使请求平均打到每个实例的每个进程上,让并发少的时候最快的生成图片,并发大的时候充分利用所有实例,加快整体生成图片时间。生成5000像素的图片,当并发数小于5时,图片可以在1s左右返回,当并发数为20,图片可以在3s左右返回,当并发数为60时,图片可以在8s之内返回。
图 12 海报系统部署方案
截止到2023年初,海报图片服务已上线海报10+个。每个海报只需开发1遍就可供给H5
、小程序、Android
和iOS
共4端进行使用。每天平均生成海报图片6000+,每张海报图片平均生成时间400ms左右,支持超长图可达10000+像素。
随着项目迭代,海报服务未来可能有更大的需求诉求,下面列出海报服务未来进化的一些展望:
1、赋能外部开发人员
目前项目是以普通项目的开发模式进行研发,如果提供给非内部研发人员使用则有很多流程和规范上的问题难以解决。未来可以支持渲染远程组件,通过远程组件的方式下发给外部研发进行开发,以此隔绝海报服务核心逻辑和业务方逻辑,使得业务方只需关心业务,也防止业务方无意间可能影响到海报核心逻辑。此外,在整个过程中还可以增加审核远程组件等项目管理能力。
2、赋能非研发人员
为了海报图片渲染极具灵活性,我们把开发人员作为首要满足对象。未来除了开发人员可以开发海报页面,同时可以支持非研发人员通过拖拽编辑等低代码方式完成海报图片的生产,这样针对简单海报图片的场景,运营、产品等非研发人员也可以进行海报图片的制作,进一步提高生产效能。