*nixBackEndDevelopmentPHPМоим ученикам

PHP-FPM и Nginx через Unix-сокет в Docker

Практически с начала обучения на курсе 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

phpinfo();

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


docker-compose up --build

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

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *