B站网关事故背后:OpenResty 与 Lua 的稳定性代价

注:本文在大模型辅助下完成。

一、一次看似普通的网关事故,为何会导致全站雪崩?

2021 年 7 月 13 日,Bilibili 发生了一次非常经典的网关事故1

事故发生后,线上出现了非常诡异的现象:

  • 所有 OpenResty worker 进程 CPU 100%

  • 但请求几乎无法处理

  • 进程没有 crash

  • 没有 core dump

  • error log 中也没有明显异常

  • 重启无法恢复

  • 回滚代码也无法恢复

最终,整个站点出现大面积故障。

更令人意外的是:

最终定位出的根因,只是一个字符串 "0"。

也就是说,一个看起来几乎不可能引发灾难的数据类型问题,最终却导致:

  • 网关整体失效

  • 请求无法转发

  • 全站雪崩

这次事故后来由 OpenResty 官方团队通过 XRay 工具进行了深度分析2,也成为 OpenResty 领域最经典的稳定性案例之一。

但真正值得关注的,其实并不是"为什么会出现一个低级 bug",而是:

为什么一个如此微小的问题,居然能够穿透整个网关基础设施,最终演变成全站级事故?

而要理解这个问题,就必须先理解:OpenResty 到底是什么。

二、什么是 OpenResty?为什么它如此流行?

很多人会误以为:OpenResty 是一个独立的 API 网关。

实际上并不是。

OpenResty 的本质是**"增强版 Nginx"**。它的核心思想是:以 Nginx 作为高性能网络框架,在 Nginx 内部嵌入 Lua 运行时,允许开发者在 Nginx 请求处理阶段执行 Lua 代码。

也就是说:

OpenResty = Nginx + Lua Runtime

在传统 Nginx 中,开发者主要依赖nginx.conf、rewrite 规则、upstream 配置来控制流量。但 Nginx 的问题是:配置能力强,但编程能力有限。很多复杂场景都很难实现:动态路由、动态鉴权、动态限流、动态灰度、动态 upstream、插件体系。

而 OpenResty 的出现,彻底改变了这一点。它允许开发者在 rewrite、access、balancer、filter 等阶段直接编写 Lua 逻辑。

于是,Nginx 从**"配置驱动"** 变成了**"代码驱动"**。这也是 OpenResty 在互联网领域快速流行的重要原因。

后来,Kong、Apache APISIX 等知名 API 网关项目,也都选择了OpenResty 作为底层架构。

因为它确实非常灵活。它让网关第一次具备了动态化、插件化、可编程化的能力。

但与此同时,"灵活"本身,也开始成为稳定性问题的来源 。这不是说灵活性是错的,而是说:灵活性与稳定性之间,始终存在架构权衡(trade-off)

三、为什么 OpenResty 的技术路线会放大稳定性风险?

很多人讨论 OpenResty 时,首先关注的是性能、动态能力、插件生态。但对于网关而言,真正最重要的能力,其实是稳定性

因为网关不是普通业务系统。它是全站入口、流量枢纽、多业务共享基础设施、高并发长生命周期系统。它一旦出现问题,影响范围往往是全局性的

因此,网关最怕的,其实不是"功能不够",而是"不可预测"。而 OpenResty 的技术路线,恰恰在某些层面放大了这种"不可预测性"。

1. Lua 动态语言的问题

Lua 是典型的动态弱类型语言。例如:

复制代码
local weight = "0"
if weight == 0 then
...
end

这里 "0" 是字符串,0 是数值。在复杂系统中,这种问题非常容易被忽略。

更关键的是:Lua 大量错误只能在运行时暴露,而无法在编译阶段发现。这意味着配置上线后才发现问题、某些边界流量才会触发问题、某些特殊数据才会暴露问题。

对于业务系统而言,这可能只是单请求失败、单实例异常。但对于网关,意味着一个运行时错误,可能直接影响整个流量入口

