Как OpenAI победила баг возрастом 18 лет: эпидемиология сбоев

Что делать, если сервис периодически падает, воспроизвести ошибку невозможно, а каждая новая гипотеза немедленно опровергается? Именно с этой головоломкой столкнулась команда инженеров OpenAI несколько месяцев назад. История о том, как они справились — это не просто байка про баг: это урок по методологии отладки больших производственных систем.

ℹ Что такое core dump?
Core dump (дамп памяти) — это снимок состояния процесса в момент его аварийного завершения: значения регистров CPU, содержимое стека вызовов, участки памяти. Это один из главных инструментов при расследовании крашей на низком уровне.

Rockset: что это и зачем он нужен OpenAI

Для понимания контекста важно разобраться, что такое Rockset. Это облачная система для поиска и аналитики в реальном времени, которую OpenAI использует для множества внутренних задач, в том числе для синхронизации данных (Rockset была приобретена OpenAI в 2024 году).

Потоковые обновления применяются для поддержания актуального индекса базы знаний рабочего пространства, чтобы ChatGPT мог искать релевантную информацию при ответах на вопросы и выполнении действий.

Исполняющий слой Rockset написан на C++. Этот язык даёт низкоуровневый доступ к CPU — что хорошо для производительности, — но это также означает, что ошибки в приложении могут приводить к некорректному обращению с памятью и к segfault (нарушению сегментации).

Для отслеживания таких сбоев команда использует обработчик фатальных сигналов из библиотеки folly — он записывает стек вызовов при краше, а соответствующие core dump-файлы (снимки состояния программы в момент сбоя) загружаются в Azure Blob Storage для последующего анализа.

Симптомы: таинственный краш

Несколько месяцев назад в сервисе Rockset начали появляться странные сбои. В каждом случае обычная C++-функция, казалось, завершалась нормально — но затем передавала управление по «мусорному» адресу, и ядро останавливало программу, поскольку указатель инструкций больше не указывал на код.

Иногда в слоте адреса возврата на стеке оказывался NULL. Иногда регистр стека CPU (%rsp) был смещён на 8 байт — как будто его значение было декрементировано посреди нормального выполнения. В обоих случаях краш происходил именно при возврате из функции.

«Каждая гипотеза, которую мы (и ChatGPT) могли придумать, немедленно опровергалась — баг казался невозможным.» — команда инженеров OpenAI

То, что считалось одной проблемой, в итоге оказалось двумя никак не связанными между собой багами, обнаруженными одновременно по чистой случайности.

Подход «доктора» против подхода «эпидемиолога»

Инженеры сформулировали ключевой выбор методологии: можно расследовать каждый конкретный краш по отдельности — как врач обследует одного пациента. А можно взглянуть на всю совокупность сбоев и поискать паттерны, которые не видны при изучении единичного случая.

Начался ли баг с конкретного релиза? Коррелирует ли он с определённым типом железа (SKU — конфигурация CPU и сервера), регионом или версией ядра? Скрываются ли внутри того, что выглядит как один синдром, несколько различных кластеров?

Именно переход к «эпидемиологическому» мышлению стал решающим шагом.

💡 Принцип эпидемиологии в отладке
Единичный инцидент редко раскрывает истинную причину. Когда сбои редкие и нестабильные, стоит собрать всю популяцию происшествий за длительный период и искать статистические закономерности: по времени, географии, версии ПО или железа.

Автоматический конвейер анализа: ChatGPT пишет скрипт

Предыдущие попытки автоматически найти все экземпляры проблемы провалились — команда пыталась использовать текстовый поиск по логам. Core dump-файлы содержат значительно больше информации, но изучать их вручную было невозможно в нужном масштабе. Поэтому было решено вложить усилия в построение конвейера для автоматического анализа дампов.

Инженеры попросили ChatGPT написать скрипт, который скачивал префикс каждого core-файла, извлекал значения регистров, фильтровал известные ложные срабатывания по логам и автоматически классифицировал краш как return-to-null (возврат на нулевой адрес), misaligned-stack (смещение стека) или other (иное).

Затем этот скрипт был запущен параллельно на всех production core dump-файлах Rockset за предыдущий год. Это стало переломным моментом: как только появился чистый датасет, корреляции проявились немедленно.


flowchart TD
    A[Редкие крашы в Rockset] --> B[Ручной анализ core dumps не масштабируется]
    B --> C[ChatGPT пишет скрипт классификации]
    C --> D[Параллельный запуск на всех дампах за год]
    D --> E{Два отдельных кластера сбоев}
    E --> F[return-to-null: широко распределённые крашы]
    E --> G[misaligned-stack: один регион, один хост]
    F --> H[Баг в GNU libunwind — 18 лет]
    G --> I[Аппаратный сбой на физическом сервере Azure]

Два разных кластера сбоев

