open atlas
↑ К треку
Инженерная практика ENG · 08 · 03

Прод-отладка без repro: perf, eBPF, flame graph и почему ptrace останавливает мир

У зависшего прод-процесса нет repro, и его нельзя остановить. Сэмплирующие инструменты — perf, eBPF/bpftrace, async-profiler — наблюдают при 1-5% оверхеда без остановки; ptrace attach останавливает процесс. USDT-пробы и flame graph находят горячий путь на живом сервисе.

ENG Senior ◷ 17 min
Уровень
ОсновыJuniorMiddleSenior

Один под был пригвождён к 100% CPU, обслуживал запросы, но медленно, и так уже час. Инстинкт on-call инженера — приаттачить gdb и снять стек — и сеньор остановил его руку. «Этот ptrace attach остановит процесс. Он принимает трафик; ты заморозишь живой сервер, и балансировщик навалит тысячу запросов на его соседей, пока ты читаешь один стек». Вместо этого они запустили perf record -F 99 -p <pid> на десять секунд — сэмплируя работающий процесс 99 раз в секунду при примерно 2% оверхеда, ни разу не останавливая, — и свернули результат в flame graph. Широкое плато наверху было безошибочным: 80% CPU в компиляции регэкспа, вызываемой на каждый запрос, потому что ключ кэша считался неверно и скомпилированный паттерн ни разу не переиспользовался. Они ни разу не остановили процесс, ни разу не потеряли запрос и получили ответ меньше чем за минуту. Урок, что унёс джун: в проде первый вопрос не «что за баг», а «могу ли я это наблюдать, не останавливая мир».

Прод-ограничение: нельзя остановить мир

Каждая техника отладчика из прошлых уроков предполагает, что можно остановить процесс. В проде это предположение переворачивается. Классический интерактивный отладчик аттачится через ptrace, а ptrace attach (PTRACE_ATTACH, что делают gdb и Delve) останавливает каждый поток цели, пока не отцепишься. На живом сервере, принимающем трафик, это катастрофа: запросы встают в очередь или таймаутят, health-чеки падают, балансировщик выбрасывает инстанс и наваливает на соседей, а автоскейлер может убить «нездоровый» под, который ты отлаживал — уничтожив живое состояние, ради чтения которого ты приаттачился. Поэтому прод-тулкит построен на другом принципе: наблюдать, не останавливая. Ты сэмплируешь или трассируешь процесс на ходу, платя несколько процентов оверхеда, и реконструируешь, что он делает, из этих сэмплов, а не замораживаешь его для инспекции.

Есть и вторая причина, почему ptrace — неверный дефолт в проде: он одноцелевой и интрузивный, тогда как вопросы обычно статистические — «куда уходит CPU», «какой syscall медленный», «какой путь аллоцирует» — на которые куда лучше отвечает агрегация тысяч дешёвых сэмплов, чем один замороженный снимок.

Сэмплирование vs инструментирование: perf, eBPF, async-profiler

Когда берёшь прод-инструмент, первый выбор — сэмплировать или инструментировать — определяет, предсказуем ли твой оверхед или ты случайно добьёшь сервис, который пришёл диагностировать. Ошибиться здесь — значит превратить «быстрый взгляд» в самостоятельный инцидент.

Низкооверхедный тулкит делится на сэмплирование против инструментирования. Сэмплирование прерывает процесс на фиксированной частоте — perf record -F 99 берёт 99 сэмплов стека в секунду на CPU — и выводит, куда уходит время, из распределения; оверхед примерно 1-5%, ведь большинство циклов идут нетронутыми и стоит лишь периодическое прерывание. Инструментирование цепляется к конкретным событиям — входу функции, syscall, tracepoint — и работает на каждом возникновении; дёшево на событие, но дорого, если событие горячее. perf, сэмплер ядра, профилирует CPU аппаратными счётчиками производительности. eBPF (extended Berkeley Packet Filter — механизм ядра Linux для запуска верифицированных мини-программ без модулей; управляемый bpftrace) запускает верифицированную, песочную программу в ядре на выбранных событиях, агрегируя в ядре, так что даже трассировка на событие остаётся дешёвой — bpftrace -e 'tracepoint:syscalls:sys_enter_read { @[comm] = count(); }' считает чтения по процессам с пренебрежимой ценой, без остановки процесса, без модуля ядра. Для управляемых рантаймов async-profiler сэмплирует JVM (или похожее) сигнальным или perf-events механизмом, который, в отличие от наивного обхода стека JVMTI, не страдает safepoint-смещением (safepoint — точка в байт-коде JVM, где поток безопасно остановить; наивные профилировщики сэмплируют только в них, занижая горячесть циклов) — он сэмплирует потоки в произвольных точках, а не только там, где рантайм их паркует, поэтому flame graph отражает реальные горячие пути, а не локации safepoint.

Викторина

