Canary deployments с Kotlin: бесшовный релиз без страха

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

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

Начать курс

Почему обычный деплой — это всегда лотерея?

Каждый разработчик хотя бы раз в жизни переживал момент истины: ты нажимаешь кнопку "Deploy", и сердце уходит в пятки. Что если новая версия уронит базу? Что если пользователи увидят 500-ю ошибку вместо главной страницы?

Обычный деплой похож на прыжок с парашютом без запасного. Либо всё работает идеально, либо ты проводишь ночь за логами, пытаясь понять, что пошло не так. Но есть способ сделать этот процесс безопасным — canary deployments (канареечное развертывание).

В этой статье мы разберем, как реализовать canary deployment на Kotlin с использованием Ktor и простого балансировщика. Вы научитесь поставлять новый код сначала 5% пользователей, а затем плавно увеличивать трафик. И да, мы напишем код, который реально работает.

Что такое Canary deployment и почему он нужен именно вам?

Представьте, что вы — капитан корабля. Вместо того чтобы сразу плыть через рифы, вы отправляете вперед маленькую лодку (канарейку). Если она проходит — плывет весь флот. Если тонет — вы теряете только одну лодку.

В мире IT это выглядит так: вы разворачиваете новую версию сервиса сначала для небольшой группы пользователей. Если метрики в порядке — трафик растет. Если нет — вы откатываетесь за секунды.

Преимущества очевидны:

  • Минимизация рисков — баги затрагивают только 1-5% аудитории.
  • Быстрая обратная связь — вы видите ошибки до того, как они станут катастрофой.
  • A/B тестирование — можно сравнить производительность старой и новой версий.
  • Zero downtime — пользователи не замечают переключения.

Но есть и ложка дегтя: нужно уметь управлять трафиком, мониторить метрики и автоматизировать процесс. Kotlin с его лаконичностью и корутинами отлично подходит для этой задачи.

Архитектура Canary deployment на Kotlin

Давайте построим простую, но рабочую архитектуру. У нас будет:

  • Балансировщик — решает, какой процент запросов идет на старую версию (stable), а какой — на новую (canary).
  • Два экземпляра сервиса — stable и canary, оба написаны на Kotlin с Ktor.
  • Мониторинг — счетчики ошибок и времени ответа.

Балансировщик будет принимать решение на основе заголовка X-Canary или случайного распределения. Мы пойдем по второму пути — он ближе к реальности.

Пишем код: балансировщик на Kotlin

Начнем с балансировщика. Он будет принимать запросы и пересылать их на один из двух сервисов. Для простоты будем использовать встроенный HTTP-клиент Ktor.

import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlinx.coroutines.runBlocking
import kotlin.random.Random

// Конфигурация
const val STABLE_URL = "http://localhost:8081"
const val CANARY_URL = "http://localhost:8082"
var canaryPercent = 5 // Начинаем с 5% трафика

fun main() {
    val client = HttpClient(CIO)

    embeddedServer(Netty, port = 8080) {
        routing {
            get("/api/data") {
                val useCanary = Random.nextInt(100) < canaryPercent
                val targetUrl = if (useCanary) CANARY_URL else STABLE_URL

                val response = runBlocking {
                    client.get("$targetUrl/api/data")
                }

                call.respondText(
                    response.bodyAsText(),
                    contentType = response.contentType()
                )
            }

            // Эндпоинт для изменения процента канарейки
            get("/canary/set/{percent}") {
                canaryPercent = call.parameters["percent"]?.toIntOrNull() ?: 5
                call.respondText("Canary percentage set to $canaryPercent%")
            }
        }
    }.start(wait = true)
}

Что здесь происходит? Балансировщик генерирует случайное число от 0 до 99. Если оно меньше canaryPercent — запрос уходит на канареечную версию. Иначе — на стабильную. Просто, как дважды два.

Обратите внимание на эндпоинт /canary/set/{percent}. С его помощью вы можете динамически менять процент трафика без перезапуска. Хотите увеличить канарейку с 5% до 20%? Просто отправьте GET-запрос.

Создаем стабильную и канареечную версии

Теперь напишем два одинаковых сервиса, которые будут имитировать разное поведение. Stable-версия возвращает старый ответ, canary — новый.

// StableService.kt
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.server.response.*
import io.ktor.server.routing.*

fun main() {
    embeddedServer(Netty, port = 8081) {
        routing {
            get("/api/data") {
                call.respondText("Stable version v1.0", contentType = ContentType.Text.Plain)
            }
        }
    }.start(wait = true)
}

// CanaryService.kt
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.server.response.*
import io.ktor.server.routing.*

fun main() {
    embeddedServer(Netty, port = 8082) {
        routing {
            get("/api/data") {
                // Имитируем новую версию с возможной ошибкой
                if (Math.random() < 0.1) { // 10% ошибок для демонстрации
                    call.respondText("Error: new version failed", status = HttpStatusCode.InternalServerError)
                } else {
                    call.respondText("Canary version v2.0", contentType = ContentType.Text.Plain)
                }
            }
        }
    }.start(wait = true)
}

В canary-версии мы специально добавили 10% ошибок. Это позволит нам протестировать механизм отката.

Мониторинг: как понять, что канарейка сдохла?

Без мониторинга canary deployment — это стрельба из пушки по воробьям. Нам нужно отслеживать:

  • Процент ошибок (error rate) — если он превышает порог, откатываем.
  • Время ответа (latency) — новая версия не должна тормозить.
  • RPS (requests per second) — нагрузка не должна упасть.

Добавим в балансировщик простой сбор метрик:

import io.ktor.server.plugins.callloging.*
import io.ktor.server.plugins.statuspages.*
import java.util.concurrent.atomic.AtomicInteger

// Внутри routing
var stableErrors = AtomicInteger(0)
var canaryErrors = AtomicInteger(0)
var stableLatency = AtomicInteger(0)
var canaryLatency = AtomicInteger(0                

Блоги

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