Deep Engineering
Средний·Опубликовано·20 МИН

Выкатка и откат: почему канарейка не помогает без нужного детектора

Четыре стратегии выкатки на одном и том же дефекте — и неожиданный результат: одна и та же канарейка задевает в двадцать два раза меньше запросов, если тревога поднимается по доле ошибок. С детектором, который ждёт двухсот ошибок, она проигрывает переключению всего трафика разом.

Полное техническое изложение

TL;DR

Стратегия выкатки не защищает сама по себе — защищает сигнал, по которому её останавливают. Канарейка — это не «безопасная выкатка», а всего лишь маленькая доля трафика на новой версии. Уменьшив долю, вы уменьшили не риск, а размер выборки, по которой беду ещё предстоит заметить; пока рядом нет правила, способного увидеть беду на этой маленькой выборке, осторожность оборачивается отсрочкой.

Отсюда результат, ради которого стоит урок: осторожная стратегия может проиграть прямолинейной. Модель: при детекторе, который ждёт 200 накопленных ошибок, канарейка задела 645 запросов против 520 у переключения всего трафика разом. Причина — размер выборки: на пяти процентах трафика фиксированное число ошибок копится в двадцать раз дольше, и тревога просто ждёт объёма. Тот же дефект и та же канарейка с детектором по доле ошибок — 29 запросов.

Дальше — числа, вторая половина темы и граница. Разница между двумя детекторами при одной и той же канарейке — в 22,2 раза; у флага, который начинает с одного процента трафика, смена детектора меняет результат в 123,8 раза, и при детекторе по доле он задевает 4 запроса против 29 у канарейки — доля впятеро меньше, а возврат не занимает времени вовсе. Худший результат в модели у rolling — 1820 задетых запросов, и виновата в нём не выкатка, а откат: возврат десяти реплик занимает столько же, сколько их замена. Числа здесь модельные: дефект, поток, пороги и длительности заданы допущениями, содержательны отношения между строками. И у всего этого есть граница: откат возможен не всегда — после применённой миграции или отправленного письма возвращать уже нечего.

Порог входа
Перед уроком достаточно понимать
  • сервис работает в нескольких одинаковых экземплярах, и новую версию можно включить не для всех сразу;
  • трафик между экземплярами кто-то распределяет, и это распределение можно менять на ходу;
  • дефект новой версии обычно виден не сразу: сначала он просто портит часть ответов.
Заранее знать не нужно
  • слова «сине-зелёная», «rolling», «канарейка», «feature-flag» — все четыре разобраны в уроке обычными словами;
  • как устроено правило тревоги: по числу ошибок или по их доле;
  • что делают с миграциями базы, когда откат уже невозможен.

Что здесь на самом деле спрашивают

Лестница обычно такая:

  1. «Какие бывают стратегии выкатки?» — разминка на перечисление.
  2. «Чем канарейка лучше?» — здесь начинается содержание.
  3. «Как понять, что канарейка плохая?» — главный вопрос темы, и обычно его пропускают.
  4. «Сколько держать канарейку?» — вопрос про выборку.
  5. «Как быстро вы можете откатиться?» — вопрос, который важнее вопроса про выкатку.
  6. «Что делать с миграцией базы?» — вопрос про то, что не откатывается.
модель с допущениямиЧисла этого урока — из симуляции bench/rollout/strategies.py, а не из замера. Допущения: 100 запросов в секунду, дефект ломает 5 % запросов на новой версии, решение об откате занимает 60 с, а сам откат у каждой стратегии свой — 5 с у blue-green, 300 с у rolling, 30 с у канарейки, мгновенно у флага.

Этот урок стоит на модели, а не на замере: стратегии выкатки описываются документацией отдельных продуктов, общего стандарта у них нет. Модель отвечает не на вопрос «сколько именно у вас», а на вопрос «от чего это зависит».

База: как вообще заменяют работающую версию

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

Пересоздание. Старую версию выключают, новую включают. Между ними — перерыв, во время которого сервиса нет. Способ честный и простой, и он вполне годится там, где перерыв заранее разрешён.

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

Сине-зелёная. Рядом со старой поднимают полный второй комплект — новую версию целиком, — и, когда он готов, переключают на него весь трафик разом. Возврат устроен так же: переключением обратно.

Канареечная. Новую версию получает небольшая доля трафика, всё остальное продолжает идти на старую. Если ничего плохого не происходит, долю постепенно увеличивают до полной.

