/posts/preprod-was-fine-prod-returned-502

$ 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 значения.

В результате получалась неприятная цепочка:

  1. приложение всё ещё работало;
  2. часть запросов обрабатывалась дольше обычного;
  3. nginx или upstream proxy не дожидался ответа;
  4. наружу уходил 502;
  5. по внешним симптомам это выглядело как нестабильный релиз.

То есть сервис “жил”, но уже не укладывался в инфраструктурные ожидания, которые были настроены под старый профиль ответа.

Почему 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 помогает. Но зрелость продовой практики начинается в тот момент, когда ты перестаёшь ждать от него невозможного.

$ cat /etc/motd

infraTales

Личный блог о DevOps, инфраструктуре, инструментах и инженерной практике.