2. Lua 的"灵活性"本身就是风险来源

Lua 为了灵活性,引入了大量动态机制:table 动态扩展、metatable 元编程、monkey patch、coroutine、动态 require、共享状态。

这些机制在业务开发中很方便。但在网关系统中,灵活往往意味着不可预测

例如:某个 table 被意外修改、某个共享状态没有同步、某个 coroutine 没有退出、某个模块被热更新覆盖,都可能导致 CPU 飙升、worker 卡死、内存泄漏、请求阻塞。

而且,这类问题往往极难复现,因为它们依赖运行时状态、并发路径、流量模式、特定数据。

3. LuaJIT 又进一步增加了复杂性

OpenResty 的高性能,很大程度来自 LuaJIT。LuaJIT 会进行动态热点编译、trace optimization、speculative optimization。

这意味着同一段 Lua 代码,测试环境正常、小流量正常,但高并发线上异常------因为JIT 的行为依赖运行时路径。这会让很多问题难复现、难定位、难解释。

而对于网关而言,"无法稳定复现的问题",往往是最危险的问题

这里需要特别说明:LuaJIT 的JIT 编译器本身在此事故中并没有 bug。官方复盘明确指出,最初怀疑 JIT 问题是因为另一个业务团队的未告知操作产生了干扰。但 JIT 的动态优化特性,确实增加了问题定位的复杂度。

4. 插件化体系进一步放大了风险

今天很多基于 OpenResty 的网关(如 Kong、Apache APISIX)都强调插件化。但插件化本身也意味着:多团队共享运行时、插件共享 worker、插件共享 Lua VM、插件运行在同一个网关进程内部。

于是,一个插件问题,就可能拖垮整个网关实例

这和传统业务系统(微服务隔离、进程隔离、容器隔离)的风险模型完全不同。在OpenResty 架构中,"业务逻辑"与"基础设施"之间,实际上没有真正隔离

四、回到 B站事故:为什么一个"0"能导致整个网关崩溃?

理解了 OpenResty 的技术路线之后,再回头看 B站事故,就会发现:这并不是一次"偶然事故",而是系统性风险的一次具体暴露

直接原因:类型误用导致死循环

事故最终定位发现:一个字符串 "0" 被错误地当成数值 0 使用。而 lua-resty-balancer 在处理过程中,最终进入了异常逻辑路径。

具体来说,在 lua-resty-balancer 的 _gcd(最大公约数)函数中,代码期望传入的是数值类型。当传入字符串"0" 时,Lua 的取模运算"0" % 0 产生了 nan(非数值),导致无法进入 b == 0 的终止条件,从而陷入无限循环。

随后导致:无限递归/循环 → worker CPU 100% → 整个网关集群全部失效。

系统性原因:动态架构放大了单点缺陷

真正可怕的地方在于,这个问题:

  • 没有 crash

  • 没有 core dump

  • 没有明显 error

  • worker 进程仍然"活着"

只是不再处理请求

而对于网关来说,"活着但无法工作",往往比直接 crash 更危险。因为:

  • 健康检查可能无法及时发现(进程还在,端口还通)

  • 自动恢复机制可能无法触发

  • 流量还会持续进入故障节点

最终形成:排队 → 超时 → 重试风暴 → 雪崩扩散。

这也是为什么,网关系统最怕的并不是"明确失败",而是"不可预测的异常状态"

五、OpenResty 最大的争议:把"业务逻辑"带进了"基础设施"

很多人认为 OpenResty 的问题只是"Lua 不够安全、动态语言不够稳定"。但实际上,更深层的问题,是"业务动态逻辑进入了基础设施"

OpenResty 最大的成功在于:它让业务团队可以快速扩展网关能力。但与此同时,它也让动态脚本、业务逻辑、运行时行为、热更新能力直接进入了 L7 网关、流量入口、核心基础设施。