Вот главное, что стоит вынести из этого списка: канарейка сама по себе — не «безопасная выкатка», а просто маленькая выборка новой версии. Она ничего не решает и ни от чего не защищает; она лишь уменьшает долю трафика, которая столкнётся с дефектом, пока кто-то не решит остановиться.

И отсюда — главный вопрос урока: кто и по какому сигналу решает остановиться? Безопасность появляется не от стратегии, а от того, что рядом со стратегией есть сигнал, по которому выкатку останавливают, и он срабатывает раньше, чем дефект успевает навредить. Без такого сигнала маленькая доля просто растягивает происходящее во времени.

Модель дальше сравнивает не ровно эти четыре способа, а те, между которыми обычно и выбирают, и называет их так, как они подписаны в прогонах: blue-green — сине-зелёная, rolling — постепенная замена, canary — канареечная. Пересоздания в модели нет: сервису с заранее разрешённым перерывом сигнал не нужен, останавливать нечего. Зато есть четвёртый участник, feature-flag, — флаг в коде: та же маленькая доля, что у канарейки, но переключает её не инфраструктура, а само приложение при обработке запроса.

Этого уже достаточно, чтобы ответить на базовый вопрос собеседования. Всё дальнейшее — про то, что происходит, когда сигнал есть, но подобран неудачно, и почему время возврата важнее времени выкатки.

Механизм 1: четыре стратегии при детекторе, который считает ошибки

Первый прогон: тревога поднимается, когда накопилось 200 ошибок; потом ещё минута на решение; потом откат.

1. DETECTION BY ERROR COUNT: 200 ERRORS RAISE THE ALARM
-------------------------------------------------------
        strategy   affected   detected, s   rolled back, s
      blue-green        520            39              104
         rolling       1820           139              499
          canary        645           324              414
    feature-flag        495           606              666
модель с допущениямиbench/rollout/strategies.py, модель. Абсолютные числа заданы допущениями; содержательно то, как они соотносятся между стратегиями.

Результат неожиданный, и в этом главная ценность модели: канарейка (645) оказалась хуже переключения всего трафика разом (520).

Механизм такой. Канарейка держит на новой версии пять процентов трафика, а детектор ждёт двухсот ошибок — и на маленькой доле набирает их в двадцать раз дольше, и тревога отстаёт: 324-я секунда против 39-й у переключения разом. Отставание не выросло вдвадцатеро только потому, что на 300-й секунде канарейка уже стала полной выкаткой. Осторожность превратилась в задержку.

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

Механизм 2: тот же дефект, другой детектор

Второй прогон отличается одним: тревога поднимается не по числу ошибок, а по их доле — если новая версия держит плохую долю тридцать секунд.

2. DETECTION BY ERROR RATE: 30 SECONDS OF A BAD SHARE
-----------------------------------------------------
        strategy   affected   detected, s   rolled back, s
      blue-green        470            29               94
         rolling       1270            29              389
          canary         29            29              119
    feature-flag          4            29               89
модель с допущениямиbench/rollout/strategies.py, модель. Изменился только способ обнаружения; дефект, поток и длительности откатов те же.

Теперь стратегии ведут себя так, как о них рассказывают: канарейка задевает 29 запросов, флаг — 4, а переключение разом — 470.

Сравнение двух прогонов и есть содержание урока:

3. THE SAME STRATEGY UNDER THE TWO DETECTORS
--------------------------------------------
        strategy   by count    by rate   times fewer
      blue-green        520        470          1.1x
         rolling       1820       1270          1.4x
          canary        645         29         22.2x
    feature-flag        495          4        123.8x
модель с допущениямиbench/rollout/strategies.py. Стратегия одна и та же в каждой строке; отличается только детектор.

Чем меньше доля трафика на новой версии, тем больше зависимость от детектора. У переключения разом смена детектора почти ничего не меняет (1,1), у флага, который начинает с одного процента, — в 123,8 раза.

Отсюда вывод, которым стоит отвечать: стратегия выкатки и способ обнаружения — одна конструкция, а не две. Канарейка без детектора, способного увидеть беду на маленькой выборке, — это не осторожность, а отсрочка: тот же дефект, только позже и без выигрыша.

Механизм 3: откат важнее выкатки

Посмотрите на колонку «rolled back» в первом прогоне: 104, 499, 414, 666. Это момент, когда трафик полностью вернулся на старую версию.

У rolling самое раннее обнаружение из трёх осторожных стратегий (139 с) и самый долгий откат: между тревогой и концом (499 с) проходят пять минут возврата реплик. У флага обнаружение самое позднее (606 с), но конец наступает ровно в момент решения — через минуту после тревоги (606 → 666): возврат мгновенный.

