腾讯技术工程 · 2020年05月09日

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

来源:腾讯技术工程微信号
作者:andyawang,腾讯 CSIG 后台开发工程师

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

腾讯课堂停课不停学

项目背景

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

极速版的诞生

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

  1. 入驻:学校各类资质和机构完全不一样,审核周期长
  2. 发课:机构发课有很多规范约束,而学校用课程表排课,一个个发课成本高
  3. 直播:学校老师转线上上课,普遍说直播工具有上手成本

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

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

640.png

极速版发布

疫情期间开发缩影

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

640-1.png
疫情期间开发

架构挑战

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

后台架构

阶段 1:先抗住后优化

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

模块梳理和接口裁剪

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

640-3.png
学习路径

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

系统容量评估

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

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

扩容扩容扩容!!

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

  1. 容器化和上 k8s 不彻底
  2. 部分 c++模块依赖 ShmAgent,扩容流程极其繁琐
  3. 扩容 svr 导致 DB 链接数爆炸 ...

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

全链路压测

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

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

  1. 校准压测模型:非常重要,压测用例设计会直接关系到压测效果
  2. 确定压测目标:把每个模块/接口的压测 qps 确定下来
  3. 执行压测任务:凌晨 12 点启动整站压测流水线,执行星海用例,输出压测结论
  4. 回归压测结果:压测不达标接口记录 doc,尽快暴露隐患,责任人分析原因给解决方案

压测 QCI 流水线:

640-4.png

压测QCI流水线

每日不达标记录:

640-5.png

每日不达标记录每日不达标记录

全链路压测方案:

640-6.png

全链路压测方案

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

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

核心 DB,一挂全挂

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

  1. 扩展性差:主机没法扩展,从机不建议超 5 组,且有主备延迟风险
  2. 耦合度高:任一 svr 链接数或 sql 没控制好,就算是边缘 Svr 都可能搞垮 DB 一挂全挂
  3. 梳理复杂:涉及 svr 数 100+,时间太赶来不及逐个梳理

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

640-7.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-8.png
接入层

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

  1. 校准压测用例,更模拟现网流量
  2. nginx 动静分离,上报等接口也独立出去
  3. nginx 规则精简,接入层尽量打薄,逻辑后移
  4. 统一机器 net.core.somaxconn 等参数配置,重点监控告警
  5. 压测完要清理战场,关注 fd 等指标是否恢复
  6. 升级 tomcat 规避挂死 bug
  7. 升级 tomcat 遵循 RFC3986 规范,规避特殊字符影响

直播:站在云的肩膀上

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

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

640-9.png

直播架构

消息:走推走拉?

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

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

640-10.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-11.png

过载保护策略

限流和熔断

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

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

640-12.png
直播限流

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

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

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

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

Apollo 配置中心

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

  1. 后端限流阈值大小,后端要过载时可调小
  2. Cache 缓存时间,数据层负载高时可调大
  3. 非核心路径后端调用开关,必须时关闭调用补上降级默认值
  4. 前端定时调用的间隔时间,后端要过载时可调大 ......

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

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

640-13.png

apollo架构图

阶段 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-14.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-15.png

tcp监听队列

挂个招聘

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



推荐阅读:


更多腾讯AI相关技术干货,请关注专栏腾讯技术工程
推荐阅读
关注数
8150
内容数
230
腾讯AI,物联网等相关技术干货,欢迎关注
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息