Canary deployments на Rust: пошаговое руководство с примерами кода

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

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

Начать курс

Canary deployments с Rust: без страха и упрёка

Представьте: вы написали новую фичу на Rust, прогнали тесты (они зелёные!), залили на продакшен... и тут 500 ошибки сыпятся, пользователи пишут в поддержку, а вы лихорадочно откатываете релиз. Знакомо? Если да — вы не одиноки. Деплой — это всегда стресс, особенно когда код работает с железом или на высоких нагрузках.

Но есть способ сделать развёртывание почти безболезненным: canary deployments (канареечные развёртывания). Идея проста: вы пускаете новый код не на всех пользователей сразу, а на небольшой процент (скажем, 5–10%). Если всё хорошо — постепенно увеличиваете долю. Если нет — откатываетесь, и об этом знает только горстка «канареек».

В этой статье мы разберём, как реализовать canary deployments для сервиса на Rust. Вы увидите примеры кода, настройку балансировщика и простой мониторинг. Поехали!

Почему Rust идеален для canary deployments?

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

  • Безопасность памяти — Rust гарантирует, что утечки памяти и гонки данных (data races) не пройдут в продакшен. Меньше багов = меньше шансов, что canary упадёт.
  • Малый размер бинарника — cargo build --release даёт компактный исполняемый файл. Его можно быстро скопировать на несколько серверов или контейнеров.
  • Высокая производительность — Rust работает на уровне C/C++. Это значит, что ваш canary-сервер не «просядет» под нагрузкой, даже если на него направлено 10% трафика.
  • Экосистема — крейты вроде actix-web, tokio, tower дают готовые инструменты для HTTP, балансировки и метрик.

В итоге: Rust-сервис стабилен, быстр и легко поддаётся гранулярному управлению трафиком. Идеально для canary!

Архитектура canary deployment на Rust

Представим, что у нас есть веб-сервис на actix-web. Обычный REST API: принимает запросы, пишет в базу, отдаёт ответы. Мы хотим обновить версию с v1 на v2, но не сразу.

Базовая схема:

  1. Балансировщик (например, Nginx или HAProxy) распределяет трафик между версиями.
  2. Основная группа (stable) — серверы с v1, принимают 90% запросов.
  3. Canary-группа — один или два сервера с v2, принимают 10% запросов.
  4. Мониторинг — собираем метрики с обеих групп: ошибки, задержки, RPS.

Когда мы убеждаемся, что v2 работает стабильно, плавно увеличиваем вес canary-группы до 100%.

Шаг 1: Пишем Rust-сервер с метриками

Наш сервер будет отдавать две версии эндпоинта /api/hello. Для простоты версии различаются строкой ответа. В реальном проекте это может быть разная бизнес-логика.

// Cargo.toml
[dependencies]
actix-web = "4"
actix-rt = "2"
serde = { version = "1", features = ["derive"] }
prometheus = { version = "0.13", features = ["process"] }
log = "0.4"
env_logger = "0.10"

Теперь сам сервер. Мы добавим метрики Prometheus, чтобы следить за здоровьем canary:

use actix_web::{web, App, HttpServer, HttpResponse, middleware::Logger};
use prometheus::{Counter, HistogramVec, register_counter, register_histogram_vec};
use std::sync::OnceLock;

// Глобальные метрики
static REQUEST_COUNT: OnceLock<Counter> = OnceLock::new();
static REQUEST_DURATION: OnceLock<HistogramVec> = OnceLock::new();

fn init_metrics() {
    REQUEST_COUNT.get_or_init(|| {
        register_counter!(
            "http_requests_total",
            "Total number of HTTP requests"
        ).unwrap()
    });
    REQUEST_DURATION.get_or_init(|| {
        register_histogram_vec!(
            "http_request_duration_seconds",
            "HTTP request duration in seconds",
            &["handler", "version"]
        ).unwrap()
    });
}

async fn hello_v1() -> HttpResponse {
    let start = std::time::Instant::now();
    REQUEST_COUNT.get().unwrap().inc();

    // Симуляция работы
    tokio::time::sleep(std::time::Duration::from_millis(10)).await;

    let duration = start.elapsed().as_secs_f64();
    REQUEST_DURATION.get().unwrap()
        .with_label_values(&["hello", "v1"])
        .observe(duration);

    HttpResponse::Ok().body("Hello from v1 (stable)")
}

async fn hello_v2() -> HttpResponse {
    let start = std::time::Instant::now();
    REQUEST_COUNT.get().unwrap().inc();

    // Новая версия может быть чуть быстрее или медленнее
    tokio::time::sleep(std::time::Duration::from_millis(5)).await;

    let duration = start.elapsed().as_secs_f64();
    REQUEST_DURATION.get().unwrap()
        .with_label_values(&["hello", "v2"])
        .observe(duration);

    HttpResponse::Ok().body("Hello from v2 (canary)")
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    env_logger::init();
    init_metrics();

    let port = std::env::var("PORT").unwrap_or_else(|_| "8080".to_string());
    let version = std::env::var("VERSION").unwrap_or_else(|_| "v1".to_string());

    log::info!("Starting server version {} on port {}", version, port);

    HttpServer::new(move || {
        App::new()
            .wrap(Logger::default())
            .route("/api/hello", web::get().to(match version.as_str() {
                "v2" => hello_v2,
                _ => hello_v1,
            }))
            .route("/metrics", web::get().to(|| async {
                use prometheus::Encoder;
                let encoder = prometheus::TextEncoder::new();
                let mut buffer = Vec::new();
                let metric_families = prometheus::gather();
                encoder.encode(&metric_families, &mut buffer).unwrap();
                HttpResponse::Ok()
                    .content_type("text/plain; charset=utf-8")
                    .body(buffer)
            }))
    })
    .bind(format!("0.0.0.0:{}", port))?
    .run()
    .await
}

Обратите внимание: переменная окружения VERSION определяет, какую версию отдаёт сервер. Это позволяет запускать один и тот же бинарник с разными флагами — идеально для canary.

Шаг 2: Настраиваем балансировщик (Nginx)

Теперь нам нужно, чтобы Nginx направлял 10% трафика на canary-сервер, а 90% — на stable. Вот конфигурация:

Блоги

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