15

2 周流量激增百倍的腾讯课堂后台扩容和性能优化实战

 4 years ago
source link: http://mp.weixin.qq.com/s?__biz=MjM5ODYwMjI2MA%3D%3D&%3Bmid=2649746136&%3Bidx=1&%3Bsn=860ef4f77e6c4ae8f18fadd3a6da5d1c
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

640?wx_fmt=gif

作者:andyawang,腾讯 CSIG 后台开发工程师

疫情期间,学校网课需求激增,腾讯课堂 2 天上线极速版,2 周内支持同时在线人数超百倍增长,对整个后台挑战非常大。整整 2 个月下来,同合作团队一起,白天 7 点开始盯监控和开发版本,凌晨 12 点例行压测和发布扩容,踩过很多坑也取得很多收获,这里拎几个关键点记录下。

腾讯课堂停课不停学

项目背景

大年初一,吃着火锅唱着歌,突然收到重庆十一中的求助信:受疫情影响,年后学校无法开学,高三老师学生都很担心影响到高考,问腾讯课堂能否提供线上平台给高三复课,拉开了整个停课不停学专项的序幕。

极速版的诞生

由于课堂是面向线上培训机构的,这次想把十一中这样的传统线下校园,搬到腾讯课堂内上课才发现困难重重:

  1. 入驻:学校各类资质和机构完全不一样,审核周期长

  2. 发课:机构发课有很多规范约束,而学校用课程表排课,一个个发课成本高

  3. 直播:学校老师转线上上课,普遍说直播工具有上手成本

耗了很多人力才把十一中的入驻发课和老师培训搞完,同时其他学校也陆续找过来了,才发现根本没人力能对接这么多学校。就在这时,小马哥发话了:“把入驻发课全砍掉,快速做个腾讯课堂极速版,老师下载完就能自助上课了”

初 3 收到军令状,公司 2 个通宵生死急速开发完,初 6 凌晨团队体验,解决完体验问题后白天急速上线外发。随着各省市教育厅陆续宣布使用急速版复课,课堂 pcu/dau 开始起飞,短短 2-3 周,各项指标百倍增长,AppStore 免费类排行榜进 Top10,教育类稳居 Top1。

640?wx_fmt=png极速版发布

疫情期间开发缩影

白天 7 点开始盯监控和开发版本,凌晨 12 点例行压测和发布扩容。才发现企业微信一周小结是按凌晨 5 点为界:

640?wx_fmt=png疫情期间开发

架构挑战

下面是课堂后台架构图,按之前的架构设计和模块部署,突然要在 2 周内支持量 100 倍增涨,同时还要开发校园版需求,时间赶且任务重。这里分五个阶段把架构挑战和解决策略介绍下。

640?wx_fmt=png后台架构

阶段 1:先抗住后优化

面对 2 周 100 倍量级增长,重构肯定来不及了,且过大改动仓促上线反而会增加不稳定因素。所以初期思路就是“先抗住后优化”:梳理极速版用户路径,评估路径上各模块容量,快速扩容后,每天凌晨例行全链路压测,持续验证迭代。

模块梳理和接口裁剪

和产品讨论完用户路径和访问量级后,各页面 qps 也基本有个数了,就开始梳理每个页面调用接口列表,明确每个接口要支撑的 qps:

640?wx_fmt=png学习路径

由于课堂微服务很多,为了争取时间,需聚焦核心路径减少梳理复杂度,对于非核心体验和风险较大的这 2 类接口,抛出来和产品讨论做页面接口裁剪。

系统容量评估

模块和接口梳理清楚后,就开始分负责人对系统做容量评估。

要特别关注木桶效应,很多新同学只评估了逻辑 Svr 这一层,其实从用户端->接入层->逻辑层->存储层全链路都得涉及,不能让任一环节成短板。

扩容扩容扩容!!

各模块扩容数算清楚后,剩下的就是申请机器部署了。扩容本该是个很简单的活,但因为历史债务等问题,部分模块费了很多周折,比如:

  1. 容器化和上 k8s 不彻底

  2. 部分 c++模块依赖 ShmAgent,扩容流程极其繁琐

  3. 扩容 svr 导致 DB 链接数爆炸 ...

针对扩容暴露的问题,从接入->逻辑->存储,沉淀了很多设计经验,后面得彻底改造掉,特别是 k8s 这块应尽快全面铺开。这里终极目标是:针对量级暴涨的情况,不用花人力做评估和扩容,整个系统就有自动伸缩的能力。

