PHP

Итак, в логах nginx:

[error] 6#6: *57200 recv() failed (104: Connection reset by peer) while reading response header from upstream, client: 10.233.82.123, server: , request: "GET /my/page HTTP/1.1", upstream: "fastcgi://127.0.0.1:9000", host: "site.ru", referrer: "https://site.ru/"

В логах php-fpm:

NOTICE: [pool www] child 255 started (иногда эта строка не появляется в логах, почему неизвестно)
WARNING: [pool www] child 255 exited on signal 9 (SIGKILL) after 158.109586 seconds from start

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

postgres_1  | LOG:  could not send data to client: Connection reset by peer
postgres_1  | FATAL:  connection to client lost

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

  1. несколько конкурентных http-запросов становятся в очередь из-за бд и обрабатываются последовательно (а если установлен таймаут php-fpm то php-fpm делает SIGKILL процессам, которые не успели обработать запрос)
  2. php-fpm порождает несколько дочерних процессов, тем самым отъедая memory, а если память заканчивается, то делает SIGKILL дочерним процессам (которым не хватает памяти)
  3. возникло большое кол-во запросов (больше, чем указано в настройках php-fpm)

Стоит заметить, что nginx:

  • в случае SIGKILL отвечает: 502 bad gateway,
  • в случае долгой обработки php-fpm-ом: 504 gateway time-out

Важно: когда в настройках php-fpm не указан таймаут, то php-fpm процессы продолжают жить до тех пор, пока не выполнятся, независимо от того, какой из php-fpm pm-режимов выбран.

А теперь, давайте рассмотрим каждый из выше указанных трех случаев.

1. Источник данных является узким горлышком

Эта ситуация возникает, когда cpu/memory сервису хватает, но одинаковые запросы в параллель выполняются медленно, в то время как сам по себе запрос выполняется достаточно быстро. Данную проблему можно побороть только исправлениями в самом приложении и обычно проблема заключается в:

  1. базе данных, которая последовательно выполняет указанные запросы
  2. сервис, который последовательно обрабатывает указанные запросы или одновременная нагрузка на который сказывается на его скорости обработки каждого запроса

2. Заканчивается память в php-fpm

Чтобы проверить эту гипотезу и повторить проблему локально, установите в docker-compose.yml: 

   php-fpm:
       image: registry.site.ru/my-php-fpm:c3db38f46f7
       environment:
           - APP_ENV=prod
       deploy:
           resources:
               limits:
                   cpus: '0.5'
                   memory: 128M

и запустив:

docker-compose -f docker-compose.test.yml --compatibility up

можно наблюдать статистику по ресурсам:

docker stats

Если в результате нагрузочных тестов вы видите, что MEMORY USAGE значение достигает или почти достигает значения в столбце LIMIT, то вам просто нужно увеличить кол-во памяти выданных контейнеру (хорошо, когда есть небольшой запас).

3. Неожиданно большое кол-во запросов

Наблюдаю ситуацию, когда при трех конкурентных (одновременных) запросах, 1 из 3 падает с ошибкой:

WARNING: [pool www] child 13 exited on signal 9 (SIGKILL) after 133.010222 seconds from start

Первым делом смотрим в /usr/local/etc/php-fpm.d/www.conf и видим настройки:

pm = dynamic - кол-во процессов PHP-FPM меняется динамически, в зависимости от количества запросов
pm.max_children = 5 - максимальное кол-во дочерних процессов, которое будет создано для пула
pm.start_servers = 2 - кол-во дочерних процессов, которые будут созданы при старте пула
pm.min_spare_servers = 1 - минимальное кол-во дочерних процессов в статусе ожидания (idle)
pm.max_spare_servers = 3 - максимальное кол-во дочерних процессов в статусе ожидания (idle)

Нужно изменить эти настройки, ведь кроме SIGKILL я вижу совет от самой php-fpm:

WARNING: [pool www] server reached pm.max_children setting (5), consider raising it

Поэтому указываю:

pm = dynamic
pm.max_children = 25
pm.start_servers = 1
pm.min_spare_servers = 1
pm.max_spare_servers = 5

но даже при таких настройках я каждый раз ловлю в логах php-fpm:

WARNING: [pool www] seems busy (you may need to increase pm.start_servers, or pm.min/max_spare_servers), spawning 8 children, there are 0 idle, and 8 total children

WARNING: [pool www] seems busy (you may need to increase pm.start_servers, or pm.min/max_spare_servers), spawning 16 children, there are 0 idle, and 9 total children

В общем, нужно подбирать параметры или переходить на режим pm = ondemand, где таких ворнингов не будет.

Года три тому назад, я тестировал разные режимы работы php-fpm для одного высоконагруженного php-приложения и для тех мощностей, которыми он обладал, мне удалось инструментом ab выяснить, что лучшие результаты (максимальное возможное кол-во одновременных процессов равное 406) можно получить такими настройками:

[www]
user = www-data
group = www-data
listen = 127.0.0.1:9000
; ondemand - FPM не будет создавать дочерних процессов, пока не появится реальный запрос для обработки, а запустит только мастер-процесс самого php-fpm
pm = ondemand
; максимальное возможное кол-во одновременно работающих процессов (вычисляется опытным путем, например с помощью ab):
pm.max_children = 406
; время, через которое Process Manager уничтожит дочерний процесс, который не обслуживает запросы:
pm.process_idle_timeout = 60

Однако, в этом проекте не были установлены таймауты в nginx и php-fpm и выше указанные настройки помогли.

Заключение

