Когда секунда простоя стоит миллиона
Представьте: вы обновляете интернет-магазин на PHP, заливаете новую версию через FTP, и в этот момент сайт падает на 5 минут. Пять минут — это 300 секунд. За это время 100 потенциальных клиентов уходят к конкурентам, а вы теряете не только деньги, но и репутацию. Знакомая боль?
В современном мире, где пользователи не прощают ошибок, zero-downtime деплой PHP сервисов — это не роскошь, а необходимость. Но как реализовать бесшовный деплой, если ваш стек — PHP, Nginx и несколько серверов? Давайте разбираться на реальных примерах.
Почему PHP сложнее, чем кажется?
В отличие от Node.js или Python, PHP не поддерживает многопоточность в классическом понимании. Каждый запрос обрабатывается отдельным процессом PHP-FPM. Это накладывает ограничения: вы не можете просто «перезагрузить» код без потери текущих сессий. Однако это же свойство даёт нам суперсилу — возможность плавно переключать трафик между версиями.
Основные проблемы при деплое PHP:
- Разрыв соединений — пользователь теряет данные формы или сессию
- Грязное кэширование — старый код возвращает данные из устаревшего кэша
- Сломанные миграции — новая версия БД не совместима со старым кодом
Чтобы избежать этих граблей, используем проверенные стратегии. И да, zero-downtime деплой — это не магия, а чёткий план.
Стратегия №1: Blue-Green Deployment
Самый надёжный способ — держать две полные копии приложения: «синюю» (текущую) и «зелёную» (новую). Nginx выступает в роли балансировщика.
Как это выглядит на практике
Допустим, у вас есть два сервера: app1.example.com (blue) и app2.example.com (green). На обоих крутится PHP-FPM. Сейчас активен blue. Вы деплоите новую версию на green, тестируете её, а затем переключаете Nginx на green.
Пример конфигурации Nginx для переключения:
# nginx.conf
upstream php_backend {
# Сначала active blue
server app1.example.com:9000 weight=1; # blue
server app2.example.com:9000 weight=0; # green (выключен)
}
server {
listen 80;
server_name myshop.com;
location ~ \.php$ {
fastcgi_pass php_backend;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
}
После деплоя на green, меняем веса:
# После деплоя — переключаем трафик
upstream php_backend {
server app1.example.com:9000 weight=0; # blue (выключен)
server app2.example.com:9000 weight=1; # green (активен)
}
Перезагружаем Nginx без падения: nginx -s reload. Все текущие соединения завершаются корректно, а новые идут на green. Zero-downtime деплой PHP обеспечен!
Плюсы и минусы
Плюсы: полная отказоустойчивость, можно быстро откатиться (просто вернуть веса).
Минусы: нужно вдвое больше ресурсов (серверов или контейнеров).
Стратегия №2: Rolling Update (постепенное обновление)
Если у вас бюджет ограничен, используем rolling update. Идея: обновляем PHP-серверы по одному, не выключая остальные. Пользователи не замечают простоя, так как Nginx перенаправляет запросы на живые серверы.
Реализация на PHP-FPM
Предположим, у вас 3 сервера с PHP-FPM. Мы поочерёдно выводим их из ротации, обновляем код и возвращаем обратно.
Шаг 1: Отключаем сервер от балансировщика (graceful shutdown):
# На сервере, который обновляем
sudo systemctl stop php8.1-fpm
# Nginx сам перестанет слать запросы на этот сервер, так как он недоступен
Шаг 2: Обновляем код (через git pull, rsync или Docker).
Шаг 3: Запускаем PHP-FPM снова:
sudo systemctl start php8.1-fpm
# Проверяем, что сервер снова в строю
systemctl status php8.1-fpm
Шаг 4: Повторяем для остальных серверов.
Важно: между шагами делайте паузу в 10-30 секунд, чтобы Nginx успел обновить список здоровых бэкендов.
Как проверить, что сервер готов?
Используем health-check в Nginx:
upstream php_backend {
server app1:9000 max_fails=3 fail_timeout=30s;
server app2:9000 max_fails=3 fail_timeout=30s;
server app3:9000 max_fails=3 fail_timeout=30s;
}
Если сервер отвечает с ошибкой 3 раза подряд, Nginx исключает его на 30 секунд. Идеально для rolling update.
Стратегия №3: Graceful Shutdown с обработчиком сигналов
Самый тонкий момент — завершить старые процессы, не оборвав запросы пользователей. PHP-FPM по умолчанию поддерживает graceful shutdown: когда вы посылаете сигнал SIGQUIT, мастер-процесс перестаёт принимать новые запросы, но дожидается завершения текущих.
Пример отправки сигнала:
# Найти PID мастер-процесса PHP-FPM
ps aux | grep php-fpm | grep master
# Отправить graceful shutdown
kill -QUIT <PID>
Но это лишь часть картины. Ваш PHP-код тоже должен корректно обрабатывать завершение. Например, если вы используете очереди (RabbitMQ, Redis), нужно завершить обработку текущего сообщения.
Пример обработки сигнала в PHP
<?php
// register_shutdown_function для корректного завершения
register_shutdown_function(function() {
// Закрываем соединения с БД
// Сохраняем кэш
// Логируем завершение
error_log("Worker завершил работу корректно");
});
// Если используете pcntl (для консольных скриптов)
pcntl_signal(SIGTERM, function ($signal) {
// Завершаем обработку текущей задачи
// Не принимаем новые
exit(0);
});
Этот подход особенно полезен, если ваш PHP-сервис работает как worker для фоновых задач. Zero-downtime деплой в таком случае гарантирует, что ни одно задание не пропадёт.
Автоматизация деплоя с помощью PHP-скрипта
Чтобы не делать всё вручную, напишем простой скрипт на PHP, который автоматизирует процесс.
Блоги
Рекомендаций по книгам
Читайте также