Zero-downtime деплой PHP: боремся с простоем без боли

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Когда секунда простоя стоит миллиона

Представьте: вы обновляете интернет-магазин на 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, который автоматизирует процесс.



                
                

Блоги

Рекомендаций по книгам