代码|7行代码让B站崩溃3小时 竟因“一个诡计多端的0”

一个小小字符“0”,竟引得B站全面崩溃 。
代码|7行代码让B站崩溃3小时 竟因“一个诡计多端的0”
文章图片

不知你是否还记得那一夜,B站“大楼停电”、“服务器爆炸”、“程序员删库跑路”的彻夜狂欢 。(手动狗头)
时隔一年,背后“真凶”现在终于被阿B披露出来——
代码|7行代码让B站崩溃3小时 竟因“一个诡计多端的0”
文章图片

没想到吧,就是这么简单几行代码,直接干趴B站两三个小时,搞得B站程序员彻夜无眠头发狂掉 。
你可能会问,这不就是个普普通通用来求最大公约数的函数吗,怎么就有如此大的威力?
背后一桩桩一件件,归根结底其实就一句话:0,它真的不兴除啊 。
代码|7行代码让B站崩溃3小时 竟因“一个诡计多端的0”
文章图片

具体详情,咱们还是一起来看看“事故报告” 。
字符串“0”引发的“血案”
先来说道说道引发惨案的根本原因,也就是开头贴出的这个gcd函数 。
学过一点编程知识的小伙伴应该都知道,这是一种用辗转相除法来计算最大公约数的递归函数 。
跟我们手算最大公约数的方法不同,这个算法是酱婶的:
举个简单的例子,a=24,b=18,求a和b的最大公约数;
a除以b,得到的余数是6,那么就让a=18,b=6,然后接着往下算;
18除以6,这回余数是0,那么6也就是24和18的最大公约数了 。
也就是说,a和b反复相除取余数,直到b=0,函数中:
if b==0 then return a end
这个判断语句生效,结果就算出来了 。
基于这样的数学原理,我们再来看这段代码,似乎没什么问题:
代码|7行代码让B站崩溃3小时 竟因“一个诡计多端的0”
文章图片

但如果输入的b是个字符串“0”呢?
B站的技术解析文章中提到,这段出事的代码是用Lua写的 。Lua具有这么几个特点:
这是一种动态类型语言,常用习惯里变量不需要定义类型,直接给变量赋值就行 。
【代码|7行代码让B站崩溃3小时 竟因“一个诡计多端的0”】Lua在对一个数字字符串进行算术操作时,会尝试将这个数字字符串转成一个数字 。
在Lua语言中,数学运算n%0的结果是nan(Not A Number) 。
我们来模拟一下这个过程:
1、当b是一个字符串“0”时,由于这个gcd函数没有对其进行类型校验,因此在碰上判定语句时,“0”不等于0,代码中“return _gcd(b, a%b)”触发,返回_gcd(“0”, nan) 。
2、_gcd(“0”, nan)再次被执行,于是返回值变成了_gcd(nan, nan) 。
这下就完犊子了,判定语句中b=0的条件永远没法达到,于是,死循环出现了 。
也就是说,这个程序开始疯狂地原地转圈,并且为了一个永远得不到的结果,把CPU占了个100%,别的用户请求自然就处理不了了 。
代码|7行代码让B站崩溃3小时 竟因“一个诡计多端的0”
文章图片

那么问题来了,这个“0”它到底是怎么进去的呢?
官方说法是:
在某种发布模式中,应用的实例权重会短暂地调整为0,此时注册中心返回给SLB(负载均衡)的权重是字符串类型的“0” 。此发布环境只有生产环境会用到,同时使用的频率极低,在SLB前期灰度过程中未触发此问题 。
SLB在balance_by_lua阶段,会将共享内存中保存的服务IP、Port、Weight作为参数传给lua-resty-balancer模块用于选择upstream server,在节点weight=“0”时,balancer模块中的_gcd函数收到的入参b可能为“0” 。
bug是如何定位的
以“事后诸葛亮”的视角来看,这个引发B站全面崩溃的根本原因多少有点让人直呼“就这” 。
但从当事程序员的视角来看,事情确实没有辣么简单 。
当天晚上22:52分——大部分程序员才刚下班或者还没下班的节骨眼(doge),B站运维收到服务不可用的报警,第一时间怀疑机房、网络、四层LB、七层SLB等基础设施出现问题 。
然后立马和相关技术人员拉了个紧急语音会议开始处理 。
5分钟后,运维发现承载全部在线业务的主机房七层SLB的CPU占用率达到了100%,无法处理用户请求,排除其他设施后,锁定故障为该层 。


推荐阅读