Zero-downtime деплой Swift-сервисов: как обновлять без простоев

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

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

Начать курс

Почему 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). Один активен, второй — в режиме ожидания. Когда вы обновляете код, вы:

  1. Собираете новый бинарник Swift.
  2. Запускаете его на свободном порту (например, 8081 вместо 8080).
  3. Проверяете health check нового экземпляра.
  4. Меняете балансировщик (Nginx, HAProxy) на новый порт.
  5. Отправляете 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: когда нужно обновить конфигурацию

Иногда вам не нужно менять би

Блоги

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