全链路压测

在初步扩容完后,为了防止梳理不全、评估不准等情况,例行全链路压测验证非常重要,可以挖掘性能瓶颈和系统隐患。

在测试同学给力支持下,每天例行执行下面流程,持续扩容优化迭代:

  1. 校准压测模型:非常重要,压测用例设计会直接关系到压测效果

  2. 确定压测目标:把每个模块/接口的压测 qps 确定下来

  3. 执行压测任务:凌晨 12 点启动整站压测流水线,执行星海用例,输出压测结论

  4. 回归压测结果:压测不达标接口记录 doc,尽快暴露隐患,责任人分析原因给解决方案

压测 QCI 流水线:

640?wx_fmt=png压测QCI流水线

每日不达标记录:

640?wx_fmt=png每日不达标记录每日不达标记录

全链路压测方案:

640?wx_fmt=png全链路压测方案

阶段 2:瓶颈最先出在 DB 数据层

根据先抗住后优化的思路,可扩容的都容易解决,架构瓶颈会最先出现在伸缩性差、不容易扩容的环节上,经常会是数据层,课堂这次也中招了。

核心 DB,一挂全挂

由于之前量较小,课堂大部分模块使用同个 DB 实例(ip+port),上量前这个核心 DB 的 cpu 在 20%、qps 在 1k 左右,评估下来风险很大:

  1. 扩展性差:主机没法扩展,从机不建议超 5 组,且有主备延迟风险

  2. 耦合度高:任一 svr 链接数或 sql 没控制好,就算是边缘 Svr 都可能搞垮 DB 一挂全挂

  3. 梳理复杂:涉及 svr 数 100+,时间太赶来不及逐个梳理

也是历史设计的坑,后面数据层设计要多考虑吞吐量和可扩展性。但回不去了,硬骨头得啃下来,立了专项找 DBA 同学一起分析优化,主要有下面几块:

640?wx_fmt=png核心DB优化

业务横向拆分

根据压测发现非常明显的 28 原则,比如 top1 的写 sql 占总量 82%,是搜索推荐模块定时刷权重用的,这类模块相对独立,和其他模块表关联 join 等操作少,方便业务拆分。对于这类模块,像搜索推荐、数据分析、评论系统等,快速切独立 DB 解耦,规避互相影响。

方便业务拆分的切走后,剩下能快速上的就是读写分离扩 ro 组了。快速扩了 4 个 ro 组,把较独立模块 sql 切过去,规避互相影响,也分摊了主机压力。因为复制模式是半同步的,也需关注主备同步延时,做好监控,特别是一些对延迟敏感的模块。

慢查询优化

横向能拆的都快速搞了,主 DB 风险还是很高,除了升级 DB 机器配置,剩下就只能逐个做慢 sql 优化了。采用 mysqldumpslow 对慢查询日志做归并排序,就可很清楚平均耗时/扫描行数/返回记录数 top 的慢 sql,基本优化也是围绕着索引来,比如:

  1. 查询没有走索引

  2. 访问的数据量太大走全表

  3. OR 的情况无法使用索引

  4. 在索引上面直接进行函数计算

  5. 没有使用联合索引

  6. 唯一索引没有命中数据

优化效果:主 db 峰值 cpu 负载从 20%下降到 5%左右。

链接数优化

链接数上也出过一些很惊险的 case:鉴权 svr 凌晨扩 100 台机器,没考虑到对 DB 链接数影响,svr 起来后 DB 链接数瞬间增长 2k+差点爆掉。

除了对 top 的 svr 减少链接数外,引入 DB 代理也是个较快的解决方案,由于之前上云对 ProxySql 和 NginxTcpProxy 都有实践过,所以这次刚好也使用上。

具体可参考之前的文章 《谈下 mysql 中间件(问题域、业内组件)》:

https://cloud.tencent.com/developer/article/1349133

优化效果:主 db 峰值链接数从 4.6k 下降到 3.8k。

阶段 3:核心模块逐个专项击破

接入:血的教训

课堂支持学生在 pc/web/app/ipad/h5/小程序等多端进行学习,接入模块属于基础组件改动不大,但这次 web 接入模块却出了 2 个问题,确实不应该:

640?wx_fmt=png接入层

