Статьи

Отладка застрявших процессов Ruby: что делать, прежде чем убить -9

Логотип RailsConf 2013RailsConf 2013 начался! Чтобы отпраздновать это, мы публикуем серию постов в блоге, в которых рассказывается о том, что нового и интересного в мире поддержки Ruby в New Relic. Не забудьте проверить всю серию до сих пор:  перекрестная трассировка приложенийпрофилирование потоковжизнь на грани с Rails 4 и Ruby 2API-интерфейсы Thread Safe и поддержка Sidekiq для ваших потоков , а также  баннеры безопасности (для, если вы забыли Примените последний патч CVE) .

Если вы потратили достаточно времени на работу с производственной системой (или даже просто с сервером непрерывной интеграции), вы наверняка столкнулись с тем, что процесс Ruby «завис». Вы знаете тип — не сбой, но и никакого реального прогресса. И не дает вам очевидного указания на то, что он делал.

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

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

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

Вот шаги, которые я выполняю при отладке зависшего процесса Ruby:

Кто что сказал?
Во-первых, точно выясните, какой процесс застрял. Это звучит тривиально, но это особенно важно для таких систем, как Unicorn , Passenger , Resque и т. Д., Где система состоит из нескольких процессов, которые выполняют разные роли (часто это один главный процесс плюс несколько рабочих).

Важно точно определить, какой процесс застрял, чтобы получить полную картину поведения системы в целом. Утилита trusty ps — это обычно моя первая остановка — быстрый `ps aux | grep <имя процесса> `.

Мне нравится хранить расшифровку команд, которые я выполняю, а также их вывод. И вывод ps aux обычно является первым, что входит в этот транскрипт.

Инструменты, такие как htop и pstree, также могут быть полезны здесь. Они дают вам визуальное дерево отношений между застрявшим процессом, его родным братом и его родительским процессом (например, мастер Unicorn и его рабочие).

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

* Куда отправляется STDOUT зависшего процесса?

* Куда отправляется STDERR застрявшего процесса?

* Существуют ли другие файлы журнала, созданные застрявшим процессом? Проверьте командную строку и файл конфигурации, используемый для процесса. Или указали путь журнала?

* Существуют ли другие файлы журналов, созданные процессами, связанными с зависшим процессом?

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

* Получить список открытых файлов и сокетов из процесса, используя lsof -p <PID>

* Если вы работаете в Linux, покопайтесь в / proc / <PID> и рассмотрите возможность сбора:

* cmdline содержит командную строку, которая использовалась для запуска процесса

* cwd — ссылка на текущий рабочий каталог процесса

* environment — список переменных среды, установленных для процесса

Проверьте использование ЦП застрявшего процесса. Существует два основных варианта зависаний: статический и динамический. (Примечание: это моя собственная терминология, но я думаю, что это полезная таксономия.) При статическом зависании потоки застрявшего процесса застряли в одном и том же точном состоянии на длительный период времени. В качестве примеров можно привести традиционные тупики и процессы, заблокированные на длительные периоды в системных вызовах (часто такие вещи, как «чтение», «выбор» и т. Д.). При динамическом зависании потоки застрявшего процесса меняют состояние, но попадают в какой-то цикл, который мешает им прогрессировать (это иногда называют «живой блокировкой»).Замки

Методы, используемые для отладки двух типов зависаний, различны. Так что полезно различать их в дикой природе. Быстрая эвристика для этого состоит в том, чтобы проверить использование процессором застрявшего процесса. Статические зависания обычно приводят к процессам без использования ЦП, тогда как динамические зависания приводят к процессам с ненулевым использованием ЦП (часто очень высоким, если процесс застрял в узком цикле).

Существует множество инструментов, которые вы можете использовать для этого, но часто достаточно быстрого просмотра `top` или` ps -o cpu <pid> `.

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

Существует множество различных инструментов для достижения этой цели, в зависимости от платформы, на которой вы работаете, и используемой вами реализации Ruby, но я собираюсь сосредоточиться на том, что, пожалуй, является наиболее распространенным случаем: MRI в Linux хост.

`gdb` — это проверенный временем и широко доступный инструмент, который можно использовать для опроса запущенного процесса на различных платформах UNIX. Вы можете присоединиться к запущенному процессу с помощью gdb, используя опцию -p, например так:

```
gdb -p
```