То, что принималось за один странный баг, на самом деле оказалось двумя отдельными популяциями крашей.

Кластер 1: аппаратный сбой

Крашы типа misaligned-stack выглядели совершенно иначе. Все они происходили в одном регионе, имели чёткую дату начала и никогда не случались на узлах, работавших длительное время.

Несмотря на то что в них были задействованы несколько виртуальных машин Azure, паттерн напоминал поведение одного физического сервера с неисправным железом, который влиял на любую VM, попавшую на этот хост.

Речь шла о тихом аппаратном повреждении данных на одном хосте Azure: процессор просто неверно выполнял математические операции.

Кластер 2: баг возрастом 18 лет

Крашы типа return-to-null были распределены по множеству кластеров и географических регионов. Их частота недавно возросла, но не было ни чёткой даты начала, ни понятной инфраструктурной границы.

Инженеры обнаружили 18-летнее состояние гонки (race condition) в библиотеке GNU libunwind — широко используемой открытой библиотеке C++ для раскрутки стека при обработке исключений.

⚠ Что такое race condition?
Race condition (состояние гонки) — ошибка в многопоточном коде, при которой результат работы программы зависит от порядка выполнения потоков. Если два потока одновременно обращаются к общему ресурсу без должной синхронизации, данные могут быть повреждены непредсказуемым образом.

Сравнение двух кластеров

ХарактеристикаКластер 1: аппаратный сбойКластер 2: баг в libunwind
Тип крашаmisaligned-stackreturn-to-null
РаспределениеОдин регион, один хостМного кластеров и регионов
Дата началаЧёткаяРазмытая
Долгожительство узловТолько новые узлыЛюбые
ПричинаНеисправный CPU на физическом сервере18-летний race condition в GNU libunwind
РешениеЗамена/изоляция хостаСмена unwinder + upstream-патч

Исправление и превентивные меры

Удаление неисправного хоста не является постоянным решением — оно не предотвращает повторение похожей проблемы в будущем. Тем не менее можно изменить программную часть так, чтобы при повторном появлении подобного сбоя его было легко обнаружить. Был улучшен обработчик фатальных сигналов: теперь он включает состояние регистров, что позволяет детектировать повторение только по логам — без необходимости в core dump.

OpenAI переключила Rockset с GNU libunwind на unwinder из libgcc в качестве немедленного решения, а инженер Натан Бронсон загрузил воспроизводящий пример и патч непосредственно в проект GNU libunwind на GitHub.

Поскольку GNU libunwind используется далеко за пределами OpenAI, другие организации, эксплуатирующие высоконагруженные C++-сервисы с интенсивной обработкой исключений, могут столкнуться с тем же скрытым багом.

# Пример команды для проверки используемого unwinder в Linux
ldd ./your_binary | grep unwind
# Если вы видите libunwind — стоит проверить версию и наличие патча
📝 Практический вывод для C++ команд
Если ваш C++-сервис использует GNU libunwind и активно работает с исключениями или сигналами, рекомендуется проверить версию этой библиотеки и рассмотреть переход на libgcc unwinder до появления исправленного релиза GNU libunwind.

Почему это важно шире, чем один баг

Даже сильные инженерные команды могут застрять, принимая два несвязанных бага за один. И этот случай говорит столько же о методологии отладки, сколько о конкретном 18-летнем изъяне в GNU libunwind: популяционный анализ данных о сбоях преуспел там, где пошаговое расследование отдельных крашей не давало результата на протяжении месяцев.

Этот эпизод является конкретной иллюстрацией более широкого принципа отладки для инфраструктурных команд: анализ случай за случаем может провалиться даже при наличии сильных инженеров, тогда как автоматизированные конвейеры популяционного анализа данных способны выявить паттерны, которые невозможно увидеть в единичном инциденте.

Мышление эпидемиолога — не просто метафора. Это конкретная инженерная практика: сначала собрать данные о всей популяции событий, а уже потом формировать гипотезы.

Итоги

История с core dump-ами в Rockset — редкий пример подробного технического постмортема от крупной AI-компании. Она учит сразу нескольким вещам:

  1. Автоматизируй классификацию — ручной просмотр не масштабируется при сотнях дампов.
  2. Смотри на популяцию, а не на один образец — статистика выявляет то, что скрыто в единичных случаях.
  3. Не предполагай одну причину — несколько несвязанных проблем могут проявляться одновременно.
  4. Используй AI-инструменты — ChatGPT помог написать скрипт классификации, ускорив прорыв.
  5. Отдавай исправления в open source — патч для GNU libunwind теперь поможет всей индустрии.

Открытый баг в библиотеке возрастом 18 лет — напоминание о том, что даже самый стабильный и проверенный код несёт в себе скрытые угрозы, которые ждут своего часа в production-системах будущего.