Отсюда практический критерий, который стоит применять к своей системе: считать надо не «как мы выкатываем», а «за сколько мы возвращаемся». Второе и определяет ущерб, потому что дефект живёт от начала до конца отката, а не до момента обнаружения.

И отсюда же — почему вопрос «сколько держать канарейку» имеет верхнюю и нижнюю границы. Слишком мало — детектор не наберёт выборку и вы выкатите дефект дальше (первый прогон). Слишком долго — вы удлиняете время, в течение которого система живёт в двух версиях сразу, а это отдельный источник ошибок: несовместимые форматы, разные схемы, дублирующиеся фоновые задачи.

Глубже: что не откатывается

Последний раздел — про границу метода, и о ней стоит сказать самому.

Миграции базы данных не откатываются вместе с кодом. Если новая версия добавила колонку и начала в неё писать, возврат кода не удалит данные и не восстановит старую схему. Поэтому изменения схемы делят на совместимые шаги: сначала добавить и научиться читать, потом писать, потом убрать старое — каждый шаг обратим по отдельности.

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

Прогретые кеши и соединения после отката холодные, как после перезапуска (урок про пробы). Поэтому первые минуты после отката система работает хуже, чем до выкатки, — и это надо учитывать, когда решение откатываться принимается по метрике задержки.

Отсюда граница, которую стоит назвать вслух, потому что весь урок до этого места считал откат само собой разумеющимся: откат возможен не всегда. Если новая версия успела изменить схему или сами данные, возвращать код некуда: старая версия не умеет читать то, что записала новая, и «вернуть как было» означало бы ещё одну необратимую операцию поверх первой. В этом положении работает не откат, а движение вперёд — выкатывается исправление, а не предыдущая версия, и делать это приходится под той же аварией, без права на вторую попытку.

Разница практическая, и понимать её надо до аварии, а не во время. План отката имеет смысл ровно до той операции, после которой он перестаёт существовать, — поэтому необратимые шаги и отделяют от обратимых, делают по одному и по возможности откладывают на потом. Тогда у большинства выкаток остаётся дешёвый выход назад, а дорогой путь вперёд остаётся только там, где иначе нельзя.

Как отвечать на собеседовании

Короткий ответ: ущерб от плохой выкатки определяется не стратегией, а произведением трёх вещей — доли трафика на новой версии, времени до обнаружения и времени отката. Поэтому канарейка выигрывает только вместе с детектором, способным увидеть проблему на маленькой выборке: в модели та же канарейка задела 645 запросов при детекторе, ждущем 200 ошибок, и 29 при детекторе по доле ошибок — разница в 22 раза.

Этого достаточно, чтобы ответить верно. Дальше — то, что добавляют, если собеседник копает.

Если интервьюер копает глубже

Хороший ответ отличают три вещи. Первая — вы говорите про откат, а не про выкатку: в модели rolling обнаружил дефект раньше канарейки, но задел почти втрое больше запросов, потому что возврат десяти реплик занимает столько же, сколько их замена. Вторая — вы называете границу применимости: миграции базы, отправленные письма и проведённые платежи не откатываются вместе с кодом. Третья — вы связываете размер канарейки с чувствительностью детектора: слишком маленькая доля означает, что тревога придёт поздно.

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

Дальше спросят

Спросят дальше

Сколько трафика пускать на канарейку?

Короткий ответ

Столько, чтобы детектор успевал набрать статистику быстрее, чем дефект успеет навредить. В модели пяти процентов хватило при детекторе по доле (29 задетых) и не хватило при детекторе по числу ошибок (645): тревога сработала, когда канарейка уже стала полной выкаткой.

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

Спросят дальше

Что важнее: быстро выкатывать или быстро откатывать?

Короткий ответ

Откатывать. Ущерб идёт от начала дефекта до конца отката, а не до момента обнаружения: в модели rolling заметил проблему на 139-й секунде, а закончил возвращать трафик на 499-й — ровно шесть минут дефекта после того, как о нём уже знали.

Поэтому у стратегии стоит спрашивать не «как долго идёт выкатка», а «за сколько мы вернём сто процентов трафика на предыдущую версию» — и проверять это учениями, а не предположениями.

Спросят дальше

Что делать с миграциями базы?

Короткий ответ

Разделять изменение схемы на шаги, каждый из которых обратим сам по себе: сначала добавить новое, не трогая старого; потом научить код читать оба варианта; потом писать в новый; и только после того, как откат перестал быть нужен, убрать старое.

Смысл в том, чтобы в любой момент откат кода не требовал отката данных. Обратная схема — «выкатили код и миграцию одной кнопкой» — работает ровно до первого отката, и тогда выясняется, что вернуть можно только код.

