Публичный лендинг лежит 30–90 секунд при КАЖДОМ деплое: подмена контейнера всплывает как 502 #3274

Open
opened 2026-08-30 09:57:38 +00:00 by bot-backend · 4 comments
Collaborator

Найдено при разборе отчёта тестировщика от 30.08.2026 («подсказки адреса падают в 502»). Симптом оказался частным случаем более общего.

Что установлено фактом

Разбор access-логов Caddy на проде (/var/log/caddy/meraocenka.ru.log, 4 суток).

Все 502 кластеризуются на окнах деплоя. Ни одного вне их.

Пример — деплой PR #3269, всё в UTC:

09:25:38  200  /            последний успех
09:26:10  502  /
09:26:36  502  /
09:26:39  502  /
09:27:08  200  /            первый успех
09:26:47       ← tradein-frontend поднялся

Окно недоступности: не меньше 29 с, не больше 90 с.

Второй кластер — те самые подсказки из отчёта:

08:32:51  смержен 10b90cb8 (бэкенд, домклик-свип)
08:40:24  502  POST /trade-in/api/public/mera/suggest
08:41:21  502  POST /trade-in/api/public/mera/suggest

То есть это деплой бэкенда, а не дефект /suggest.

Почему длительность важна. У всех 502 duration < 2 мс. Перегруженный апстрим отвечал бы дольше — это мгновенный отказ соединения, апстрима в этот момент не существует.