Если вы не используете таймауты в nginx и php-fpm, то вы идете неправильной дорожкой, потому что любой сервис (в том числе на пхп-фпм) не должен класть в долгий ящик все запросы, а клиенты не должны вечно ждать ответа, это неправильная тактика, сервис должен отвечать достаточно быстро (в согласованное время), а если не может уложиться по времени (согласно договоренности), то прекращать обрабатывать запрос (по возможности сразу, как только обговоренное время вышло), тем самым отдавая честный ответ клиенту - не успел/не смог (извини).

А если не указывать лимиты, то ваш сервис столкнется с ситуацией, когда ему не будет хватать, то памяти (потому что очень много запросов в обработке) то процессора, и клиенты будут недовольны медленной работой сервиса.

Отсюда мне видится, что подходить к настройке каждого приложения нужно индивидуально (серебренной пули не выйдет), но, уйти от стандартно настроенного пхп-фпм это здравая мысль хотя бы по двум причинам:

  1. новое приложение может сразу разрабатываться с учетом принятых в компании лимитов (nginx и php-fpm таймаутов)
  2. если контейнер обладает большим кол-вом ресурсов, то сервис в нем должен уметь потреблять все доступные ресурсы

Таким образом, для пхп-фпм можно для любого из pm вариантов, просто задрать показатели повыше (но из опыта не советую pm = static). А если хотим, чтобы приложение немного экономило память + при изменении мощностей контейнера не требовало частой корректировки php-fpm конфига, то предлагаю использовать pm = ondemand

Вопросы на будущее (домашнее задание):

  • мониторинг php-fpm (настройка pm.status_path = /fpm_status )
  • сбор статистики с /fpm_status (обычно делают в забикс/прометеус).

p.s. выше обозначенные проблемы иногда удобно тестировать так:

<?php
// тестируем расход memory:
$a = str_repeat("ПриветМир", 3000000);
echo memory_get_peak_usage(true);

// тестируем время выполнения:
// sleep(3);

// тестируем аутпут
// $fo = fopen('php://stdout', 'w');
// fwrite($fo, str_repeat("Hi-", 90));

// тестируем нагрузку на CPU:
// for ($i=0; $i< 300000; $i++) {
//     // квадратный корень timestamp-а
//     sqrt(time()-$i);
// }

exit;

маленький итог по тестам:

  • когда в master-php-fpm приходит Х запросов, он создает из них очередь и согласно очереди начинает создавать sub-php-fpm процессы
  • когда все созданные sub-php-fpm процессы утилизируют всю доступную память, но не все запросы обслужены, master-php-fpm не порождает более sub-php-fpm до тех пор, пока не освободится память
  • когда не хватает памяти для обслуживания (не создания, а именно обслуживания sub-php-fpm), например уже используется 100% памяти, но один из sub-php-fpm запросил еще памяти, то master-php-fpm убивает такой sub-php-fpm, при этом отписывая в stderr: WARNING: [pool www] child 25 exited on signal 9 (SIGKILL)
  • когда master-php-fpm + все sub-php-fpm утилизировали всю доступную память, но запросы продолжают приходить, то master-php-fpm пытается добавить их в очередь, но т.к. памяти больше нет, то master-php-fpm просто падает отписывая в stderr: exited with code 137
  • nginx прерывает запросы, которые не успевает обработать, возвращая 504 upstream timed out (110: Operation timed out) while reading response header from upstream

p.s. если вы видите следующую проблему, то скорее всего дело в срабатывании php-fpm настройки pm.max_requests

NOTICE: [pool www] child 10 exited with code 0 after 13.274903 seconds from start

А еще такие сообщения появляются когда изменено дефолтное значение pm.max_requests - если указать больше 0, то php-fpm-master будет убивать php-fpm-kid процесс после того, как php-fpm-kid обслужил указанное тут кол-во http-запросов. Это полезно для избежания утечек памяти при использовании сторонних библиотек, однако т.к. я использую request_terminate_timeout=2, то мне выгоднее дефолтное значение pm.max_requests=0 тем, что не засоряет логи выше указанными нотисами.

p.s. 2 проверить изменение конфига php-fpm можно командой:

php-fpm -tt

Мониторинг и алертинг

Как это делают в настоящий момент:

1. https://github.com/hipages/php-fpm_exporter каждую секунду собирает с php-fpm данные https://www.php.net/manual/en/fpm.status.php
2. https://github.com/prometheus/prometheus каждые 15 секунд собирает с php-fpm_exporter-а данные, которые он накопил
Что интересно, а главное важно: php-fpm_exporter при сборе всегда инкрементирует собранные значения, и сбрасывает их после того, как Prometheus забрал их (поэтому не переживайте за лаг в 15 секунд).

Q: Как знания выше, помогут нам мониторить кол-во дочерних php-fpm процессов, которые килнул мастер php-fpm процесс 
A: php-fpm делая "terminating" делает это когда скрипт потребил слишком большое кол-во памяти (ситуация не частая, за всю жизнь встречал ее на проде 2-3 раза) ИЛИ дочерний php-fpm процесс работал слишком долго (см. php-fpm настройку request_terminate_timeout). Как раз последнее может происходить достаточно часто, и мониторить можно не количество "terminating", а количество phpfpm_slow_requests.

По умолчанию phpfpm_slow_requests всегда равно нулю, т.к. в php-fpm образе не указан request_slowlog_timeout - https://www.php.net/manual/en/install.fpm.configuration.php#request-slowlog-timeout поэтому в образе php-fpm нужно (через ENV-переменную) указывать настройку request_slowlog_timeout + при этом slowlog направив в /dev/null, иначе образ может пухнуть в следствии появления слишком большого количества медленных запросов

Источник: 1 - 2 - 3


25.02.2011 14:48