segfault в Docker — найти призрака в php-fpm

Все было хорошо и тут в dmesg или /var/log/kern.log начинает сыпать. Каждую секунду.

Descartes kernel: [8345072.451703] php-fpm[2031915]: segfault at 0 ip 0000000000000000 ...
Descartes kernel: [8345073.452431] php-fpm[2031982]: segfault at 0 ip 0000000000000000 ...
Descartes kernel: [8345074.453447] php-fpm[2031991]: segfault at 0 ip 0000000000000000 ...

Холодный пот. segfault — это всегда плохо. Это значит, что php-fpm пытается залезть в память, куда ему нельзя, и ядро его принудительно прибивает.

Но вот главная проблема: вы смотрите на этот PID (2031915) и пытаетесь понять, что это. Вы делаете ps aux | grep 2031915… и ничего не находите. Потому что процесс уже упал. Или вы смотрите и видите просто php-fpm.

А у вас на сервере, скажем, 30 сайтов, и все в Docker-контейнерах. Какой из них?

Поздравляю, вы играете в «PID Shell Game» — игру, в которой ядро Linux дает вам PID хост-машины, в то время как виновник сидит внутри одного из десятков контейнеров и думает, что его PID — это 1 или 50.

Я проходил через это. Давайте разберемся, как этого зверя выследить.


 

Глава 1: Искусство детективной работы (Утилиты)

 

Нам нужно сопоставить PID хоста с контейнером. Поскольку процессы php-fpm мрут как мухи (в логах мы видим, что PID постоянно меняются), у нас нет времени на долгие раздумья. Нам нужны быстрые инструменты.

 

Прием №1: «Ковровая бомбардировка» через docker top

 

Это мой любимый «глупый», но эффективный метод. Мы просто просим каждый запущенный контейнер показать нам его процессы.

Идея в том, чтобы быстро пролистать все контейнеры и посмотреть, в каком из них «мелькают» PIDs, похожие на те, что в логах.

Выполните эту команду в терминале хоста:

Bash

. Сервер работает, клиенты довольны. И
<span class="hljs-keyword">for</span> c <span class="hljs-keyword">in</span> $(docker ps -q); <span class="hljs-keyword">do</span> \
  <span class="hljs-built_in">echo</span> <span class="hljs-string">"--- Контейнер: <span class="hljs-subst">$(docker ps --no-trunc -f id=$c --format '{{.Names}} ({{.ID}})</span>') ---"</span>; \
  docker top <span class="hljs-variable">$c</span> -eo pid,comm | grep php-fpm; \
<span class="hljs-keyword">done</span>

Что это делает?

  1. docker ps -q получает список ID всех запущенных контейнеров.
  2. for c in ... запускает цикл по этому списку.
  3. echo ... печатает красивое имя контейнера (потому что с ID работать неудобно).
  4. docker top $c -eo pid,comm показывает процессы внутри контейнера, но самое главное — он показывает их хостовые PID.
  5. grep php-fpm отсеивает все лишнее.

Вывод будет примерно таким:

--- Контейнер: old-crappy-wordpress-site (a1b2c3d4e5f6...) ---
2031900 php-fpm
2031915 php-fpm
2031982 php-fpm
--- Контейнер: shiny-new-laravel-app (z9y8x7w6v5u4...) ---
2100123 php-fpm
--- Контейнер: my-postgres-db (f4a5b6c7d8e9...) ---
(тут пусто)

Бинго! Мы видим наши PIDs из лога (2031915, 2031982) и видим, что они принадлежат контейнеру old-crappy-wordpress-site. Мы нашли виновника.

 

Прием №2: «Снайперский выстрел» через cgroups

 

Этот способ более «хирургический». Docker использует cgroups (контрольные группы) для изоляции. Имя cgroup процесса содержит ID ко. Сервер работает, клиенты довольны. Интейнера.

Если процессы живут достаточно долго, чтобы их поймать, это самый быстрый способ:

Bash

ps -eo pid,comm,cgroup | grep php-fpm

Вы увидите что-то вроде этого:

 2031915 php-fpm     12:pids:/docker/a1b2c3d4e5f6a1b2c3d4e5f61234567...
 2100123 php-fpm     12:pids:/docker/z9y8x7w6v5u4z9y8x7w6v5u4abcdeff...