Под пригвождён к 100% CPU и обслуживает живой трафик. On-call хочет приаттачить gdb, чтобы снять стек. Почему это неверный первый ход и что лучше?

Почему это работает

Почему сэмплирование на 99 Гц так надёжно выявляет горячий путь, глядя на процесс лишь ~99 раз в секунду? Потому что горячесть статистична. Если 80% времени CPU тратится в одной функции, то по тысячам сэмплов примерно 80% поймают стек внутри этой функции — широкое плато на flame graph. Ты не пытаешься увидеть каждую инструкцию; ты оцениваешь распределение, а распределение сходится быстро с дешёвыми сэмплами. Нечётное 99 (не 100) избегает синхронизации с таймерами, тикающими на круглых частотах, что систематически промахивались бы или перечитывали периодическую работу. Дешёвое, слегка-иррациональное сэмплирование бьёт дорогую исчерпывающую трассировку для вопроса «куда уходит время».

USDT-пробы и flame graph: находим путь на живом

Два куска превращают сэмплы в ответ. Flame graph агрегируют тысячи сэмплов стека в одну картину: каждый прямоугольник — функция, ширина — доля сэмплов (время), сложены по глубине вызова. Широкий прямоугольник у вершины — туда CPU реально уходит — плато компиляции регэкспа из Hook считывается одним взглядом, без шагов. USDT-пробы (userland statically-defined tracing) — нулевого-оверхеда-когда-выключены маркеры, вкомпилированные в бинарь (базы данных, рантаймы языков, твой сервис), к которым eBPF/perf цепляются в именованных, семантически осмысленных точках — «commit транзакции», «старт запроса» — давая стабильное инструментирование без изменения или остановки процесса. Вместе они дают ответить «что этот зависший процесс делает прямо сейчас» на живом, нетронутом сервисе: сэмплируй его, сверни в flame graph, прочти горячий путь и подтверди USDT или kernel-tracepoint, что подозреваемый путь — тот, что срабатывает. Всё расследование стоит несколько процентов CPU и ноль даунтайма — единственный вид отладки, что разрешает прод-SLA.

Викторина

Ты хочешь посчитать, как часто срабатывает конкретный syscall на всех процессах загруженной прод-ноды, с минимальным оверхедом и без остановок процессов. Какой подход подходит и почему?

Вспомните перед уходом
  1. 01
    Почему ptrace/gdb attach — неверный дефолт в проде и что его заменяет?
  2. 02
    Различи сэмплирование и инструментирование и объясни, как flame graph и USDT-пробы находят горячий путь на живом.
Итог

Прод-отладка переворачивает ключевое предположение интерактивного отладчика: нельзя остановить мир. ptrace attach — PTRACE_ATTACH, что делают gdb и Delve — останавливает каждый поток цели до отцепления, и на живом сервере, принимающем трафик, это значит запросы в очереди, упавшие health-чеки, балансировщик, выбрасывающий инстанс и наваливающий на соседей, и автоскейлер, потенциально убивающий тот самый под, чьё живое состояние ты приаттачился прочесть. ptrace также одноцелевой и интрузивный, когда реальные прод-вопросы статистические: куда уходит CPU, какой syscall медленный, какой путь аллоцирует. Поэтому прод-тулкит наблюдает без остановки. Сэмплирование прерывает процесс на фиксированной частоте — perf record -F 99 берёт 99 сэмплов стека в секунду — при примерно 1-5% оверхеда, ведь большинство циклов идут нетронутыми и стоит лишь периодическое прерывание; оно оценивает распределение, что сходится быстро, и нечётное 99 избегает синхронизации с таймерами круглых частот. Инструментирование цепляется к конкретным событиям и работает на каждом, точно для счёта, но дорого на горячих событиях; eBPF, управляемый bpftrace, запускает верифицированные песочные программы на событиях ядра и агрегирует в ядре, так что даже трассировка на событие остаётся дешёвой, ничего не останавливает и не нужен модуль ядра. async-profiler сэмплирует управляемые рантаймы вроде JVM без safepoint-смещения, поэтому flame graph отражают реальные горячие пути, а не локации safepoint. Два куска превращают сэмплы в ответ: flame graph сворачивают тысячи стеков в одну картину, где ширина прямоугольника — доля времени, поэтому самое широкое плато наверху — туда CPU уходит — компиляция регэкспа на каждый запрос из Hook, прочитанная взглядом; и USDT-пробы, нулевой-цены-когда-выключены маркеры, вкомпилированные в осмысленных точках, дают eBPF/perf подтвердить подозреваемый путь на живом, нетронутом сервисе. Всё расследование стоит несколько процентов CPU и ноль даунтайма — единственная отладка, что разрешает прод-SLA, и причина, почему первый прод-вопрос не что за баг, а могу ли я это наблюдать, не останавливая мир.

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 5 завершено

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.