Почему zero-downtime деплой — это не роскошь, а необходимость для Swift-разработчика
Представьте: ваш Swift-сервис на Vapor обрабатывает тысячи запросов в секунду. Вы выпускаете новую фичу — и пользователи получают 502 Bad Gateway, а в логах — SIGTERM и оборванные соединения. Знакомо? Если вы пишете на Swift для бэкенда, то рано или поздно столкнётесь с проблемой: как обновить сервис, не потеряв ни одного запроса.
Zero-downtime деплой — это не магия, а набор проверенных техник, которые позволяют заменить работающий бинарник Swift на новый, не разрывая активные соединения. В этой статье я покажу, как настроить graceful shutdown, организовать горячую замену процессов и подружить Swift-сервис с Nginx. Всё — с реальным кодом на Swift и без воды.
Что такое zero-downtime и зачем это Swift-проектам
Zero-downtime deployment (деплой без простоев) — это когда вы обновляете приложение, а пользователи не замечают ни миллисекунды простоя. Для Swift-сервисов это особенно важно, потому что:
- Клиенты не терпят ошибок — оборванный запрос = потерянные данные или деньги.
- Swift-серверы часто работают под высокой нагрузкой (финтех, чаты, real-time API).
- Перезапуск бинарника может длиться секунды — а это вечность для активного соединения.
Классический подход — убить старый процесс и запустить новый — не работает. Swift-приложение на Vapor или Hummingbird должно плавно завершить обработку текущих запросов, а затем уступить место новому экземпляру. Именно об этом — весь дальнейший код.
Шаг 1. Graceful shutdown в Swift: пишем корректное завершение
Первый кирпичик zero-downtime — это graceful shutdown. Ваш Swift-сервис должен уметь ловить сигнал SIGTERM и не обрывать соединения на полуслове. Vapor 4+ уже умеет это делать из коробки, но давайте проверим и, если нужно, добавим кастомную логику.
Базовая настройка Vapor для graceful shutdown
import Vapor
let app = Application()
// Регистрируем обработчик сигнала
app.lifecycle.use(ShutdownSignalHandler())
// Стандартный код Vapor
app.get("health") { req in
return "OK"
}
try app.run()
Но что, если вам нужно дождаться завершения долгих задач? Например, обработки файлов или отправки email. Добавим кастомный ShutdownSignalHandler:
import Vapor
import NIOCore
final class ShutdownSignalHandler: LifecycleHandler {
private var runningTasks: [EventLoopFuture<Void>] = []
func willBoot(_ app: Application) throws {
// Подписываемся на SIGTERM
app.running?.onStop = {
print("⏳ Получен сигнал остановки. Завершаем активные задачи...")
return self.runningTasks.flatten(on: app.eventLoopGroup.next())
}
}
func registerTask(_ task: EventLoopFuture<Void>) {
runningTasks.append(task)
}
}
Теперь, когда придёт SIGTERM, сервер подождёт завершения всех зарегистрированных задач. Это критично для zero-downtime деплоя Swift-сервисов: старый процесс не убьёт запросы, которые уже начал обрабатывать.
Проверяем в консоли
Запустите сервер и отправьте ему сигнал:
kill -SIGTERM <PID>
В логах вы увидите сообщение о плавном завершении. Если всё работает — переходим к следующему шагу.
Шаг 2. Blue-green деплой на Swift: два бинарника — ни одного простоя
Graceful shutdown решает проблему обрыва соединений, но не решает проблему времени обновления. Пока старый процесс завершается, новый должен уже принимать запросы. Идеальная схема — blue-green деплой.
Суть проста: у вас два экземпляра Swift-сервиса (blue и green). Один активен, второй — в режиме ожидания. Когда вы обновляете код, вы:
- Собираете новый бинарник Swift.
- Запускаете его на свободном порту (например, 8081 вместо 8080).
- Проверяете health check нового экземпляра.
- Меняете балансировщик (Nginx, HAProxy) на новый порт.
- Отправляете
SIGTERMстарому процессу.
Реализуем это на Swift с помощью скрипта и пары трюков.
Скрипт деплоя для Swift-сервиса
#!/bin/bash
# deploy.sh — zero-downtime деплой для Vapor
APP_NAME="MySwiftService"
OLD_PORT=8080
NEW_PORT=8081
BINARY_PATH="./.build/release/$APP_NAME"
# 1. Собираем новый бинарник
echo "🔨 Building new binary..."
swift build -c release
# 2. Запускаем новый экземпляр на NEW_PORT
echo "🚀 Starting new instance on port $NEW_PORT..."
$BINARY_PATH --port $NEW_PORT &
NEW_PID=$!
# 3. Ждём, пока сервер поднимется (health check)
echo "⏳ Waiting for health check..."
for i in {1..10}; do
if curl -s http://localhost:$NEW_PORT/health | grep -q "OK"; then
echo "✅ New instance is healthy!"
break
fi
sleep 1
done
# 4. Меняем Nginx на новый порт (см. ниже)
# ...
# 5. Завершаем старый процесс
echo "🛑 Stopping old instance on port $OLD_PORT..."
kill -SIGTERM $(lsof -t -i:$OLD_PORT)
wait $OLD_PID 2>/dev/null
echo "✅ Zero-downtime deploy complete!"
Этот скрипт — основа. В реальном проекте вы добавите сюда интеграцию с CI/CD (GitHub Actions, GitLab CI). Ключевой момент: health check — без него вы рискуете переключить трафик на ещё не прогревшийся сервис.
Nginx: переключение трафика без потери запросов
Теперь нужно, чтобы Nginx перенаправлял запросы на новый порт. Вот минимальная конфигурация:
upstream swift_backend {
server 127.0.0.1:8080;
# При деплое меняем порт:
# server 127.0.0.1:8081;
}
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://swift_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
В скрипте деплоя добавьте шаг, который заменяет порт в конфиге Nginx и выполняет nginx -s reload. Nginx перезагружается без разрыва соединений — это стандартная фича.
Шаг 3. Hot reload для Swift: когда нужно обновить конфигурацию
Иногда вам не нужно менять би