Смотрите на путь после /docker/. Этот длинный 64-значный хэш — это полный ID контейнера.

Вам даже не нужен весь ID. Просто скопируйте первые 10-12 символов (например, a1b2c3d4e5f6) и найдите его:

Bash

docker ps | grep a1b2c3d4e5f6

Эта команда мгновенно покажет вам имя нужного контейнера.


 

Глава 2: Так что же это было? Анатомия segfault at 0

 

Хорошо, мы нашли контейнер. Но почему он падает?

Ошибка segfault at 0 ip 0000000000000000 — это классика. Это обращение к null-указателю (null pointer dereference).

Если переводить с C-шного на человеческий: программа (в данном случае php-fpm) попыталась прочитать или выполнить инструкцию по адресу памяти «ноль». Это все равно что пытаться позвонить по номеру «0» — система просто не знает, что с этим делать, и вешает трубку.

Это почти никогда не ошибка в вашем PHP-коде (типа index.php). Это баг на более низком уровне.

Главные подозреваемые:

  1. Opcache (90% случаев). opcache — это расширение, которое кэширует скомпилированный PHP-код (опкоды) в памяти. Это значительно ускоряет PHP. Но иногда этот кеш… портится. Он может повредиться из-за нехватки памяти, бага в самом opcache или просто неудачного стечения обстоятельств. Когда php-fpm пытается выполнить поврежденный опкод из кеша, он обращается по «битому» адресу и падает.
  2. Другие расширения C (9% случаев). Любое стороннее расширение (.so файл) — это потенциальный источник проблем. xdebug, newrelic, ioncube, imagick… если они написаны с ошибками управления памятью, они могут вызывать segfault. Запускать xdebug на продакшене — вообще плохая идея, он печально известен утечками памяти и нестабильностью.
  3. Баг в самом PHP (1% случаев). Да, такое бывает, но крайне редко и обычно исправляется в минорных обновлениях.

 

Глава 3: «Я просто перезагрузил» (И почему это сработало)

 

И вот, пока я готовил свой отладчик gdb и собирался копаться в core dump-ах… мой коллега (или я сам, в приступе отчаяния) просто сделал:

Bash

docker-compose down
docker-compose up -d

…и все. Ошибки прекратились.

Чувствуешь себя немного глупо, да? Ты провел целое расследование, а помог банальный «выключить и включить».

Но давайте разберемся, почему это сработало.

. Сервер работает, клиенты довольны. Иdocker-compose down (в отличие от stop) — это не просто остановка. Это полное уничтожение контейнеров.

Что именно происходит:

  1. Контейнер удаляется: Вся его эфемерная файловая система стирается.
  2. Opcache стерт: Если opcache хранил свой кеш в файлах (например, в /tmp), эти файлы физически удалены.
  3. Shared Memory очищена: Opcache также использует общую память (shm) на хосте. docker-compose down отключает контейнер от нее, и эта память очищается.
  4. «Чистый лист»: docker-compose up создает абсолютно новый контейнер из вашего image. Он запускает php-fpm с нуля, и тот начинает заново кэшировать ваши скрипты, создавая «чистый», неповрежденный opcache.

По сути, вы «вылечили» симптом (поврежденный кеш), но не обязательно причину.

 

Заключение: Когда стоит беспокоиться

 

Если segfault вернулся через день или неделю — у вас более серьезная проблема. Простого перезапуска уже недостаточно.

В этом случае ваш план действий:

  1. Найти контейнер (как мы делали в Главе 1).
  2. Зайти внутрь: docker exec -it <имя_контейнера> bash
  3. Отключить Opcache: Найти php.ini (или conf.d/opcache.ini) и выставить opcache.enable=0.
  4. Перезапустить php-fpm внутри контейнера (или просто перезапустить контейнер).

Если падения прекратились — вините opcache. Возможно, стоит выделить ему больше памяти (opcache.memory_consumption) или просто периодически чистить кеш.

Если падения продолжаются даже с выключенным opcache — начинайте отключать другие расширения (xdebug, newrelic и т.д.) по одному, пока не найдете виновника.

Логи ядра выглядят страшно, но, как видите, за ними почти всегда стоит что-то вполне объяснимое. Главное — знать, куда светить фонариком.

Оставить ответ

Ваш адрес email не будет опубликован. Обязательные поля помечены *