Объём: 15 × 502 на meraocenka.ru и 23 на gendsgn.ru за 4 суток. Затронуты оба апстрима — / идёт во фронт, /trade-in/api/* в бэкенд.

Почему тестировщик не смог установить частоту

Его контрольный залп в 24 запроса дал 0 × 502 — деплоя в тот момент просто не было. Частота здесь не свойство трафика, а свойство расписания мержей: сегодня было 5 мержей в tradein, то есть 5 окон простоя.

Чего делать НЕ надо

lb_try_duration в reverse_proxy — первое, что приходит в голову, и здесь оно не лечит. Держать запрос посетителя 30–60 секунд в ожидании ретрая хуже, чем честно отдать ошибку. Инструмент рассчитан на разрыв в сотни миллисекунд, а у нас окно на два порядка длиннее.

Сейчас во всех 13 блоков reverse_proxy в caddy/sites/apps.caddy нет ни lb_try_duration, ни health-check — Caddy набирает новый контейнер сразу, тот ещё не слушает.

Что обсудить

  1. Настоящее лечение — деплой без простоя: поднять новый контейнер, дождаться готовности, потом переключить. Это правка docker-compose.prod.yml + deploy-tradein.yml, не конфига Caddy. Оценка M, требует решения владельца.
  2. Дешёвая частичная мера: handle_errors с человеческой страницей «сервис обновляется, обновите через минуту» вместо голого 502. Не убирает простой, но перестаёт выглядеть поломкой. Оценка S.
  3. Отдельно: у контейнеров нет healthcheck — без него любой вариант из п.1 не на что опереть.

Приёмка

  • Решение по п.1 против п.2 (владелец)
  • После правки: тот же разбор access-лога за сутки с ≥3 деплоями — 502 на публичных путях должно стать 0
  • Критерий записан ДО правки: считаем по grep '"status":502' в логах обоих доменов, окно — сутки после мержа

Refs: отчёт тестировщика 30.08.2026 п.1.

Найдено при разборе отчёта тестировщика от 30.08.2026 («подсказки адреса падают в 502»). Симптом оказался частным случаем более общего. ## Что установлено фактом Разбор access-логов Caddy на проде (`/var/log/caddy/meraocenka.ru.log`, 4 суток). **Все 502 кластеризуются на окнах деплоя.** Ни одного вне их. Пример — деплой PR #3269, всё в UTC: ``` 09:25:38 200 / последний успех 09:26:10 502 / 09:26:36 502 / 09:26:39 502 / 09:27:08 200 / первый успех 09:26:47 ← tradein-frontend поднялся ``` Окно недоступности: **не меньше 29 с, не больше 90 с.** Второй кластер — те самые подсказки из отчёта: ``` 08:32:51 смержен 10b90cb8 (бэкенд, домклик-свип) 08:40:24 502 POST /trade-in/api/public/mera/suggest 08:41:21 502 POST /trade-in/api/public/mera/suggest ``` То есть это деплой бэкенда, а не дефект `/suggest`. **Почему длительность важна.** У всех 502 `duration < 2 мс`. Перегруженный апстрим отвечал бы дольше — это мгновенный отказ соединения, апстрима в этот момент не существует. **Объём:** 15 × 502 на `meraocenka.ru` и 23 на `gendsgn.ru` за 4 суток. Затронуты оба апстрима — `/` идёт во фронт, `/trade-in/api/*` в бэкенд. ## Почему тестировщик не смог установить частоту Его контрольный залп в 24 запроса дал 0 × 502 — деплоя в тот момент просто не было. Частота здесь не свойство трафика, а свойство расписания мержей: **сегодня было 5 мержей в tradein, то есть 5 окон простоя.** ## Чего делать НЕ надо `lb_try_duration` в `reverse_proxy` — первое, что приходит в голову, и здесь оно не лечит. Держать запрос посетителя 30–60 секунд в ожидании ретрая хуже, чем честно отдать ошибку. Инструмент рассчитан на разрыв в сотни миллисекунд, а у нас окно на два порядка длиннее. Сейчас во всех 13 блоков `reverse_proxy` в `caddy/sites/apps.caddy` нет ни `lb_try_duration`, ни health-check — Caddy набирает новый контейнер сразу, тот ещё не слушает. ## Что обсудить 1. **Настоящее лечение — деплой без простоя:** поднять новый контейнер, дождаться готовности, потом переключить. Это правка `docker-compose.prod.yml` + `deploy-tradein.yml`, не конфига Caddy. Оценка M, требует решения владельца. 2. **Дешёвая частичная мера:** `handle_errors` с человеческой страницей «сервис обновляется, обновите через минуту» вместо голого 502. Не убирает простой, но перестаёт выглядеть поломкой. Оценка S. 3. **Отдельно:** у контейнеров нет healthcheck — без него любой вариант из п.1 не на что опереть. ## Приёмка - [ ] Решение по п.1 против п.2 (владелец) - [ ] После правки: тот же разбор access-лога за сутки с ≥3 деплоями — 502 на публичных путях должно стать 0 - [ ] Критерий записан ДО правки: считаем по `grep '"status":502'` в логах обоих доменов, окно — сутки после мержа Refs: отчёт тестировщика 30.08.2026 п.1.
Author
Collaborator

Замер 05.09 при деплое PR #3348 (пробник curl https://meraocenka.ru/ каждые 2 с): три образца code=000 (соединение не установлено вовсе, не 502/503) в 18:19:39 → 18:20:46 UTC — ~67 секунд без ответа на всех доменах хоста.

Механизм — не подмена фронта, а пересоздание самого Caddy: PR менял корневой docker-compose.prod.yml (бинд-монт снипета) → полный деплой ПТИЦЫ → deploy.yml:1005 up -d --force-recreate --no-deps caddy. Для Caddyfile-only правок работает graceful caddy reload (deploy.yml:1290, отдельная джоба deploy-caddy), для правок compose — recreate. То есть третий класс окна простоя, помимо описанных п.1 (подмена фронта/бэкенда): любая правка volumes Caddy-сервиса = ~1 минута полного отказа всех сайтов, и handle_errors из PR #3348 его не смягчает (Caddy в этот момент не существует).

Не дефект правки (compose-монт без recreate не применить), но аргумент к п.1 (zero-downtime) — и повод не трогать compose Caddy без нужды. Критерий для п.1 стоит расширить: «серия не-ответов (code=000) на / в окне мержа < 2 с», не только серия 5xx.

503-страница из #3348 в этом окне не наблюдалась — по построению не могла; проверка на окне подмены фронта/бэкенда — следующим пробником.

Замер 05.09 при деплое PR #3348 (пробник `curl https://meraocenka.ru/` каждые 2 с): три образца `code=000` (соединение не установлено вовсе, не 502/503) в **18:19:39 → 18:20:46 UTC — ~67 секунд без ответа на всех доменах хоста**. Механизм — не подмена фронта, а пересоздание самого Caddy: PR менял корневой `docker-compose.prod.yml` (бинд-монт снипета) → полный деплой ПТИЦЫ → `deploy.yml:1005` `up -d --force-recreate --no-deps caddy`. Для Caddyfile-only правок работает graceful `caddy reload` (`deploy.yml:1290`, отдельная джоба deploy-caddy), для правок compose — recreate. То есть третий класс окна простоя, помимо описанных п.1 (подмена фронта/бэкенда): **любая правка volumes Caddy-сервиса = ~1 минута полного отказа всех сайтов**, и `handle_errors` из PR #3348 его не смягчает (Caddy в этот момент не существует). Не дефект правки (compose-монт без recreate не применить), но аргумент к п.1 (zero-downtime) — и повод не трогать compose Caddy без нужды. Критерий для п.1 стоит расширить: «серия не-ответов (code=000) на `/` в окне мержа < 2 с», не только серия 5xx. 503-страница из #3348 в этом окне не наблюдалась — по построению не могла; проверка на окне подмены фронта/бэкенда — следующим пробником.
Author
Collaborator

Прод-приёмка п.2 (PR #3348) на окне деплоя #3356, пробник каждые 2 с по meraocenka.ru/ и gendsgn.ru/trade-in/api/v1/me:

18:33:40 … 18:34:24  landing=503  api_me=503  retry-after: 30   (10 образцов подряд)
18:35:56             landing=200  api_me=timeout
18:36:25             landing=000  api_me=401                    (Caddy reload на deploy-caddy)

Оба публичных тракта в окне подмены контейнеров отдают 503 + Retry-After: 30 (HTML на лендинге, JSON на API) — ни одного 502. Окно простоя при этом ~45 с + хвост ≈ 1,5 мин до полного восстановления — п.1 (zero-downtime) остаётся открытым, критерий записан в предыдущем комментарии (серия не-2xx/000 в окне мержа < 2 с).

Побочно: 18:36:25 code=000 на лендинге — секундный обрыв на caddy reload (Caddyfile-only путь), в отличие от 67 с при пересоздании контейнера Caddy (compose-правка) — см. замер выше.

Прод-приёмка п.2 (PR #3348) на окне деплоя #3356, пробник каждые 2 с по `meraocenka.ru/` и `gendsgn.ru/trade-in/api/v1/me`: ``` 18:33:40 … 18:34:24 landing=503 api_me=503 retry-after: 30 (10 образцов подряд) 18:35:56 landing=200 api_me=timeout 18:36:25 landing=000 api_me=401 (Caddy reload на deploy-caddy) ``` Оба публичных тракта в окне подмены контейнеров отдают **503 + Retry-After: 30** (HTML на лендинге, JSON на API) — ни одного 502. Окно простоя при этом ~45 с + хвост ≈ 1,5 мин до полного восстановления — **п.1 (zero-downtime) остаётся открытым**, критерий записан в предыдущем комментарии (серия не-2xx/000 в окне мержа < 2 с). Побочно: `18:36:25 code=000` на лендинге — секундный обрыв на `caddy reload` (Caddyfile-only путь), в отличие от 67 с при пересоздании контейнера Caddy (compose-правка) — см. замер выше.
Author
Collaborator

Разбор и разворот решения, записанного в теле этой issue (11.09).

Корень оказался не тем, что в постановке. «Подмена контейнера медленная» — неверно. docker compose up -d <список> идёт в две фазы: сначала create всех сервисов (старый контейнер каждого останавливается и удаляется — занято container_name), потом start. Между фазами фронта физически нет. Улика с прода (docker inspect, деплой 10.09 15:01): создания разнесены (19-я и 40-я секунды), а старты слиты в одну точку 15:02:09.96–15:02:10.15 — то есть фронт отсутствовал ~30 с, и в логе Caddy ровно в этом окне три 503 (1789052506.113 / 512.534 / 526.274 = 15:01:46.1 / 15:01:52.5 / 15:02:06.3). Фаза create сериализуется ещё и на остановке соседей (stop_grace_period 60/90/120 с у backend/browser/tgbot-scraper).

Поэтому решение — не «ускорить старт», а вывести фронт из общей команды (часть 1, PR #3442, в main): в его графе один сервис, фазы идут подряд → окно ~0,5 с.

Разворот записи «чего делать НЕ надо: lb_try_duration». Для окна 30–90 с возражение было верным: ретрай превратил бы честный отказ в минутное ожидание. Оно снимается порядком, а не спором: сначала окно сжато до полусекунды правкой деплоя, и только после этого ретрай (2 с, часть 2a — PR #3446) осмыслен — он покрывает остаток, а не маскирует минутный простой. Проверено исполнением, что ретрай не задевает API и платежи и не дублирует неидемпотентный POST (детали в PR #3446).

Что остаётся открытым по самой issue. Приёмка сформулирована как «502 на публичных путях = 0», а под ними два апстрима: фронт и бэкенд. Правка закрывает фронт; окно /trade-in/api/* (30–50 с, пока перезапускается tradein-backend) остаётся и этой работой не трогается. Поэтому issue не закрываю частями 1/2a — нужен либо отдельный шаг по бэкенду, либо явное сужение критерия до публичного лендинга.

Побочный эффект части 1, который стоит знать: пока бэкенд перезапускается, СТАРЫЙ фронт жив и отдаёт 200. Лендинг при недоступном бэкенде рендерится с пустыми блоками (mera-public/page.tsx:34-38), а при revalidate = 60 перегенерация, попавшая в это окно, закэширует страницу без ленты/сверки/разброса на минуту. Раньше посетитель видел честную 503-заглушку. Обычно гасится подменой фронта сразу следом (кэш пустой), но не в случае «образ фронта не менялся». Лечится переносом подмены фронта после /health бэкенда — ценой того, что caddy reload перестанет быть последним касанием прокси. Решение за владельцем.

Отдельно заведён #3443: полный деплой ПТИЦЫ роняет сам Caddy на 67 с (все домены code=000), и заглушка окна деплоя там бессильна по построению.

Разбор и разворот решения, записанного в теле этой issue (11.09). **Корень оказался не тем, что в постановке.** «Подмена контейнера медленная» — неверно. `docker compose up -d <список>` идёт в две фазы: сначала create всех сервисов (старый контейнер каждого останавливается и **удаляется** — занято `container_name`), потом start. Между фазами фронта физически нет. Улика с прода (`docker inspect`, деплой 10.09 15:01): создания разнесены (19-я и 40-я секунды), а старты слиты в одну точку 15:02:09.96–15:02:10.15 — то есть фронт отсутствовал ~30 с, и в логе Caddy ровно в этом окне три 503 (`1789052506.113 / 512.534 / 526.274` = 15:01:46.1 / 15:01:52.5 / 15:02:06.3). Фаза create сериализуется ещё и на остановке соседей (`stop_grace_period` 60/90/120 с у backend/browser/tgbot-scraper). **Поэтому решение — не «ускорить старт», а вывести фронт из общей команды** (часть 1, PR #3442, в main): в его графе один сервис, фазы идут подряд → окно ~0,5 с. **Разворот записи «чего делать НЕ надо: `lb_try_duration`».** Для окна 30–90 с возражение было верным: ретрай превратил бы честный отказ в минутное ожидание. Оно снимается **порядком**, а не спором: сначала окно сжато до полусекунды правкой деплоя, и только после этого ретрай (2 с, часть 2a — PR #3446) осмыслен — он покрывает остаток, а не маскирует минутный простой. Проверено исполнением, что ретрай не задевает API и платежи и не дублирует неидемпотентный POST (детали в PR #3446). **Что остаётся открытым по самой issue.** Приёмка сформулирована как «502 на публичных путях = 0», а под ними два апстрима: фронт и бэкенд. Правка закрывает фронт; окно `/trade-in/api/*` (30–50 с, пока перезапускается `tradein-backend`) остаётся и этой работой не трогается. Поэтому issue не закрываю частями 1/2a — нужен либо отдельный шаг по бэкенду, либо явное сужение критерия до публичного лендинга. **Побочный эффект части 1, который стоит знать:** пока бэкенд перезапускается, СТАРЫЙ фронт жив и отдаёт 200. Лендинг при недоступном бэкенде рендерится с пустыми блоками (`mera-public/page.tsx:34-38`), а при `revalidate = 60` перегенерация, попавшая в это окно, закэширует страницу без ленты/сверки/разброса на минуту. Раньше посетитель видел честную 503-заглушку. Обычно гасится подменой фронта сразу следом (кэш пустой), но не в случае «образ фронта не менялся». Лечится переносом подмены фронта после `/health` бэкенда — ценой того, что `caddy reload` перестанет быть последним касанием прокси. Решение за владельцем. Отдельно заведён #3443: полный деплой ПТИЦЫ роняет сам Caddy на 67 с (все домены `code=000`), и заглушка окна деплоя там бессильна по построению.
Author
Collaborator

Приёмка части 1 (PR #3442) снята на живом деплое 11.09 20:33 UTC — критерий выполнен.

Непрерывная проба с самого хоста (scripts/probe-deploy-window.sh, https://meraocenka.ru/, каждые 0,2 с, 3877 образцов через всё окно деплоя):

распределение кодов:  200 → 3875,  503 → 2
все не-200:           20:33:40Z 503,  20:33:40Z 503

Максимальная серия не-200 — 2 образца ≈ 0,4 с (критерий приёмки был «< 2 с»), 000 — ноль.

Контроль на контейнерах, который отличает «правка сработала» от «фронт просто не пересоздавали»:

frontend: Created 20:33:40.117 → Started 20:33:40.493   = 0,376 с
backend:  Created 20:33:15.544 → Started 20:33:39.699   = 24,2 с   (он остался в общей пачке — так и задумано)

Фронт действительно пересоздавался (метка Created сдвинулась на сегодня), и разрыв create→start у него 0,38 с против 24–30 с у соседей по пачке. До правки замер того же деплоя давал 30 с и три 503 в логе Caddy; замер из тела issue от 05.09 — 10 образцов 503 подряд ≈ 45 с.

Остаток в 0,4 с должна съесть часть 2a (ретрай в Caddy, PR #3446) — её приёмка: в окне следующего деплоя МЕРЫ ни одного не-200.

Побочно подтвердилось, что окно /trade-in/api/* осталось прежним (бэкенд 24 с в пачке) — это не регресс правки, а незакрытая половина критерия «502 на публичных путях = 0»; поэтому issue не закрываю.

**Приёмка части 1 (PR #3442) снята на живом деплое 11.09 20:33 UTC — критерий выполнен.** Непрерывная проба с самого хоста (`scripts/probe-deploy-window.sh`, `https://meraocenka.ru/`, каждые 0,2 с, 3877 образцов через всё окно деплоя): ``` распределение кодов: 200 → 3875, 503 → 2 все не-200: 20:33:40Z 503, 20:33:40Z 503 ``` **Максимальная серия не-200 — 2 образца ≈ 0,4 с** (критерий приёмки был «< 2 с»), `000` — ноль. Контроль на контейнерах, который отличает «правка сработала» от «фронт просто не пересоздавали»: ``` frontend: Created 20:33:40.117 → Started 20:33:40.493 = 0,376 с backend: Created 20:33:15.544 → Started 20:33:39.699 = 24,2 с (он остался в общей пачке — так и задумано) ``` Фронт действительно пересоздавался (метка `Created` сдвинулась на сегодня), и разрыв create→start у него 0,38 с против 24–30 с у соседей по пачке. До правки замер того же деплоя давал 30 с и три 503 в логе Caddy; замер из тела issue от 05.09 — 10 образцов 503 подряд ≈ 45 с. Остаток в 0,4 с должна съесть часть 2a (ретрай в Caddy, PR #3446) — её приёмка: в окне следующего деплоя МЕРЫ **ни одного** не-200. Побочно подтвердилось, что окно `/trade-in/api/*` осталось прежним (бэкенд 24 с в пачке) — это не регресс правки, а незакрытая половина критерия «502 на публичных путях = 0»; поэтому issue не закрываю.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#3274
No description provided.