以B站20210713为案例
2021年7月13日22:52,SRE收到大量服务和域名的接入层不可用报警,客服侧开始收到大量用户反馈B软件质量保障站无法使用,同时内部同学也反馈B站无法打开,甚至APP首页也无法打开。
1.故障描述
1.1 故障类型
故障类型可以分为 1、代码错误 2、设计缺陷 3、性能问题 4、配置相关 5、安全相关 等
1.2 故障等级
一般决定故障等级的因素就是对公司核心业务的影响程度,例如本司是做支付的,那么核心业务就是资金的流动,等级可以根据资金损失的多少来制定(当然还会结合其他因素综合评定例如客诉等)。B站的缺陷等级应该和我司的有所不同,我也在网上Google到其故障等级,如下图:
根据漏洞的危害程度将漏洞等级分为【严重】、【高危】、【中危】、【低危】、【无效】五个等级。每个漏洞基础经验值最高为10,由 BILISRC 结合利用场景中漏洞的严重程度、利用难度等综合因素给予相应分值的经验值、安全币和漏洞定级,
边缘业务的安全漏洞根据具体情况进行降级或忽略。边缘业务范围依据公司发展情况不定期调整,主要包括但不限于:
测试环境的业务
哔哩哔哩相关公众号
与哔哩哔哩合作的产品和业务,如唑哩哩漫画、音频、猫耳、客服系统、我的世界中文论坛、轻视频等
【高危】经验值 6~8/安全币 200~400
敏感信息泄漏。包括但不仅限于非核心DB SQL注入、源代码压缩包泄漏、服务器应用加密可逆或明文、移动API访问摘要硬编码等问题引起的敏感信息泄露。
敏感信息越权访问。包括但不仅限于绕过认证直接访问管理后台、后台弱密码、获取大量内网敏感信息的SSRF
直接导致业务拒绝服务的漏洞。包括但不仅限于直接导致移动网关业务API业务拒绝服务、网站应用拒绝服务等造成较高影响的远程拒绝服务漏洞。
越权敏感操作。包括但不仅限于账号越权修改重要信息
高危漏洞 毫无疑问应该是P0故障了。
1.3 影响面
这个就是说这个故障影响了多少在线用户使用体验,导致了多少资金上面的损失。
这个一般公司会有各种业务指标的大盘,会根据故障发生的这段时间统计出来影响到的流量。(本司的话就是交易量、用户量、资金金额等)
1.4 应急时间
23:07 远程在家的同学紧急联系负责VPN和内网鉴权系统的同学后,了解可通过绿色通道登录到内网系统。
01:40 主站、电商、漫画、支付等核心业务陆续切换到SLB新集群,业务恢复。
01:50 此时在线业务基本全部恢复。
可以理解为故障止损时间,即从开始处理故障到故障处理完成的时间。
1.5 问题如何发现的
首次发现是通过业务告警(系统具备业务告警能力是非常重要的),其次是客诉。
2.背景介绍
背景
B站在19年9月份从Tengine迁移到了OpenResty,基于其丰富的Lua能力开发了一个服务发现模块,从我们自研的注册中心同步服务注册信息到Nginx共享内存中,SLB在请求转发时,通过Lua从共享内存中选择节点处理请求,用到了OpenResty的lua-resty-balancer模块。到发生故障时已稳定运行快两年时间。
在故障发生的前两个月,有业务提出想通过服务在注册中心的权重变更来实现SLB的动态调权,从而实现更精细的灰度能力。SLB团队评估了此需求后认为可以支持,开发完成后灰度上线。
诱因
在某种发布模式中,应用的实例权重会短暂的调整为0,此时注册中心返回给SLB的权重是字符串类型的”0″。此发布模式只有生产环境会用到,同时使用的频率极低,在SLB前期灰度过程中未触发此问题。
SLB 在balance_by_lua阶段,会将共享内存中保存的服务IP、Port、Weight 作为参数传给lua-resty-balancer模块用于选择upstream server,在节点 weight = “0” 时,balancer 模块中的 _gcd 函数收到的入参 b 可能为 “0”。
这块建议把缺陷引入的迭代与上个迭代进行业务对比来说明。例如这个缺陷引入前的功能是怎样的,然后缺陷引入的迭代是业务是怎样的。通过对比可以分析但业务影响点。
3.根因分析
Lua 是动态类型语言,常用习惯里变量不需要定义类型,只需要为变量赋值即可。
Lua在对一个数字字符串进行算术操作时,会尝试将这个数字字符串转成一个数字。
在 Lua 语言中,如果执行数学运算 n % 0,则结果会变为 nan(Not A Number)。
_gcd函数对入参没有做类型校验,允许参数b传入:”0″。同时因为”0″ != 0,所以此函数第一次执行后返回是 _gcd(“0”,nan)。如果传入的是int 0,则会触发[ if b == 0 ]分支逻辑判断,不会死循环。
_gcd(“0”,nan)函数再次执行时返回值是 _gcd(nan,nan),然后Nginx worker开始陷入死循环,进程 CPU 100%。
对于互联网产品来说,问题根源基本上是代码逻辑的问题。这块的分析重点肯定就是代码逻辑,可以重点分析问题代码引入前后两个线上版本的差异。
4.解决方案
应急策略一般是临时打补丁(治标不治本),而解决方案则是治本的。
01:00 SLB新集群搭建完成后,在给业务切量止损的同时,SLB运维开始继续分析CPU 100%的原因。
01:10 – 01:27 使用Lua 程序分析工具跑出一份详细的火焰图数据并加以分析,发现 CPU 热点明显集中在对 lua-resty-balancer 模块的调用中,从 SLB 流量入口逻辑一直分析到底层模块调用,发现该模块内有多个函数可能存在热点。
01:28 – 01:38 选择一台SLB节点,在可能存在热点的函数内添加 debug 日志,并重启观察这些热点函数的执行结果。
01:39 – 01:58 在分析 debug 日志后,发现 lua-resty-balancer模块中的 _gcd 函数在某次执行后返回了一个预期外的值:nan,同时发现了触发诱因的条件:某个容器IP的weight=0。
01:59 – 02:06 怀疑是该 _gcd 函数触发了 jit 编译器的某个 bug,运行出错陷入死循环导致SLB CPU 100%,临时解决方案:全局关闭 jit 编译。
02:07 SLB运维修改SLB 集群的配置,关闭 jit 编译并分批重启进程,SLB CPU 全部恢复正常,可正常处理请求。同时保留了一份异常现场下的进程core文件,留作后续分析使用。
02:31 – 03:50 SLB运维修改其他SLB集群的配置,临时关闭 jit 编译,规避风险。
5.反思
反思是故障分析的核心,可以从 事前事后 出发。以B站这个问题为例,它主要分析故障发生后的一些反思。但我认为二者同等重要。
事前
当时测试这块功能的用例是怎样的?用例覆盖的业务全面吗?
为什么当时测试没发现呢?(线上为何没发现。。)
事后
- 为何故障刚发生时无法登陆内网后台?
事后复盘发现,用户在登录内网鉴权系统时,鉴权系统会跳转到多个域名下种登录的Cookie,其中一个域名是由故障的SLB代理的,受SLB故障影响当时此域名无法处理请求,导致用户登录失败。流程如下:
图片
事后我们梳理了办公网系统的访问链路,跟用户链路隔离开,办公网链路不再依赖用户访问链路。
- 为何多活SLB在故障开始阶段也不可用?
多活SLB在故障时因CDN流量回源重试和用户重试,流量突增4倍以上,连接数突增100倍到1000W级别,导致这组SLB过载。后因流量下降和重启,逐渐恢复。此SLB集群日常晚高峰CPU使用率30%左右,剩余Buffer不足两倍。如果多活SLB容量充足,理论上可承载住突发流量, 多活业务可立即恢复正常。此处也可以看到,在发生机房级别故障时,多活是业务容灾止损最快的方案,这也是故障后我们重点投入治理的一个方向。
图片
- 为何在回滚SLB变更无效后才选择新建源站切量,而不是并行?
我们的SLB团队规模较小,当时只有一位平台开发和一位组件运维。在出现故障时,虽有其他同学协助,但SLB组件的核心变更需要组件运维同学执行或review,所以无法并行。
- 为何新建源站切流耗时这么久?
我们的公网架构如下:
图片
此处涉及三个团队:
SLB团队:选择SLB机器、SLB机器初始化、SLB配置初始化
四层LB团队:SLB四层LB公网IP配置
CDN团队:CDN更新回源公网IP、CDN切量
SLB的预案中只演练过SLB机器初始化、配置初始化,但和四层LB公网IP配置、CDN之间的协作并没有做过全链路演练,元信息在平台之间也没有联动,比如四层LB的Real Server信息提供、公网运营商线路、CDN回源IP的更新等。所以一次完整的新建源站耗时非常久。在事故后这一块的联动和自动化也是我们的重点优化方向,目前一次新集群创建、初始化、四层LB公网IP配置已经能优化到5分钟以内。
- 后续根因定位后证明关闭jit编译并没有解决问题,那当晚故障的SLB是如何恢复的?
当晚已定位到诱因是某个容器IP的weight=”0″。此应用在1:45时发布完成,weight=”0″的诱因已消除。所以后续关闭jit虽然无效,但因为诱因消失,所以重启SLB后恢复正常。
如果当时诱因未消失,SLB关闭jit编译后未恢复,基于定位到的诱因信息:某个容器IP的weight=0,也能定位到此服务和其发布模式,快速定位根因。
6.改进措施
首先明白一点,线上问题的锅不能只让测试来背(不容置疑,很多小公司的作风就是这样的,测试就是背锅侠),改进措施也需要从开发、测试的角度出发。
- 多活建设
在23:23时,做了多活的业务核心功能基本恢复正常,如APP推荐、APP播放、评论&弹幕拉取、动态、追番、影视等。故障时直播业务也做了多活,但当晚没及时恢复的原因是:直播移动端首页接口虽然实现了多活,但没配置多机房调度。导致在主机房SLB不可用时直播APP首页一直打不开,非常可惜。通过这次事故,我们发现了多活架构存在的一些严重问题:
多活基架能力不足
机房与业务多活定位关系混乱。
CDN多机房流量调度不支持用户属性固定路由和分片。
业务多活架构不支持写,写功能当时未恢复。
部分存储组件多活同步和切换能力不足,无法实现多活。
业务多活元信息缺乏平台管理
哪个业务做了多活?
业务是什么类型的多活,同城双活还是异地单元化?
业务哪些URL规则支持多活,目前多活流量调度策略是什么?
上述信息当时只能用文档临时维护,没有平台统一管理和编排。
多活切量容灾能力薄弱
多活切量依赖CDN同学执行,其他人员无权限,效率低。
无切量管理平台,整个切量过程不可视。
接入层、存储层切量分离,切量不可编排。
无业务多活元信息,切量准确率和容灾效果差。
我们之前的多活切量经常是这么一个场景:业务A故障了,要切量到多活机房。SRE跟研发沟通后确认要切域名A+URL A,告知CDN运维。CDN运维切量后研发发现还有个URL没切,再重复一遍上面的流程,所以导致效率极低,容灾效果也很差。
所以我们多活建设的主要方向:
多活基架能力建设
优化多活基础组件的支持能力,如数据层同步组件优化、接入层支持基于用户分片,让业务的多活接入成本更低。
重新梳理各机房在多活架构下的定位,梳理Czone、Gzone、Rzone业务域。
推动不支持多活的核心业务和已实现多活但架构不规范的业务改造优化。
多活管控能力提升
统一管控所有多活业务的元信息、路由规则,联动其他平台,成为多活的元数据中心。
支持多活接入层规则编排、数据层编排、预案编排、流量编排等,接入流程实现自动化和可视化。
抽象多活切量能力,对接CDN、存储等组件,实现一键全链路切量,提升效率和准确率。
支持多活切量时的前置能力预检,切量中风险巡检和核心指标的可观测。
- SLB治理
架构治理
故障前一个机房内一套SLB统一对外提供代理服务,导致故障域无法隔离。后续SLB需按业务部门拆分集群,核心业务部门独立SLB集群和公网IP。
跟CDN团队、四层LB&网络团队一起讨论确定SLB集群和公网IP隔离的管理方案。
明确SLB能力边界,非SLB必备能力,统一下沉到API Gateway,SLB组件和平台均不再支持,如动态权重的灰度能力。
运维能力
SLB管理平台实现Lua代码版本化管理,平台支持版本升级和快速回滚。
SLB节点的环境和配置初始化托管到平台,联动四层LB的API,在SLB平台上实现四层LB申请、公网IP申请、节点上线等操作,做到全流程初始化5分钟以内。
SLB作为核心服务中的核心,在目前没有弹性扩容的能力下,30%的使用率较高,需要扩容把CPU降低到15%左右。
优化CDN回源超时时间,降低SLB在极端故障场景下连接数。同时对连接数做极限性能压测。
自研能力
运维团队做项目有个弊端,开发完成自测没问题后就开始灰度上线,没有专业的测试团队介入。此组件太过核心,需要引入基础组件测试团队,对SLB输入参数做完整的异常测试。
跟社区一起,Review使用到的OpenResty核心开源库源代码,消除其他风险。基于Lua已有特性和缺陷,提升我们Lua代码的鲁棒性,比如变量类型判断、强制转换等。
招专业做LB的人。我们选择基于Lua开发是因为Lua简单易上手,社区有类似成功案例。团队并没有资深做Nginx组件开发的同学,也没有做C/C++开发的同学。
- 故障演练
本次事故中,业务多活流量调度、新建源站速度、CDN切量速度&回源超时机制均不符合预期。所以后续要探索机房级别的故障演练方案:
模拟CDN回源单机房故障,跟业务研发和测试一起,通过双端上的业务真实表现来验收多活业务的容灾效果,提前优化业务多活不符合预期的隐患。
灰度特定用户流量到演练的CDN节点,在CDN节点模拟源站故障,观察CDN和源站的容灾效果。
模拟单机房故障,通过多活管控平台,演练业务的多活切量止损预案。
- 应急响应
B站一直没有NOC/技术支持团队,在出现紧急事故时,故障响应、故障通报、故障协同都是由负责故障处理的SRE同学来承担。如果是普通事故还好,如果是重大事故,信息同步根本来不及。所以事故的应急响应机制必须优化:
优化故障响应制度,明确故障中故障指挥官、故障处理人的职责,分担故障处理人的压力。
事故发生时,故障处理人第一时间找backup作为故障指挥官,负责故障通报和故障协同。在团队里强制执行,让大家养成习惯。
建设易用的故障通告平台,负责故障摘要信息录入和故障中进展同步。
本次故障的诱因是某个服务使用了一种特殊的发布模式触发。我们的事件分析平台目前只提供了面向应用的事件查询能力,缺少面向用户、面向平台、面向组件的事件分析能力:
跟监控团队协作,建设平台控制面事件上报能力,推动更多核心平台接入。
SLB建设面向底层引擎的数据面事件变更上报和查询能力,比如服务注册信息变更时某个应用的IP更新、weight变化事件可在平台查询。
扩展事件查询分析能力,除面向应用外,建设面向不同用户、不同团队、不同平台的事件查询分析能力,协助快速定位故障诱因。