OpenAI 核心转储流行病学:修复一个 18 年前的 libunwind 错误
OpenAI 核心转储流行病学:修复一个 18 年前的 libunwind 错误
OpenAI 已经解决了 Rockset 中的一系列无法解释的崩溃,Rockset 是一个基于 C++ 的数据基础设施服务,用于 ChatGPT 的数据插件和对话搜索。调查发现,最初看来是一个问题的事情实际上是两个无关的错误:特定 Azure 主机上的静默硬件损坏以及 GNU libunwind 库中长期存在的竞争条件。
人群级诊断与逐个案例调试
调查的转折点是从“医生”方法(分析单个核心转储)转变为“流行病学家”方法(分析整个崩溃人群)。通过构建一个自动化管道来分析一年的生产核心转储,OpenAI 能够根据寄存器状态和模式将崩溃分类为不同的簇。
这种数据驱动的方法揭示了两个独立的崩溃人群:
- Misaligned-stack 崩溃: 这些崩溃局限于一个地区的单个物理主机,具有明确的开始日期,表明是硬件故障。
- Return-to-null 崩溃: 这些崩溃分布在多个簇和地区,表明存在软件层面的系统性问题。
错误 1:静默硬件损坏
一个崩溃簇被追溯到一个特定的物理 Azure 主机,该主机的 CPU 在执行过程中进行了错误的计算。这导致栈指针(%rsp)在执行过程中错位,从而在函数返回时导致崩溃。
由于该损坏是静默的,且在受控环境中无法重现,OpenAI 通过将该主机加入拒绝名单来缓解了该问题。为了防止未来再次发生,团队改进了致命信号处理程序,使其在日志中包含寄存器状态,从而在不需要完整核心转储的情况下实现更快的检测;并更新了控制平面,以倾向于 VM 重用而非回收,以便更容易检测到故障节点。
错误 2:18 年前的 GNU libunwind 竞争条件
技术机制
当 GNU libunwind 执行展开传递时,它会在栈上合成一个 ucontext_t 结构体以保存所需的寄存器状态。漏洞存在于 _Ux86_64_setcontext 汇编例程中:
- 该例程将栈指针(
%rsp)更新为活动栈的新底部。 - 一旦
%rsp被更新,ucontext_t结构体不再是活动栈或“红色区域”(内核保护的%rsp下方 128 字节)的一部分。 - 如果在此确切时刻传递一个信号(例如
SIGUSR2),内核会在%rsp-128处构建信号帧,可能会覆盖ucontext_t内存。 - 如果从此损坏的内存中读取恢复的指令指针(
%rip),程序将崩溃——通常会返回到 NULL。
竞争窗口和概率
竞争窗口大约宽一条指令,估计为大约 100 皮秒。虽然极其狭窄,但由于 Rockset 环境中的三个因素,崩溃频率变得可以在运营中观察到:
- 高异常率: Rockset 使用异常进行内部摄入背压,在过载的主机上每秒抛出高达 $10^4$ 次异常。
- 高信号率:
coarse_thread_cputime_clock每几毫秒 CPU 时间发送一次SIGUSR2信号。 - 增加的栈使用: 对
SIGUSR2处理程序的最近更新添加了对timer_getoverrun的调用,增加了处理程序使用的栈量,使其更有可能覆盖陈旧的ucontext_t内存。
解决方案和缓解措施
OpenAI 通过从 GNU libunwind 切换到 libgcc 的展开器来解决了 libunwind 问题,这也通过减少大型 VM 上的锁竞争带来了性能优势。此外,OpenAI 将一个自包含的重现器和修复上游提交到了 GNU libunwind 项目,以便为更广泛的社区解决该错误。
SUMMARY: OpenAI 在其 Rockset 数据基础设施中识别并解决了两种不同的崩溃人群,包括一次静默硬件故障和 GNU libunwind 中罕见的 18 年前竞争条件。
TITLE: OpenAI 核心转储流行病学:修复一个 18 年前的 libunwind 错误