Обратная отладка в большом масштабе / Блог компании SkillFactory / Хабр

0 Favorite

[

Отладка — неотъемлемая часть профессионального программирования. К старту курса о Fullstack-разработке на Python делимся переводом о том, как отладка устроена в Facebook; в статье вы найдёте ссылку на разработанный FB плагин трассировки для LLDB, который преобразует необработанную трассировку в удобочитаемый формат.

Допустим, вы получаете уведомление по электронной почте о том, что сервис терпит крах сразу после развёртывания вашего последнего изменения кода. Сбой происходит только на 0,1 % серверов, где запущен сервис. Но вы работаете в крупной компании, поэтому 0,1 % равняется тысячам серверов, и эту проблему будет трудно воспроизвести. Несколько часов спустя вы всё ещё не можете воспроизвести проблему, хотя потратили целый день на её решение.


Здесь вступает в действие обратная отладка. Существующие методы позволяют инженерам записывать приостановленную (или потерпевшую крах) программу, затем перематывать её назад и воспроизводить, чтобы найти первопричину. Однако для таких крупных компаний, как Facebook, эти решения требуют слишком больших накладных расходов, чтобы быть практичными в производственной среде.

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

Звучит здорово, но как это работает?

Для разработки мы использовали Intel Processor Trace (Intel PT), который использует специализированное оборудование для ускорения сбора шагов в программе, что позволяет нам использовать его в производстве и в масштабе. Однако эффективная трассировка программы — это только первый шаг.

Непрерывный сбор и быстрое хранилище в производственной среде

Поскольку мы не знаем, когда произойдёт сбой и какой именно процесс потерпит крах, нам необходимо постоянно собирать трассировку Intel PT всех запущенных процессов. Чтобы ограничить объём памяти, необходимой для работы, мы храним трассировку в кольцевом буфере, где новые данные перезаписывают старые:

Когда несколько процессов (A и B) выполняются одновременно, данные трассировки хранятся в буфере. В t+8 данные процесса B начинают перезаписывать самые старые данные (данные процесса A) в буфере.
Когда несколько процессов (A и B) выполняются одновременно, данные трассировки хранятся в буфере. В t+8 данные процесса B начинают перезаписывать самые старые данные (данные процесса A) в буфере.

На больших серверах, наподобных тем, что применяются для обучения моделей ИИ, Intel PT может генерировать сотни мегабайт данных в секунду даже в сжатом формате. Когда процесс терпит крах, сборщик должен приостановить сбор и скопировать содержимое буфера для потерпевшего крах процесса. Для этого операционная система должна уведомить нашего сборщика о крахе процесса, на что требуется время. Чем больше задержка между крахом и копированием содержимого буфера, тем больше данных будет перезаписано новыми данными из других процессов.

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

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

Декодирование и символизация

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

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

С помощью указанного на рисунке выше потока мы можем преобразовать необработанный файл трассировки в такой:

Символизация инструкций — это только первая часть работы LLDB. Следующая часть — реконструкция истории вызовов функций. Для этого мы построили алгоритм, который способен пройти по инструкциям и построить дерево вызовов функций, то есть какая функция вызвала другую функцию и когда. Самое интересное в этом то, что трассировка может начинаться в произвольный момент выполнения программы, и не обязательно с самого начала.

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

Это дерево может помочь нам быстро ответить на вопрос, например, о том, какова трассировка стека в данный момент истории; вопрос решается простым движением вверх по дереву. С другой стороны, более сложный вопрос — где останавливается обратный шаг с обходом?

Представьте, что вы находитесь в строке 16 этого небольшого куска кода. При обратном шаге с обходом вы должны остановиться либо на строке 13, либо на строке 15, в зависимости от того, было ли захвачено if или нет. Наивный подход заключается в том, чтобы проверять инструкции в истории по одной, пока не будет найдена одна из этих строк, но это может быть очень неэффективно, так как функция foo может содержать миллионы инструкций. Вместо этого можно воспользоваться вышеупомянутым деревом, чтобы перемещаться по строкам, не перемещаясь внутри вызовов. В отличие от предыдущего подхода обход дерева почти тривиален.

Кроме того, добавлена поддержка точек останова. Предположим, что вы находитесь в середине сеанса отладки и хотите вернуться назад во времени к последнему вызову function_a. Вот что можно сделать:

Наконец, мы планируем в какой-то момент интегрировать этот поток с VSCode, чтобы получить богатый опыт визуальной обратной отладки.

Анализ и профилирование задержек

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

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

Сервер получает несколько запросов от инженеров, и вы хотите разобраться с длинным хвостом задержки в вашем сервисе (например, P99). Вы берёте свой профайлер и собираете несколько выборок стеков вызовов для двух основных типов запросов. Они выглядят, как на графике ниже, который не позволяет понять, чем они отличаются, поскольку выборка стека вызовов показывает только агрегированные цифры. Нам нужно преобразовать трассировки в информацию о пути выполнения и на этом пути символизировать инструкции.

На изображении ниже показана трассировка, позволяющая легко понять происходящее: запрос B очищает данные перед их получением.

Тогда как отладчик может помочь вам двигаться в пределах трассировки по шагам, визуализация поможет вам легко увидеть общую картину. Вот почему мы также работаем над добавлением графиков, подобных графику “Трассировка стека вызовов” выше, в наш инструмент анализа производительности Tracery. Мы создаём инструмент, который может производить эту трассировку и временные данные и объединять их с производимой LLDB символизацией. Наша цель — предложить разработчикам способ увидеть всю эту информацию сразу, а затем увеличить или уменьшить масштаб и получить необходимые данные на нужном им уровне.

Помните нежелательный сценарий в начале этого поста? Теперь представьте, что вы получаете уведомление по электронной почте о том, что ваш сервис падает на 0,1 % машин, но на этот раз оно приходит с кнопкой “Reverse debug on VSCode”. Вы нажимаете на кнопку, а затем перемещаетесь по истории программы, пока не найдёте вызов функции, который не должен был произойти, или ветку if, которая не должна была выполняться, и исправляете её в течение нескольких минут.

Такие технологии позволяют нам решать проблемы на уровне, который мы раньше не считали возможным. Большинство наших работ (если судить по истории) в этой области привели к значительным накладным расходам, что ограничивало наши возможности масштабироваться. Сейчас мы создаём инструменты отладки, которые интерактивно и наглядно помогают разработчикам решать проблемы производства и разработки гораздо более эффективным способом. Это захватывающее начинание, которое поможет нам ещё больше расширить наши масштабы.

Узнайте, как прокачаться и в других специальностях или освоить их с нуля:

Другие профессии и курсы

ПРОФЕССИИ

КУРСЫ



Перейти в источник

Похожие статьи

О классах Program и Startup — инициализация ASP.NET приложения. Часть II: IWebHostBuilder и Startup / Хабр

0 Favorite [ Введение Это – продолжение статьи, первая часть которой была опубликована ранее. В той части был рассмотрен процесс инициализации, общий для любого приложения…

Цифровая трансформация офисной печати от зарождения до современных технологий

0 Favorite [ СодержаниеГлава №1. Краткая история зарождения офисной печати1.1. Пионеры1.2. ЭнтузиастыГлава №2. От CapEx к MPS и далее к DaaS2.1. Капитальные расходы (CapEx)2.2. Управляемые…

Инвентаризация ИТ-активов штатными средствами Windows с минимальными правами доступа

0 Favorite [ Коллеги, в предыдущей статье мы обсудили принципы эффективной работы с событиями аудита ОС Windows. Однако, для построения целостной системы управления ИБ важно…

Ответы