| 导语 疫情爆发,腾讯发起“停课不停学”专项,腾讯课堂一下子被推到风口浪尖上,2天上线极速版,2周内支持同时在线人数超百倍增长,对整个后台挑战非常大。整整2个月下来,同合作团队一起,白天7点开始盯监控和开发版本,凌晨12点例行压测和发布扩容,踩过很多坑也取得很多收获,这里拎几个关键点记录下
腾讯课堂停课不停学
项目背景
大年初一,吃着火锅唱着歌,突然收到重庆十一中的求助信:受疫情影响,年后学校无法开学,高三老师学生都很担心影响到高考,问腾讯课堂能否提供线上平台给高三复课,拉开了整个停课不停学专项的序幕
极速版的诞生
由于课堂是面向线上培训机构的,这次想把十一中这样的传统线下校园,搬到腾讯课堂内上课才发现困难重重:
- 入驻:学校各类资质和机构完全不一样,审核周期长
- 发课:机构发课有很多规范约束,而学校用课程表排课,一个个发课成本高
- 直播:学校老师转线上上课,普遍说直播工具有上手成本
耗了很多人力才把十一中的入驻发课和老师培训搞完,同时其他学校也陆续找过来了,才发现根本没人力能对接这么多学校。就在这时,小马哥发话了:“把入驻发课全砍掉,快速做个腾讯课堂极速版,老师下载完就能自助上课了”
初3收到军令状,公司2个通宵生死急速开发完,初6凌晨团队体验,解决完体验问题后白天急速上线外发。随着各省市教育厅陆续宣布使用急速版复课,课堂pcu/dau开始起飞,短短2-3周,各项指标百倍增长,AppStore免费类排行榜进Top10,教育类稳居Top1
疫情期间开发缩影
白天7点开始盯监控和开发版本,凌晨12点例行压测和发布扩容。才发现企业微信一周小结是按凌晨5点为界:
架构挑战
下面是课堂后台架构图,按之前的架构设计和模块部署,突然要在2周内支持量100倍增涨,同时还要开发校园版需求,时间赶且任务重。这里分五个阶段把架构挑战和解决策略介绍下
阶段1:先抗住后优化
面对2周100倍量级增长,重构肯定来不及了,且过大改动仓促上线反而会增加不稳定因素。所以初期思路就是“先抗住后优化”:梳理极速版用户路径,评估路径上各模块容量,快速扩容后,每天凌晨例行全链路压测,持续验证迭代
模块梳理和接口裁剪
和产品讨论完用户路径和访问量级后,各页面qps也基本有个数了,就开始梳理每个页面调用接口列表,明确每个接口要支撑的qps:
由于课堂微服务很多,为了争取时间,需聚焦核心路径减少梳理复杂度,对于非核心体验和风险较大的这2类接口,抛出来和产品讨论做页面接口裁剪
系统容量评估
模块和接口梳理清楚后,就开始分负责人对系统做容量评估
要特别关注木桶效应,很多新同学只评估了逻辑Svr这一层,其实从用户端->接入层->逻辑层->存储层全链路都得涉及,不能让任一环节成短板
扩容扩容扩容!!
各模块扩容数算清楚后,剩下的就是申请机器部署了。扩容本该是个很简单的活,但因为历史债务等问题,部分模块费了很多周折,比如:
- 容器化和上k8s不彻底
- 部分c 模块依赖ShmAgent,扩容流程极其繁琐
- 扩容svr导致DB链接数爆炸 ...
针对扩容暴露的问题,从接入->逻辑->存储,沉淀了很多设计经验,后面得彻底改造掉,特别是k8s这块应尽快全面铺开。这里终极目标是:针对量级暴涨的情况,不用花人力做评估和扩容,整个系统就有自动伸缩的能力
全链路压测
在初步扩容完后,为了防止梳理不全、评估不准等情况,例行全链路压测验证非常重要,可以挖掘性能瓶颈和系统隐患。
在测试同学给力支持下,每天例行执行下面流程,持续扩容优化迭代:
- 校准压测模型:非常重要,压测用例设计会直接关系到压测效果
- 确定压测目标:把每个模块/接口的压测qps确定下来
- 执行压测任务:凌晨12点启动整站压测流水线,执行星海用例,输出压测结论
- 回归压测结果:压测不达标接口记录doc,尽快暴露隐患,责任人分析原因给解决方案
压测QCI流水线:
每日不达标记录:
全链路压测方案:
阶段2:瓶颈最先出在DB数据层
根据先抗住后优化的思路,可扩容的都容易解决,架构瓶颈会最先出现在伸缩性差、不容易扩容的环节上,经常会是数据层,课堂这次也中招了
核心DB,一挂全挂
由于之前量较小,课堂大部分模块使用同个DB实例(ip port),上量前这个核心DB的cpu在20%、qps在1k左右,评估下来风险很大:
- 扩展性差:主机没法扩展,从机不建议超5组,且有主备延迟风险
- 耦合度高:任一svr链接数或sql没控制好,就算是边缘Svr都可能搞垮DB一挂全挂
- 梳理复杂:涉及svr数100 ,时间太赶来不及逐个梳理
也是历史设计的坑,后面数据层设计要多考虑吞吐量和可扩展性。但回不去了,硬骨头得啃下来,立了专项找DBA同学一起分析优化,主要有下面几块:
业务横向拆分
根据压测发现非常明显的28原则,比如top1的写sql占总量82%,是搜索推荐模块定时刷权重用的,这类模块相对独立,和其他模块表关联join等操作少,方便业务拆分。对于这类模块,像搜索推荐、数据分析、评论系统等,快速切独立DB解耦,规避互相影响
方便业务拆分的切走后,剩下能快速上的就是读写分离扩ro组了。快速扩了4个ro组,把较独立模块sql切过去,规避互相影响,也分摊了主机压力。因为复制模式是半同步的,也需关注主备同步延时,做好监控,特别是一些对延迟敏感的模块
慢查询优化
横向能拆的都快速搞了,主DB风险还是很高,除了升级DB机器配置,剩下就只能逐个做慢sql优化了。采用mysqldumpslow对慢查询日志做归并排序,就可很清楚平均耗时/扫描行数/返回记录数top的慢sql,基本优化也是围绕着索引来,比如:
- 查询没有走索引
- 访问的数据量太大走全表
- OR的情况无法使用索引
- 在索引上面直接进行函数计算
- 没有使用联合索引
- 唯一索引没有命中数据
优化效果:主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个问题,确实不应该:
针对这2个问题也做了专项总结:
- 校准压测用例,更模拟现网流量
- nginx动静分离,上报等接口也独立出去
- nginx规则精简,接入层尽量打薄,逻辑后移
- 统一机器net.core.somaxconn等参数配置,重点监控告警
- 压测完要清理战场,关注fd等指标是否恢复
- 升级tomcat规避挂死bug
- 升级tomcat遵循RFC3986规范,规避特殊字符影响 ......
直播:站在云的肩膀上
课堂最核心的模块就是音视频,直播的进房成功率/首帧延迟/卡顿率/音画同步时延/分辨率等指标直接影响用户核心体验。由于直播模块之前已全部切云,这次站在云的肩膀上,业务不仅直接使用了云的多种直播模式,云音视频团队在整个疫情期间也提供非常给力的质量保障。
下面是具体的直播架构,业务通过流控Svr来控制各端走哪种直播模式:
消息:走推走拉?
随着极速版普及,各学校对单房间同时在线人数要求也越来越高,从3w->6w->30w->150w。
对于大房间消息系统而言,核心要解决消息广播风暴的问题(30w人房间,有100人在同1秒发言,会产生30w*100=3kw/s消息),随着扩散因子的变大,之前纯推的方案已不能满足需求,参考了业内直播间的一些IM方案:
结合对比方案,也针对课堂产品特性,在原来纯推架构上新迭代一版推拉结合的架构
对于IM系统的设计,推荐一下这个文章:《新手入门一篇就够:从零开发移动端IM》
http://www.52im.net/thread-464-1-1.html
阶段4:做好核心路径的防过载和柔性降级
在非常有限的时间里,逻辑层根本来不及重构,容量评估也不一定精准,过载保护和柔性降级就显得尤其重要
因为没时间全盘优化,在量突然暴涨的情况下,某个模块过载或者爆bug的概率会变大,所以在用户登录->查课表->上课的核心路径上,必须增加足够容错能力来提高可用性
雪崩来得猝不及防
疫情初期课堂就遇到一个雪崩的case:直播间拉取成员列表接口有失败毛刺,因为web没做异常保护,失败直接把循环拉取间隔时间置0,导致接口调用量越滚越大,B侧拉取涨了10倍后雪崩超时。由于没预埋开关等控制策略,得回滚web版本才解决。
这个case暴露了过载保护的缺失,一方面web没对异常返回做合理处理保护后端,一方面svr没有识别雪崩请求做限频,除此之外,也缺少一些配置开关可快速控制web循环间隔时间,导致雪崩来得猝不及防
过载保护策略
针对这类badcase,对核心路径的服务做了很多过载保护和柔性降级策略,这里把一些典型方案记录下:
限流和熔断
高并发场景为了规避过载的级联传递,防止全链路崩溃,制定合理的限流和熔断策略是2个常见的解决方案。
以这次疫情互动直播限流场景为例,互动直播默认只部署最多支撑600w同时在线的接口机资源,如果哪天突发超过了600w学生:
限流算法选择上,最常见就是漏桶和令牌桶。不是说令牌桶就是最好的,只有最合适的,有时简单的计数器反而更简单。golang拓展库 golang.org/x/time/rate 就提供了令牌桶限流器,3个核心api:
代码语言:txt复制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配置中心来做,实现配置热更新,在高可用上实践也不错。很多时候多预埋这些配置就是用来保命的,当监控发现趋势不对时,可快速调整规避事故发生,简单列些例子:
- 后端限流阈值大小,后端要过载时可调小
- Cache缓存时间,数据层负载高时可调大
- 非核心路径后端调用开关,必须时关闭调用补上降级默认值
- 前端定时调用的间隔时间,后端要过载时可调大 ......
当然,如果可做到系统自动触发调整配置就更进一步了,当时有想过但时间太赶没实践,有兴趣同学可思考实践下
Apollo是携程开源的分布式配置中心,能够集中化管理应用不同环境配置,实现配置热更新,具备规范的权限、流程治理等特性,适用于微服务配置管理场景。补个架构图推荐下:
阶段5:服务性能优化实战
在抗住前2周最猛的流量增长后,下来很长一段时间都是在优化服务的性能和稳定性、处理用户反馈和打磨产品体验上。这里沉淀3个服务性能优化上印象较深刻的点
分析利器 pprof torch
在性能分析上,对比c ,golang提供了更多好用的工具,基本每次性能分析都是先用pprof torch跑一把。通过框架中嵌入net/http/pprof并监听http遥测端口,管理后台就可随时得到svr协程/cpu/内存等相关指标,比如优化前的成员列表svr火焰图case
结合代码,便可快速有些优化思路,比如:
- 分离B/C调用部署
- 优化pb序列化,如做些cache
- 简化定时器使用场景
- 调整大对象使用优化gc消耗
最终根据这些优化思路改版,让超200ms的比例从0.2%降到0.002%以下
缓存设计和踩坑
回过头看,大部分服务性能瓶颈还是在数据层或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供参考:
- 锁竞争:如死锁、锁粒度太大等,关注锁时间上报
- 打日志:日志量过大等导致磁盘IO彪高,在高并发场景尤其要注意精简日志量
- 进程重启:如panic或oom导致进程被kill,重启过程请求超时,要补齐进程重启监控
- 队列丢包:如请求缓存队列设置过小等,要关注队列溢出监控 ......
比如最后这点,就遇过这样的case:一次凌晨压测,其他机器都正常,就2个新机器死活一直超时,业务指标也看不出区别,折腾了好一阵,才发现monitor上 监听队列溢出(ListenOverflows) 这个值有毛刺异常。
继续深挖下去,证明请求在tcp队列就溢出了,tcp的accept队列长度=min(backlog,SOMAXCONN),查看新机器内核配置 net.core.somaxconn=128,确实比其他机器小,神坑
所以后续也增加了服务器tcp的半连接和全连接队列相关监控:
挂个招聘
在这次疫情的推动下,在线教育越来越普及,各大互联网公司都持续加码,教育也是个有温度的事业,百年大计,教育为本。团队聚焦golang和云原生,还有大量后台HC希望大家推荐或自荐,欢迎随时勾搭。