1. 1
  2. 2
  3. 3
  4. 4

# теория · шаг 2 из 4

Канареечная выкладка и зеркало трафика

Коротко

Два способа проверить новую версию до того, как о проблеме расскажут пользователи. split_clients отдаёт ей долю трафика и делает это постоянно: один и тот же ключ всегда попадает в ту же группу, поэтому ключ должен быть стабилен для человека. mirror шлёт новой версии копию запроса и выбрасывает её ответ - клиент не рискует и, вопреки расхожему совету, не ждёт: медленная копия его не задерживает. Платит за неё сервер - слотами соединений.

Знаешь оба инструмента - листай до второго разбора: там замер, опровергающий совет «медленное зеркало тормозит клиента».

Зеркало проксирует копию запроса в новую версию, а та отвечает десять секунд. На сколько это задержит основной запрос?

↳  выводстенд, зеркало отвечает 10 секунд
основной запрос: код=200 время=0.001611
счётчики бэкенда сразу после ответа: {"main": 1, "slow-mirror-start": 1}
счётчики через 12 секунд:            {"main": 1, "slow-mirror-start": 1, "slow-mirror-done": 1}

Полторы миллисекунды. Клиент ушёл давно, а копия ещё выполнялась - и это не означает, что медленное зеркало ничего не стоит.

На что это похоже

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

mirror - это второй кассир, который повторяет все операции для тренировки, но чеки не выдаёт. Клиенту его работа не видна вовсе. Зато он занимает место в зале, и если он медленный, зал постепенно забивается.

Механизм: две схемы деления трафика

запрос nginx ответ клиенту копия, ответ выбрасывается клиента не задерживает даже медленная копия
Ответ зеркала не участвует в ответе клиенту вообще никак

Правило

/etc/nginx/nginx.confhttp
split_clients "$remote_addr$http_user_agent" $backend_pool {
    5%      app_canary;
    *       app_stable;
}

upstream app_stable { server app-1:8000; server app-2:8000; }
upstream app_canary { server app-new:8000; }
/etc/nginx/conf.d/shop.local.confhttp server location /api/
proxy_pass http://$backend_pool;

Модуль считает хеш от строки и раскидывает по долям. Долю меняют в конфиге и применяют reload-ом: 5% на час, 25% на день, 100% - и старую группу можно гасить. Откат такой же быстрый, в этом вся ценность.

/etc/nginx/conf.d/shop.local.confhttp server
location /api/ {
    mirror     /mirror;
    mirror_request_body on;
    proxy_pass http://app_stable;
}

location = /mirror {
    internal;
    proxy_pass http://app-new:8000$request_uri;
}

Разбор: постоянство ключа в цифрах

Ключ должен быть стабилен для человека и разным у разных людей. Что бывает, когда это правило нарушено, видно на замере: один и тот же клиент, 200 запросов, доля канарейки 5%.

ключ карты запросов попало в канарейку
$http_x_key (стабильный, один и тот же) 5 0 - все пять в ту же группу
$http_x_key, 400 разных значений 400 18, то есть 4,5% при заявленных 5%
$request_id (новый на каждый запрос) 200 от одного клиента 7

Первая строка - то, ради чего инструмент и берут: человек не прыгает между версиями. Третья - что получается с неудачным ключом: тот же клиент попал в новую версию семь раз из двухсот, то есть корзина у него собиралась то там, то здесь. Вторая строка доказывает, что доли настоящие, а не декоративные.

Годятся $remote_addr, cookie приложения, идентификатор сессии. Не годится ничего, что меняется от запроса к запросу.

Разбор: что на самом деле стоит медленное зеркало

Совет «держи зеркало быстрым, иначе оно тормозит основной запрос» встречается часто. Замер из крючка его не подтверждает: основной ответ ушёл за 0,0016 секунды, пока копия ещё работала. Ответ подзапроса не участвует в ответе клиенту, и ждать его nginx не обязан.

Цена другая, и она обнаруживается под нагрузкой. Каждая незавершённая копия - это открытое соединение к бэкенду, то есть занятый слот worker_connections из прошлого урока. Тот же стенд, worker_connections 4, тридцать параллельных запросов с десятисекундным зеркалом:

