CGI, FastCGI, php-fpm, nginx и Laravel

Источник: «CGI, FastCGI, php-fpm, nginx, and Laravel»
Если вы запускаете приложения PHP/Laravel в продакшене, есть большая вероятность, что вы используете некоторые из этих вещей:CGI, FastCGI, php-fpm, nginx и Laravel. Как разработчик, я считаю важным понимать хотя бы основы этих компонентов. Итак, давайте разбираться.

Обслуживание статического контента с nginx

Во-первых, давайте начнём с обслуживания статического контента с помощью nginx. Предположим довольно простой сценарий, в котором у нас всего три файла:

Каждый из этих файлов находится в папке /var/www/html/demo. Нет подкаталогов и PHP файлов.

# `events` сейчас не важны..
events {}

http {
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
include mime.types;

server {
listen 80;
server_name 138.68.81.14;
root /var/www/html/demo;
}
}

Этот файл конфигурации не для продакшена! Он только для демонстрационных целей.

В конфигурации nginx есть два важных термина контекст и директива:

Контекст похож на область видимости/scope в языках программирования. http, server, location и events — контексты конфигурации. Они определяют область, в которой мы можем настроить вещи, связанные с областью действия. http является глобальным для всего сервера. Итак, если у нас есть 10 сайтов на этом сервере, каждый из них будет писать логи в /var/log/nginx/access.log, что, очевидно, не очень хорошо, но пока нормально.

Другой контекст — это server, который относится к одному конкретному сайту. В данном случае сайт http://138.68.81.14. Внутри контекста server у нас может быть контекст location (но сейчас у нас его нет), который ссылается на определённые URL-адреса на этом сайте.

Таким образом с помощью контекстов мы можем описать иерархию вещей:

http {
# server-level

server {
# site-level

location {
# URL-level
}
}
}

Внутри контекстов мы можем писать директивы. Они похожи на вызов функции или присвоение значения в PHP. Например, listen 80; это директива. Теперь давайте посмотрим, что они делают построчно.

access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;

nginx будет писать лог каждого входящего запроса в файл access.log в следующем формате: 172.105.93.231 - - [09/Apr/2023:19:57:02 +0000] "GET / HTTP/1.1" 200 4490 "-" "Mozilla/5.0 (Windows NT 6.3; WOW64; Trident/7.0; rv:11.0) like Gecko".

Если что-то пойдёт не так, это записывается в файл error.log. Однако есть одна важная вещь: ответ 404 или 500 не считается ошибкой. Файл error.log содержит только специфичные для nginx ошибки, например, если его невозможно запустить из-за некорректного файла конфигурации.

include mime.types;

Вы помните старую добрую функцию include() из PHP? Директива nginx include делает то же самое. Она загружает другой файл. mime.types — это файл, расположенный в том же каталоге, что и этот файл конфигурации (то есть /etc/nginx). Содержимое этого файла выглядит так:

types {
text/html html htm shtml;
text/css css;
text/xml xml;
# ...
}

Как видите, он содержит общие типы mime и расширения файлов. Если мы не включим эти типы, nginx будет отправлять каждый ответ с заголовком Content-Type: text/plain, и браузер будет неправильно загружать CSS и JavaScript. С помощью этой директивы, если я отправлю запрос на файл CSS, nginx отправляет ответ, например:

HTTP/1.1 200 OK
Server: nginx/1.23.4
Date: Fri, 05 May 2023 08:05:29 GMT
Content-Type: text/css
Content-Length: 21205
Last-Modified: Sat, 29 Apr 2023 06:50:37 GMT
Connection: keep-alive
ETag: "644cbe3d-52d5"
Accept-Ranges: bytes

Кстати, я не написал mime.type поставляется с nginx по умолчанию.

Далее у нас конфигурация связанная с сервером:

listen 80;
server_name 138.68.81.14;

Эта конфигурация использует HTTP без SSL, поэтому она слушает порт 80. server_name обычно является доменным именем, но у меня есть только IP-адрес, поэтому я использую его.

root /var/www/html/demo;

Директива root определяет корневую папку данного сайта. Каждое имя файла после этой директивы будет ссылаться на этот путь. Итак, если вы пишете index.html, это означает /var/www/html/demo/index.html.

По умолчанию, если приходит такой запрос: GET http://138.68.81.14, nginx будет искать index.html внутри корневой папки. Который, если вы помните, существует для того, чтобы вернуть его клиенту.

Когда браузер анализирует HTML и отправляет запрос на style.css, запрос выглядит следующим образом: http://138.68.81.14/style.css, который также существует, поскольку находится в корневой папке.

Вот и всё! Это минимальная конфигурация nginx для обслуживания статического контента. Ещё раз, он не для продакшена и вообще не оптимизирован.

nginx ничего не знает о PHP. Если я добавлю index.php в корневую папку и попытаюсь запросить его, я получу следующий ответ:

