$ cat post.md
automationПочему в preprod всё было нормально, а в prod после релиза полезли 502
Кейс, который выглядит как баг приложения после релиза, а на деле оказывается стыком данных, нагрузки, таймаутов и слишком оптимистичного preprod.
Больше всего проблем после релиза у меня случалось не тогда, когда сервис честно падал, а тогда, когда всё выглядело почти нормально. Healthcheck зелёный, контейнеры живы, CPU не упирается в потолок, а часть пользовательских запросов уже начинает вести себя так, будто релиз вышел криво.
Это один из самых неприятных сценариев, потому что он долго маскируется под “наверное, баг в коде”, хотя реальная причина часто лежит между приложением и инфраструктурой.
Один из типовых случаев выглядел так: в preprod релиз прошёл без сюрпризов, а в prod через несколько минут после выкладки полезли 502, выросла latency и начали отваливаться только тяжёлые ручки. Внешне всё было похоже на плохой релиз. На практике проблема оказалась в другом.
Контекст
Сервис был типичный для внутренних highload-систем:
- приложение за
nginx; - контейнерный деплой;
- база с уже немаленьким объёмом данных;
- несколько ручек, которые в обычное время работают быстро, но чувствительны к объёму выборки и состоянию зависимостей;
preprod, который по конфигам похож наprod, но всё равно живёт в более щадящем мире.
Сам релиз не выглядел рискованным. В нём не было архитектурной революции, миграции схемы или новых внешних интеграций. Но одна часть бизнес-логики стала тяжелее: на некоторых запросах приложение теперь делало более дорогую работу с данными.
В preprod это прошло незаметно.
Что увидели после релиза
Первые минуты после выкладки были спокойными:
- deploy завершился штатно;
- приложение поднялось;
- базовые smoke checks прошли;
- несколько ручных запросов отработали.
Потом началась знакомая, неприятная деградация:
- на
nginxпоявились499и502; - средняя latency выросла не везде, а только на части API;
- пользователи жаловались не на полную недоступность, а на “иногда долго грузится, иногда падает”;
- график ошибок был рваным, без красивого одномоментного падения.
Именно это опасно в проде больше всего. Когда всё сломалось целиком, причина обычно ищется быстрее. Когда ломается только часть сценариев, команда легко уходит в неверные гипотезы.
Почему preprod не поймал проблему
На бумаге preprod был очень похож на prod. Те же контейнеры, те же конфиги, тот же nginx, те же переменные окружения. Именно из-за этого он и создавал ложное чувство уверенности.
Реальная разница была в вещах, которые хуже всего имитируются “похожестью”:
- объём данных;
- профиль запросов;
- конкуренция за ресурсы;
- реальные тайминги зависимостей;
- плотность трафика.
В preprod новые запросы просто не успевали дойти до той зоны, где приложение начинало отвечать заметно дольше. А в prod под реальной нагрузкой они в неё пришли почти сразу.
Первые гипотезы
Первые версии происходящего были довольно стандартными:
- проблема в приложении после релиза;
- выросло число ошибок в одном из новых участков кода;
- приложение не выдерживает нагрузку;
- зависла база;
- контейнеры стартовали, но часть воркеров работает нестабильно.
Логика была понятной: релиз только что выкатили, деградация началась сразу после него, значит проблема почти наверняка в коде. И это тот момент, где легко потратить лишние двадцать минут не туда.
Что начали проверять
Нормальный разбор здесь начинается не с угадывания, а с разделения слоёв.
Сначала смотрели, падает ли само приложение:
- есть ли рестарты контейнеров;
- отвечает ли приложение на health endpoint;
- есть ли всплеск 5xx внутри самого сервиса;
- видно ли явное падение по CPU, memory или пулу соединений.
Потом проверяли слой перед ним:
- что именно пишет
nginxв access и error log; - сколько живут upstream-запросы;
- кто именно рвёт соединение первым;
- одинаково ли ведут себя лёгкие и тяжёлые ручки.
И только после этого уже стало видно важное: приложение не умирало. Оно просто начало отвечать дольше, чем раньше, а старые таймауты в proxy-слое оказались уже слишком короткими для новой реальности.
Где была настоящая причина
Реальная проблема была не в одном “битом” месте, а в несогласованности ожиданий между слоями системы.
После релиза часть запросов стала тяжелее:
- выросло время на чтение из базы;
- под нагрузкой увеличились хвосты latency;
- часть ответов перестала укладываться в старые proxy timeout значения.
В результате получалась неприятная цепочка:
- приложение всё ещё работало;
- часть запросов обрабатывалась дольше обычного;
nginxили upstream proxy не дожидался ответа;- наружу уходил
502; - по внешним симптомам это выглядело как нестабильный релиз.
То есть сервис “жил”, но уже не укладывался в инфраструктурные ожидания, которые были настроены под старый профиль ответа.
Почему rollback не был мгновенным ответом
Инстинктивно в такой ситуации хочется сразу откатиться. И это нормальная реакция. Но в реальном проде rollback не всегда самый быстрый и безопасный ответ.
Нужно было понять:
- проблема только в новой версии или в общем росте нагрузки;
- не затронула ли выкладка зависимые сервисы;
- не окажется ли откат просто возвращением к версии, которая тоже близка к лимиту;
- не лучше ли сначала быстро подтвердить гипотезу и снять часть деградации на уровне proxy/config.
В этом кейсе стало ясно, что приложение формально живо и деградация концентрируется именно на таймаутах и тяжёлых сценариях. После этого решение уже было не “панически возвращать всё назад”, а быстро привести систему к согласованному состоянию.
Что сделали в моменте
Дальше действия были очень прагматичные.
Сняли остроту проблемы
Первое, что важно сделать в такой ситуации, — не искать идеальное решение, а уменьшить пользовательскую боль.
Для этого:
- ограничили проблемный путь в трафике;
- подняли внимание к latency именно на тяжёлых ручках;
- сверили timeout-цепочку между proxy и приложением;
- исключили явное падение базы и воркеров.
Подтвердили, что проблема именно в ожиданиях инфраструктуры
Когда стало видно, что приложение отвечает, но часть запросов уходит за привычный бюджет времени, картина сложилась:
- код изменил поведение времени ответа;
preprodэтого не проявил;prodпроявил сразу;- текущие timeout-настройки перестали совпадать с новой фактической задержкой.
Привели систему к рабочему состоянию
После этого уже корректировали то, что действительно было узким местом:
- пересмотрели timeout на proxy-слое;
- отдельно посмотрели на тяжёлые запросы и их стоимость;
- добавили более явный post-deploy контроль по latency, а не только по availability.
То есть “починка” была не в одном магическом параметре, а в признании факта: после релиза система стала другой, а конфигурация вокруг неё осталась старой.
Что изменили после инцидента
Самая полезная часть таких кейсов — не сама починка, а то, что меняется после неё.
После этого случая я стал жёстче относиться к нескольким вещам.
Healthcheck больше не считается достаточной проверкой
Если сервис отвечает на /health, это ещё не значит, что он выдерживает реальный рабочий сценарий. После релиза меня интересует не только “жив ли процесс”, но и:
- как ведут себя тяжёлые ручки;
- выросли ли хвосты latency;
- не вышли ли запросы за бюджет времени proxy/upstream.
Preprod оценивается честнее
Я перестал воспринимать preprod как доказательство того, что в prod всё будет так же. Теперь это просто полезный этап, который снимает часть риска, но не отменяет разницу в данных, нагрузке и реальных таймингах.
Таймауты — это часть продукта
До определённого момента очень легко думать о timeout как об инфраструктурной мелочи. На практике это часть пользовательского поведения системы. Если ваш сервис отвечает за 2.8 секунды, а proxy ждёт 2.5, то пользователь не увидит “почти успешный ответ”. Он увидит ошибку.
Главный вывод
Этот кейс хорошо показал мне простую вещь: в проде ломается не только код. Ломается ещё и совпадение между кодом, данными, нагрузкой и инфраструктурными ожиданиями.
Именно поэтому post-deploy анализ для меня давно перестал быть формальностью. Успешный релиз — это не только зелёный pipeline и живой healthcheck. Это ещё и уверенность, что реальный трафик, реальные данные и реальные timeout-цепочки всё ещё согласованы между собой.
Preprod помогает. Но зрелость продовой практики начинается в тот момент, когда ты перестаёшь ждать от него невозможного.