Canary deployments с JavaScript: как выпускать обновления, не боясь сломать всё
Вы когда-нибудь выкатывали новую фичу в полночь, молясь, чтобы сервер не лёг? Или просыпались от уведомления «500 ошибка на всех страницах»? Знакомо, да? Релизы — это всегда стресс. Но есть способ сделать их спокойными, как утро выходного дня: Canary deployments (канареечные развертывания).
Название пошло от старой шахтёрской практики — брать с собой канареек в шахту. Если птичка замолкала — значит, воздух отравлен, и нужно срочно выбираться. В мире JavaScript-разработки принцип тот же: сначала пускаем маленький процент пользователей на новую версию, следим за «самочувствием» системы, и только потом — полный релиз.
В этой статье мы разберём, как реализовать canary deployment на чистом JavaScript (и немного Node.js). Без сложных корпоративных инструментов — только код, логика и здравый смысл.
Почему canary, а не синий-зелёный или rolling update?
Существует несколько стратегий деплоя. Blue-green — это когда у вас две идентичные среды, и вы просто переключаете трафик. Rolling update — постепенная замена подов в Kubernetes. Но у canary есть суперсила: реальный A/B тест на живых пользователях.
- Меньше риска: если новая версия «убивает» базу данных, пострадает только 1% пользователей, а не все 100%.
- Быстрая обратная связь: вы видите метрики нового кода до того, как он доберётся до каждого.
- Автоматический откат: если ошибка растёт — canary гасится сам.
Архитектура canary deployment на JavaScript
Представьте, что у нас есть Express-приложение, которое раздаёт HTML-страницы. Мы хотим выкатить новую вёрстку главной страницы. Вместо того чтобы менять роутер для всех, мы сделаем прослойку — canary middleware.
Компоненты системы
- Feature flag — флаг, который включает новую версию для определённой группы.
- Распределитель трафика — логика, которая решает: отдать старую версию (v1) или новую (v2).
- Мониторинг — сбор метрик (ошибки, latency, конверсии).
- Автоматический откат — если метрики упали ниже порога.
Простой пример на Node.js и Express
Начнём с базового сервера. Допустим, у нас есть два обработчика: v1 (старый) и v2 (новый).
const express = require('express');
const app = express();
const PORT = 3000;
// Старая версия
app.get('/api/v1/home', (req, res) => {
res.json({ version: 'v1', message: 'Старый добрый дизайн' });
});
// Новая версия (canary)
app.get('/api/v2/home', (req, res) => {
res.json({ version: 'v2', message: 'Новый, сверкающий дизайн!' });
});
app.listen(PORT, () => console.log(`Server running on ${PORT}`));
Теперь нам нужно решить, кому показывать v2. Самый простой способ — рандомный выбор на основе user ID.
Распределение трафика: детерминированный рандом
Мы не хотим, чтобы один и тот же пользователь прыгал между версиями при каждом запросе. Поэтому используем хеш от user ID:
function getCanaryGroup(userId, percentage = 5) {
// Простейший хеш: сумма кодов символов
const hash = userId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0);
return (hash % 100) < percentage;
}
// Пример использования
console.log(getCanaryGroup('user_123')); // true или false
Теперь в middleware мы проверяем, попадает ли пользователь в canary-группу:
app.use('/api/home', (req, res, next) => {
const userId = req.headers['x-user-id'] || 'anonymous';
if (getCanaryGroup(userId, 5)) { // 5% трафика
req.url = '/api/v2/home';
} else {
req.url = '/api/v1/home';
}
next();
});
app.get('/api/home', (req, res) => {
// Этот роут уже не используется напрямую
res.redirect(req.url);
});
Готово! Теперь 5% пользователей увидят новую версию. Но это только начало.
Продвинутая логика: взвешенное распределение и сегменты
Иногда нужно пускать canary не на всех подряд, а на определённые сегменты: например, только на пользователей из Европы или только на премиум-аккаунты. Реализуем это через конфигурационный объект.
const canaryConfig = {
enabled: true,
percentage: 5,
segments: {
premium: 20, // 20% премиум-пользователей
free: 2 // 2% обычных
}
};
function shouldShowCanary(user) {
if (!canaryConfig.enabled) return false;
const segmentKey = user.isPremium ? 'premium' : 'free';
const segmentPercentage = canaryConfig.segments[segmentKey];
return getCanaryGroup(user.id, segmentPercentage);
}
Такой подход позволяет точечно тестировать фичи на «теплой» аудитории, которая простит ошибки, или наоборот — на самых лояльных.
Мониторинг и автоматический откат
Самое страшное в canary — это не заметить, что новая версия сыпется. Нам нужно собирать метрики и сравнивать их с базовой линией (v1).
Сбор метрик в памяти (для демонстрации)
const metrics = {
v1: { errors: 0, requests: 0, latency: [] },
v2: { errors: 0, requests: 0, latency: [] }
};
// Middleware для сбора метрик
app.use('/api/home', (req, res, next) => {
const start = Date.now();
const version = req.url.includes('v2') ? 'v2' : 'v1';
res.on('finish', () => {
metrics[version].requests++;
metrics[version].latency.push(Date.now() - start);
if (res.statusCode >= 500) {
metrics[version].errors++;
}
});
next();
});
Проверка здоровья canary
Каждые 10 секунд мы проверяем, не превысила ли ошибка в v2 порог (например, 1%):
setInterval(() => {
const v2 = metrics.v2;
const v1 = metrics.v1;
if (v2.requests < 10) return; // слишком мало данных
const v2ErrorRate = v2.errors / v2.requests;
const v1ErrorRate = v1.errors / v1.requests;
console.log(`V2 error rate: ${(v2