curl http://localhost/index.php
<?php

echo datetime();

curl -I http://localhost/index.php
HTTP/1.1 200 OK
Server: nginx/1.24.0
Date: Sat, 06 May 2023 13:48:39 GMT
Content-Type: text/plain
Content-Length: 24
Last-Modified: Sat, 06 May 2023 13:45:47 GMT
Connection: keep-alive
ETag: "64565a0b-18"
Accept-Ranges: bytes

Он возвращает содержимое файла в виде обычного текста (text/plain). Давайте это исправим!

CGI, FastCGI, php-fpm

Как я уже писал, nginx не умеет запускать и интерпретировать PHP-скрипты. И это верно не только для PHP, он также не знает, что делать со скриптов Ruby и Perl. Итак, нам нужно что-то, что соединяет веб-сервер с нашими PHP-скриптами. Это то, что делает CGI.

CGI

CGI означает Common Gateway Interface (Общий Интерфейс Шлюза). Как следует из названия, это не пакет или библиотека. Нет, это интерфейс, протокол. Первоначальная спецификация определяет CGI следующим образом:

Общий интерфейс шлюза (CGI) — простой интерфейс для запуска внешних программ, программного обеспечения или шлюзов на информационном сервере независимым от платформы способом. - Спецификация CGI

CGI даёт унифицированный способ запуска сценариев на веб-серверах для создания динамического контента. Он не зависит от платформы и языка, поэтому скрипт может быть написан на PHP, Python или чем-то ещё. Это может быть даже программа на C++ или Delphi, она необязательно должна быть классическим языком сценариев. Его можно реализовать на любом языке, поддерживающим сетевые сокеты.

CGI использует модель один процесс на запрос. Это означает, что когда в веб-сервер поступает запрос, он создаёт новый процесс для выполнения php-скрипта:

Схема CGI, один запрос один процесс

Если приходит 1000 запросов, создаётся 1000 процессов и так далее. Основным преимуществом этой модели является простота, а недостатком большая ресурсоёмкость и трудность масштабирования при высоком трафике. Стоимость создания и уничтожения процессов довольно высока. WG также должен довольно часто переключать контекст, что становится дорогостоящей задачей, когда нагрузка на сервер велика.

FastCGI

FastCGI также является протоколом. Он построен на основе CGI и, как следует из названия, работает быстрее. Это означает, что он может справиться с большой нагрузкой. FastCGI делает это, отказываясь от модели один процесс на запрос. Вместо этого у него есть постоянные процессы, которые могут обрабатывать несколько запросов за время своего существования. Поэтому он снижает нагрузку на процессор при создании/уничтожении процессов и переключении между ними. Он также реализует мультиплексирование, но это не тема нашего обсуждения.

Он выглядит примерно так:

Схема FastCGI

FastCGI может быть реализован через сокеты Unix или TCP.

php-fpm

fpm означает диспетчер процессов FastCGI. php-fpm — это не протокол или интерфейс, а реальная исполняемая программа. Linux пакет. Это компонент, реализующий протокол FastCGI и соединяющий nginx с нашим Laravel приложением.

Он запускается как отдельный процесс на сервере, и мы можем указать nginx передавать каждый запрос PHP в php-fpm, который запустит Laravel приложение и вернёт HTML или JSON ответ в nginx.

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

Между прочим, эта архитектура процесса master-worker очень похожа на то, как работает nginx (подробнее об этом мы поговорим позже).

nginx и PHP

С этими знаниями мы готовы обрабатывать PHP-запросы от nginx! Во-первых, давайте установим php-fpm:

apt-get install php-fpm

После установки всё должно быть готово к работе. Он также должен запустить службу systemd, которую вы можете проверить выполнив следующие команды:

systemctl list-units | grep "php"
systemctl status php8.2-fpm.service # в моём случае это php8.2

Вывод должен выглядеть примерно так:

root@base:~$ systemctl status php8.2-fpm.service
● php8.2-fpm.service - The PHP 8.2 FastCGI Process Manager
Loaded: loaded (/lib/systemd/system/php8.2-fpm.service; enabled; vendor preset: enabled)
Active: active (running) since Sun 2023-05-07 14:15:15 +07; 28s ago
Docs: man:php-fpm8.2(8)
Process: 350 ExecStartPost=/usr/lib/php/php-fpm-socket-helper install /run/php/php-fpm.sock /etc/php/8.2>
Main PID: 278
Status: "Processes active: 0, idle: 2, Requests: 0, slow: 0, Traffic: 0req/sec"
Tasks: 3 (limit: 9473)
Memory: 20.1M
CGroup: /system.slice/php8.2-fpm.service
├─278 php-fpm: master process (/etc/php/8.2/fpm/php-fpm.conf)
├─348 php-fpm: pool www
└─349 php-fpm: pool www

