回到当时的时间,B站崩溃这个消息仅半小时就冲上了热搜,B站崩溃不仅仅让本公司程序员加班,以及其他网站微博,DB,ZH等网站的程序员,当时大家都在瞎猜测,火灾了?删库跑路了?
B站时隔一年终于发布了崩溃原因!竟然是这么一点代码!!
要了解其中底层原因,实际问题并不复杂,我们先来了解一下B站的官方架构图如下:
由CDN,LVS,OpenResty七层SLB等组件组成,多机房部署实现异地多活的架构来保证整个架构的高可用性。
其中:
CDN是内容分发网络,它提供了地域就近访问的功能,为用户获取服务器信息提供加速功能。
LVS是一个四层的负载均衡器,它提供了一个ip加端口的负载,为OpenResty提供了一个高可用集群。
OpenResty七层SLB是一个基于Nginx+Lua脚本语言的Web平台。
当我们的用户发起请求后,经过CDN分发到业务主机,通过LVS四层负载路由到我们的OpenResty的服务器上,然后转发到对应的应用服务器实例来获取相关的数据并进行返回。
故障解决过程
第一步
先是B站的运维人员采取了常规措施去定位寻找问题。他们发现七层负载服务器的CPU的利用率100%,跑慢了,于是采取重新加载和冷重启SLB的方式,结果没有用。
第二步
他们发现多活机房在CPU利用率正常的情况下,SLB仍然有大量的超时请求,于是重启了多活机房,恢复了部分业务,但是业务主机房仍然没有恢复。
第三步
运维人员通过Perf系统的分析工具定位到主机房SLB服务器的CPU热点集中在一个Lua函数上,于是采取了常规操作版本回滚,但是并没有解决问题。
第四步
最后新建了SLB集群,前后从事故开始共耗时3个多小时,损失了大量的人力物力等,才终于解决。
后续分析
OpenResty里面的一个Lua函数是罪魁祸首。这个函数的作用是从注册中心同步服务注册地址,以及该服务节点的一个访问权重,来保存到Nginx的一个共享内存中,然后使用Lua-resty-balance这样的一个模块对服务地址进行一个动态路由,其中在进行目标服务器动态路由的时候用到了一个加权轮询算法,并使用到了下图方法去计算所有实例的权重的最大公约数。
方法没有问题,但是当权重weight=“0”
时,_gcd函数收到b有可能是字符串类型的“0”,而Lua脚本语言又是动态语言,弱类型语言,允许传入字符串“0”,这个时候“if”条件的字符串“0”和数字0它们是不相等的,所以会执行_gcd函数的递归调用其中在执行a%b时
用字符串和数字取模会得到一个NaN的结果于是再次执行_gcd的时候就变成_gcd(NaN,NaN),就这样两个参数一直进行递归调用,从而导致了死循环,导致OpenResty的CPU满了,用户的请求就无法响应,进而导致B站崩了。
感叹!居然是这个原因,一个数据类型的问题导致的,可能有人会疑惑,为什么测试之前没发现,因为这个模块很少使用,传入字符串“0”的情况更是少之又少。
总结
正是千里之堤溃于蚁穴的写照。我们要做好代码规范,多思考,多学习。