Как деплоить Kotlin-сервисы, не разбудив прод в 3 часа ночи
Представьте: вы выкатываете обновление своего Kotlin-микросервиса, и вдруг — бах! — пользователи видят 502 ошибку, а в логах краснеют StackTrace. Знакомо? Если ваша команда практикует деплой с остановкой сервиса на 30 секунд, то каждый релиз — это лотерея. Но есть и хорошая новость: zero-downtime деплой Kotlin решает эту проблему раз и навсегда.
В этой статье я покажу три реальные стратегии, которые используют в продакшене: rolling update, blue-green deployment и graceful shutdown. Вы получите готовые примеры кода на Kotlin для Ktor и Spring Boot, которые можно внедрить уже сегодня. Поехали.
Почему zero-downtime деплой — это не роскошь, а необходимость
Когда ваш сервис перестает отвечать хотя бы на 10 секунд, это не просто технический сбой. Это потеря клиентов, падение конверсии и удар по репутации. Современные пользователи не прощают задержек. А если вы работаете с финансовыми транзакциями или потоковыми данными — простой может стоить миллионы.
Zero-downtime деплой (или бесшовный деплой) позволяет обновлять код без разрыва соединений. Пользователи даже не замечают, что сервис только что перезагрузился. Для Kotlin-разработчиков это особенно актуально, потому что JVM-приложения стартуют не мгновенно — и каждая секунда простоя ощутима.
Давайте разберем основные стратегии от простых к сложным.
Стратегия 1: Graceful shutdown — база, без которой никуда
Прежде чем переходить к сложным оркестрациям, убедитесь, что ваш Kotlin-сервис умеет корректно завершать работу. Graceful shutdown — это способность приложения завершить обработку текущих запросов, прежде чем выключиться. Без этого даже rolling update не спасет: вы просто убьете активные соединения.
Реализация graceful shutdown в Ktor
Ktor из коробки поддерживает graceful shutdown. Но по умолчанию он ждет всего 5 секунд. Для продакшена этого мало. Давайте настроим:
fun Application.module() {
install(Shutdown) {
timeout = 30.seconds // Ждем до 30 секунд
onShutdown = {
log.info("Shutdown hook: завершаем активные запросы")
closeResources()
}
}
routing {
get("/") {
call.respondText("Hello, Zero-Downtime!")
}
}
}
fun closeResources() {
// Закрываем connection pool, kafka consumer и т.д.
println("Ресурсы освобождены")
}
Ключевой момент: timeout должен быть больше, чем максимальное время обработки одного запроса. Если ваш эндпоинт иногда отвечает 15 секунд — ставьте таймаут 20-25 секунд.
Graceful shutdown в Spring Boot на Kotlin
В Spring Boot 2.3+ graceful shutdown настраивается одной строчкой в application.yml:
server:
shutdown: graceful
spring:
lifecycle:
timeout-per-shutdown-phase: 30s
А в коде можно добавить хук для освобождения ресурсов:
@Component
class GracefulShutdownListener : ApplicationListener<ContextClosedEvent>() {
override fun onApplicationEvent(event: ContextClosedEvent) {
println("Spring Boot: начинаем graceful shutdown")
// Закрываем WebSocket сессии, RabbitMQ и т.д.
}
}
Теперь при получении сигнала SIGTERM (который посылает Kubernetes или Docker) ваш сервис не убьет запросы, а даст им завершиться. Это первый шаг к zero-downtime деплою.
Стратегия 2: Rolling update — плавное обновление без взрывов
Rolling update — это когда вы обновляете поды (или контейнеры) не все сразу, а постепенно. Один старый под выключается — один новый запускается. И так, пока все не обновится. Это стандартный подход в Kubernetes.
Но есть нюанс: если ваш Kotlin-сервис стартует 20 секунд, а вы убили старый под через 5 секунд после запуска нового — будет простой. Нужно настроить readiness probe и lifecycle hooks.
Пример deployment.yaml для Kotlin-сервиса на Ktor
apiVersion: apps/v1
kind: Deployment
metadata:
name: kotlin-service
spec:
replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0 # Ни один старый под не уйдет, пока новый не готов
maxSurge: 1 # Можно запустить один дополнительный под
template:
spec:
containers:
- name: app
image: myrepo/kotlin-service:latest
ports:
- containerPort: 8080
readinessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 5
periodSeconds: 3
failureThreshold: 5
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 15"]
Разберем ключевые моменты:
- maxUnavailable: 0 — гарантирует, что всегда будет доступно 3 пода. Никогда не останетесь без сервиса.
- readinessProbe — проверяет, готов ли под принимать трафик. Пока не ответит 200 — трафик не пойдет.
- preStop — добавляет задержку в 15 секунд перед завершением. Это дает время балансировщику убрать под из ротации. Без этой паузы вы рискуете убить под, который еще обрабатывает запросы.
Такой подход обеспечивает rolling update без простоев для Kotlin-сервисов. Но есть одно "но": если ваше обновление ломает обратную совместимость API — rolling update не спасет. Тут нужна другая стратегия.
Стратегия 3: Blue-green deployment — переключаемся без риска
Blue-green deployment — это когда у вас работают две полные копии сервиса: "синяя" (старая версия) и "зеленая" (новая). Вы направляете трафик на синюю, разворачиваете зеленую, тестируете ее, а потом одним щелчком переключаете трафик на зеленую. Если что-то пошло не так — переключаетесь обратно.
Это идеальный вариант для критичных сервисов, где каждая секунда простоя недопустима. Минус — нужно в два раза больше ресурсов.
Как реализовать blue-green деплой для Kotlin-сервиса
В Kubernetes это делается через Service, который переключается между двумя Deployment:
# blue-deployment.yaml (старая версия)
apiVersion: apps/v1
kind: Deployment
metadata:
name: kotlin-service-blue
labels:
app: kotlin-service