一年之后,B 站终于把故障复盘写了出来。
我简单看了一下,和我当初猜测的原因部分吻合,猜对了由于某接口负载过大导致雪崩效应,但没有猜对导致负载过大的原因。
很显然,这篇文章是从纯技术角度解读本次故障的。
B 站有 UP 主做了这方面的视频,但受篇幅所限,讲的还不够细致,我尽力做了补充,但最终还是决定单独写一篇文章讲讲这件事。
如果大家对技术有一定了解,比如你听说过微服务、负载均衡、集群、多活,并且大概知道它们的原理和作用,那么建议你去读一读 原文。不看也没关系,后续的内容我会尽可能用通俗易懂的方式描述。
阅读这篇文章不需要技术基础,但需要一定的耐心,故障原因有些复杂,所以文章很长。
那我们开始吧。
至暗时刻
原文的内容理解起来比较困难,有很多专业词汇,我们一句一句看。
2021 年 7 月 13 日 22:52,SRE 收到大量服务和域名的接入层不可用报警,客服侧开始收到大量用户反馈 B 站无法使用,同时内部同学也反馈 B 站无法打开,甚至 App 首页也无法打开。
SRE,全称 Site Reliability Engineer,网站可靠性工程师,很高大上的名字,其实就是负责保证网站运转正常的。
大型网站一般会有内部的系统状态看板,显示一些关键的指标,比如访问成功率、延时、每秒访问量等。
这些指标决定了最基础的用户体验,网站能不能打开,操作会不会很慢,会不会频繁报错。
团队会为这些指标设置报警值,当故障发生时,自动通知相关负责人。
服务,这里指的是“用户访问 B 站,看视频、看弹幕、点赞投币评论”这些操作。
域名,其实就是网址,https://www.bilibili.com/。
既然 B 站崩了,站内的客服系统肯定也无法使用,客服应该是从其它渠道收到的反馈。
这里为什么要强调 App 首页无法打开呢,因为现在的网页都是动态的,比如你在 B 站首页看到的推荐会不断变化。
其实,网页的框架部分是不变的,浏览器将框架部分下载下来,然后运行一些代码,从服务器上(并不一定是下载框架的这台服务器)获取动态的内容,比如用户登录信息,推荐信息,然后将它们填充到对应位置展示出来。
而 App 中的框架部分是内置在安装包里的,如果只是下载框架的服务器(专业名词叫”网页服务器“)出了问题,App 应该不受影响,既然 App 也无法打开,说明提供动态内容的服务器也无法使用了。
当然,不排除有些 App 内置一个浏览器,然后直接打开网页的情况,这时网页服务器出问题也会导致 App 不可用,这种技术还挺流行,因为它降低了开发成本。
基于报警内容,SRE 第一时间怀疑机房、网络、四层 LB、七层 SLB 等基础设施出现问题,紧急发起语音会议,拉各团队相关人员开始紧急处理(为了方便理解,下述事故处理过程做了部分简化)。
机房,字面意思是存放服务器的房间,现在很多网站都使用了云计算、有专门的云服务提供商(阿里云、腾讯云等)买好硬件,设置好网络,装好系统,把账号密码给你,你直接登录上去,安装好需要的软件使用即可。
这种情况下,你不需要关心基础架构部分,它叫做 PaaS,Platform as a service,平台即服务。
同样的,还有 SaaS 软件即服务,比如 B 站大会员,只需要购买之后使用即可,不需要关心它是如何运作的。
网络,包括服务器之间的网络,数据中心之间的网络,数据中心到用户的网络,我们后面会解释多个数据中心的作用。
四层 LB,Load Balancer,负载均衡,就是把用户的请求分给多台服务器处理,避免单台服务器负载过大。
这里的四层指的是传输层,传输层有一个叫做四元组的概念,指的是 (源 IP, 目的 IP, 端口号, 协议) 四个基本要素。
IP 可以理解为每台电脑在互联网上的地址,是独一无二的,端口号每台电脑有 65536 个,一般我们使用 80 和 443 这两个端口访问网站,协议则是互联网传输协议,对于网页访问,一般是 TCP,这是一种稳定的传输协议。
负载均衡也是运行在服务器上的程序,根据一定规则将用户的请求转发给多台服务器,这个规则可以自定义,比如延迟最低、成功率最高、负载最低等。
云服务商也有现成的负载均衡服务,可以直接购买,将请求转发给购买的其它服务器,这算是 SaaS 了。
七层 SLB,Server Load Balancer,服务器负载均衡,和上面讲的四层 LB 基本相同,运行在应用层,也就是网络模型中的最高层,不局限于通过四元组进行均衡,还可以通过设备类型、具体网址等进行转发。
提到基础设施,其实还有 IaaS,Infrastructure as a Service,基础设施即服务,买服务器,连网线,但不帮你装系统。原文中的基础设施其实包括 PaaS 和 SaaS,表述有些不准确。
初因定位
22:55 远程在家的相关同学登陆 VPN 后,无法登陆内网鉴权系统(B 站内部系统有统一鉴权,需要先获取登录态后才可登陆其他内部系统),导致无法打开内部系统,无法及时查看监控、日志来定位问题。
现在远程工作是趋势,理解。
VPN,Virtual Private Network,虚拟专用网络,这是一种技术,能在两个不同的网络间架起一座桥梁,将它们连接起来,上大学的朋友们可能有了解,在校外访问学校的文献库,就需要使用 VPN。
至于访问一些国外网站,那只是 VPN 的用途之一,而且在这个用途中,VPN 已经被更先进的连接方式取代了,这只是一个口口相传的名字,内部实现并非如此。
公司有内网,需要通过 VPN 访问,很合理,内部系统需要登录(鉴权),登录态其实就是一个凭证,同时存储在浏览器和服务器上,互相比对来确定你的身份,和大家登录 B 站的原理是相同的。
凭证有过期时间,因此你重启电脑后打开 B 站不需要重新登录,但过几个月再访问就可能需要重新登录了。
无法打开内部系统,后续发现是因为获取凭证的过程中访问了一个普通用户使用的服务,但这个服务已经挂掉了,造成登录流程卡住。
这里的监控指的是服务器的各种指标,和前面提到的状态看板差不多,只不过每个部门有自己负责范围内的看板。
日志指的是服务器的运行状态记录,大平台会有统一的日志中心,可以进行分类查询。
22:57 在公司 Oncall 的 SRE 同学(无需 VPN 和再次登录内网鉴权系统)发现在线业务主机房七层 SLB(基于 OpenResty 构建) CPU 100%,无法处理用户请求,其他基础设施反馈未出问题,此时已确认是接入层七层 SLB 故障,排除 SLB 以下的业务层问题。
Oncall 指随时待命,也就是值班,核心部门都会有值班人员处理紧急情况。
在公司的员工已经登录过内网系统,获取到了凭证,所以不会遇到登录流程卡住造成的问题。
在线业务,可以理解为给大家用的,它还有一个名字叫生产环境,让用户充钱,生产出商业价值的环境。
同样的还有测试环境、预发布环境、开发环境,这些都是普通用户感知不到的,在生产环境的开发过程中会用到。
23:07 远程在家的同学紧急联系负责 VPN 和内网鉴权系统的同学后,了解可通过绿色通道登录到内网系统。
这里的绿色通道应该是直接关闭了相应的权限控制手段,为了处理严重问题暂时牺牲一点点安全性,可以理解。
23:17 相关同学通过绿色通道陆续登录到内网系统,开始协助处理问题,此时处理事故的核心同学(七层 SLB、四层 LB、CDN)全部到位。
CDN,Content Delivery Network,内容分发网络,也是一系列服务器,可以理解为京东在各地的仓库,就近拿货派送,减轻核心仓储中心的压力,加快派送速度,对应在 B 站的服务上就是减轻核心机房压力,降低用户访问延迟。
CDN 一般只用来存储静态内容,类似于仓储中心不会储存报纸这种强时效性、经常更新的货物。
静态内容包括我们前面提到的网站骨架,还有各种图片,以及热门视频,毕竟 UP 主修改视频属于小概率事件,也有对应的解决方法,从 CDN 服务器获取内容前先询问一下核心服务器内容是否有更新,配合 CDN 服务器定期重新下载数据。
用户请求的内容如果恰巧能在 CDN 中找到,不需要向核心服务器获取,叫做 CDN 命中,反之则叫做 CDN 未命中,CDN 服务器没有向核心服务器请求数据的过程叫做 CDN 回源,”回流“到数据的”源头“。
故障止损
23:20 SLB 运维分析发现在故障时流量有突发,怀疑 SLB 因流量过载不可用。因主机房 SLB 承载全部在线业务,先 Reload SLB未恢复后尝试拒绝用户流量冷重启 SLB,冷重启后 CPU 依然 100%,未恢复。
突发流量可能会造成 SLB 服务器过载,继而不能转发用户请求,导致它管理的服务器一并失效,这一排查思路完全正确。
主机房 SLB 是整个服务的核心,负责所有服务器的负载均衡,它下面还有其它负载均衡服务器。
Reload 可以看作重启,众所周知这能解决 90% 的问题,实际上是给这个服务器发送一个信号,让它关闭负载均衡程序重新启动。
拒绝用户流量冷重启,相当于拔掉数据中心通向用户的网线,重启之后再插上,目的是避免未完全重启的负载均衡服务被大量用户请求再次打垮。
23:22 从用户反馈来看,多活机房服务也不可用。SLB 运维分析发现多活机房 SLB 请求大量超时,但 CPU 未过载,准备重启多活机房 SLB 先尝试止损。
多活,多个活跃机房,可以理解为多个通道一起做核酸,如果一个通道遇到了问题,只需把排队的人引导到其它通道即可。
听起来像前面提到的负载均衡,其实多活确实可以通过负载均衡实现,它是一种模式,而不是一种服务。
这里的用户反馈应该是“所有服务均不可用”,绝大多数用户并了解服务架构,无法定位到具体的故障位置,而 B 站的推荐、视频观看、弹幕、基础互动等核心业务都使用了多活,这些服务不可用说明多活也遇到了问题。
这里的多活方案叫做“热备”,还有主从等多活方案,它们都有各自的优缺点,热备是难度较高但非常可靠的一种方案。
请求大量超时,指的是延迟很高,由于延迟太高,服务器直接向用户返回失败信息,避免大量请求堆积耗尽资源。
这里的实际情况是大量的 CDN 服务器由于主机房不可用而回源到多活机房,同时加上用户的不断重试,导致多活机房的 SLB 负载过大,并非此次事故的核心原因。
23:23 此时内部群里同学反馈主站服务已恢复,观察多活机房 SLB 监控,请求超时数量大大降低,业务成功率恢复到 50% 以上。此时做了多活的业务核心功能基本恢复正常,如 App 推荐、App 播放、评论&弹幕拉取、动态、追番、影视等。非多活服务暂未恢复。
这里主站服务恢复是因为 CDN 回源反复超时而不再重试,加上用户重试频率随时间降低,过载的 SLB 恢复正常。
后面提到的这些恢复正常的服务就是 B 站的核心业务。
23:25 - 23:55 未恢复的业务暂无其他立即有效的止损预案,此时尝试恢复主机房的 SLB。
多活机房撑住了,情况暂时稳定下来,这时开始着手完全恢复服务。
我们通过 Perf 发现 SLB CPU 热点集中在 Lua 函数上,怀疑跟最近上线的 Lua 代码有关,开始尝试回滚最近上线的 Lua 代码。
Perf 指的是性能分析,可以获取到程序对资源的使用情况,继而进行故障定位或者优化。
热点 HotPoint,是程序消耗资源最多的位置,一般是故障的根源或者性能瓶颈。
Lua 是一种编程语言,它的特性和 Python 相似,都属于脚本语言。
编程语言中的函数是一段代码,可以像数学中的函数一样接收输入,返回输出,在内部进行一些数据处理。
上线指部署到生产环境供用户使用。
回滚指回到上一个版本,上线后出现问题时可以回滚两个版本间改变的代码,以此确认问题是否是变更部分引起的。
后续排查发现问题并非新版本引起,但这一操作是正确的。
近期SLB配合安全同学上线了自研 Lua 版本的 WAF,怀疑 CPU 热点跟此有关,尝试去掉 WAF 后重启 SLB,SLB 未恢复。
WAF,Web Application Firewall,网络应用防火墙,是一种安全措施,这是一个牺牲安全性尝试定位故障的操作。
SLB 两周前优化了 Nginx 在 balance_by_lua 阶段的重试逻辑,避免请求重试时请求到上一次的不可用节点,此处有一个最多10次的循环逻辑,怀疑此处有性能热点,尝试回滚后重启 SLB,未恢复。
Nginx 是一个 Web 服务器软件,可以用于负载均衡,以高性能和高度自定义著称,而且它的源代码是公开的,基本不需要怀疑 Nginx 本身的代码,但 Nginx 中调用的其它代码可能存在问题。
balance_by_lua 应该是一个函数名,意为“通过 Lua 进行负载均衡”,这里的优化是为了提高重试的成功率,重试上限为 10 次,工程师怀疑这里出现了死循环,或者在重试逻辑中有大量消耗资源的操作。
SLB 一周前上线灰度了对 HTTP2 协议的支持,尝试去掉 H2 协议相关的配置并重启 SLB,未恢复。
HTTP 是访问网站时使用的协议,在 TCP 的上层(TCP 是传输层协议,HTTP 是应用层协议),HTTP/2 是这个协议的第二版,带来了一些性能提升,这个版本已经发布了很长一段时间,现在进入测试阶段是正常的,不算过于激进。
灰度全称灰度测试,是生产环境中使用的一种测试方法,将一定比例的请求使用新版本处理,其余大部分仍使用旧版本,通过观察新版本的成功率和性能,发现隐藏的问题,如果新版本在一段时间内运行正常,就可以“全量上线”,也就是把所有的请求都使用新版本处理。
新建源站SLB
00:00 SLB 运维尝试回滚相关配置依旧无法恢复 SLB 后,决定重建一组全新的 SLB 集群,让 CDN 把故障业务公网流量调度过来,通过流量隔离观察业务能否恢复。
这里的重建其实并不是给负载均衡服务器重新装系统,而是在云服务商的控制台买一组新的负载均衡服务,这也是云计算的优势之一,可以快速进行故障修复。
CDN 负责网站静态资源的缓存,将流量调度到新的负载均衡服务上,与原有的负载均衡服务隔离开来。
00:20 SLB 新集群初始化完成,开始配置四层 LB 和公网 IP。
由于没有对完整的配置过程做过演练,这里的配置速度其实是比较慢的。
01:00 SLB 新集群初始化和测试全部完成,CDN 开始切量。SLB运维继续排查 CPU 100% 的问题,切量由业务 SRE 同学协助。
CDN 逐渐将用户请求从故障服务切换到正常运行的新服务上,算是一种故障恢复中的灰度测试。
其余工作人员现在的主要工作并不是排查故障,而是记录故障服务的状态,在生产环境恢复正常后再分析。
01:18 直播业务流量切换到 SLB 新集群,直播业务恢复正常。
01:40 主站、电商、漫画、支付等核心业务陆续切换到 SLB 新集群,业务恢复。
01:50 此时在线业务基本全部恢复。
这里大家能感知到的服务已经全部恢复。
恢复SLB
01:00 SLB新集群搭建完成后,在给业务切量止损的同时,SLB 运维开始继续分析 CPU 100% 的原因。
01:10 - 01:27 使用 Lua 程序分析工具跑出一份详细的火焰图数据并加以分析,发现 CPU 热点明显集中在对 lua-resty-balancer 模块的调用中,从 SLB 流量入口逻辑一直分析到底层模块调用,发现该模块内有多个函数可能存在热点。
很多编程语言自带或者有第三方的性能分析工具。
火焰图是性能分析中常用的一种图表,可以直观显示程序运行过程中的函数调用情况和时间消耗。
[图片上传失败...(image-424729-1658900045359)]
横轴代表运行时间,纵轴从下到上代表调用栈,也就是程序中调用其它函数的层级。
如果发现图中有不正常的高峰(过深的调用栈)或者山顶部分过于平缓(单函数执行耗时过长),则说明对应位置可能存在性能瓶颈。
01:28 - 01:38 选择一台 SLB 节点,在可能存在热点的函数内添加 debug 日志,并重启观察这些热点函数的执行结果。
debug 即修复 Bug,debug 日志是最详细的日志等级,记录了大量的运行细节,便于定位故障。
热点函数可以理解为频繁使用的函数。
01:39 - 01:58 在分析 debug 日志后,发现 lua-resty-balancer 模块中的 _gcd 函数在某次执行后返回了一个预期外的值:nan,同时发现了触发诱因的条件:某个容器 IP 的 weight=0。
程序自微观到宏观可分为以下几层:
- 语句:一行代码,例如返回一条数据
- 函数:一段代码,执行特定操作,例如查询用户的点赞量并返回
- 模块:一个文件,包含一系列函数,例如用户数据查询模块
- 包:一个文件夹,包含一系列模块,例如用户包,包含数据查询、数据修改等模块
- 服务:一系列包,实现一整块功能,例如互动服务
这里定位到的模块名为 lua-resty-balancer,意为 OpenResty 这个程序中使用 Lua 语言的负载均衡器,函数名为 _gcd,意为计算最大公约数(不同语言中的下划线有不同语义)。
NaN,Not a Number,不是数字,一种特殊变量,一般在计算无效时返回此值。
这里涉及到了一个重要概念:容器。
目前的服务端程序,有一个行业标准级的设计思想,微服务。
顾名思义,微服务就是把一个大的服务拆分成多个小服务,通过内部网络连接起来。
例如 B 站的视频点赞,可能包含以下几个服务:
- 网关,负责接收用户和其它服务的请求,并进行负载均衡
- 计数服务,由于点赞是高频互动操作,并且需要保证数字精确,所以需要一个单独的服务负责计数
- 数据库,用于存储数据,为了提高性能,计数服务的数据一般存储在内存中,需要数据库将其存储到硬盘
而容器就是某个微服务的载体,是一个轻量级的虚拟机,与物理服务器的环境隔离,可以在不同硬件、不同环境的服务器上运行。
每个服务可以有多个容器,容器调度引擎会负责管理它们,在某个容器出现故障时停止向其调度请求,并根据规则重启或重新创建容器。
容器的数据是存储在外部的,不随容器的销毁而消失,我们称容器是无状态的,也就是内部的数据并不需要长期存储。
目前业内主流的容器引擎是 Docker,主流的容器调度引擎是 Kuberneters,简称 k8s。
k8s 是一个非常完善的容器调度解决方案,拿来一台新的服务器,只需要安装并配置好 k8s,使这台服务器加入集群,k8s 就会自动调度适合的容器到这台机器上运行。
网络通信与存储也可以分散到多台机器上,集群被虚拟成了理论性能无限的单台机器,不同设备的存储被虚拟成一个或几个巨大的磁盘。
顺带一提,在容器调度时,可以设置对应的条件,如某个容器只能调度到剩余内存大于 8GB 的机器上,k8s 会尽可能遵循这些限制,以最佳方式调度容器,最大化集群性能。
某个服务进行升级时,可以一个个升级对应的容器,并按照一定规则逐渐将用户请求分配给新版本容器,这种用户无感知,不会造成服务中断的升级被称为平滑升级。
关于我们使用 Docker 的方式,请参照这篇文章:技术说 | Docker 如何帮助我们构建面向未来的服务。
weight 在这里代表权重,可以理解为权重越高的容器被选中处理用户请求的概率越大,这里将权重设置为 0 是想要在容器不可用期间避免被调度到处理用户请求。
01:59 - 02:06 怀疑是该 _gcd 函数触发了 JIT 编译器的某个 bug,运行出错陷入死循环导致 SLB CPU 100%,临时解决方案:全局关闭 JIT 编译。
JIT,Just In Time,即时编译,一种提升程序性能的技术。
可以简单理解为程序觉得某一段代码经常被使用,所以自动优化了它的执行逻辑,之后使用时就能获得性能提升。
全局关闭 JIT 编译会降低整体性能,其实这里就属于碰运气了,Lua 的代码是公开的,如果 JIT 编译器在如此简单的一个函数上都能出现问题,早就被开发者们修复了。
02:07 SLB 运维修改 SLB 集群的配置,关闭 JIT 编译并分批重启进程,SLB CPU 全部恢复正常,可正常处理请求。同时保留了一份异常现场下的进程 core 文件,留作后续分析使用。
02:31 - 03:50 SLB 运维修改其他 SLB 集群的配置,临时关闭 JIT 编译,规避风险。
进程 core 文件,又名内存转储文件,遇到过 Windows 蓝屏并尝试分析过原因的朋友们可能听说过,它保存了出错时内存中的一些重要信息,是分析故障的重要资料之一。
根因定位
11:40 在线下环境成功复现出该 bug,同时发现 SLB 即使关闭 JIT 编译也仍然存在该问题。此时我们也进一步定位到此问题发生的诱因:在服务的某种特殊发布模式中,会出现容器实例权重为 0 的情况。
猜对了,问题并非出现在 JIT 编译器上。
线下环境指的应该是测试环境,或者为了排查故障临时创建的新环境,在这个环境上的操作不影响用户的正常使用。
一般来说,k8s 作为成熟的容器调度引擎,会自动检测容器的状态,并在发生故障时暂停该容器的调度、,但在这个所谓“特殊发布模式”中,容器的权重被手动设置成了 0。
12:30 经过内部讨论,我们认为该问题并未彻底解决,SLB 仍然存在极大风险,为了避免问题的再次产生,最终决定:平台禁止此发布模式;SLB 先忽略注册中心返回的权重,强制指定权重。
先禁用存在问题的发布模式,避免问题重新出现,同时忽略掉手动设置的权重,牺牲灵活性保证安全性。
注册中心同样是微服务中的概念,它用来接收服务的上线与下线信息,配合负载均衡器实现请求的转发。
例如,一个容器在升级过程中会向注册中心发送这些信息:
- 旧版本服务:请求下线,原因为升级
- 注册中心:操作成功,已停止调度请求,请在处理完现有请求后下线
- 旧版本服务:已处理完现有请求,开始下线
- 新版本服务:服务已成功启动,请求上线,权重为 xxx
- 注册中心:操作成功,开始调度请求
13:24 发布平台禁止此发布模式。
14:06 SLB 修改 Lua 代码忽略注册中心返回的权重。
14:30 SLB 在 UAT 环境发版升级,并多次验证节点权重符合预期,此问题不再产生。
15:00 - 20:00 生产所有 SLB 集群逐渐灰度并全量升级完成。
直到这里,故障诱因已经被消除。
原因说明
这里原文中的内容偏技术性,不再逐句解析。
背景
B 站在 2019 年 9 月更换了负载均衡系统,新的系统 OpenResty 支持使用 Lua 语言编写代码来扩展功能,于是 B 站自己对接了注册中心和负载均衡系统,其中注册中心是自研的。
在故障发生的两个月之前,有人提出了这样一个需求:通过变更服务在注册中心中的权重,实现负载均衡系统中服务权重的动态调整,经相关团队讨论后接受。
诱因
在一种特殊发布模式中,容器权重会短暂调整为 0,这时注册中心给负载均衡系统的权重数值并非整数 0,而是字符串的 "0"。
这种发布模式只在生产环境中使用,而且使用频率极低,因此没有在测试中发现。
在 "0" 被传递给负载均衡系统时,我们的主角 _gcd 函数会接收到字符串 "0" 作为传入参数。
根因
终于来了,上代码!
[图片上传失败...(image-4c23e6-1658900045359)]
一段很简单的代码,我们逐行来解释。
17 行定义了一个变量,这是 Lua 语法的规定。
18 行定义了 _gcd 这个函数,它有两个参数,分别为 a 和 b。
19、20、21 行是一个条件判断,如果 b 为 0,则返回 a 的值。
23 行返回了一个函数的结果,在函数里调用自身,这种编程方式叫做递归,自己调用自己。
递归调用中的 a 参数为 b 的值,b 参数为 a 除以 b 的余数,% 是取余运算。
正常情况下,这个函数调用到一定次数时,最内层将满足条件 a 等于 b,进而返回 a,函数逐层向上返回,最终结束调用。
但当参数 b 为 "0" 时,情况就会变成这样:
字符串的 "0" 和整数 0 并不相等,所以不会直接返回 a 的值。
递归调用,参数 a 为 b 的值,也就是"0",参数 b 为 a % "0"。
在 Lua 语言中,如果取余运算遇到了非数字,会尝试自动转换类型,因此 b 被转换成了整数 0。
而设计者又规定任何整数与 0 取余,结果为 NaN。
因此,递归调用就变成了这样:_gcd(a, NaN)
。
之后,函数向下调用一层,NaN 自然不等于 0,程序依然不会返回,之后的调用就是这样的:_gcd(NaN, NaN)
。
因此,这个函数会不断向内部递归,永远不会退出,也就是“死递归”。
死递归会迅速耗尽服务器的 CPU 和内存资源。
好巧不巧,这个函数是由负载均衡系统的核心之一 Nginx 程序执行的,因此 Nginx 进程陷入死循环,耗尽了服务器的资源,无法处理用户请求。
之后的事情就很明朗了,主机房 SLB 因此过载,CDN 回源与用户重试使多活机房 SLB 过载,所有服务不可用。
这次事故可以称为诡计多端的"0"。
故障总结
SRE 发现服务不可用,其他员工证实
由于内网系统登录流程与用户侧有关联,远程工作的员工无法协助处理问题
根据故障表现判断故障发生在 SLB 中
远程工作的员工通过绿色通道成功登录
怀疑主机房 SLB 过载导致问题,尝试重载失败,冷重启失败
重启多活机房 SLB,核心业务恢复
-
尝试恢复主机房 SLB
- 回滚近期代码,无效
- 关闭 WAF,无效
- 回滚请求重试逻辑,无效
- 回滚 HTTP/2 协议支持,无效
新建源站 SLB,CDN 切量,所有服务恢复
对故障 SLB 进行性能分析,加入日志记录
发现 _gcd 函数的某次执行返回值为 NaN
发现诱因为某个容器的 weight=0
怀疑为 JIT 编译器问题,全局关闭 JIT
保存 core 文件留待分析
重启原有 SLB 集群,恢复正常
关闭所有 SLB 集群的 JIT,规避风险
发现在某种特殊发布模式种容器权重可能为 0
禁止对应发布模式,SLB 忽略注册中心权重
个人看法
B 站至少有 5 个方法阻止这次事故发生,或者降低事故影响。
第一,如果在新的 SLB 集群测试时包含了所有可能的发布模式,就能检测出特殊发布模式产生的问题,继而完全避免事故发生。
第二,如果在检测到故障时立刻停止所有正在发布的服务,并将其回滚到旧版本,SLB 重启后服务就可以立刻恢复正常,大大缩短故障时长。
第三,如果多活机房的容量足够大,扛住了当时 4 倍的流量和 100 倍的 TCP 连接数,核心业务就不会受到影响。
第四,如果多活机房 SLB 在明知无法承受如此大流量的情况下,迅速进入熔断状态,牺牲部分用户的体验,就不会造成请求的大量超时,CDN 在请求失败率过高的情况下放弃重试后,流量进一步降低,熔断恢复,就可以大幅提升核心业务的访问成功率。
第五,如果 SLB 团队提前演练过相应的故障处理方法,就能迅速完成集群的新建和配置,继而大大缩短故障时长。
再大的平台也终究被海因里希法则打垮了,可谓是看到了高楼大厦内部脆弱的地基。
无论如何,我很高兴看到 B 站没有采取冷处理的方式,虽然这份报告姗姗来迟,但终究告诉了我们事情的真相。
如果能在故障恢复后尽快发布复盘报告,或者至少发表一份声明,否认遭到外界攻击导致故障,也能让大家安下心来。
这次 B 站算是坐实“小破站”的名号了,激进的技术选型背后,核心业务还是要保证稳定性。
下次别用动态语言了,用 Go 重写吧,真的。
参考资料
2021.07.13 我们是这样崩的 - 哔哩哔哩技术
2021.07.13 B站是这样崩的 - 三太子敖丙
7.13 B站事故当事人:这是我从业以来压力最大的一晚 - 三太子敖丙
SRE - 百度百科
负载均衡(LB)简略介绍 - 后端码匠
四层负载均衡 - backzy
SLB(服务器负载均衡)- 百度百科
WAF - 百度百科
火焰图(Flame Graphs)- 0x007c00
海因里希法则 - 百度百科