Вот конфигурация nginx связывающая запросы с php-fpm:

user www-data;

events {}

http {
include mime.types;
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;

server {
listen 80;
server_name 138.68.79.28;

root /var/www/html/demo;

index index.php index.html;

location / {
try_files $uri $uri/ =404;
}

location ~\.php {
include fastcgi.conf;
fastcgi_pass unix:/run/php/php-fpm.sock;
}
}
}

Большая часть файла конфигурации должна быть знакома, но есть некоторые новые директивы. Теперь в нашем проекте есть PHP файлы, поэтому рекомендуется добавить index.php в качестве индексного файла:

index index.php index.html;

Если его не удаётся найти, nginx по умолчанию использует index.html.

Далее идёт область видимости location:

location / {
try_files $uri $uri/ =404;
}

try_files — отличное имя для исключения, потому что оно буквально пытается загрузить данные файлы по порядку. Но что такое $uri или =404.

$uri — переменная предоставляемая nginx. Она содержит нормализованный URI из URL. Вот несколько примеров:

Поэтому если запрос содержит определённое имя файла, nginx пытается его загрузить. Вот что делает первая часть:

try_files $uri

Если запрос mysite.com/about.html, то он возвращает содержимое about.html.

Что делать, если запрос содержит имя каталога? Я знаю, что сейчас это не так популярно (или в Laravel), но nginx был опубликован давным-давно. Второй параметр try_files позволяет запросить определённую папку:

try_files $uri $uri/

Например, если запрос mysite.com/articles и нам нужно вернуть index.html из папки articles, $uri/ делает это возможным. Вот что происходит:

Третий параметр — резервное значение. Если на файл, ни папка не найдены, nginx вернёт ответ 404:

try_files $uri $uri/ =404;

Таким образом первый блок location содержит статический контент. Второй обрабатывает запросы для PHP-файлов. Помните, Laravel и причудливые удобные URL-адреса пока не задействованы. На данный момент запрос PHP означает что-то вроде mysite.com/phpinfo.php.

Чтобы поймать эти запросы, нам нужен такой блок location:

location ~\.php {}

Как видите, это регулярное выражение, поскольку мы хотим сопоставить любые PHP файлы:

Так что это будет соответствовать любом PHP файлу.

Внутри блока location есть:

include fastcgi.conf;

Как я уже говорил, nginx поставляется с некоторыми предопределёнными конфигурациями, которые можно использовать. Этот файл определяет некоторые основные переменные среды для php-fpm:

fastcgi_param  SCRIPT_FILENAME    $document_root$fastcgi_script_name;
fastcgi_param QUERY_STRING $query_string;
fastcgi_param REQUEST_METHOD $request_method;

php-fpm нужна информация о методе запроса, исполняемом файле и так далее.

И в последней строке происходит волшебство:

fastcgi_pass unix:/run/php/php-fpm.sock;

Эта директива указывает nginx передать запрос php-fpm через сокет Unix. Если вы помните, FastCGI можно использовать через Unix сокеты или TCP-соединения. Мы используем первое. Я мало что знаю о сокетах Unix, но они позволяют передавать двоичные данные между процессами. Это именно то, что здесь происходит.

Вот команда для определения местонахождения сокета php-fpm:

find / -name *fpm.sock

Она находит любые имена файлов *fpm.sock внутри директории / (везде на сервере, начиная от корневой директории /).

Вот и вся конфигурация nginx для передачи запросов в php-fpm:

location ~\.php {
include fastcgi.conf;
fastcgi_pass unix:/run/php/php-fpm.sock;
}

Позже мы сделаем то же самое внутри контейнеров docker с Laravel. Мы также поговорим о том, как оптимизировать nginx и php-fpm.

nginx и Vue

Когда вы запускаете npm run build, он преобразует весь фронтенд в статические файлы HTML, CSS и JavaScript, которые можно использовать как простые статические файлы. После того как браузер загружает HTML, он отправляет запросы в API. В этом случае обслуживание приложения Vue требует не столько усилий, сколько для обслуживания PHP API.

Тем не менее в разделе Обслуживание статического контента с nginx, я показал вам довольно простую конфигурацию для демонстрации ваших целей, так что вот вариант по лучше:

server {
listen 80;
server_name 138.68.80.16;
root /var/www/html/posts/frontend/dist;
index index.html;

location / {
try_files $uri $uri/ /index.html;
}
}

Как видите, каталог dist является корневым. Именно здесь команда build генерирует весь свой вывод. Конфигурации фронтэнда нужен только один location где мы пытаемся загрузить:

Это всё ещё не оптимизированная конфигурация, но она довольно хорошо работает. Мы поговорим об оптимизации в отдельной статье.

Дополнительные материалы

Предыдущая Статья

Десять основных проблем аудита безопасности Laravel

Следующая Статья

Искусство записи Laravel Логов: Рекомендации и примеры