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 汇编例程中:

  1. 该例程将栈指针(%rsp)更新为活动栈的新底部。
  2. 一旦 %rsp 被更新,ucontext_t 结构体不再是活动栈或“红色区域”(内核保护的 %rsp 下方 128 字节)的一部分。
  3. 如果在此确切时刻传递一个信号(例如 SIGUSR2),内核会在 %rsp-128 处构建信号帧,可能会覆盖 ucontext_t 内存。
  4. 如果从此损坏的内存中读取恢复的指令指针(%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 错误

Sources