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

B站网关事故背后:OpenResty 与 Lua 的稳定性代价 注本文在大模型辅助下完成。一、一次看似普通的网关事故为何会导致全站雪崩2021 年 7 月 13 日Bilibili 发生了一次非常经典的网关事故[1]。事故发生后线上出现了非常诡异的现象所有 OpenResty worker 进程 CPU 100%但请求几乎无法处理进程没有 crash没有 core dumperror 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没有明显 errorworker 进程仍然活着只是不再处理请求。而对于网关来说活着但无法工作往往比直接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聚焦研发面向云和大模型场景的现代化流量管理平台。