Как только GDB успешно подключится к целевому процессу, он выведет вас в ответ на приглашение. Самое первое, что вы должны сделать здесь — это собрать трассировки уровня C для всех потоков застрявшего процесса, например:

```
(gdb) t a a bt
```

Это сокращение от `thread apply all backtrace`. Скопируйте этот вывод и сохраните его в файле расшифровки, даже если он не является сразу информативным.

Иногда для отладки зависания все, что вам нужно, — это трассировки уровня С. В других случаях вам действительно нужна обратная трассировка уровня Ruby, чтобы увидеть, что происходит. К счастью, GDB может помочь вам создать их тоже.

Получение Ruby Backtrace с помощью GDB Чтобы получить Ruby Backtrace
из GDB, необходимо взаимодействовать с интерпретатором Ruby в процессе выполнения, к которому вы подключены. И чтобы получить это, переводчик должен быть в полуработающем состоянии. Если основной причиной проблемы является ошибка Ruby, процесс может зависнуть так, что вообще невозможно получить обратную трассировку.

Тем не менее, это всегда стоит попробовать. Вот техника, которую я использую:

Большинство механизмов для сброса обратных трассировок Ruby выводит их в stdout или stderr. Хорошо, если вы знаете, куда идут stdout и stderr для вашего процесса, и у вас есть к ним доступ. Если нет, сначала вам нужно перенаправить выходные потоки в доступное место.

Для этого мы полагаемся на соглашения POSIX и некоторые хитрости. stdout и stderr являются файловыми дескрипторами 1 и 2 в POSIX-совместимых системах. Мы хотим закрыть эти файловые дескрипторы и заново открыть их, прикрепленные к файлам, из которых мы можем фактически прочитать. Закрытие легко:

```
(gdb) call (void) close(1)
(gdb) call (void) close(2)
```

Команда call указывает gdb на вызов close из целевого процесса. Затем нам нужно заново связать файловые дескрипторы 1 и 2 с файлом, из которого мы действительно можем прочитать. Мы можем использовать файл в файловой системе, но еще удобнее видеть вывод непосредственно в GDB. Для этого мы хотим выяснить файл устройства, соответствующий нашему текущему TTY, и дважды открыть его. Поскольку файловые дескрипторы назначаются последовательно, дескрипторы 1 и 2 в конечном итоге будут связаны с нашим устройством TTY. Вот как это выглядит:

```
(gdb) shell tty
/dev/pts/0
(gdb) call (int) open("/dev/pts/0", 2, 0)
$1 = 1
(gdb) call (int) open("/dev/pts/0", 2, 0)
$2 = 2
```

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

Finally, we need to get the Ruby interpreter in the target process to spit out a backtrace. There are many ways to do this. But the easiest way is just to use gdb to call the rb_backtrace() function from the Ruby interpreter directly:

```
(gdb) call (void)rb_backtrace()
```

With luck, you’ll see a Ruby backtrace dumped to your console.

Digging Deeper with gdb
If you know what you’re looking for in the stuck process, there’s a lot more you can do with gdb. Jon Yurek at thoughtbot has a great post on this topic that explains how you can get gdb to evaluate arbitrary Ruby code in the target process to inspect the values of Ruby variables, get backtraces for all threads and any number of other things

Dynamic Hangs
A stuck process that shows high CPU usage is likely stuck in a tight loop. This means that the exact backtraces you get out of it may vary from one sample to the next, but backtraces can still be very helpful to point you in the right direction.

If you find a stuck process with high CPU usage, it may be worth gathering backtraces 2 or 3 times to get a more representative sample. (You can use gdb’s continue command to resume the process after you’ve attached).

You can also use other tracing tools to examine the behavior of the looping process. On Linux, strace -p <pid> allows you to view the system calls being made by the process. If you’re on an OS that has dtrace available, you can use dtruss -p <PID> instead to get a similar output.

Thanks, from your Future Self
You might not always have the luxury of being able to spend time debugging a stuck process. But when you do dive in, you’ll probably be glad you did. I’ve only scratched the surface of the tools available for analyzing problems like this, but hopefully I’ve given you enough information that you’ll be comfortable doing more than just sighing, SIGKILL-ing, and waiting for your problem to reappear.

Bonus: Mac OS X
If you happen to be debugging a stuck process on Mac OS X, check out this sample utility (a handy tool that ships with OS X). It will automate the collection of multiple C-level backtraces, aggregate them together and print out a nicely formatted call tree report showing where the stuck process is spending most of its time.