Глава 63 · Практика
Запуск в production: PHP-FPM, релизы, cron и очереди
Как перенести работающий код на сервер и сохранить управляемость при сбоях.
Почему работающий локально файл ещё не готовый релиз
На своём компьютере вы запускаете PHP вручную. На сервере запросы принимаются постоянно, конфигурация отличается, а ошибка видна посетителям. Развёртывание переносит проверенный код вместе с зависимостями и настройками в это окружение.
Что уже пригодится. зависимости, проверки
- Исходный код и composer.lock
- Установка нужных зависимостей
- Конфигурация окружения отдельно от кода
- Проверки и совместимые миграции
- Переключение и наблюдение
Секреты не записывают в публичные PHP-примеры или Git. getenv читает значение из окружения процесса и возвращает строку либо false, если переменной нет. Даже конфигурацию проверяют по типу и допустимым значениям.
<?php
declare(strict_types=1);
function readDebugFlag(string|false $value): bool
{
return $value === '1';
}
foreach (['1', '0', false] as $value) {
echo readDebugFlag($value) ? "debug on\n" : "debug off\n";
}
Ожидаемый результат
debug on
debug off
debug off
Здесь явно заданы возможные результаты getenv('APP_DEBUG'). false означает отсутствие настройки; строка "false" тоже не включила бы режим. В production ошибки записывают в защищённый журнал, а не показывают посетителям.
Следующая ступень. Ниже разбираются PHP-FPM, права, OPcache, миграции, воркеры и проверяемый откат. Копирование файлов — лишь один шаг.
Локальный сервер и production решают разные задачи
Новый код должен попасть и в сайт, и в workers
Зелёные тесты ещё не означают обновлённый сервис. Важны окружение, миграции и процессы, которые уже работают.
- Собрать один артефакт
Код, зависимости из lock-файла и версия собираются вместе. Этот же артефакт проходит проверки.
- Проверить окружение
Версия PHP, расширения, конфигурация FPM, права каталогов и совместимые миграции данных.
- Переключить релиз
Сайт начинает использовать проверенный код; порядок миграции согласован со старой и новой версиями.
- Обновить долгоживущие workers
Дать текущим задачам завершиться и перезапустить процессы, чтобы они загрузили новый код.
- Проверить реальный сценарий
Запрос, запись в БД, очередь и журнал ошибок показывают, что сервис работает после обновления.
Откат кода сам по себе не откатывает данные. Встроенный php -S удобен для разработки, но не заменяет production-настройку.
Встроенный php -S удобен для разработки и ручной проверки маршрутов. Для публичной эксплуатации нужен поддерживаемый веб-сервер и правильно настроенный PHP runtime, часто PHP-FPM. Веб-сервер принимает соединения и отдаёт статику, FPM управляет рабочими процессами PHP. Доступность сайта зависит не только от синтаксиса: важны права каталогов, расширения, CA-сертификаты, конфигурация сессий и соединения с базой.
Разделите dev, test, staging и production по данным и секретам. В production display_errors выключен, log_errors включён, журнал закрыт от веб-доступа. Документный корень указывает на public, а не корень репозитория. У пользователя процесса минимальные права: исходники читаются, а запись разрешена только в специально выделенные storage/cache/session каталоги. Загруженные файлы не должны становиться исполняемыми скриптами.
Версию PHP, расширения и настройки фиксируют в описании окружения. php --ini и php -m для CLI не доказывают конфигурацию FPM: это могут быть разные ini и даже разные версии. Проверяйте рабочий SAPI безопасным диагностическим способом, не оставляя публичный phpinfo с секретной конфигурацией. После проверки диагностический endpoint удаляют или закрывают.
Собираем артефакт, проверяем, переключаем
php -v
php --ini
php -m
composer validate --strict
composer install --no-dev --prefer-dist --optimize-autoloader --no-interaction
composer check-platform-reqs
php -l public/index.php
# После этого: тесты проекта, smoke-test собранного артефакта,
# контролируемая миграция и переключение релиза по процедуре сервера.
Команды приведены для проекта с Composer, не выполняются автоматически. php -l проверяет один файл, поэтому сборка должна охватывать все PHP-файлы. Секреты и реальные адреса не включены.
Хороший релиз устанавливает проверенный артефакт с lock-файлом, а не редактирует рабочие файлы по одному. Переключение каталога или эквивалентный механизм уменьшает период смешения старого и нового кода. Миграции схемы должны быть совместимы с порядком переключения: сначала добавление, затем переход к использованию, позднее удаление старого. Откат кода не всегда способен откатить уже изменённые данные.
Проверяйте резервную копию восстановлением в изолированной среде. Наличие файла backup.zip не доказывает, что в нём полная база и нужные файлы. После релиза выполняют короткий smoke-test чтения и безопасного тестового сценария, смотрят ошибки, latency и очередь. При отключённой проверке timestamp OPcache нужно предусмотренное обновление кеша/процессов, иначе сервер продолжит выполнять старый код.
Фоновая задача должна уметь повторяться и останавливаться
<?php
declare(strict_types=1);
$handle = fopen('import.lock','c');
if ($handle === false) throw new RuntimeException('Cannot open lock');
try {
if (!flock($handle,LOCK_EX|LOCK_NB)) {
echo "already-running
";
} else {
try { echo "one import batch
"; }
finally { flock($handle,LOCK_UN); }
}
} finally {
fclose($handle);
}
Ожидаемый результат
one import batch
Пример создаёт lock только в изолированном рабочем каталоге. flock подходит для процессов с общим поддерживаемым файловым хранилищем и одинаковым протоколом блокировки; это не распределённый lock для нескольких независимых серверов.
Cron запускает команду по расписанию, но не ведёт автоматически историю задания и не предотвращает наложение. Укажите абсолютные пути к PHP, конфигурации и рабочему каталогу, сохраняйте exit code и безопасный журнал. Если выполнение дольше интервала, нужна блокировка или очередь. Отсутствие вывода не означает успех: команда должна завершаться корректным кодом и фиксировать результат.
Очередь хранит задания, воркер резервирует их на ограниченное время, выполняет и подтверждает. Падение до подтверждения приводит к повтору, поэтому обработчик идемпотентен. Число попыток ограничивают, проблемные задания переводят в отдельное состояние для разбора. Lease/visibility timeout должен учитывать длительность работы, иначе два воркера начнут одно задание. Heartbeat или продление аренды требуют отдельного протокола.
У долгоживущего процесса другая память
CLI-воркер обрабатывает много заданий одним процессом. Статические свойства, кеши и singleton-объекты живут между ними. То, что автоматически очищалось в конце HTTP-запроса, теперь может накапливаться или хранить данные предыдущего клиента. После каждого задания освобождайте крупные ссылки, сбрасывайте контекст пользователя и проверяйте транзакции. Периодический перезапуск по числу задач/памяти полезен, но не заменяет поиск утечки.
Сигнал завершения должен останавливать приём новых заданий и позволять текущему безопасно закончиться либо вернуть его в очередь. После релиза воркеры перезапускают, чтобы они загрузили новый код. Максимальные времена PHP, FPM, прокси, HTTP-клиента и очереди должны быть согласованы; бесконечный set_time_limit(0) не создаёт надёжный процесс сам по себе.
Мониторинг разделяет здоровье приложения и готовность обслуживать запросы. Простая страница 200 может работать при сломанной базе. Проверки зависимостей делают с короткими таймаутами и без изменения данных, а подробности не раскрывают публично. Наблюдайте возраст старейшего задания, число повторов, заполнение диска, ошибки соединений и память воркеров.
Релиз проверяется вместе с данными и очередью
Перед переключением оцените совместимость старых заданий с новым кодом. В очереди может лежать сообщение прежней версии схемы, а воркер после релиза получит его через час. Версионируйте формат сообщения и поддерживайте разумное переходное окно. Не сериализуйте произвольный PHP-объект как долгосрочный контракт: изменение имени класса или свойств усложнит восстановление.
Health-check не должен создавать заказ, отправлять письмо или выполнять дорогой полный отчёт. Он проверяет минимально достаточную готовность с коротким бюджетом. Глубокая диагностика доступна только обслуживающему персоналу. При падении зависимости важно не раскрыть пароль DSN в публичном ответе и не заставить балансировщик ещё сильнее нагружать уже больной сервис.
План отката включает код, конфигурацию, миграции, кеш и воркеры. Для удаления столбца или преобразования данных одного переключения symlink недостаточно. Используйте совместимые этапы, резервную копию и заранее проверенную процедуру восстановления. После аварийного отката отдельно проверьте отложенные события: они могли быть созданы новой версией и требуют совместимого обработчика.
Практика: проверьте решение, а не только успешный путь
После релиза веб-страницы обновились, но очередь продолжает отправлять старый шаблон письма. Почему и что входит в исправление?
Разбор решения
Долгоживущие воркеры уже загрузили старый код. Нужен управляемый перезапуск с завершением текущих заданий, затем проверка нового процесса и тестового сообщения в безопасном транспорте. Одного обновления файлов и OPcache веб-SAPI недостаточно.
Какие данные оставить в техническом журнале
Полный контракт и режимы записи: error_log.
В обычном режиме 0 отправляет сообщение настроенному обработчику PHP. Возвращаемый bool не является гарантией наблюдаемой доставки во всех режимах.
<?php
declare(strict_types=1);
function logOrderFailure(string $requestId,int $orderId): void {
error_log(json_encode(['event'=>'order_failed','requestId'=>$requestId,'orderId'=>$orderId],JSON_THROW_ON_ERROR));
}
Определение без записи в журнал. Идентификаторы должны быть проверены, а политика персональных данных согласована.
<?php
declare(strict_types=1);
function logDatabaseFailure(Throwable $error): void {
error_log(json_encode(['event'=>'database_failure','class'=>$error::class],JSON_THROW_ON_ERROR));
}
Это минимальный безопасный контекст. Закрытый диагностический слой может хранить больше с редактированием секретов.
Рядом по задаче. PSR-3 logger удобнее для структурированного контекста; HTTP-пользователь получает request_id, а не трассировку.
Функции и методы этой темы
Откройте карточку, чтобы увидеть договор функции, примеры, ограничения и связанные инструменты.
getenvПрочитать одну переменную окружения или карту окружения текущего процесса.
getenv(?string $name = null, bool $local_only = false): array|string|falseПрочитать одну переменную окружения или карту окружения текущего процесса.
Окружение часто передаёт endpoint, имя очереди и режим запуска в CLI/container. Без name функция возвращает массив, который способен содержать секреты; обычный код читает один белый список ключей. В FastCGI SAPI может подменять значения окружения: local_only=true просит только локально установленное ОС/putenv значение.
Первый запуск и развитие задачи
<?php
declare(strict_types=1);
putenv('PHP_MANUAL_REGION=eu-test');
echo getenv('PHP_MANUAL_REGION', true), "\n";
putenv('PHP_MANUAL_REGION');
var_dump(getenv('PHP_MANUAL_REGION', true));
Ожидаемый результат
eu-test
bool(false)
Строка NAME=value меняет окружение процесса, NAME без = удаляет переменную. Значение не записывается в php.ini и не является безопасным хранилищем секрета.
<?php
declare(strict_types=1);
putenv('PHP_MANUAL_MISSING');
$value = getenv('PHP_MANUAL_MISSING', true);
var_dump($value);
putenv('PHP_MANUAL_EMPTY=');
var_dump(getenv('PHP_MANUAL_EMPTY', true));
putenv('PHP_MANUAL_EMPTY');
Ожидаемый результат
bool(false)
string(0) ""
Строгое сравнение с false сохраняет пустую, но существующую строку. Это важно для флагов, где пустое значение может иметь отдельный смысл.
Полный договор вызова
| Параметр | Что передаём и зачем |
|---|---|
| name | Имя переменной или null для полного массива. Пустое имя не заменяет null. |
| local_only | При строковом name: true игнорирует значение, внесённое SAPI, и читает ОС/putenv; особенно важно при FastCGI. При name=null этот аргумент не влияет на результат. |
| Часть договора | Что получает программа |
|---|---|
| Сигнатура | getenv(?string $name = null, bool $local_only = false): array|string|false |
| Результат | string для существующего имени, false при отсутствии, array при name=null. |
| Ошибки и особые значения | Пустая строка — существующее значение, поэтому проверяйте === false. Полный массив нельзя бездумно писать в лог/ответ. |
| Версии | PHP 4–8. local_only добавлен в 7.0.9; чтение всей карты без name — в 7.1; name nullable с 8.0. Официальный диапазон узла: (PHP 4, PHP 5, PHP 7, PHP 8). |
| Среда | Все SAPI, если ниже не указано иное. |
Более простой или безопасный выбор. Конфигурационный слой приложения валидирует отдельные ключи и передаёт типизированные значения дальше.
Рядом: putenv, php_sapi_name.
Вернуться к карте задач или полному дереву функций.
Для имени возвращает строку либо false, если переменная отсутствует; без имени — массив. Не следует печатать всё окружение в диагностике.
<?php
declare(strict_types=1);
function requiredEnvironment(string $name): string {
$value=getenv($name);
if ($value===false || $value==='') throw new RuntimeException('Missing configuration: '.$name);
return $value;
}
Определение не читает реальные секреты. В журнале допустимо имя настройки, но не значение.
<?php
declare(strict_types=1);
function environmentFlag(string $name): bool {
$raw=getenv($name);
if ($raw===false) return false;
$value=filter_var($raw,FILTER_VALIDATE_BOOL,FILTER_NULL_ON_FAILURE);
if ($value===null) throw new RuntimeException('Invalid boolean configuration');
return $value;
}
Не использует (bool) "false", которое дало бы true. Конфигурация валидируется при запуске.
Рядом по задаче. Явный объект конфигурации проверяет типы один раз в точке сборки; filter_var помогает boolean-значениям.