Как мы добились нуля ошибок 502 при деплоях
Каждый раз, когда мы деплоили бэкенд Mythex, несколько запросов падали с 502 Bad Gateway. Немного, но ошибка 502 во время деплоя означает, что чей-то клик ничего не сделал или сборка, за которой кто-то следил, на мгновение зависла. Мы выяснили, что проблем две: самовосстанавливающийся сторожевой процесс, который воевал с нашими деплоями, и разрыв между моментом, когда сервер перестал слушать порт, и моментом, когда балансировщик перестал слать ему трафик. За три подряд продакшен-деплоя после исправлений количество упало с 10 до 6 и затем до 0.
Схема
Наш API и оркестратор работают как контейнеры на небольшой группе серверов за Application Load Balancer (ALB) в AWS. Деплой обновляет по одному серверу за раз: он выводит старые контейнеры, запускает новые и переходит дальше. Сторожевой процесс на каждом сервере проверяет, что контейнеры запущены, и перезапускает то, что не работает, — он нужен, чтобы упавший в 3 часа ночи процесс вернулся без того, чтобы кто-то просыпался.
Оба механизма разумны сами по себе. Вместе они не работали.
Проблема 1: сторожевой процесс конкурировал с деплоем
Сторожевой процесс запускался раз в минуту и «лечил» после одной неудачной проверки работоспособности. А деплой намеренно выводит сервисы из работы до 150 секунд — и в это время проверка работоспособности должна падать. Поэтому сторожевой процесс срабатывал во время каждого деплоя.
Чаще всего он проигрывал гонку безвредно. Однажды он выиграл: переписал compose-файл (описание того, какие контейнеры должны работать) посреди деплоя и оставил сервер вообще без контейнера шлюза — при этом задача деплоя сообщила об успехе.
Исправления:
- Одна блокировка. Теперь деплой берёт ту же файловую блокировку, что и скрипт первоначальной настройки сервера, поэтому сторожевой процесс и деплой никогда не могут работать одновременно.
- Четыре попытки, а не одна. Сторожевой процесс ждёт четыре подряд неудачные проверки, прежде чем действовать. Выводимый из работы сервис больше не выглядит мёртвым.
- Одно определение. Compose-файл теперь имеет один источник, публикуемый на серверы, вместо копий, которые могли разойтись.
- Проверяем, а не доверяем. Задача деплоя больше не считается успешной, потому что команда вернула код. Перед успехом она проверяет API, оркестратор, шлюз и воркер публикации на каждом сервере.
Проблема 2: сервер ушёл раньше, чем трафик
Вторая проблема была менее заметной. Когда деплой выводит сервер из работы, сервер начинает отвечать на свой эндпоинт работоспособности кодом 503, чтобы балансировщик вывел его из ротации. Наш сервер делал так 5 секунд, а потом выключался.
Но ALB проверял работоспособность лишь время от времени и требовал несколько неудачных проверок подряд, прежде чем перестать направлять трафик на цель, — до примерно 60 секунд целиком. Поэтому примерно 55 секунд балансировщик продолжал слать настоящие запросы на сервер, который уже перестал слушать. Это и были наши 502.
Исправление — согласовать две временные шкалы:
- Целевая группа теперь проверяет каждые 5 секунд и помечает цель как нездоровую после 2 неудач — примерно за 10 секунд.
- Вывод из работы продолжает обслуживать настоящий трафик 15 секунд после того, как начинает проваливать проверки работоспособности, поэтому перекрытие всегда есть.
- Эти настройки находятся в скрипте, который поднимает инфраструктуру, а не в консоли, которую кто-то однажды щёлкнул.
Чтобы числа согласовывались друг с другом
Такие таймауты зависят друг от друга. Вывод из работы должен длиться дольше, чем балансировщику нужно, чтобы заметить проблему; вывод плюс выключение должны завершиться до того, как контейнер будет принудительно убит. Через полгода кто-нибудь легко изменит одно число и незаметно вернёт ошибку.
Поэтому мы записали эти зависимости в виде тестов: вывод из работы должен быть длиннее интервала проверки, умноженного на порог неудач; время до возврата в статус здорового должно оставаться коротким; весь вывод должен укладываться в период остановки контейнера. Меняете одно число без остальных — CI падает. Мы также проверили сами тесты мутационным тестированием: меняли каждое число на неверное значение и смотрели, поймает ли тест ошибку.
Измерять деплой легко неправильно
Наше первое измерение говорило, что чистый деплой ломается примерно в 3% случаев. Это было неверно. Мы считали каждый ответ, отличный от 200, а 503 от эндпоинта работоспособности во время вывода из работы — не сбой. Это вывод из работы делает свою работу, сообщая балансировщику не выбирать этот сервер, пока тот продолжает обслуживать настоящие запросы.
Что чувствуют пользователи, — это 502: запрос не дошёл ни до чего. Подсчёт только таких ошибок дал настоящую картину — 10, затем 6, затем 0 за три продакшен-деплоя.
Что мы из этого вынесли
- Самовосстановлению нужно знать о плановом простое. Сторожевой процесс, который не отличает деплой от падения, рано или поздно начнёт воевать с деплоем.
- Вывод из работы — это рукопожатие. Сервер должен продолжать обслуживать запросы, пока балансировщик не перестанет доказуемо слать ему трафик, а такой тайминг задаётся на балансировщике, а не на сервере.
- Фиксируйте инварианты в тестах. Операционные числа со временем расходятся. Тесты, которые связывают их друг с другом, — нет.
- Решите, что вы измеряете, до измерения. «Ошибки» была неверной метрикой; «запросы, которые ни до чего не дошли» — правильной.
Если вы публикуете приложение в Mythex, всё это вам не видно — и в этом смысл. Смежное: как ваше опубликованное приложение остаётся работающим во время повторной публикации.