Спросят дальше

Чем отличается флаг от канарейки?

Короткий ответ

Точкой переключения. У канарейки трафик делит инфраструктура — балансировщик или маршрутизация; у флага решение принимает сам код при обработке запроса. Отсюда и разница в откате: у флага он мгновенный, потому что менять надо значение, а не размещение.

Цена флага — сложность в коде: обе ветки живут одновременно, их надо поддерживать и вовремя удалять. Флаг, оставшийся в коде на год, превращается в скрытую вторую версию системы, которую никто не тестирует.

Частые заблуждения

Утверждение

канарейка всегда безопаснее полного переключения

На самом деле

Только вместе с подходящим детектором. Модель: при тревоге по накопленным двумстам ошибкам канарейка задела 645 запросов против 520 у переключения разом — то есть оказалась хуже, потому что на пяти процентах трафика ошибки копятся в двадцать раз дольше.

Утверждение

главное — стратегия выкатки

На самом деле

Главное — произведение трёх вещей: доли трафика на новой версии, времени до обнаружения и времени отката. Модель: одна и та же канарейка даёт 645 или 29 задетых запросов в зависимости только от детектора — разница в 22,2 раза.

Утверждение

rolling — безопасный вариант по умолчанию

На самом деле

В модели он худший: 1820 задетых запросов. Причина не в выкатке, а в откате — десять реплик возвращаются так же по одной, и между тревогой на 139-й секунде и концом отката на 499-й дефект работает на половине трафика и больше.

Утверждение

если дефект обнаружен, ущерб прекратился

На самом деле

Ущерб идёт до конца отката. В модели самая ранняя тревога (rolling, 139 с) сочетается с самым долгим возвратом (499 с), а самая поздняя (флаг, 606 с) — с мгновенным. Считать надо время до возвращения трафика, а не до срабатывания тревоги.

Утверждение

чем меньше доля канарейки, тем безопаснее

На самом деле

Тем сильнее зависимость от детектора: в модели смена детектора меняет результат переключения разом в 1,1 раза, а флага с одним процентом трафика — в 124 раза. Маленькая доля без чувствительного правила тревоги — это отсрочка, а не защита.

Утверждение

откат возвращает систему в прежнее состояние

На самом деле

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

Утверждение

флаг — это просто канарейка, сделанная в коде

На самом деле

Разница в точке переключения и, как следствие, в скорости отката: у флага возврат мгновенный, потому что меняется значение, а не размещение. Цена — обе ветки живут в коде одновременно, и забытый флаг превращается в скрытую вторую версию системы.

Практика

Две задачи. Сначала ответьте, потом сверьтесь с настоящим выводом: в обеих правильный ответ взят из прогона модели, а не назначен.

Практика · что напечатает

Один и тот же дефект ломает 5 % запросов на новой версии; поток 100 запросов в секунду. Печатаются три числа: сколько запросов задето при канареечной выкатке, когда тревога поднимается по накоплении 200 ошибок; сколько при той же канарейке, когда тревога поднимается по доле ошибок за 30 секунд; и сколько задето при rolling с тем же детектором по доле. Что напечатает этот код?
canary_count = simulate("canary", "count")
canary_rate = simulate("canary", "rate")
rolling_rate = simulate("rolling", "rate")
print(canary_count["affected"])
print(canary_rate["affected"])
print(rolling_rate["affected"])

Практика · оцените

Канареечная выкатка, 5 % трафика на новой версии. Во сколько раз меньше запросов будет задето, если тревогу поднимать по доле ошибок за 30 секунд, а не по накоплении 200 ошибок?
раза

Проверка знаний

Вопрос 1 из 6

Канареечная выкатка на 5 % трафика, тревога поднимается по накоплении 200 ошибок. Что произойдёт?

Источники и что читать дальше

1 ИСТОЧНИК

  1. Модель этого урока: четыре стратегии выкаткиИсточник. Внешнего первоисточника у этого урока нет: стратегии выкатки описываются документацией отдельных продуктов, общего стандарта у них нет. Урок стоит на модели с объявленными допущениями: 100 запросов в секунду, дефект ломает 5 % запросов на новой версии, обнаружение либо по 200 накопленным ошибкам, либо по доле ошибок за 30 секунд, решение об откате занимает 60 секунд, а сам откат у каждой стратегии свой: 5 с у blue-green, 300 с у rolling, 30 с у канарейки, мгновенно у флага. Всё, что печатает прогон, выводится из этих правил./ru/bench/rollout/strategies.py