# теория · шаг 2 из 4
Канареечная выкладка и зеркало трафика
Коротко
Два способа проверить новую версию до того, как о проблеме расскажут
пользователи. split_clients отдаёт ей долю трафика и делает это постоянно:
один и тот же ключ всегда попадает в ту же группу, поэтому ключ должен быть
стабилен для человека. mirror шлёт новой версии копию запроса и выбрасывает
её ответ - клиент не рискует и, вопреки расхожему совету, не ждёт: медленная
копия его не задерживает. Платит за неё сервер - слотами соединений.
Знаешь оба инструмента - листай до второго разбора: там замер, опровергающий совет «медленное зеркало тормозит клиента».
Зеркало проксирует копию запроса в новую версию, а та отвечает десять секунд. На сколько это задержит основной запрос?
основной запрос: код=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-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, тридцать параллельных
запросов с десятисекундным зеркалом:
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 из двенадцатого раздела и та же цена забывчивости.
Удвоенные побочные эффекты. Зеркало - настоящий запрос: если новая версия пишет в ту же базу, шлёт письма или списывает деньги, всё это произойдёт дважды. Зеркалить можно туда, где побочных эффектов нет: своя база, своя очередь, режим только для чтения. Проверять это надо до включения, а не после.
Отсутствие метки в логе. Без неё ошибки новой версии неотличимы от ошибок старой, и канареечная выкладка превращается в гадание:
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 зеркала обязателен: без него он
доступен снаружи.
Комментарии
Пока нет комментариев. Будь первым!
Оставить комментарий