Практически с начала обучения на курсе PHP Pro мы со студентами разбираемся в том, как получить рабочую среду не только для обучения, но и для дальнейшей работы. Для удобства и гибкости погружаемся мы в применение Docker-контейнеров. Конечно, можно спросить: «А чего не Кубер?». Но к нему мы приступаем чуть позже.
Стандартным способом дружбы Nginx и FPM для работы является TCP-соединение. Оно присутствует во всех мануалах, понятно всем, привычно. При этом такое соединение дает неплохую гибкость. За него, например, можно убрать балансер, чтобы сделать скалируемую систему. Но зачем-то и в Nginx, и в PHP-FPM добавили еще один способ обмена данными — Unix-сокет. Поэтому предлагаю разобраться для начала с тем, что же это за зверь такой.
Плюсы и минусы Unix-сокета
В Unix-like системах сокет — это средство для межпроцессного взаимодействия (InterProcess Communication или коротко — IPC). Это особый тип, который используется для обмена данными между разными процессами на одном и том же компьютере. В рассматриваемом нами случае — Nginx и PHP-FPM.
В файловой системе Unix-сокет чаще всего представляет собой файл. У него есть адрес, к нему можно подключаться, он подчиняется стандартной системе доступов и прав. Отсюда мы понимаем, что в случае с сокетом Nginx и FPM должны находиться в рамках одной машины (пусть даже и виртуальной).
И здесь появляется преимущество сокета. Поскольку данные не передаются по сети (пусть даже и в локали), а остаются внутри одного компьютера, Unix-сокеты обеспечивают высокую скорость и низкую задержку. На деле при использовании TCP протокола будут происходить handshake’и, которые и будут формировать задержку. Однако не надо думать, что разница по скорости будет отличаться в разы. Чаще всего речь о нескольких процентах. Но они будут играть роль в высоконагруженных системах, где сокет-соединение встречается чаще всего.
Использование Unix-сокетов в PHP-FPM не лишено и минусов. Как я уже обозначил выше, Unix-сокеты работают только на одной машине. Если приложение требует общения между веб-сервером и PHP-FPM, которые находятся на разных машинах, TCP-сокет будет единственным вариантом.
Unix-сокеты не обеспечивают возможность удаленного доступа, что делает их непригодными для распределенных систем, где требуется взаимодействие между процессами на разных машинах.
Одним неприятным подводным камнем, с которым мне пришлось столкнуться во время работы, была чувствительность Unix-сокетов в PHP-FPM к количеству доступных семафоров в Linux системе. В какой-то момент FPM начинал падать без видимых в логах причин.
Семафор — это механизм синхронизации, используемый для управления доступом к ресурсам в многозадачных или многопроцессных системах. Он помогает координировать выполнение потоков или процессов, чтобы избежать проблем, связанных с одновременным доступом к общим ресурсам, таким как данные или устройства.Семафоры могут использоваться для решения различных проблем синхронизации и предотвращения состояний гонки (race conditions).
В Linux количество доступных семафоров можно увеличить, если это необходимо для работы приложений, которые используют семафоры. Это можно сделать через настройки ядра и параметры системы. Основные параметры, которые влияют на количество семафоров, включают
-
: Максимальное количество семафоров в одном семафорном наборе.SEMMSL
-
: Общее количество семафоров в системе.SEMMNS
-
: Максимальное количество семафорных наборов в системе.SEMMNI
-
: Максимальное количество операций над семафорами, которое может быть выполнено одним вызовомSEMOPM.semop
Эти параметры можно изменить в реальном времени или навсегда, редактируя конфигурацию системы. Например, для системы из 6 ядер и 128 GB RAM цифры будут примерно следующими
sudo sysctl -w kernel.sem="1024 131072 1024 128"
Но мы стали уходить в сторону — речь все таки о том, как настроить Nginx и PHP-FPM.
Вкратце, Unix-сокеты могут быть отличным выбором для многих сценариев использования в локальных средах, но важно учитывать вышеупомянутые минусы и проверять, соответствуют ли они вашим требованиям и архитектуре приложения.
Настройки системы
Теперь приступим к созданию системы. Я приведу пример с простым взаимодействием — Nginx и PHP-FPM. БД и прочие зависимости не вижу смысла рассматривать в этой статье.
Начну с того, что во многих мануалах, которые я успел увидеть, работая со студентами, есть ряд странностей
- Где-то предлагают в Dockerfile применять sed, чтобы собрать внутри контейнера файл. А это неудобно, так как итоговый файл конфигурации плохо читаем и не виден целиком.
- Где-то применяют кастомные образы, для которых наворачивают ненужные шаги.
- Где-то просто делают лишние движения.
Мы пойдем самым простым путем. Наши образы будут собираться на базе официальных nginx и php-fpm.
Определимся со структурой директорий
- code — тут будет лежать код на PHP
- fpm — сюда сложим Dockerfile и конфигурацию PHP-FPM
- nginx — здесь будет Dockerfile и конфигурация Nginx
- docker-compose.yaml — собственно, конфигурация сборки
Между контейнерами сделаем два volume
- с кодом (директория code)
- с файлом сокета — ведь если его не создать, то созданный FPM файл не будет виден для Nginx
Начнем с Dockerfile для PHP. Я опишу нюансы в комментариях
# какую бы версию PHP я тут ни указал,
# она устареет с этой статьей
# а вот принцип сборки - нет
FROM php:7.4-fpm
# для соединения через сокет буду использовать отдельный конфиг
# он специально идет отдельно от стандартного www-conf,
# но подключается автоматически и переопределяет нужные настройки
COPY ./zz-docker.conf /usr/local/etc/php-fpm.d/zz-docker.conf
# ставим необходимые для нормальной работы модули
RUN apt-get update && apt-get install -y \
curl \
wget \
git \
libfreetype6-dev \
libjpeg62-turbo-dev \
libpng-dev \
libonig-dev \
libzip-dev \
libmemcached-dev \
libmcrypt-dev \
&& pecl install mcrypt-1.0.3 \
&& docker-php-ext-enable mcrypt \
&& docker-php-ext-install -j$(nproc) iconv mbstring mysqli pdo_mysql zip \
&& docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install -j$(nproc) gd
# Устанавливаем PDO
# Все равно же потом пригодится
RUN apt-get update && apt-get install -y libpq-dev && docker-php-ext-install pdo pdo_pgsql
WORKDIR /data
VOLUME /data
# Запускаем PHP-FPM
CMD ["php-fpm"]
Как видите, нам необходимо создать zz-docker.conf. Кладем его рядом с Dockerfile для FPM
[global]
daemonize = no
error_log = /proc/self/fd/2
log_limit = 8192
[www]
; if we send this to /proc/self/fd/1, it never appears
; access.log = /proc/self/fd/2';
clear_env = no
; Ensure worker stdout and stderr are sent to the main error log.
catch_workers_output = yes
decorate_workers_output = yes
access.log = /dev/null
; вот именно эта настройка волнует нас больше всего
; это будет адрес и имя будущего файла сокета
listen = /var/run/php/php-fpm.sock
; задаем режим для чтения
listen.mode = 0660
pm = dynamic
pm.max_children = 9
pm.start_servers = 2
pm.min_spare_servers = 2
pm.max_spare_servers = 6
Здесь я обращаю внимание на то, что многие мануалы рекомендуют переопределять listen.group и listen.owner. Но этого делать не надо — иначе Nginx не достучится до файла, а вы будете получать ошибку Permission denied. Оставляем мы только listen.mode.
Теперь перейдем к Nginx. Dockerfile тут будет совсем простым.
FROM nginx
# копируем настройку виртуального хоста
COPY ./hosts/mysite.local.conf /etc/nginx/conf.d/mysite.local.conf
WORKDIR /data
VOLUME /data
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
Опишу настройку виртуального хоста
server {
# указываем 80 порт для соединения
listen 80;
# нужно указать, какому доменному имени принадлежит наш конфиг
server_name mysite.local;
# задаём корневую директорию
root /data/mysite.local;
# стартовый файл
index index.php;
# при обращении к статическим файлам логи не нужны, равно как и обращение к fpm
location ~* .(jpg|jpeg|gif|css|png|js|ico|html)$ {
access_log off;
expires max;
}
# помним про единую точку доступа
# все запросы заворачиваются в корневую директорию root на index.php
location / {
try_files $uri $uri/ /index.php?$query_string;
}
# и наконец правило обращения к php-fpm
location ~* \.php$ {
try_files $uri = 404;
fastcgi_split_path_info ^(.+.php)(/.+)$;
include fastcgi_params;
fastcgi_pass unix:/var/run/php/php-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
}
Самой важной настройкой тут снова будет адрес и имя сокета. Нужно убедиться, что имя совпадает с тем, что мы указали в zz-docker.conf.
fastcgi_pass unix:/var/run/php/php-fpm.sock;
Теперь можем приступать к описанию сборки
# версия синтаксиса
version: '3.8'
# в этом блоке мы описываем контейнеры, которые будут запускаться
services:
#Контейнер с PHP, назовём его app
app:
# Если нет секции build, то система будет искать образ в репозиториях
build:
context: ./fpm
dockerfile: Dockerfile
image: myapp/php # имя будущего образа
container_name: app # имя контейнера после запуска
volumes:
- ./code:/data/mysite.local # volume с кодом
- ./php-socket:/var/run/php # volume с сокетом
# мы можем создать для контейнеров внутреннюю сеть
networks:
- app-network
#контейнер с Nginx
webserver:
build:
context: ./nginx
dockerfile: Dockerfile
image: myapp/nginx
container_name: webserver
# проброс портов
ports:
- "80:80"
depends_on:
- app
volumes:
- ./code:/data/mysite.local
- ./php-socket:/var/run/php
networks:
- app-network
#Docker Networks
networks:
app-network:
driver: bridge
Здесь обращаю внимание, что volume будем пробрасывать в обоих случаях в /var/run/php, чтобы иметь однообразные адреса. Конечно, можно и поколдовать, но зачем? Будет проще запутаться.
Теперь добавим простенький index.php в директорию code
<?php
phpinfo();
Все готово! Можем запускаться. Для первого запуска стоит указывать принудительную сборку, а также не запускать в фоне, чтобы удобно и быстро видеть логи запросов.
docker-compose up --build
Так мы получили рабочую среду на готовых компонентах без лишних телодвижений.