导语 | 上个月,我有幸参与了腾讯视频国庆阅兵直播页面开发的相关工作,最终,累计观看2.38亿人次,经受住了高并发的考验。在参于Glama框架的开发维护及平时基础建设相关讨论实践中,对高并发有一些部分实践心得,正好老友也想了解腾讯视频这边的经验,特撰写本文,对相关经验进行梳理总结,与大家探讨。(本文作者:Lucienduan,腾讯视频Web前端高级工程师)
本文将从服务可用性、缓存、日志三个维度总结视频侧开发高并发 Node.js 服务的一些经验。
一、视频前端网络结构
先介绍腾讯视频前端网络结构,网络架构如下图所示。
腾讯视频 Node.js 服务的网络示意图
流程简述如下:
用户首先请求GSLB,找到最佳接入IP,就近访问CDN节点;
CDN缓存命中时,直接响应缓存, 如果有CDN缓存失效或未配缓存, 会直接回源到TGW(Tencent Gateway), TGW主要处理容灾、负载匀衡;
请求从TGW(STGW)转发到业务层Nginx,在Nginx中也会有简单的容灾策略(主要由max_fails,fail_timeout两个设置配置)和缓存机制,最后到达Node服务;
在Node中用cluster模板转发到对应的worker进程处理,worker中会跑具体的业务, 请求对应的后台服务器。
这里TGW主要功能:负载均衡、容灾、IP收敛、多通接入。
系统整体的可靠性需要各个节点相互配合,本文主要针对由前端开发的负责的模块, Node和业务这一节点为中心从可用性, 缓存和日志发散来说高并发服务需要关注的点。
二、可用性
运维老司机说:没有绝对可靠的系统,局部故障是常态。
但通过一些方法兜底和保护,可以保证核心业务无异常。
保证业务可用首先需要保证相关的进程工作正常,进程异常时能容灾兜底。
进程守护
Node.js主进程守护,腾讯视频这边用shell脚本来描述执行:
通过 crontab 命令,定时1min钟去检查一次进程(用ps指令)和端口(用nc指令)是否正常, 异常时重启服务。
在Nodejs Cluster模块,主进程会把TCP分配给worker进程处理,worker进程主要三个问题, 僵尸进程, 内存泄露和进程异常退出。
僵尸(无响应)进程:当程序运行到死循环,就不再响应任何请求了,需要及时重启:
在Master进程定时向worker进程发心跳包,当worker进程在一段时间多次不回包时, 杀死重启。
内存监听:主要为了兜底内存泄露问题, 当worker进程达到阈值时, 杀死重启
进程退出:进程异常退出时, 需要重启。
目前社区有比较多的工具可以实现进程守护,比如pm2。
页面静态化/预渲染
最安全的进程是没有进程……即整个请求链中不依赖的Node.js服务。
静态化示意图
对于一些只有少数的几个运营同学更新数据且可用性要求极高的页面,可以直接由运营的发布动作触发页面更新的CDN。
整个请求链环节少,无回源请求,异常的概率最低。
即使Node.js有多级的守护,但还是有可能进程内的分支逻辑或接口出现异常,当分支逻辑或接口异常出现时,合理的容灾策略可以提供降级服务让核心业务无影响,用户无感知。
三、三层容灾策略
如果上面守护异常,或是底层的依赖服务挂了,H5页面有三层容灾策略。
容灾策略示意图
1. 接口容灾
接口容灾主要应对依赖的底层接口异常。当后台接口正常返回时,把数据缓存到redis,异常时,用redis的旧数据兜底。
2. 页面HTML
兜底思路与口容灾差不多,当页面渲染异常时,中间件检测到返回5xx,同样用正常的缓存在redis的旧HTML兜底。
3. NodeJS容灾
主要应对NodeJS工作异常,当NodeJS进程正常响应时,把静态的HTML推到CDN作为备份文件, 如果NodeJS返回5xx时, 在Nginx代理层重定向到静态备份文件。
从实践来看,上面的进程worker的守护和容灾兜底,可以很好的保证源站业务的稳定性,对于高并发业务,缓存和告警必不可少。
四、缓存
缓存在高并发的系统扮演着至关主要的角色,除了用户态、推荐等少数业务场景不能用缓存外,缓存是应对流量冲击简单有效的方式, 目前视频侧主要有三级缓存, CDN缓存,代理层Nginx缓存,应用层redis缓存。
三级缓存示意图
图片来源:《Web前端与中间层缓存的故事》
CDN 缓存
CDN的OC节点不但可以减少用户的访问延时,也可以减少源站的负载,但Node.js站点在用CDN抗量时同时需要注意两个问题。
1. 更新时间
由于CDN一般用于缓存静态文件或更新粒度比较小的页面,默认的缓存时间比较长,在接口上使用时需要注意更新时间,同时接口不能带有随机参数。
在70周年阅兵主持人页面中,轮询请求量非常大,接口用了CDN缓存,由于没有太关注更新时间,导致接口更新不及时, 对于自建CDN, 需要注意cache-control和 last-modified字段的更新,或是关注status头查看回源状态。
2. 缓存穿透、雪崩
目前自建CDN缓存没有缓存锁,当缓存失效到下一次缓存更新这一小段时间(一般在40~500ms),所有的请求都回源到源站,并发比较高时,会有大量穿透到源站,底层没有保护的话可能引起雪崩, 所以需要多级缓存。
Nginx自带缓存锁,通过简单的配置就可以解决这个问题。
Nginx代理层缓存
Nginx 除了提供基本的缓存能力外,还提供缓存锁、缓存容错能力,
proxy_cache_use_stale可以配置,错误, 超时,更新中和其它异常状态时, 使用旧缓存兜底和避免过多的的流量穿透到源站。
同时proxy_cache_lock配置,可以防止配置没有预热时,缓存的穿透的问题。
当proxy_cache_lock被启用时,当多个客户端请求一个缓存中不存在的文件(或称之为一个MISS),只有这些请求中的第一个被允许发送至服务器。其他请求在第一个请求得到满意结果之后在缓存中得到文件。如果不启用proxy_cache_lock,则所有在缓存中找不到文件的请求都会直接与服务器通信。
所以Nginx通过正常的配置,可以大大减少回源的请求,减轻源站的负载。
页面缓存
在应用层或框架层,可以用redis实现第三层缓存,这层的redis缓存也是HTML渲染异常时兜底的基础。实现思路比较简单,需要关注两个问题:
页面缓存版本不同步时,有无适配问题,如果需要识别版本,版本不匹配的缓存直接失效。
是否需要设计缓存锁来避免穿透问题,如果上层已处理(比如Nginx),或下层能抗量流量可以忽略不加锁。
整页缓存粒度比较大,可以针对业务场景做拆分,比如针对部分推荐数据的页面拆分页面片缓存或接口缓存。
从CDN、Nginx到redis,每一层的工作量、业务侵入性,粒度不一样,业务需要根据自身场景, 选用适合自己业务的缓存即可。
五、日志与告警
告警和日志,对故障早发现早处理,复盘根本原因至关重要。
目前视频除了基础平台提供的CDN、TGW和机器的物理资源的监控外,在用户侧和代理层, 源站均有不同维度的告警和监控。
监控示意图
客户端提供了前端监控和告警,提供用户侧的监控,比如页面质量,CGI质量, 用户流水及手动上报的能力。
反向代理层 由Nginx上报监控,监控访问波动,错误量占比(4xx, 5xx)时耗时。
请求日志 主要记录原站的总请求数,请求失败数据及平均耗时。
Nodejs进程日志 主要进程异常退出,内存泄露,僵尸进程等进程日志, 对业务稳定运行, 非常重要。
Node请求流水日志 主要记录请求维度的开发自定义日志,用于问题的定位复盘, 进程状态观测。
模调监控 监控请求方和服务方的错误和响应时间的情况,当前模块与底层依赖模块的接口实时接口质量。
每层的监控和日志可以帮助业务快速了解业务状态,定位业务异常。
总结来说:单个用户异常,查看客户端啄木鸟流水和Node请求流水日志,服务大概率异常查模调和请求日志,Node进程异常查看 代理层日志和进程日志,响应时间异常可以从客户端、代理层、源站及模调的耗时逐步分析。
六、总结
可用性永远是做框架和业务的同学需要关心重要指标,但是现状如文初所说:没有绝对可靠的系统。
除了关注Node.js的业务开发质量,如何在流程和架构层面避免局部异常不影响整体业务和用户体验更值得更进一步思考。腾讯视频在架构和框架的设计层面防呆,故障前进程守护,监控告警等方法避免和发现问题;故障中通过多级容灾兜底提供降级服务;故障后通过各个节点的日志定位问题改进回顾。保证质量参差不齐业务都能抗住高并发且高可用。