↳  выводstub_status после наплыва
server accepts handled requests
 38 31 30

Семь соединений отброшено. То есть медленное зеркало бьёт не по времени ответа, а по ёмкости: слоты кончаются, и страдают запросы, к зеркалу отношения не имеющие. Отсюда правильные меры - короткий proxy_read_timeout у подзапроса и запас по worker_connections, а не «главное, чтобы клиент не ждал».

Что зеркало действительно делает, проверяется счётчиками на бэкенде:

прогон что увидел бэкенд
GET /api/cart main: 1, mirror: 1
POST /api/order с телом 27 байт main-body-27: 1, mirror-body-27: 1

mirror_request_body on действительно передаёт тело - новая версия получает настоящие запросы, а не их очертания.

Что ломается без этого

Забытый internal. Замер: с ним прямой запрос к /mirror снаружи даёт 404, без него - 204, то есть служебный адрес открыт всему интернету и через него можно дёргать новую версию напрямую, минуя лимиты и авторизацию основного маршрута. Это тот же internal из двенадцатого раздела и та же цена забывчивости.

Удвоенные побочные эффекты. Зеркало - настоящий запрос: если новая версия пишет в ту же базу, шлёт письма или списывает деньги, всё это произойдёт дважды. Зеркалить можно туда, где побочных эффектов нет: своя база, своя очередь, режим только для чтения. Проверять это надо до включения, а не после.

Отсутствие метки в логе. Без неё ошибки новой версии неотличимы от ошибок старой, и канареечная выкладка превращается в гадание:

/etc/nginx/nginx.confhttp
log_format canary '$status $request_time $backend_pool "$request"';

Зачем это в работе

Порядок выкатки, который стоит держать в голове: сначала зеркало на день - оно ловит несовместимость и падения на настоящих данных; потом пять процентов на час; потом четверть; потом все. На каждом шаге смотрят три метрики из прошлого урока, и на каждом шаге откат - это правка одной строки и reload.

Задача Инструмент
Проверить новую версию на живых людях split_clients
Проверить совместимость без риска mirror
Вывести узел на обслуживание, не рвя сессии sticky + drain (раздел 8)
Убрать узел из группы совсем down в upstream
Заглушка на время работ файл-флаг и return 503 (следующая глава)

Теперь сам

Канареечная выкладка идёт вторые сутки. В мониторинге доля 5xx выросла с 0,1% до 0,4%, но по какой версии - непонятно. Ключ карты - $remote_addr, метка группы в лог не пишется. Что сделать сейчас, чтобы понять, виновата ли новая версия, и что стоило сделать заранее?

Ответ: сейчас - дописать $backend_pool в log_format и сделать reload; ключ стабильный, значит распределение не изменится и сравнение будет честным уже через несколько минут. Заранее стоило добавить метку сразу вместе с картой: без неё канареечная выкладка не отвечает на единственный вопрос, ради которого затевалась. Заодно проверить, что доля действительно та, что объявлена, - посчитать по логу отношение групп: заявленные 5% и полученные 4,5% на четырёх сотнях ключей это норма, а вот 50% означали бы ошибку в ключе.

Главное

split_clients делит трафик по долям постоянно: замер показал, что один и тот же ключ всегда даёт ту же группу, 400 разных ключей дали 4,5% при заявленных 5%, а неудачный ключ вроде $request_id отправил одного и того же клиента в канарейку 7 раз из 200. Метку группы обязательно писать в лог, иначе ошибки версий не различить. mirror шлёт копию запроса (вместе с телом при mirror_request_body on) и выбрасывает ответ: клиента медленная копия не задерживает - замер 0,0016 с против десятисекундного зеркала, - но каждая незавершённая копия держит слот соединения, и под нагрузкой это выливается в отброшенные соединения. internal у location зеркала обязателен: без него он доступен снаружи.

Комментарии

Пока нет комментариев. Будь первым!

Оставить комментарий

Комментарий появится после проверки. Email не публикуется. Войти, чтобы не вводить имя каждый раз.