Концепция канареечного развертывания довольно проста, благодаря ей в определенные серверы отправляется определенный процент трафика.

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

Этот метод развертывания приходится кстати и при выпуске служб продакшена  —  для обеспечения безопасного и плавного выпуска.

Каким образом перенаправляется трафик?

Отправляемый пользователем запрос добирается до API-шлюза, откуда перенаправляется на другой сервер в зависимости от авторизации перенаправления.

Чтобы избежать перегруженности сервера из-за огромного трафика, в шлюзе обычно добавляется балансировщик нагрузки.

Но каким будет примерный фрагмент кода? Разберем различные подходы к развертыванию.

Вероятностная маршрутизация трафика

Согласно этому подходу перенаправление трафика основывается на вероятности. Например, если цель  —  направлять 10 % трафика на сервер 1, каждый запрос с вероятностью 10 % направляется на сервер 1. Со временем и при большом количестве запросов распределение, как правило, приближается к 10 %, но оно не будет ровно 10 % для каждого блока запросов.

Вот фрагмент кода, в котором две конечные точки stableServiceURL и canaryServiceURL связываются с URL-адресом другой конечной точки, здесь ожидается 10%-ная вероятность отправки запроса в конечную точку canaryServiceURL:


func trafficRouter(w http.ResponseWriter, r *http.Request) {
// Генерируется случайное число от 0 до 100
randomVal := rand.Intn(100)

// Если случайное значение оказывается внутри канареечного процента, перенаправляется в «canary»
if randomVal < canaryPercentage {
redirectToService(w, r, canaryServiceURL)
} else {
redirectToService(w, r, stableServiceURL)
}
}

Здесь основным trafficRouter трафик направляется с 10%-ной вероятностью.

Детерминированная маршрутизация трафика

В отличие от предыдущего подхода с 10%-ной вероятностью, здесь 10 % точно перенаправляется на канареечный сервер:

// Этим «trafficRouter» траффик направляется в службу «canary» или «stable» детерминированно, исходя из счетчика запросов
func trafficRouter(w http.ResponseWriter, r *http.Request) {
mu.Lock() // Защищается доступ к счетчику «totalRequests»
totalRequests++
requestNumber := totalRequests
mu.Unlock()

// Подсчитывается, сколько запросов должно направиться в «canary», исходя из процента
if requestNumber%100 < canaryPercentage {
redirectToService(w, r, canaryServiceURL)
} else {
redirectToService(w, r, stableServiceURL)
}
}

В этом методе подсчитывается общее количество запросов и трафик перенаправляется, если находится в диапазоне.

То есть на каждые 100 запросов в канареечный сервер направляется 10.

Как эти подходы проявляются при тестировании?

Перепишем код для проверки счетчика для stable и для canary:

func trafficRouter(w http.ResponseWriter, r *http.Request) {
// Генерируется случайное число от 0 до 100
randomVal := rand.Intn(100)

// Если случайное значение оказывается внутри канареечного процента, перенаправляется в «canary»
if randomVal < canaryPercentage {
canaryCount++
fmt.Println("Canary Count", canaryCount)
} else {
StableCount++
fmt.Println("Stable Count", StableCount)
}
}

Вот скриншот вероятностной маршрутизации трафика после отправки 100 запросов:

Посмотрим на счетчик для детерминированной маршрутизации трафика:

Так, в зависимости от сценария, выбирается метод маршрутизации с соответствующим подходом  —  детерминированным или вероятностным.

Фрагмент кода всего проекта:

package main

import (
"fmt"
"io"
"net/http"
"sync"

"golang.org/x/exp/rand"
)

var (
totalRequests = 0
canaryCount = 0
StableCount = 0
canaryPercentage = 10 // 10 % траффика направляется в «canary»
stableServiceURL = "https://api.thedogapi.com/v1/breeds?limit=2&page=0"
canaryServiceURL = "https://jsonplaceholder.typicode.com/posts/1"
mu sync.Mutex
)

func main() {
http.HandleFunc("/", trafficRouter)
fmt.Println("Starting server on :8081")
err := http.ListenAndServe(":8081", nil)
if err != nil {
fmt.Println("Failed to start server")
}
}

func trafficRouter(w http.ResponseWriter, r *http.Request) {
randomVal := rand.Intn(100)

// Подсчитывается, сколько запросов должно направиться в «canary», исходя из процента
if randomVal < canaryPercentage {
redirectToService(w, r, canaryServiceURL)
} else {
redirectToService(w, r, stableServiceURL)
}
}

// Этим «redirectToService» запрос передается в выбранную службу — «canary» или «stable»
func redirectToService(w http.ResponseWriter, r *http.Request, serviceURL string) {
// В URL-адрес службы выполняется GET-запрос
req, err := http.NewRequest("GET", serviceURL, nil)
if err != nil {
http.Error(w, "Invalid request", http.StatusInternalServerError)
return
}

// Добавляются необходимые заголовки для службы «stable» — Dog API
if serviceURL == stableServiceURL {
req.Header.Set("Content-Type", "application/json")
}

client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
http.Error(w, "Service unavailable", http.StatusServiceUnavailable)
return
}
defer resp.Body.Close()

// Из ответа выбранной службы копируются заголовки и код состояния
for key, values := range resp.Header {
for _, value := range values {
w.Header().Add(key, value)
}
}
w.WriteHeader(resp.StatusCode)

// Из выбранной службы копируется содержимое тела
body, err := io.ReadAll(resp.Body)
if err != nil {
http.Error(w, "Failed to read response", http.StatusInternalServerError)
return
}
_, err = w.Write(body)
if err != nil {
http.Error(w, "Failed to write response", http.StatusInternalServerError)
return
}
}

Так с нуля создается собственный проект и реализуется канареечное развертывание, в котором trafficRouter заменяется фрагментом кода любого из двух подходов.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи SHIVAM SOURAV JHA: Go Projects: How to make own Canary Deployment

Предыдущая статьяАрхитектура BFF  —  бэкенд для фронтенда
Следующая статьяNext.js: шаблоны управления состоянием через React Server Components