针对这 2 个问题也做了专项总结:

  1. 校准压测用例,更模拟现网流量

  2. nginx 动静分离,上报等接口也独立出去

  3. nginx 规则精简,接入层尽量打薄,逻辑后移

  4. 统一机器 net.core.somaxconn 等参数配置,重点监控告警

  5. 压测完要清理战场,关注 fd 等指标是否恢复

  6. 升级 tomcat 规避挂死 bug

  7. 升级 tomcat 遵循 RFC3986 规范,规避特殊字符影响

直播:站在云的肩膀上

课堂最核心的模块就是音视频,直播的进房成功率/首帧延迟/卡顿率/音画同步时延/分辨率等指标直接影响用户核心体验。由于直播模块之前已全部切云,这次站在云的肩膀上,业务不仅直接使用了云的多种直播模式,云音视频团队在整个疫情期间也提供非常给力的质量保障。

下面是具体的直播架构,业务通过流控 Svr 来控制各端走哪种直播模式:

640?wx_fmt=png直播架构

消息:走推走拉?

随着极速版普及,各学校对单房间同时在线人数要求也越来越高,从 3w->6w->30w->150w。

对于大房间消息系统而言,核心要解决消息广播风暴的问题(30w 人房间,有 100 人在同 1 秒发言,会产生 30w*100=3kw/s 消息),随着扩散因子的变大,之前纯推的方案已不能满足需求,参考了业内直播间的一些 IM 方案:

640?wx_fmt=png消息系统方案

结合对比方案,也针对课堂产品特性,在原来纯推架构上新迭代一版推拉结合的架构。

对于 IM 系统的设计,推荐一下这个文章:《新手入门一篇就够:从零开发移动端 IM》

http://www.52im.net/thread-464-1-1.html

阶段 4:做好核心路径的防过载和柔性降级

在非常有限的时间里,逻辑层根本来不及重构,容量评估也不一定精准,过载保护和柔性降级就显得尤其重要。

因为没时间全盘优化,在量突然暴涨的情况下,某个模块过载或者爆 bug 的概率会变大,所以在用户登录->查课表->上课的核心路径上,必须增加足够容错能力来提高可用性。

雪崩来得猝不及防

疫情初期课堂就遇到一个雪崩的 case:直播间拉取成员列表接口有失败毛刺,因为 web 没做异常保护,失败直接把循环拉取间隔时间置 0,导致接口调用量越滚越大,B 侧拉取涨了 10 倍后雪崩超时。由于没预埋开关等控制策略,得回滚 web 版本才解决。

这个 case 暴露了过载保护的缺失,一方面 web 没对异常返回做合理处理保护后端,一方面 svr 没有识别雪崩请求做限频,除此之外,也缺少一些配置开关可快速控制 web 循环间隔时间,导致雪崩来得猝不及防。

过载保护策略

针对这类 badcase,对核心路径的服务做了很多过载保护和柔性降级策略,这里把一些典型方案记录下:

640?wx_fmt=png过载保护策略

限流和熔断

高并发场景为了规避过载的级联传递,防止全链路崩溃,制定合理的限流和熔断策略是 2 个常见的解决方案。

以这次疫情互动直播限流场景为例,互动直播默认只部署最多支撑 600w 同时在线的接口机资源,如果哪天突发超过了 600w 学生:

640?wx_fmt=png直播限流

限流算法选择上,最常见就是漏桶和令牌桶。不是说令牌桶就是最好的,只有最合适的,有时简单的计数器反而更简单。golang 拓展库 golang.org/x/time/rate 就提供了令牌桶限流器,3 个核心 api:

1、func (*Limiter) Allow: 没有取到token返回false,消耗1个token返回true
2、func (*Limiter) Wait: 阻塞等待,直到取到1个token
3、func (*Limiter) Reserve: 返回token信息,返回需要等待多久才有新的token

除了算法外,怎么把限流集成到框架、分布式限流实现、限流后请求优先级选择等问题,可以做得更深入,但很遗憾这次没时间搞,后面继续实践。

熔断是另一个重要防过载策略,其中熔断器 Hystrix 最为著名,github.com/afex/hystrix-go 就提供了其 golang 版本实现,使用也简单。其实 L5 就包含了熔断能力,包括熔断请求数阈值、错误率阈值和自动恢复探测策略。

Apollo 配置中心

