单核QPS提升10倍,亿级日调用!QQ频道前端网关升级之路
👉目录
1 生于 B 端:先让自己满意
2 长于 C 端:跨越边界
3 提升安全/可用性
4 提升性能/效率:实用第一
5 提升可观测性:深度是被逼出来的
6 总结:以躺平的心态完成不躺平
本文亮点提炼:
每日请求量从几千到上亿
小程序和 H5 都能使用腾讯开源 tRPC 接口
单核 QPS 从 2K 提升至 2W
请求成功率从 99.985% 提升至 99.997%
性能:支持 WebSocket
本文通过介绍整个历程中遇到的关键问题和解决办法,提供网关建设中的一些可复用经验。长文干货,建议先点赞收藏,慢慢品阅!
(从左到右的量级为累计)
01
1.1 背景
问后台要 proto 文件; 生成桩代码; 配置代理、实例化 Client、发送请求;
1.2 分析
message RequestProtocol {// 协议版本// 具体值与TrpcProtoVersion对应uint32 version = 1;// 请求的调用类型// 比如: 普通调用,单向调用// 具体值与TrpcCallType对应uint32 call_type = 2;// 请求唯一iduint32 request_id = 3;// 请求的超时时间,单位msuint32 timeout = 4;// 主调服务的名称// tRPC协议下的规范格式: tRPC.应用名.服务名.pb的service名, 4段bytes caller = 5;// 被调服务的路由名称// tRPC协议下的规范格式,tRPC.应用名.服务名.pb的service名[.接口名]// 前4段是必须有,接口可选。bytes callee = 6;// 调用服务的接口名// 规范格式: /package.Service名称/接口名bytes func = 7;// 框架信息透传的消息类型// 比如调用链、染色key、灰度、鉴权、多环境、set名称等的标识// 具体值与TrpcMessageType对应uint32 message_type = 8;// 框架透传的信息key-value对,目前分两部分// 1是框架层要透传的信息,key的名字要以tRPC-开头// 2是业务层要透传的信息,业务可以自行设置map<string, bytes> trans_info = 9;// 请求数据的序列化类型// 比如: proto/jce/json, 默认proto// 具体值与TrpcContentEncodeType对应uint32 content_type = 10;// 请求数据使用的压缩方式// 比如: gzip/snappy/..., 默认不使用// 具体值与TrpcCompressType对应uint32 content_encoding = 11;}
1.3 思路和实现
只需对 HTTP 的路径进行一致性约定,即可按照固定规则提取出 RequestProtocol 中的 callee 和 func;例如,频道使用的约定如下: 路径:
HTTP 头中的其他信息可以根据业务场景映射到 trans_info 中; content_type 采用 json; 包头的 proto 固定,我们使用这个固定的 proto 把上面的数据序列化为二进制。
1.4 效果
02
2.1 背景:小程序接入
2.2 方案
标准服务场景:直接与后台 Server 进行通信。; 经过 OIDB 代理的服务场景:借助 OIDB 代理和后台 Server 通信。 经过 SSO 代理的服务场景:借助 SSO 代理和后台 Server 通信。
请求元信息转换按照 QQ 频道后台 tRPC Server 的默认约定进行。(这里略过,不同业务有不同约定) OIDB 头部编解码。 SSO 协议编解码。
2.3 成果:难路是捷径
第一次和这么多团队打交道:客户端、跨平台、基础平台、互联、业务后台。 两周打了最多的电话,麻烦了十几位大佬请教代码逻辑、开权限。 虽然难,但是价值很大: 参照 QQ NT 和 SSO,实现了第一版协议转换,可以从 JSAPI 无缝切到 HTTP,向更灵活的服务调用模式迈出关键一步; 此外,对 QQ 频道的服务架构和网络链路有了整体框架,很快在下次问题定位中派上了大用场;
03
3.1 安全:遭遇黑产(背景)
复制频道链接乱码(对应短链码服务); 频道邀请链接和一个无鉴权的分享接口(对应协议转换服务)调用飙升; 失败率的上升趋势肉眼可见; pod 高负载,已经触发了二次扩容;
是不是有什么大型的营销活动? 或者是进行了现网的压力测试? 还是,被攻击了?
短链码服务高负载,但是进程没挂,而且扩容没有让失败率略微下降! 这种表现,最可能什么原因? 某个服务链接数有限制,或者耗尽,而且最有可能是 MySQL。
扩大 connection pool; 对 pod 缩容,扩容反而会提高失败率;
针对无鉴权的接口,一定要配置安全策略,对 IP、UA 等进行更严格的限频; 特别关注 k8s 二次扩容特性,的二次扩容特性,虽然它是一种紧急保命策略,但也可能引发其他问题(比如这里):
3.2 可用性:请求超时
背景
问题分析
问题节点
STGW L5 缓存更新问题
无法找到服务的 IP 地址; 部分请求找到了错误的 IP 地址;
bug 原因
解决办法
尽量固定服务 IP 地址,最大限度地减少变动。 通过业务网关进行管理:由于业务服务(如缩扩容、迁移等)经常变动,而网关相对稳定,因此,将业务服务配置在一个稳定的网关之后是最佳解决方案。
教训
基于指标的告警固然重要,基于日志的告警也不能少(敏感度更高); 正是由于这个问题,,催生了 STGW 后建设业务网关的想法;
3.3 可用性:发布自检
背景
方案
step 1: review 发布变更准备完整的代码 diff,供二次检查; repeated:枚举变更点(变更点包括:配置、数据库表结构、接口、依赖服务)变更内容; 当前变更的依赖项(依赖的其他变更项,比如配置、数据库变更等); 变更影响范围; 变更的向上兼容性。 step 2: 确定发布步骤根据变更及变更的依赖项确定发布次序。 同时确定回滚次序(微服务发布必须); 按发布次序和回滚次序,列出每一步的操作。 step 3: 确定更新策略手动分批发布:微小修改(包括配置变更) 灰度发布:不变更原集群; 增加新版本集群; 逐步切流(uin/比例)发布; step 4: 发布过程亲眼看到第一个 pod 重建; 打印成功日志; 观察指标无异常;
效果
04
4.1 性能:改造 TSW/NGW
背景
问题分析
为了使性能评估结果具有实际意义,我们需要一份能够完全镜像真实环境的性能报告。
import { sleep, check } from "pts";import HTTP from "pts/HTTP";export const options = {};export default function main() {let response;response = HTTP.get("HTTPs://xx.com/apisix/");check("status equals 200", () => response.statusCode.toString() === "200");}
import { sleep, check } from "pts";import HTTP from "pts/HTTP";export const options = {};export default function main() {let response;response = HTTP.get("HTTPs://xx.com/tsw/");check("status equals 200", () => response.statusCode.toString() === "200");}
使用 APISIX 平稳替换 NGW 和 TSW 的功能; 补齐工具建设;
APISIX 无缝承接 NGW/TSW 的路由/灰度/染色功能 ; 单核 QPS 从 2K 提升至 2W,显著降低了 CPU 和内存的资源消耗; 根据最近 7 天的 STGW 监控数据,请求成功率从 99.985% 提升至 99.997%;(整体平均耗时略有下降,但不具备可比较性,因为后端负载有不重叠部分)。
4.2 性能:支持 WebSocket
背景
实现方案
协议选择
socketio 已经在公司内其他业务的生产环境中经受了百万级消息每秒的考验; SDK 封装成本;
H5; QQ 小程序; cocos;
低成本接入
WS server 只做 “接线员” 角色:把 client 和 server 的 socket 连起来,就算完事了; WS server 做 “邮局” 的角色:业务 server 不用管 client 的存在,只要告诉 WS server 消息发给谁就可以了,WS server 保证最大可能送达;
消息可靠送达
业务 server 指定向某个用户推消息,但是用户不在线(没有活跃 socket)怎么办? 用户接收消息的时候,链接中断、网络切换怎么办?
消息缓存:每一个待发送的消息先储存; 只有两种情况才删除缓存的消息:过期、被确认接收; 消息确认:client 接收每个消息需要回复 ack 命令字,否则进入补发机制; 消息补发: 补发时机:重新建链、加入房间; 补发内容:缓存的所有相关消息;
成果
适配三端(H5/QQ 小程序/cocos) 的 SDK; 复用 HTTP 鉴权(无需额外鉴权环节); 支持 HTTP 接口作为命令字; 支持下发 tRPC 接口轮询任务;
4.3 效率:文档沉淀
背景
4.3 效率:工具建设
背景
开发过程中,比如设置接口白名单、接入 oidb; 测试联调中,比如染色、设置转发;
发布过程,比如检查配置、灰度比例设置、配置更新等都需要手动进行。
误区
容器新建(或更新); 设置缩扩容; 变更名字服务; 分配负载; 设置流量权重; 更新关联服务配置;
方案
新网关也接入企微机器人设置染色/转发,和 tsw 一样的方式;
重要配置(白名单、oidb 命令字等),使用 git 管理,采用代码一样的 review 进行变更;
配合 ci,配置变更 -> 审批 -> 生效,完全自动化。 cli 负责代码生成:以命令行的形式,自动生成符合网关约定的调用 SDK、接口出入参数类型、Mock 规则等;
效果
05
5.1 背景
pskey invalid 比例上升,略微下降后再上升; 服务整体平均耗时也表现类似规律,波动上升和下降;
5.2 问题分析
猜想、验证
polaris 版本有变动,而新版本修改了 server ip —— 这是首先想到的最可能原因。
版本确实升级了(0.3.x -> 0.4.x),经过 diff 两个版本相关源码后,发现 server ip 没有任何更改。
否认了 server ip 变更的猜想后,必须调整 Debug 思路:
继续向上排查 server ip 的调用代码;
寻求北极星助手帮助;
寻求协助
两个版本之间的变动十分多。为了更快解决问题,决定立即寻求相关开发团队的帮助:
可能并不是应用代码存在问题,也稍微放心了点。
但是,运行一段时间后,重建的pod 再次出现了上面的报错。放下的心再次悬起来:立即登录 pod,对报错的 ip telnet 测试,网络连通没问题,所以还是应用代码的问题!
非常手段
时间紧迫,无奈只能走钢丝:对线上环境开启实时调试。
太久没这样搞过,一边隔离 pod,一边手忙脚乱查自定义调试端口的写法(生产环境对可访问端口有严格限制):
终止 Node 进程;
以 node --inspect=0.0.0.0:80 的方式重启进程;
打开 chrome;
在 chrome 输入 chrome://inspect 打开 Node DevTools;
(...略掉大量无借鉴意义的试验细节)
最终在 memory 的快照中发现了端倪:
按 retained size 倒序后,出现了 PolarisGRPCClient。
展开 PolarisGRPCClient 后发现,每个 client 还都一个 pool:
既然有 pool,client 被设计为复用,但却有这么多 client,每个还有独立的 pool,显然有问题:要么该销毁的时候没销毁,要么缺失了缓存机制。
瞬间惊醒:缓存!
18 个月前写下这个函数的时候,特意留下提醒:
之前一直把目标聚焦在北极星,问题压根不在这,而是使用北极星的库:trpc-rpc-client。
查看 trpc-rpc-client 最新版的源码,果然 createObjectProxy 没了缓存。
5.3 解决思路
在应用代码里补充 worker 的缓存及缓存淘汰机制;
分集群处理:为 QQ 频道、转 OIDB 、小程序预拉取、大数据通道等,分别部署独立集群,最大限度利用 worker 缓存。(类似一致性哈希的思路)
5.4 修后感
我喜欢听法医笔记,99% 依靠摄像头就能破案,但传统刑侦技能依然不能丢,为了可能的 1%。
希望线上调试永远不会成为必须的选项,如果 bug 真的落到监控死角,不要忘了这一救命办法:node --inspect=0.0.0.0:80。
用了一周才走完这 “10 分钟的 bug 修复”,期间思考各个现象、指标背后的关联信息、翻看库代码。如果下次,我希望有监控能帮助我直击要害,而不是绕了这么远:
搜集正式环境的全部标准输出,并具备全文检索的能力;
如果性能允许,补齐进程的监控;
5.5 成果
实现应用内链接缓存和复用,建链+编解码平均在 5ms 内;
完善业务指标的监控:关键页面 PV/UV、关键接口耗时;
加强分集群部署的监控:核心系统指标、服务指标的横向对比;
06
在流量增长、功能扩展与性能优化的交织下,把一个协议转换发展为业务网关。每一步的发展都不是事先规划的,而且和最初的设想完全不同。
当时同时存在其他可用的替代方案:JSAPI(客户端方案)、HTTPSSO(后台方案),为什么最后落到 Node (前端)来做呢?我觉得一句话总结,是开发选择了 Node 。
Node 只是三个选项中的一个,最后前端、游戏、其他业务的开发(比如腾讯文档)、包括后台,选择了 Node。通过上面的过程,我归纳了 4 个原因:
1. 使用体验
每个开发会用脚投票,不好用就会选择其他替代方案。我熟悉前端最习惯的调用方式:
想要不更换 URL 就能切测试环境;
想要不改 cookie 也能切环境;
想要一致的返回结构;
想要固定的传参方式;
不要操作 cookie、操作票据、重复传一大堆不想关心的数据;
同时为了让后台的服务不改动也能被尽可能多的方式调用**(APP、小程序、H5、管理端),我做了大量工作,这也是不少后台希望走 Node 调用的原因。
2. 稳定第一
Node 在测试环境、正式环境的稳定性都被认可。相对于其他调用方法,Node 测试环境的稳定赢得了开发的心。并且,Node 在正式环境的可用性也达到了 4个 9。
3. 及时响应、敏捷高效
毕竟我就坐在旁边,前端小伙伴吼一声就可以了。一个最近的例子,海外版开始也是计划先走 JSAPI,但是一个 cookie 的修改可能就得排期,通过 Node 5分钟,需求响应及时、高效。
4. 持续优化的性能
ngw/tsw -> apisix,编解码、连接等,性能一直在变好,目前可能还不是性能最佳的,但也不会成为瓶颈,是已有替代方案中体验和性能综合最好的。
当然还有十分重要的,频道放量和业务潜力,为网关发展提供根本动力:
不需要胸怀大志,把一件件小事做好,顺势而为。
最后引用一位大佬的话:希望每个看完的小伙伴,忘掉,再重新总结出属于自己的东西。
📢📢欢迎加入腾讯云开发者社群,享前沿资讯、大咖干货,找兴趣搭子,交同城好友,更有鹅厂招聘机会、限量周边好礼等你来~
(长按图片立即扫码)