Все было хорошо и тут в 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, похожие на те, что в логах.
Выполните эту команду в терминале хоста:
<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>
Что это делает?
docker ps -qполучает список ID всех запущенных контейнеров.for c in ...запускает цикл по этому списку.echo ...печатает красивое имя контейнера (потому что с ID работать неудобно).docker top $c -eo pid,commпоказывает процессы внутри контейнера, но самое главное — он показывает их хостовые PID.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 ко. Сервер работает, клиенты довольны. Интейнера.
Если процессы живут достаточно долго, чтобы их поймать, это самый быстрый способ:
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) и найдите его:
docker ps | grep a1b2c3d4e5f6
Эта команда мгновенно покажет вам имя нужного контейнера.
Глава 2: Так что же это было? Анатомия segfault at 0
Хорошо, мы нашли контейнер. Но почему он падает?
Ошибка segfault at 0 ip 0000000000000000 — это классика. Это обращение к null-указателю (null pointer dereference).
Если переводить с C-шного на человеческий: программа (в данном случае php-fpm) попыталась прочитать или выполнить инструкцию по адресу памяти «ноль». Это все равно что пытаться позвонить по номеру «0» — система просто не знает, что с этим делать, и вешает трубку.
Это почти никогда не ошибка в вашем PHP-коде (типа index.php). Это баг на более низком уровне.
Главные подозреваемые:
- Opcache (90% случаев).
opcache— это расширение, которое кэширует скомпилированный PHP-код (опкоды) в памяти. Это значительно ускоряет PHP. Но иногда этот кеш… портится. Он может повредиться из-за нехватки памяти, бага в самомopcacheили просто неудачного стечения обстоятельств. Когдаphp-fpmпытается выполнить поврежденный опкод из кеша, он обращается по «битому» адресу и падает. - Другие расширения C (9% случаев). Любое стороннее расширение (
.soфайл) — это потенциальный источник проблем.xdebug,newrelic,ioncube,imagick… если они написаны с ошибками управления памятью, они могут вызыватьsegfault. Запускатьxdebugна продакшене — вообще плохая идея, он печально известен утечками памяти и нестабильностью. - Баг в самом PHP (1% случаев). Да, такое бывает, но крайне редко и обычно исправляется в минорных обновлениях.
Глава 3: «Я просто перезагрузил» (И почему это сработало)
И вот, пока я готовил свой отладчик gdb и собирался копаться в core dump-ах… мой коллега (или я сам, в приступе отчаяния) просто сделал:
docker-compose down
docker-compose up -d
…и все. Ошибки прекратились.
Чувствуешь себя немного глупо, да? Ты провел целое расследование, а помог банальный «выключить и включить».
Но давайте разберемся, почему это сработало.
. Сервер работает, клиенты довольны. Иdocker-compose down (в отличие от stop) — это не просто остановка. Это полное уничтожение контейнеров.
Что именно происходит:
- Контейнер удаляется: Вся его эфемерная файловая система стирается.
- Opcache стерт: Если
opcacheхранил свой кеш в файлах (например, в/tmp), эти файлы физически удалены. - Shared Memory очищена: Opcache также использует общую память (shm) на хосте.
docker-compose downотключает контейнер от нее, и эта память очищается. - «Чистый лист»:
docker-compose upсоздает абсолютно новый контейнер из вашегоimage. Он запускаетphp-fpmс нуля, и тот начинает заново кэшировать ваши скрипты, создавая «чистый», неповрежденныйopcache.
По сути, вы «вылечили» симптом (поврежденный кеш), но не обязательно причину.
Заключение: Когда стоит беспокоиться
Если segfault вернулся через день или неделю — у вас более серьезная проблема. Простого перезапуска уже недостаточно.
В этом случае ваш план действий:
- Найти контейнер (как мы делали в Главе 1).
- Зайти внутрь:
docker exec -it <имя_контейнера> bash - Отключить Opcache: Найти
php.ini(илиconf.d/opcache.ini) и выставитьopcache.enable=0. - Перезапустить
php-fpmвнутри контейнера (или просто перезапустить контейнер).
Если падения прекратились — вините opcache. Возможно, стоит выделить ему больше памяти (opcache.memory_consumption) или просто периодически чистить кеш.
Если падения продолжаются даже с выключенным opcache — начинайте отключать другие расширения (xdebug, newrelic и т.д.) по одному, пока не найдете виновника.
Логи ядра выглядят страшно, но, как видите, за ними почти всегда стоит что-то вполне объяснимое. Главное — знать, куда светить фонариком.