于是,网关开始逐渐业务化、状态化、动态化、不可预测化 。最终,基础设施不再是**"稳定内核"** ,而变成了**"动态运行平台"**。

而对于基础设施来说,动态能力越强,稳定性复杂度通常也越高。这也是为什么,近年来越来越多的新一代基础设施开始强调:强类型、静态分析、内存安全、可验证性、沙箱隔离、可预测执行模型。

因为对于基础设施而言,"稳定、可预测、可控制"往往比"灵活"更重要

当然,这并不意味着 OpenResty 的技术路线是错误的。Kong、APISIX 等项目的成功证明,在需要快速迭代、灵活扩展的场景下,OpenResty 的权衡是合理的 。但 B站事故说明:当这种灵活性缺乏足够的防御机制时,一个字符串 "0",就足以让整个大型网站的网关系统全面失效

结语

B站网关事故已经过去数年,但它留下的警示依然鲜活:

对于基础设施,"不可预测"比"不够灵活"更致命。

OpenResty 用 Lua 的动态性重新定义了网关的灵活性,但也让我们看到了动态语言在基础设施中的脆弱一面。这不是 Lua 的错,也不是 OpenResty 的错,而是我们在享受灵活性带来的便利时,往往低估了防御性编程和边界校验的必要性

一个字符串 "0",最终演变成全站雪崩。这个看似荒诞的因果链,恰恰揭示了分布式系统中最深刻的真理:

系统的稳定性,不取决于最强的一环,而取决于最脆弱的边界条件。

参考资料

1 2021.07.13 我们是这样崩的,哔哩哔哩技术,2022年7月

2 OpenResty XRay 分析和解决 B 站重大线上事故,OpenResty软件,2022年7月

作者简介

章淼,博士,1994年进入清华大学计算机科学与技术系学习,2004年获得博士学位,2004年至2006年在清华大学留校任教,在清华期间曾参与中国第一代核心路由器的研制工作。2012年起在百度工作超过十年,聚焦云网络基础架构的研发工作,是BFE开源项目的发起人。在百度期间积极推动软件工程能力提升,曾担任百度代码规范委员会主席,2021年10月被授予百度代码规范委员会荣誉主席。2022年出版《代码的艺术:用工程思维驱动软件开发》。2023年4月起担任瑛菲网络CEO,聚焦研发面向云和大模型场景的现代化流量管理平台。

相关推荐
半亩码田2 小时前
【.NET新特性·第4篇】.NET Aspire 入门:云原生开发新姿势
云原生·.net
椰椰椰耶2 小时前
[SpringCloud][11] Nacos 负载均衡,服务下线、权重配置、同集群优先访问
java·spring cloud·负载均衡
装不满的克莱因瓶2 小时前
Spring 全家桶与 Spring 6 新特性详解:从 IoC 到云原生时代
java·spring·云原生·jdk·新特性·spring6
Trouvaille ~3 小时前
【Redis篇】Redis 事务:原子性与脚本执行机制
数据库·redis·后端·算法·junit·lua·原子性
IT策士3 小时前
第 35 篇 k8s之PVC 与 StorageClass:动态存储供应
云原生·容器·kubernetes
武子康3 小时前
调查研究-156 Vercel 全栈应用 前端零配置极速上线:Serverless + 边缘网络 + CI/CD 全栈实战
前端·网络·ci/cd·ai·云原生·serverless·vecel
牧羊狼的狼3 小时前
基于阿里云落地SpringCloudAlibaba云原生微服务:从部署、CI/CD到性能调优、线上排障全体系实战
阿里云·微服务·云原生
FFZero13 小时前
[mpv脚本系统] (二) Lua三层闭包实现自动资源管理
junit·单元测试·lua
这个DBA有点耶12 小时前
云上运维新挑战:当数据库不再“看得见摸得着”
数据库·sql·程序人生·云原生·运维开发·学习方法·dba