Настройка и highload-тюнинг php-fpm

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

Прежде всего стоит определить расположение файла-конфигурации пула. Если вы устанавливали php-fpm из системного репозитория, то конфигурация пула www будет расположена примерно тут /etc/php5/fpm/pool.d/www.conf. В случае если используется свой билд или другая ОС (не debian) следует поискать расположение файла в документации, или указывать его вручную. Попробуем рассмотреть конфигурацию подробней.

Переходим на UNIX-сокеты

Наверное первое, на что следует обратить внимание, это то как проходят данные от веб-сервера к вашим php процессам. Это отражено в директиве listen:

listen = 127.0.0.1:9000

В случае если установлен адрес:порт, то данные идут через стек TCP, и это наверное не очень хорошо. Если же там путь к сокету, например:

listen = /var/run/php5-fpm.sock

то данные идут через unix-сокет, и можно пропустить этот раздел.

Почему все таки стоит перейти на unix-сокет? UDS (unix domain socket), в отличии от комуникции через стек TCP, имеют значительные преимущества:

  • не требуют переключение контекста, UDS используют netisr)
  • датаграмма UDS записываться напрямую в сокет назначения
  • отправка дейтаграммы UDS требует меньше операций (нет контрольных сумм, нет TCP-заголвоков, не производиться маршрутизация)

И вот некоторые тесты:

TCP средняя задержка: 6 us
UDS средняя задержка: 2 us
PIPE средняя задержка: 2 us
TCP средняя пропускная способность: 253702 msg/s
UDS средняя пропускная способность: 1733874 msg/s
PIPE средняя пропускная способность: 1682796 msg/s

Таким образом, у UDS задержка на ~66% меньше и пропускная способность в 7 раз больше TCP. Поэтому, скорей всего стоит перейти на UDS. В моем случае сокет будет расположен по адресу /var/run/php5-fpm.sock.

; закоментируем это - listen = 127.0.0.1:9000
listen = /var/run/php5-fpm.sock

Также следует убедиться что веб-сервер (или любой другой процесс, которому необходима коммуникация) имеет доступ на чтение/запись в ваш сокет. Для этого существуют настройки listen.grup и listen.mode Проще всего — запускать оба процесса от одного пользователя или группы, в нашем случае php-fpm и веб-сервер будет запущен с группой www-data:

listen.owner = www-data
listen.group = www-data
listen.mode = 0660

Проверяем выбранный механизм обработки событий

Для работы с эффективной работы с I/O (вводом-выводом, дескрипторами файлов/устройств/сокетов) стоит проверить правильно ли указана настройка events.mechanism. В случае если php-fpm установлен из системного репозитория, скорей всего там все в порядке — он либо не указан (устанавливаться автоматически), либо указан корректно.

Его значение зависит от ОС, для чего есть подсказка в документации:

; - epoll      (linux >= 2.5.44)
; - kqueue     (FreeBSD >= 4.1, OpenBSD >= 2.9, NetBSD >= 2.0)
; - /dev/poll  (Solaris >= 7)
; - port       (Solaris >= 10)

К примеру если мы работаем на современном linux-дистрибутивe нам необходим epool:

events.mechanism = epoll

Выбор типа пула — dynamic / static / ondemand

Также, стоит обратить внимание на настройки менеджер процессов (pm). По сути это главный процесс (master process), который будет управлять всеми дочерними (которые выполняют код приложения) по определенной логике, которая собственно и описана в файле конфигурации.

Всего доступно 3 схемы управления процессами:

  • dynamic
  • static
  • ondemand

Наиболее простой — это static. Схема его работы заключается в следующем: запустить фиксированное количество дочерних процессов, и поддерживать их в рабочем состоянии. Данная схема работы не очень эффективна, так как количество запросов и их нагрузка может меняться время от времени, а количество дочерних процессов нет — они всегда занимают определенный объем ОЗУ и не могут обрабатывают пиковые нагрузки в порядке очереди.

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

ondemand пул очень похож на static, но он не запускает дочерних процессов при старте главного процесса. Только когда придет первый запрос — будет создан первый дочерний процесс, и по истечении определенного времени ожидания (указанного в конфигурации) он будет уничтожен. Потому он актуален для серверов с ограниченными ресурсами, или той логики которая не требует быстрой реакции.

Утечки памяти и OOM killer

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

  • pm.max_requests
  • request_terminate_timeout

pm.max_requests это максимальное количество запросов, которое обработает дочерний процесс, прежде чем будет уничтожен. Принудительное уничтожение процесса позволяет избежать ситуации в которой память дочернего процесса “разбухнет” по причине утечек (т.к процесс продолжает работу после от запроса к запросу). С другой стороны, слишком маленькое значение приведет к частым перезапускам, что приведет к потерям в производительности. Стоит начать с значения в 1000, и далее уменьшить или увеличить это значение.

request_terminate_timeout устанавливает максимальное время выполнения дочернего процесса, прежде чем он будет уничтожен. Это позволяет избегать долгих запросов, если по какой-либо причине было изменено значение max_execution_time в настройках интерпретатора. Значение стоит установить исходя из логики обрабатываемых приложений, скажем 60s (1 минута).

Настройка dynamic пула

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

  • pm.max_children — максимальное количество дочерних процессов
  • pm.start_servers — количество процессов при старте
  • pm.min_spare_servers — минимальное количество процессов, ожидающих соединения (запросов для обработки)
  • pm.max_spare_servers — максимальное количество процессов, ожидающих соединения (запросов для обработки)

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

  • сколько памяти в среднем потребляет дочерний процесс
  • объем доступного ОЗУ

Выяснить среднее значение памяти на один php-fpm процесс на уже работающем приложении можно с помощью планировщика:

# ps -ylC php-fpm --sort:rss
S   UID   PID  PPID  C PRI  NI   RSS    SZ WCHAN  TTY          TIME CMD
S     0  1445     1  0  80   0  9552 42588 ep_pol ?        00:00:00 php5-fpm

Нам необходимо среднее значение в колонке RSS (размер резидентной памяти в килобайтах). В моем случае это ~20Мб. В случае, если нагрузки на приложения нет, можно использовать Apache Benchmark, для создания простейшей нагрузки на php-fpm.

Объем общей / доступной / используемой памяти можно посмотреть с помощью free:

# free -m
            total   used    free   ...
Memory:     4096    600     3496

Далее, возьмем за основу формулу для расчета pm.max_children и проведем расчет на примере:

Total Max Processes = (Total Ram - (Used Ram + Buffer)) / (Memory per php process)
Всего ОЗУ: 4Гб
Используется ОЗУ: 1000Мб
Буфер безопасности: 400Мб
Память на один дочерний php-fpm процесс (в среднем): 30Мб
Максимально возможное кол-во процессов = (4096 - (1000 + 400)) / 30 = 89
Четное количество: 89 округлили в меньшую сторону до 80

Значение остальных директив можно установить исходя из ожидаемой нагрузки на приложение а также учесть чем еще занимается сервер кроме работы php-fpm (скажем СУБД также требует ресурсов). В случае наличия множества задач на сервере — стоит снизить к-во как начальных / максимальных процессов.

К примеру учтем что на сервере находиться 2 пула www1 и www2 (к примеру 2 веб-ресурса), тогда конфигурация каждого из них может выглядеть как:

pm.max_children = 40    ; 80 / 2
pm.start_servers = 15
pm.min_spare_servers = 15
pm.max_spare_servers = 25