24. strace, ltrace и чтение поведения процесса
Зачем лезть в процессы без gdb?
Когда бэкенд-приложение виснет, медленно отвечает или потребляет память, логи часто молчат. Они не показывают, какие системные ресурсы блокируют поток, или почему процесс не возвращается с сетевого вызова.
"Логи программы — это всего лишь дневник ее благих намерений. Когда процесс намертво зависает, он уже не может ничего записать в лог. Но ядро операционной системы помнит каждый сделанный шаг и никогда не врет."
Здесь на сцену выходят strace и ltrace — инструменты отладки на уровне ядра и динамической линковки, которые позволяют увидеть реальный диалог процесса с ОС.
Для Go-разработчика понимание этих утилит критично, потому что Go скрывает от нас нативные потоки, планирует горутины в пользовательском пространстве и по умолчанию линкуется статически. Это ломает привычные паттерны отладки из C/C++ или Java. Запустить отладчик gdb на боевом сервере под нагрузкой — верный способ уронить сервис. А вот умение быстро снять трассировку системных вызовов — ключевой навык инженера при разборе инцидентов.
strace: Трассировка системных вызовов
strace перехватывает все системные вызовы (syscalls) и сигналы, передаваемые процессом ядру. Это самый надежный способ понять, с чем процесс взаимодействует на уровне ОС.
Принцип работы и ключевые флаги
# Запуск нового процесса с трассировкой
strace -f -T -s 256 -o app.strace ./myapp
# Подключение к уже работающему процессу
strace -p 12345 -e trace=network,read,write -c
# Фильтрация только сетевых и файловых вызовов
strace -e trace=network,read,write,epoll_pwait -p $(pidof myapp)
| Флаг | Назначение |
|---|---|
-f |
Отслеживать fork/exec дочерних процессов (критично для Go) |
-T |
Показывать время выполнения каждого syscall в микросекундах |
-c |
Суммарная статистика (count, time, errors) после завершения |
-s |
Максимальная длина выводимой строки (по умолчанию 32 символа) |
-e |
Фильтр вызовов (например, trace=network или trace=epoll_pwait) |
Под капотом: ptrace и цена перехвата
strace работает через системный вызов ptrace(2). Это не магия, а официально поддерживаемый ядром механизм отладки (сродни тому, как работает GDB или Go debugger dlv).
- Трассировщик вызывает
ptrace(PTRACE_ATTACH, pid, 0, 0). - Ядро ставит трассируемый процесс на паузу и посылает ему сигнал
SIGSTOP. straceиспользуетptrace(PTRACE_SYSCALL, ...)для установки точки останова на каждом входе и выходе из syscall.- При вызове
syscallпроцесс прерывается, управление передаетсяstrace. Тот снимает регистры, печатает вызов, восстанавливает контекст и разрешает выполнение дальше.
; x86-64 syscall entry
mov rax, <номер_вызова>
mov rdi, <аргумент_1>
mov rsi, <аргумент_2>
; ...
syscall
; Возврат: результат в rax
sequenceDiagram
autonumber
participant Target as Целевой процесс (Go)
participant Kernel as Ядро Linux
participant Strace as Утилита strace
Target->>Kernel: Инструкция SYSCALL (например, read)
Note over Kernel: Точка останова PTRACE_SYSCALL
Kernel->>Target: Остановка потока (SIGTRAP)
Kernel->>Strace: Уведомление о входе в syscall
Strace->>Kernel: Чтение регистров RAX, RDI, RSI...
Strace->>Strace: Форматирование строки: read(3, buf, 4096)
Strace->>Kernel: Возобновить выполнение (PTRACE_SYSCALL)
Kernel->>Kernel: Реальное выполнение системного вызова
Note over Kernel: Точка останова при возврате
Kernel->>Strace: Уведомление о выходе из syscall
Strace->>Kernel: Чтение результата из RAX (4096)
Strace->>Strace: Допечатать: = 4096 <0.000045>
Strace->>Kernel: Возобновить работу процесса
Kernel->>Target: Возврат в User Space
ltrace: Трассировка библиотечных вызовов
ltrace перехватывает вызовы динамических библиотек (libpthread, libc, libssl и т.д.). Как и strace, он работает через ptrace (и требует тех же прав на трассировку процесса), но перехватывает не системные вызовы, а вызовы библиотечных функций.
Принцип работы
ltrace не перехватывает системные вызовы, а ловит вход в библиотечные функции, используя ptrace и точки останова:
- При старте процесса
ld.soзагружает нужные.soфайлы. ltraceчерезptrace(PTRACE_TRACEME/PTRACE_ATTACH) берёт процесс под контроль.- Он находит в ELF-бинарнике таблицы импорта (
.rela.plt) и ставит точки останова (int 3) на записи PLT. - Когда процесс натыкается на точку останова, ядро останавливает его и будит
ltrace; тот логирует вызов, возвращает исходную инструкцию и продолжает процесс.
ltrace -e malloc,free,calloc ./myapp
ltrace -c ./myapp # Статистика по библиотекам
flowchart LR
subgraph DynamicApp["Динамический бинарник (C/Python/Ruby)"]
CallPLT["Вызов malloc()"] --> PLT[".plt секция (Trampoline ltrace)"]
PLT --> Intercept["Логирование в ltrace"]
Intercept --> RealLib["Реальный вызов malloc() в libc.so"]
end
subgraph GoApp["Go бинарник (Статическая линковка)"]
GoCall["Вызов runtime.mallocgc()"] --> Direct["Прямой CALL по фиксированному адресу"]
Direct --> InternalAlloc["Встроенный аллокатор Go (нет PLT/GOT)"]
end
style DynamicApp fill:#1e293b,stroke:#818cf8,color:#fff
style GoApp fill:#064e3b,stroke:#34d399,color:#fff
Почему ltrace почти бесполезен в Go?
Go-компилятор по умолчанию создает статически линкованные бинарники (-linkmode=external или встроенные библиотеки). Это означает:
- Нет динамических символов:
ld.soне знает, какие функции нужно перехватывать. - CGO меняет правила: Если приложение использует CGO,
ltraceможет видеть вызовы C-библиотек, но не видит вызовы внутри Go-рантайма (runtime.malloc,netpoll,epoll_pwait). - Нет PLT:
ltraceработает черезptraceи точки останова на записях PLT (механизмLD_PRELOADон не использует). У статически собранного Go-бинарника PLT нет, а собственный аллокатор Gomallocиз libc не вызывает.
Практический пайплайн отладки для Backend
Как Senior-инженер, я не запускаю strace наугад. Я использую его как диагностический скальпель:
1. Поиск блокировок и hang-состояний
Если сервис не отвечает, подключаемся к PID и смотрим, где он застрял:
strace -p $(pidof myapp) -T -e trace=epoll_pwait,futex,read,write
- Если видим
epoll_pwait(-1, ...)-> это обычно штатное ожидание: простаивающие потокиMспят вepoll_pwait(с таймаутом до ближайшего таймера или-1). Сам по себе такой вызов не признак багаnetpoller. - Если видим
futex(0x..., FUTEX_WAIT, ...)-> чаще всего это штатный сон простаивающих потоковM. Блокировку горутины наsync.Mutexили канале паркует планировщик Go без системного вызова, так что поfutexо захваченном мьютексе судить нельзя; для этого используйте профилиmutex,blockиgoroutine. - Если видим
recvfrom(0, ...)-> процесс ждет данные из сокета.
2. Анализ дисбаланса IO
strace -c -p PID
Вывод покажет, на какие вызовы уходит 90% времени. Если write или read доминируют — проблема в синхронном IO или отсутствии буферизации. Если epoll_pwait занимает много времени — проверьте, не переключает ли Go слишком много горутин в user-space без реального ожидания.
3. Сетевые проблемы
strace -e trace=network -p PID
Покажет connect, sendto, recvfrom, setsockopt. Если видим ECONNREFUSED или ETIMEDOUT — проблема на уровне сети или TIME_WAIT/CLOSE_WAIT состояний (см. 50. TIME_WAIT, CLOSE_WAIT и проблемы сокетов).
4. Альтернативы для production
strace слишком тяжел для боевого продакшена. Вместо него используют:
perf trace/bpftrace— перехват через eBPF, минимальный оверхед (~0.1–1%).cat /proc/PID/syscall— быстрый снимок текущего состояния без отладчика.gdb -p PID -batch -ex "info goroutines"— для Go-специфичной диагностики.
Итог
strace и ltrace — это диагностические инструменты, которые показывают "честный" слой взаимодействия приложения с ОС. Для Go-разработчика важно помнить:
straceвидит только ОС-потоки (M), а не горутины. Планирование горутин происходит в user-space и невидимо для трассировщика.- Оверхед
ptraceделаетstraceинструментом для разработки и staging, а не для production-мониторинга. ltraceбесполезен для чистого Go из-за статической линковки и собственного рантайма.- Комбинация
strace -T+pprof+netpollerлогирования дает полную картину: от системного уровня до Go-рантайма.
Мы разобрали, как наблюдать за процессом на уровне ядра. Но чтобы понять, почему процесс делает именно эти вызовы, нужно знать, как ОС управляет ресурсами, особенно файлами и дескрипторами. В следующей статье мы закроем фундаментальный концепт Linux: 25. Файловые дескрипторы. Все в Linux является файлом.