Canary deployments на Go: безопасный деплой без страха

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

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

Начать курс

Canary deployments с Go: как выпускать обновления без страха и риска

Представьте: вы годами выстраивали архитектуру, писали тесты, настраивали мониторинг. И вот — релиз. Один клик, и новая версия летит в прод. Сердце колотится, пальцы дрожат. Знакомо? А что, если я скажу, что можно деплоить новую версию всего на 5% пользователей, наблюдать за метриками, и только потом раскатывать на всех? Без даунтаймов, без паники, с полным контролем.

Это называется canary deployment (канареечное развертывание). Название пошло от шахтеров, которые брали с собой канареек — птица погибала от газа раньше людей, предупреждая об опасности. В DevOps-мире такими «канарейками» становятся первые 5-10% пользователей, которые получают новую версию. Если что-то пошло не так — откатываемся, не замечая проблем.

В этой статье мы разберем, как реализовать canary-деплой на Go с реальными примерами кода, балансировщиком и метриками. Поехали!

Почему именно Go для canary-деплоя?

Go — язык, созданный для микросервисов. Он компилируется в статический бинарник, не требует зависимостей на сервере, легко масштабируется. Но главное — в Go есть встроенная поддержка конкурентности и отличные библиотеки для HTTP-серверов, что делает его идеальным кандидатом для реализации умного роутинга трафика.

  • Производительность — Go-сервер обрабатывает тысячи запросов в секунду на одном ядре.
  • Простота деплоя — один бинарник, никаких виртуальных машин или интерпретаторов.
  • Богатая экосистема — пакеты для метрик (Prometheus), трассировки (OpenTelemetry) и балансировки.

Если вы уже пишете на Go и хотите автоматизировать деплой — canary-стратегия станет вашим лучшим другом.

Архитектура canary-деплоя на Go

Давайте представим, что у нас есть микросервис на Go, который обрабатывает запросы пользователей. Обычно мы деплоим новую версию сразу на все инстансы. При canary-подходе мы запускаем новую версию параллельно со старой, но направляем на неё только часть трафика.

Вот упрощенная схема:

  1. Стабильная версия (v1) — работает на 90% инстансов.
  2. Canary-версия (v2) — запущена на 10% инстансов.
  3. Балансировщик — распределяет трафик пропорционально.
  4. Мониторинг — сравнивает метрики (latency, errors, throughput).
  5. Ручное или автоматическое принятие решения — раскатываем v2 на 100% или откатываем.

Реализовать это можно разными способами: через Kubernetes (Service Mesh), через Nginx с кастомными весами, или написать свой балансировщик на Go. Мы пойдем третьим путем — он дает полный контроль и отлично подходит для обучения.

Пишем балансировщик canary-трафика на Go

Напишем простой reverse-proxy, который будет направлять запросы на два бэкенда: stable и canary. Вес canary-версии будем задавать через конфиг или переменную окружения.

package main

import (
    "log"
    "math/rand"
    "net/http"
    "net/http/httputil"
    "net/url"
    "os"
    "strconv"
)

type CanaryProxy struct {
    stable       *httputil.ReverseProxy
    canary       *httputil.ReverseProxy
    canaryWeight int // процент трафика на canary (0-100)
}

func NewCanaryProxy(stableURL, canaryURL string, weight int) *CanaryProxy {
    stableTarget, _ := url.Parse(stableURL)
    canaryTarget, _ := url.Parse(canaryURL)

    return &CanaryProxy{
        stable:       httputil.NewSingleHostReverseProxy(stableTarget),
        canary:       httputil.NewSingleHostReverseProxy(canaryTarget),
        canaryWeight: weight,
    }
}

func (p *CanaryProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    // Принимаем решение на основе случайного числа
    if rand.Intn(100) < p.canaryWeight {
        log.Printf("[CANARY] Routing request to canary: %s", r.URL.Path)
        p.canary.ServeHTTP(w, r)
    } else {
        log.Printf("[STABLE] Routing request to stable: %s", r.URL.Path)
        p.stable.ServeHTTP(w, r)
    }
}

func main() {
    stableURL := getEnv("STABLE_URL", "http://localhost:8081")
    canaryURL := getEnv("CANARY_URL", "http://localhost:8082")
    weightStr := getEnv("CANARY_WEIGHT", "10")
    weight, _ := strconv.Atoi(weightStr)

    proxy := NewCanaryProxy(stableURL, canaryURL, weight)

    http.Handle("/", proxy)
    log.Printf("Starting canary proxy on :8080 with canary weight %d%%", weight)
    log.Fatal(http.ListenAndServe(":8080", nil))
}

func getEnv(key, fallback string) string {
    if value, ok := os.LookupEnv(key); ok {
        return value
    }
    return fallback
}

Что здесь происходит? Мы создаем два reverse proxy — один для стабильной версии, другой для canary. На каждый запрос генерируем случайное число от 0 до 99. Если оно меньше веса canary (например, 10), то запрос уходит на новую версию. Иначе — на старую.

Это простейшая реализация. В реальном проекте вы захотите добавить:

  • Поддержку sticky sessions (чтобы один пользователь всегда попадал на одну версию).
  • Логирование метрик в Prometheus.
  • Graceful shutdown при откате.

Метрики и мониторинг canary-деплоя

Без метрик canary-деплой — это просто лотерея. Вам нужно знать, не упала ли производительность, не увеличилось ли время ответа, не появилось ли больше ошибок. Для Go отлично подходит Prometheus с клиентской библиотекой prometheus/client_golang.

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

import (
    "github.com/prometheus/client_golang/prometheus"
    "github.com/prometheus/client_golang/prometheus/promhttp"
)

var (
    requestsTotal = prometheus.NewCounterVec(
        prometheus.CounterOpts{
            Name: "canary_requests_total",
            Help: "Total number of requests by version",
        },
        []string{"version"},
    )
    requestDuration = prometheus.NewHistogramVec(
        prometheus.HistogramOpts{
            Name:    "canary_request_duration_seconds",
            Help:    "Request duration in seconds by version",
            Buckets: prometheus.DefBuckets,
        },
        []string{"version"},
    )
)

func init() {
    prometheus.MustRegister(requestsTotal)
    prometheus.MustRegister(requestDuration)
}

Теперь в методе ServeHTTP мы можем замерять время и считать запросы. Это позволит вам на дашборде Grafana увидеть, что

Блоги

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