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-подходе мы запускаем новую версию параллельно со старой, но направляем на неё только часть трафика.
Вот упрощенная схема:
- Стабильная версия (v1) — работает на 90% инстансов.
- Canary-версия (v2) — запущена на 10% инстансов.
- Балансировщик — распределяет трафик пропорционально.
- Мониторинг — сравнивает метрики (latency, errors, throughput).
- Ручное или автоматическое принятие решения — раскатываем 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 увидеть, что