好的组件都是用脚投票,这次疫情期间,很多策略开关和阈值控制都是用 Apollo 配置中心来做,实现配置热更新,在高可用上实践也不错。很多时候多预埋这些配置就是用来保命的,当监控发现趋势不对时,可快速调整规避事故发生,简单列些例子:

  1. 后端限流阈值大小,后端要过载时可调小

  2. Cache 缓存时间,数据层负载高时可调大

  3. 非核心路径后端调用开关,必须时关闭调用补上降级默认值

  4. 前端定时调用的间隔时间,后端要过载时可调大 ......

当然,如果可做到系统自动触发调整配置就更进一步了,当时有想过但时间太赶没实践,有兴趣同学可思考实践下。

Apollo 是携程开源的分布式配置中心,能够集中化管理应用不同环境配置,实现配置热更新,具备规范的权限、流程治理等特性,适用于微服务配置管理场景。补个架构图推荐下:

640?wx_fmt=pngapollo架构图

阶段 5:服务性能优化实战

在抗住前 2 周最猛的流量增长后,下来很长一段时间都是在优化服务的性能和稳定性、处理用户反馈和打磨产品体验上。这里沉淀 3 个服务性能优化上印象较深刻的点。

分析利器 pprof+torch

在性能分析上,对比 c++,golang 提供了更多好用的工具,基本每次性能分析都是先用 pprof+torch 跑一把。通过框架中嵌入 net/http/pprof 并监听 http 遥测端口,管理后台就可随时得到 svr 协程/cpu/内存等相关指标,比如优化前的成员列表 svr 火焰图 case。

结合代码,便可快速有些优化思路,比如:

  1. 分离 B/C 调用部署

  2. 优化 pb 序列化,如做些 cache

  3. 简化定时器使用场景

  4. 调整大对象使用优化 gc 消耗

最终根据这些优化思路改版,让超 200ms 的比例从 0.2%降到 0.002%以下:

640?wx_fmt=png性能优化效果

缓存设计和踩坑

回过头看,大部分服务性能瓶颈还是在数据层或 Rpc 调用上,很多时候数据一致性要求没那么高,加缓存是最简单的首选方案。

关于缓存的设计,无论是本地缓存、分布式缓存、多级缓存,还是 Cache Aside、Read/Write Through、Write Behind Caching 等缓存模式,就看哪种更适合业务场景,这里也不累赘,核心说下这次实践中踩的 2 个坑:

1、缓存击穿

  • 案例:高频访问的缓存热 key 突然失效,导致对这个 key 的读瞬间压到 DB 上飙高负载

  • 方案:使用异步更新或者访问 DB 加互斥锁

2、缓存穿透

  • 案例:访问 DB 中被删除的 key,这些 key 在缓存中也没有,导致每次读直接透到 DB

  • 方案:把这些 key 也缓存起来,但要关注恶意扫描的影响

为啥 qps 压不上去?

疫情期间,有一个现象很奇怪但又经常出现:压测时 cpu 很低,pprof+torch 看不出什么异常,数据层返回也很快,但吞吐量就是上不去。一开始思路较少,后面也慢慢知道套路了,这里列几个真实的 case 供参考:

  1. 锁竞争:如死锁、锁粒度太大等,关注锁时间上报

  2. 打日志:日志量过大等导致磁盘 IO 彪高,在高并发场景尤其要注意精简日志量

  3. 进程重启:如 panic 或 oom 导致进程被 kill,重启过程请求超时,要补齐进程重启监控

  4. 队列丢包:如请求缓存队列设置过小等,要关注队列溢出监控 ......

比如最后这点,就遇过这样的 case:一次凌晨压测,其他机器都正常,就 2 个新机器死活一直超时,业务指标也看不出区别,折腾了好一阵,才发现 monitor 上 监听队列溢出(ListenOverflows) 这个值有毛刺异常。

继续深挖下去,证明请求在 tcp 队列就溢出了,tcp 的 accept 队列长度=min(backlog,SOMAXCONN),查看新机器内核配置 net.core.somaxconn=128,确实比其他机器小,神坑。

所以后续也增加了服务器 tcp 的半连接和全连接队列相关监控:

640?wx_fmt=pngtcp监听队列

挂个招聘

在这次疫情的推动下,在线教育越来越普及,各大互联网公司都持续加码,教育也是个有温度的事业,百年大计,教育为本。团队聚焦 golang 和云原生,还有大量后台 HC 希望大家推荐或自荐,联系和简历投递:[email protected]

640?wx_fmt=gif


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK