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

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

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

Коротко

Для постепенной выкладки split_clients распределяет ключи по группам, а mirror отправляет копию запроса другому обработчику.

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

Знаешь оба инструмента - листай до второго разбора: там замер, что именно медленное зеркало задерживает, а что нет.

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

↳  выводстенд, зеркало отвечает 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_canary$request_uri;
}

В proxy_pass с переменной стоит имя объявленной группы, а не адрес app-new:8000. Это не вкусовщина: имя с переменной nginx разрешает во время запроса, и без resolver копия молча не доедет. Замер на 1.31.8: основной ответ 200, а в error_log - no resolver defined to resolve app-new. Имя группы upstream резолвер не требует, копия с ним дошла.

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

Ключ должен быть стабилен для человека и разным у разных людей. Что бывает, когда это правило нарушено, видно на замере: один и тот же клиент, 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 не ждёт.

Но запрос считается законченным, только когда закончились и все копии, и соединение клиента до этого занято. Замер на 1.31.8, зеркало отвечает пять секунд:

как ходит клиент первый ответ что дальше
Connection: close 0,002 с соединение закрылось через 5,007 с
keepalive, второй запрос сразу после первого 0,001 с второй ответ пришёл через 5,003 с

Браузер шлёт по одному соединению HTTP/1.1 запрос за запросом, поэтому медленное зеркало всё-таки тормозит пользователя - не первый ответ, а следующие.

Второе - тело запроса. mirror_request_body по умолчанию включён, и тогда nginx дочитывает тело целиком до того, как отправить запрос основному бэкенду, а потоковая передача тела (proxy_request_buffering off) в этом location отключается. Замер: клиент шлёт тело три секунды. Без зеркала бэкенд получил запрос через 0,001 с и читал тело по мере прихода, с зеркалом - через 3,0 с. Для больших загрузок и потоковых сценариев это заметно. Выключать mirror_request_body тоже надо с умом: копия уходит без тела, но с исходным Content-Length, и бэкенд копии ждёт тела, которого не будет, - на стенде до таймаута в 60 секунд. Поэтому вместе с off в location зеркала пишут proxy_pass_request_body off; и proxy_set_header Content-Length "";. С ними основной бэкенд получил запрос сразу, и всё заняло 3,0 с - ровно столько, сколько клиент слал тело.

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

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

Семь соединений отброшено. То есть помимо задержки следующих запросов по тому же HTTP/1.1-соединению (её показал замер выше) медленные копии занимают слоты и снижают ёмкость сервера: слоты кончаются, и страдают запросы, к зеркалу отношения не имеющие. Отсюда правильные меры - короткий 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% означали бы ошибку в ключе.

Главное

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

Комментарии

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

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

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