Практически с начала обучения на курсе 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
. Эти параметры контролируют:

  • 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. БД и прочие зависимости не вижу смысла рассматривать в этой статье.

Начну с того, что во многих мануалах, которые я успел увидеть, работая со студентами, есть ряд странностей

  1. Где-то предлагают в Dockerfile применять sed, чтобы собрать внутри контейнера файл. А это неудобно, так как итоговый файл конфигурации плохо читаем и не виден целиком.
  2. Где-то применяют кастомные образы, для которых наворачивают ненужные шаги.
  3. Где-то просто делают лишние движения.

Мы пойдем самым простым путем. Наши образы будут собираться на базе официальных 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
$host = gethostname();
$date = date('Y-m-d H:i:s');
echo "Host: {$host}<br>";
echo "PHP: " . PHP_VERSION . "<br>";
echo "Time: {$date}<br>";
echo "SAPI: " . php_sapi_name() . "<br>";

Все готово! Можем запускаться. Для первого запуска стоит указывать принудительную сборку, а также не запускать в фоне, чтобы удобно и быстро видеть логи запросов.

docker-compose up --build

Так мы получили рабочую среду на готовых компонентах без лишних телодвижений.