# теория · шаг 2 из 4
Канареечная выкладка и зеркало трафика
Коротко
Для постепенной выкладки split_clients распределяет ключи по группам, а mirror отправляет копию запроса другому обработчику.
- Устойчивый ключ сохраняет группу клиента; метку группы нужно записывать в лог.
- Ответ зеркала отбрасывается, но его запрос выполняется и может иметь побочные эффекты.
- Основной ответ не ждёт ответа копии, однако незавершённое зеркало удерживает ресурсы и может задержать следующий запрос того же keepalive-соединения.
- Проверь нагрузку зеркала и закрой его служебный location от внешнего доступа.
Знаешь оба инструмента - листай до второго разбора: там замер, что именно медленное зеркало задерживает, а что нет.
Зеркало проксирует копию запроса в новую версию, а та отвечает десять секунд. На сколько это задержит основной запрос?
основной запрос: код=200 время=0.001611
счётчики бэкенда сразу после ответа: {"main": 1, "slow-mirror-start": 1}
счётчики через 12 секунд: {"main": 1, "slow-mirror-start": 1, "slow-mirror-done": 1}
Полторы миллисекунды. Ответ ушёл давно, а копия ещё выполнялась - и это не означает, что медленное зеркало ничего не стоит.
На что это похоже
split_clients - это очередь, которую разделили лентой: часть людей идёт к
новому окну. Человек, попавший в новую очередь, идёт в неё и завтра - иначе он
получал бы то новую услугу, то старую, и жаловался бы на обе.
mirror - это второй кассир, который повторяет все операции для тренировки, но
чеки не выдаёт. Свой чек клиент получает сразу, но следующую покупку на этой
кассе пробьют, только когда стажёр закончит. И он занимает место в зале: если
он медленный, зал постепенно забивается.
Механизм: две схемы деления трафика
Правило
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; }
proxy_pass http://$backend_pool;
Модуль считает хеш от строки и раскидывает по долям. Долю меняют в конфиге и
применяют reload-ом: 5% на час, 25% на день, 100% - и старую группу можно
гасить. Откат такой же быстрый, в этом вся ценность.
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, тридцать параллельных
запросов с десятисекундным зеркалом:
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 из двенадцатого раздела и та же цена забывчивости.
Удвоенные побочные эффекты. Зеркало - настоящий запрос: если новая версия пишет в ту же базу, шлёт письма или списывает деньги, всё это произойдёт дважды. Зеркалить можно туда, где побочных эффектов нет: своя база, своя очередь, режим только для чтения. Проверять это надо до включения, а не после.
Отсутствие метки в логе. Без неё ошибки новой версии неотличимы от ошибок старой, и канареечная выкладка превращается в гадание:
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% означали бы ошибку в ключе.
Главное
При канареечной выкладке выбирай устойчивый ключ и записывай группу запроса в лог. При зеркалировании проверь побочные эффекты и длительность копии: её ответ отбрасывается, но работа бэкенда и занятые соединения остаются. Медленное зеркало способно задержать следующий запрос того же клиента.
Комментарии
Пока нет комментариев. Будь первым!
Оставить комментарий