Служба работала ровно и вдруг перестала отвечать: процесс числится в памяти, порт открыт, но соединения не обслуживаются, журнал не растёт. Ни аварийного завершения, ни внятного сообщения в логах, ни подсказки от системы мониторинга. Знакомая сцена для любого администратора Linux. Вместо перезапуска вслепую существует честный путь: подключить трассировку к живому процессу по его идентификатору и посмотреть, что именно программа просит у ядра прямо сейчас. Утилита strace печатает каждый системный вызов построчно, и по рисунку вызовов туманное "зависло" превращается в конкретную строку с именем операции, дескриптором и кодом ошибки. Маршрут диагностики складывается сам: сначала оценивается состояние процесса, затем подключается трасса на несколько секунд, собирается сводка, распознаётся типовая картина, через дескрипторы выходим на ресурс и заканчиваем осмысленным действием вместо слепой попытки.
Состояние зависшего процесса в ps top и файлах proc перед подключением трассировки
Перезапуск стирает картину, поэтому диагностику начинают со снимка состояния. Ключевая развилка одна: процессор занят самим процессом или процесс стоит на вызове. Утилита ps отвечает мгновенно:
ps -o pid,ppid,stat,%cpu,etime,cmd -p 1234
Колонка STAT коротко описывает положение задачи. Буква R означает runnable: ядро держит процесс готовым к исполнению, процессор занят самим процессом. Если зависшая служба показывает R и загрузку около ста процентов, перед нами цикл, и трассировка покажет его содержимое. Буква S означает прерываемое ожидание: процесс стоит на вызове и проснётся, когда ядро вернёт результат или придёт сигнал. Буква D описывает непрерываемое ожидание, чаще всего ввод и вывод на накопителе или сетевой файловой системе; такой процесс не реагирует даже на сигналы, пока вызов не завершится сам.
Файловая система proc уточняет картину без обращения к внутренностям программы:
grep State /proc/1234/status
cat /proc/1234/wchan
Первая команда повторяет состояние словами, вторая называет функцию ядра, на которой стоит ожидание. Не на всех сборках ядра файл wchan показывает имя, иногда он печатает ноль, и это тоже ответ: ожидание есть, имя скрыто конфигурацией ядра. Стек на стороне ядра раскрывает файл /proc/1234/stack, для чтения нужны права root. Для многопоточной службы утилита ps с ключом -T показывает потоки по отдельности:
ps -T -o pid,spid,stat,%cpu,cmd -p 1234
Так видно, какой именно поток загружает процессор, а какой спит. Идентификатор главного процесса подскажет systemctl status app: команда печатает основной идентификатор и перечень процессов службы. Развилка определена, дальше подключается трасса.
Подключение strace к живому процессу по идентификатору и чтение потока вызовов
У трассировки два старта: запуск программы под наблюдением с первой инструкции и подключение к уже работающему процессу. Для зависаний важен второй путь: процесс живёт в памяти со всем состоянием, и картину не нужно воспроизводить заново. Подключение выполняется ключом -p с правами root:
sudo strace -p 1234
Утилита печатает строку о подключении и открывает поток вызовов. Каждая строка читается одинаково: имя вызова, аргументы в скобках, знак равенства и результат. Нескольких секунд наблюдения достаточно, чтобы увидеть рисунок целиком; отключение по Ctrl+C не меняет состояние процесса, он продолжает работу без наблюдателя. Для многопоточной службы ключ -f собирает вызовы всех потоков, иначе трасса покажет только один из них. Метки времени и длительности превращают поток в хронологию:
sudo strace -f -tt -T -p 1234
15:12:40.118209 futex(0x7f2c98002b48, FUTEX_WAIT_PRIVATE, 0, {tv_sec=0, tv_nsec=1000000}) = 0 <0.001018>
15:12:40.119301 read(4, 0x7f2c981f39c0, 4096) = -1 EAGAIN (Resource temporarily unavailable) <0.000012>
15:12:40.119340 clock_gettime(CLOCK_MONOTONIC, {tv_sec=73301, tv_nsec=441441209}) = 0 <0.000009>
Правило чтения простое: вызов без строки результата означает, что процесс стоит на нём прямо сейчас. Длительность в угловых скобках описывает завершившийся вызов; у последнего незавершённого вызова длительности нет, зато метка времени называет момент начала ожидания. Длинную трассу направляют в файл ключом -o, а ключ -s 256 снимает урезание строковых аргументов до размера по умолчанию:
sudo strace -f -tt -T -s 256 -o /tmp/trace-app.txt -p 1234
Ограничения лучше знать заранее. Утилита работает через механизм ядра ptrace: каждый вызов наблюдаемого процесса ядро останавливает дважды, на входе и на выходе, поэтому активная служба замедляется, и окно наблюдения держат коротким. Подключение к чужому процессу требует прав root или полномочия CAP_SYS_PTRACE, а настройка yama в файле /proc/sys/kernel/yama/ptrace_scope способна ограничить трассировку даже для собственных процессов. Сообщение Operation not permitted означает не поломку утилиты, а действие правил доступа. Если процесс стоит в состоянии D, подключение может не завершиться, пока не завершится сам вызов: трассировка ждёт остановки, а остановки нет.
Сводный режим strace с распределением вызовов и ошибок за короткое окно
Живой поток информативен, но активная служба выдаёт тысячи вызовов в секунду, и глаза разбегаются раньше, чем складывается картина. Сводный режим собирает статистику вместо строк: ключ -c печатает таблицу по завершении сеанса. Окно ограничивают утилитой timeout, имитирующей Ctrl+C сигналом INT:
sudo timeout -s INT 10 strace -c -w -p 1234
Через десять секунд strace отключится сам и напечатает примерно такую сводку:
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
78.14 7.812000 1110 7040 0 futex
9.62 0.961000 616 1560 1560 read
4.71 0.470000 734 640 640 openat
3.08 0.308000 307 1003 0 clock_gettime
2.45 0.245000 490 500 0 write
2.00 0.200000 6060 33 0 epoll_wait
------ ----------- ----------- --------- --------- ----------------
100.00 9.996000 10776 2200 total
Колонки читаются прямо: calls показывает интенсивность, errors считает неудачи, seconds распределяет время между вызовами. В примере futex забрал больше трёх четвертей времени, и перед нами ожидания блокировок; read с ошибкой на каждом вызове означает цикл опроса дескриптора в неблокирующем режиме; openat со стопроцентными ошибками ведёт к отсутствующему файлу. Ключ -w в команде заставляет сводку считать время по часам, а не по тратам процессора, и долгие блокировки не прячутся за короткими вычислениями. Сводка не ставит диагноз сама, она выбирает следующий шаг: лидер таблицы подсказывает имя вызова или группу для точечного фильтра вроде -e trace=%network для сокетов или -e trace=%file для файловых операций, а фильтр с отрицанием вида -e trace=!futex убирает шум из трассы.
Типовые картины зависания в потоке вызовов и их однозначная расшифровка
Картины зависания повторяются настолько регулярно, что по нескольким строкам опытный администратор называет причину раньше, чем успевает открыться журнал. Ниже собраны главные рисунки и их значения.
Первая картина - бесконечный цикл мелких вызовов без продвижения. Поток состоит из коротких повторов: read возвращает -1 EAGAIN, clock_gettime отмечает время, nanosleep выдерживает паузу, futex коротко спит и снова read. Промежутки между вызовами крошечные, процессор загружен под сто процентов, состояние процесса в ps держится на R. Приложение крутит цикл ожидания события, которое никогда не наступит: флага в памяти, записи в очереди, изменения файла. Внешние ресурсы ни при чём, причина внутри самого приложения.
Вторая картина - блокирующее чтение на дескрипторе. Последняя строка трассы остаётся без результата: read, recvfrom или readv начался и не вернулся. Метка времени называет момент старта ожидания, вычитание из текущего времени даёт длительность. Процесс стоит на вызове, потому что внешний ресурс молчит: сокет без ответа, канал без пишущей стороны, специальный файл без данных. Дальше вопрос один: что за дескриптор.
Третья картина - ожидание на блокировке. Вызов futex с аргументом FUTEX_WAIT без возврата означает, что поток ждёт освобождения блокировки внутри приложения. Если два потока удерживают по блокировке и ждут друг друга либо держатель сам завис, очередь не рассосётся никогда: перед нами тупик внутри приложения. Быстрое средство одно - перезапуск, настоящее лечение - в коде. Уточняющий вариант: futex возвращается с ошибкой ETIMEDOUT и тут же вызывается снова, значит блокировка с таймаутом не берётся бесконечно, и приложение честно повторяет попытки.
Четвёртая картина - чтение с нулевым результатом и повторами. Строки вида read(6, ...) = 0 идут бесконечной чередой: пишущая сторона канала или сокета закрылась, ядро возвращает конец потока, а приложение трактует ноль как повод попробовать снова. Цикл съедает процессор, журналы молчат, служба выглядит зависшей, хотя формально она чрезвычайно активна.
Пятая картина - ожидание ответа сети. Цепочка socket, connect, sendto прошла успешно, и трасса упирается в recvfrom без возврата: соединение установлено, запрос отправлен, удалённая сторона молчит. Вариант тяжелее - зависший connect: соединение не подтверждается, узел недоступен или пакеты отбрасываются по пути. Шестая картина - ожидание блокировки файла: вызов flock или fcntl с командой F_SETLKW без возврата означает, что файл удерживает другой процесс. Утилита fuser по имени файла покажет держателя, и дальнейший выбор очевиден.
Отдельный случай - состояние D. Если подключение трассировки не проходит или подвисает, а ps показывает D, процесс стоит в непрерываемом ожидании ввода и вывода, и причину ищут на уровне накопителя, сети или драйвера, а не приложения.
Поиск ресурса ожидания через lsof и каталог открытых дескрипторов
Имя вызова отвечает на вопрос "что делает процесс", число дескриптора - на вопрос "на чём". Утилита списка открытых файлов lsof и каталог /proc/1234/fd переводят число из строки трассы в конкретный ресурс:
sudo lsof -p 1234
Вывод содержит колонки FD, TYPE и NAME, по которым читается характер ресурса:
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
app 1234 app 3u IPv4 21014 0t0 TCP 10.0.0.5:33412->10.0.0.8:5432 (ESTABLISHED)
app 1234 app 6r REG 8,1 52428800 1044 /var/lib/app/queue.dat
app 1234 app 8r FIFO 0,13 0t0 8831 pipe
app 1234 app 9u unix 0xd3f1a2c0 0t0 8832 /run/app.sock type=STREAM
Строка read(6, ...) из трассы указывает на файл queue.dat, recvfrom(3, ...) - на соединение с узлом 10.0.0.8 на порту 5432, read(8, ...) - на канал pipe, дескриптор 9 - на локальный сокет службы. Тип ресурса задаёт направление: сокет отправляет к сетевой диагностике, канал - к проверке пишущей стороны, файл - к содержимому и правам. Пометка deleted в имени описывает файл, стёртый после открытия: дескриптор жив, имени в каталоге уже нет, и служба, пишущая в удалённый журнал, создаёт именно такую картину.
Тот же ответ даёт каталог процессов без утилит:
ls -l /proc/1234/fd
readlink /proc/1234/fd/3
readlink раскрывает, куда ведёт дескриптор: socket для сокета, pipe для канала, прямой путь для файла. Ещё короче работает ключ -y утилиты strace: он приписывает путь прямо в строку вызова, а ключ -yy добавляет сведения о сокетах и устройствах:
sudo strace -y -p 1234
read(6</var/lib/app/queue.dat>, 0x7ffd1c0a9b10, 8192) = 8192
Сопоставление трассы и дескрипторов закрывает половину диагностики: остаётся проверить найденную сторону ресурса.
Сетевые ожидания и проверка удалённой стороны без остановки службы
Если дескриптор оказался сокетом, вопрос переходит к соединению: живо ли оно, кто на другом конце и где теряется ответ. Утилита ss показывает сокеты процесса с адресами и состояниями:
sudo ss -tanp | grep 1234
ESTAB 0 0 10.0.0.5:33412 10.0.0.8:5432 users:(("app",pid=1234,fd=3))
Состояние ESTAB означает рабочее соединение: запрос ушёл, ответа нет, и проверять нужно удалённую сторону. Состояние CLOSE-WAIT означает, что удалённая сторона закрыла соединение, а приложение не закрыло свой конец и не заметило конца потока: тот самый дефект, который проявляется чтением с нулевым результатом. Состояние SYN-SENT описывает зависший connect: подтверждение не приходит, узел недоступен или пакеты отбрасывает фильтр на пути.
Дальше проверяют дорогу до удалённой стороны. Утилита ping подтверждает достижимость узла, nc -vz 10.0.0.8 5432 проверяет, принимает ли удалённый порт соединение, без передачи данных:
nc -vz 10.0.0.8 5432
Если порт отвечает, а зависший процесс всё равно ждёт, смотрят счётчики правил фильтрации пакетов: утилиты iptables и nftables показывают, сколько пакетов совпало с каждым правилом, и молчаливое отбрасывание видно по растущему счётчику. Для тонких случаев подходит tcpdump, перехватывающий пакеты на сетевом интерфейсе:
sudo tcpdump -i eth0 host 10.0.0.8 and port 5432
Перехват ответит, уходят ли запросы и возвращаются ли ответы. Параллельно смотрят на удалённую сторону: принимает ли её служба соединения от соседних клиентов, растёт ли её журнал. Если у соседей всё работает, а один процесс ждёт вечно, вопрос в соединении или в приложении; если удалённая служба молчит для всех, вопрос в ней самой.
Действия после диагноза от контролируемого перезапуска до таймаутов
Диагноз без действия бесполезен, действие без диагноза опасно. Порядок шагов подсказывает сама картина наблюдений:
- сохранить трассу, сводку и список дескрипторов в файл, собрав протокол наблюдения до любых изменений;
- сверить картину с типовыми сценариями и назвать вызов, дескриптор и ресурс ожидания;
- для тупика внутри приложения выполнить перезапуск средствами системы инициализации, дождавшись аккуратной остановки;
- для сетевого ожидания проверить удалённую сторону и счётчики фильтрации, не трогая саму службу;
- задать таймауты в приложении там, где ожидание было бесконечным: на соединение, на чтение сокета, на блокировки;
- обрабатывать нулевое чтение как конец потока вместо слепого повтора, чтобы цикл не собрался снова.
Протокол наблюдения сохраняют первым шагом не из педантизма: после перезапуска картина исчезает вместе с состоянием процесса, и собранные трассы останутся единственным вещественным следом. Файл трассы, таблица сводки и список дескрипторов позволяют сравнить следующее зависание с предыдущим и отличить случайность от закономерности.
Перезапуск выбирают контролируемый. Команда systemctl restart app отправляет службе сигнал завершения, даёт ей время закончить текущие операции и запускает её заново; аккуратно написанная служба по сигналу SIGTERM доделывает активные запросы и закрывает соединения. Сигнал SIGKILL означает принудительное завершение без возможности очистки: ядро освободит ресурсы, но приложение не допишет буферы, поэтому к такому варианту прибегают после отказа от мягкой остановки. Команда systemctl reload app, если единица службы поддерживает её, перечитывает конфигурацию без остановки и выручает, когда причина найдена в настройке.
Таймауты в приложении превращают вечное ожидание в обычную ошибку с обработкой. Подключение к базе, чтение сокета, взятие блокировки: у каждой операции разумно задать предельное время, после которого приложение вернёт ошибку, запишет её в журнал и решит, повторять попытку или отказаться. Отсутствие таймаутов и превращает медленную удалённую сторону в зависшую службу: у процесса без ограничений нет причины выйти из ожидания.
Сетевые ожидания лечатся диагностикой сети: проверкой достижимости узла, состояний сокетов, счётчиков фильтрации и поведения удалённой службы. Перезапуск здесь снимает симптом, но не причину: соединение соберётся заново и упрётся в то же молчание.
strace не исправляет зависания и не заменяет отладчик. Его сила в честности: утилита показывает ровно то, что процесс просит у ядра, без домыслов. Несколько секунд живой трассы, короткая сводка, взгляд на дескрипторы - и вместо слова "зависло" в отчёте появляется имя вызова, номер дескриптора и адрес ресурса. За этими тремя строчками стоит конкретное действие: исправить цикл, дождаться внешний ресурс, освободить блокировку, поднять таймауты. Такой диагноз занимает минуты, а служба после него чинится с первого раза, а не с третьего.