fix(deploy): подменять фронт МЕРЫ отдельной командой — окно 30–90 с уходит
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 10s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 2m0s
CI / backend-tests (pull_request) Successful in 17m25s
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 10s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 2m0s
CI / backend-tests (pull_request) Successful in 17m25s
Публичный лендинг meraocenka.ru лежал 30–90 с на КАЖДОМ деплое (#3274). Причина не в скорости подмены контейнера: она стоит полсекунды. `docker compose up -d` со СПИСКОМ сервисов работает в две фазы — сначала create (старый контейнер каждого сервиса останавливается и УДАЛЯЕТСЯ, иначе занято container_name), потом start, в порядке зависимостей и с ожиданием их условий. Между фазами старого фронта уже нет, а новый ещё не запущен. Прод, 10.09, два деплоя подряд (docker inspect .Created/.StartedAt): пачка сервисов: tradein-backend создан 15:01:40 → запущен 15:02:10 (30 с), в логе Caddy три 503 на лендинге: 15:01:46/:52 и 15:02:06; ОДИН сервис: tradein-frontend создан 16:42:17.5 → запущен 16:42:18.0 (0,5 с), 503 в логе нет ни одного. Тот же двухфазный порядок воспроизведён на стенде (реальный образ фронта + Caddy 2): соседи создаются сразу, стартуют через 41 с. Поэтому frontend убран из общего `up -d $SERVICES` и пересоздаётся своей командой после пачки: в его графе один сервис, create и start идут подряд. Остаток ~0,5 с добирает ретрай подключения в Caddy — отдельным коммитом, он мержится своим путём (caddy_only → graceful reload, без пересборки). Проба для замера на живом деплое — scripts/probe-deploy-window.sh: считает коды и САМУЮ ДЛИННУЮ серию не-200 в секундах, 000 отдельной строкой (его даёт и отбой периметра, не только простой). Запускать НА хосте прода: с внешнего адреса частая серия сама ловит 30–50 % 000. Refs #3274
This commit is contained in:
parent
a659b18771
commit
dacd298b21
2 changed files with 139 additions and 1 deletions
|
|
@ -1137,7 +1137,31 @@ jobs:
|
|||
# tradein-mvp/backend/** включает app/tgbot_main.py), никакого
|
||||
# in-flight state вроде scrape_runs → пересоздаётся безусловно вместе
|
||||
# с browser/backend/frontend, отдельного graceful-drain не требует.
|
||||
SERVICES="browser backend frontend tgbot"
|
||||
#
|
||||
# ── FRONTEND ЗДЕСЬ НЕТ. ЭТО И ЕСТЬ ЛЕЧЕНИЕ #3274 ─────────────────
|
||||
# Публичный лендинг лежал 30–90 с на КАЖДОМ деплое. Причина не в
|
||||
# том, что подмена контейнера медленная — она занимает полсекунды.
|
||||
# Причина в том, что `docker compose up -d` со СПИСКОМ сервисов
|
||||
# работает в две фазы: сначала create (старый контейнер каждого
|
||||
# сервиса останавливается и УДАЛЯЕТСЯ — иначе занято container_name),
|
||||
# и только потом start, в порядке зависимостей и с ожиданием их
|
||||
# условий. Между фазами фронта уже нет, а нового ещё нет.
|
||||
#
|
||||
# Замер на проде (docker inspect, 10.09, оба деплоя МЕРЫ):
|
||||
# пачка сервисов: tradein-backend создан 15:01:40 → запущен
|
||||
# 15:02:10 = 30 с (и 503 на лендинге в
|
||||
# 15:01:46/15:01:52/15:02:06 — ровно окно);
|
||||
# ОДИН сервис: tradein-frontend создан 16:42:17.5 → запущен
|
||||
# 16:42:18.0 = 0,5 с, 503 в логе нет вообще.
|
||||
# Тот же двухфазный порядок воспроизведён на стенде по меткам
|
||||
# .Created/.StartedAt: соседи создаются сразу, стартуют через 41 с.
|
||||
#
|
||||
# Поэтому фронт пересоздаётся ОТДЕЛЬНОЙ командой ниже, после этой
|
||||
# пачки: в его графе один сервис, фазы create и start идут подряд.
|
||||
# Остаток (~0,5 с) добирает ретрай подключения в Caddy — см.
|
||||
# снипет (tradein_frontend_retry) в caddy/sites/apps.caddy.
|
||||
# Гейт на обе половины: scripts/check-frontend-swap-window.py.
|
||||
SERVICES="browser backend tgbot"
|
||||
SCRAPER_STOP_TS=""
|
||||
scraper_stale=""
|
||||
if [ "${SCRAPER_RECREATE:-true}" = "true" ]; then
|
||||
|
|
@ -1233,6 +1257,21 @@ jobs:
|
|||
" || echo "WARNING: startup-reap query failed — orphaned runs (if any) fall back to the 6h zombie reaper"
|
||||
fi
|
||||
|
||||
# (4b) Фронт — ОТДЕЛЬНОЙ командой, один сервис в графе (#3274).
|
||||
# Обоснование и прод-замеры — у SERVICES выше. Здесь важен ПОРЯДОК:
|
||||
# эта команда идёт ПОСЛЕ пачки (backend уже поднят — новый SSR сразу
|
||||
# ходит в новый бэкенд) и ДО `caddy reload` ниже, чтобы reload, как
|
||||
# и раньше, оставался последним касанием прокси.
|
||||
#
|
||||
# ЗАЧЕМ ЖДАТЬ ПОСЛЕ КОМАНДЫ. `up -d` возвращает управление, когда
|
||||
# контейнер ЗАПУЩЕН, а не когда Next начал слушать (на проде между
|
||||
# ними ~0,1 с, см. journald «Ready in 110ms», но это не гарантия).
|
||||
# Полноценная проверка фронта — health-check ниже по файлу, он же
|
||||
# валит деплой при неудаче; здесь только короткая пауза, чтобы
|
||||
# ретрай Caddy (2 с) не пришёлся на ещё не слушающий порт.
|
||||
docker compose -p gendesign-tradein $COMPOSE_FILES up -d --no-deps frontend
|
||||
sleep 1
|
||||
|
||||
# (5) `docker restart tradein-backend` БОЛЬШЕ НЕ НУЖЕН (issue #2216).
|
||||
# История (PR #493 / deploy 1156): backend раньше поднимался ПЕРЕД
|
||||
# миграциями, его lifespan-hook (ensure_fdw_user_mapping) падал с
|
||||
|
|
|
|||
99
scripts/probe-deploy-window.sh
Executable file
99
scripts/probe-deploy-window.sh
Executable file
|
|
@ -0,0 +1,99 @@
|
|||
#!/bin/sh
|
||||
# Проба окна недоступности при деплое (#3274).
|
||||
#
|
||||
# ЗАЧЕМ. Статус джобы деплоя зелёный независимо от того, видел ли посетитель
|
||||
# ошибку: подмена контейнера происходит ВНУТРИ успешного прогона. Единственный
|
||||
# честный замер — непрерывный опрос публичного адреса во время деплоя и подсчёт
|
||||
# САМОЙ ДЛИННОЙ СЕРИИ подряд идущих не-200. Приёмка #3274 сформулирована именно
|
||||
# так (комментарий от 05.09): «серия не-2xx/000 на / в окне мержа < 2 с».
|
||||
#
|
||||
# ГДЕ ЗАПУСКАТЬ — НА САМОМ ХОСТЕ (ssh poincare), не с ноутбука. Боевой периметр
|
||||
# отбивает частые серии с одного внешнего адреса: замер 11.09 дал 30–50 %
|
||||
# `000` с внешнего IP при интервале 0 и ноль при 2 с, а с самого хоста — 0/10
|
||||
# без пауз. Иначе защита периметра читается как простой сервиса.
|
||||
#
|
||||
# ПОЧЕМУ 000 СЧИТАЕТСЯ ОТДЕЛЬНО. curl отдаёт `000`, когда HTTP-ответа не было
|
||||
# вообще (TCP/TLS не встал, таймаут). Это может быть и простой (Caddy
|
||||
# пересоздан), и отбой периметра — сваливать его в одну кучу с 502/503 нельзя,
|
||||
# иначе своя же защита попадёт в числа простоя.
|
||||
#
|
||||
# Запуск:
|
||||
# scripts/probe-deploy-window.sh [URL] [ИНТЕРВАЛ_С] [ДЛИТЕЛЬНОСТЬ_С] [ФАЙЛ]
|
||||
# scripts/probe-deploy-window.sh --summary <ФАЙЛ> # пересчитать сводку
|
||||
# scripts/probe-deploy-window.sh --selftest # проверка счётчика серий
|
||||
#
|
||||
# Пример (окно деплоя, 10 минут с шагом 200 мс):
|
||||
# ssh poincare 'nohup /opt/gendesign/scripts/probe-deploy-window.sh \
|
||||
# https://meraocenka.ru/ 0.2 600 /tmp/probe-mera.tsv >/dev/null 2>&1 &'
|
||||
set -eu
|
||||
|
||||
# Сводка по TSV (epoch<TAB>ISO-время<TAB>код<TAB>секунды): сколько чего, и главное — самая
|
||||
# длинная серия подряд идущих не-200 в СЕКУНДАХ (по меткам времени самих
|
||||
# образцов, а не «число образцов × интервал»: curl с таймаутом растягивает шаг,
|
||||
# и умножение занизило бы реальную длину окна).
|
||||
summarize() {
|
||||
awk -F'\t' '
|
||||
# Серия закрывается первым 200. Длина в секундах считается ТОЛЬКО в END:
|
||||
# средний шаг известен лишь после прохода, а внутри цикла он ещё 0 — из-за
|
||||
# этого первая версия занижала окно ровно на один шаг (поймал --selftest).
|
||||
{ total++; code[$3]++
|
||||
if ($3 == "200") {
|
||||
if (streak > max_n) { max_n = streak; max_first = first_bad_ts; max_last = last_bad_ts; max_at = first_bad_at }
|
||||
streak = 0
|
||||
} else {
|
||||
if (!streak) { first_bad_ts = $1; first_bad_at = $2 }
|
||||
streak++; last_bad_ts = $1
|
||||
}
|
||||
if (prev_ts && $1 - prev_ts < 60) { gaps += $1 - prev_ts; gapn++ }
|
||||
prev_ts = $1
|
||||
}
|
||||
END {
|
||||
step = (gapn ? gaps / gapn : 0)
|
||||
if (streak > max_n) { max_n = streak; max_first = first_bad_ts; max_last = last_bad_ts; max_at = first_bad_at }
|
||||
printf "образцов: %d, шаг ~%.2f с\n", total, step
|
||||
for (c in code) printf " %s: %d\n", c, code[c]
|
||||
printf "максимальная серия не-200: %d образцов ≈ %.1f с (начало %s)\n", \
|
||||
max_n, (max_n ? max_last - max_first + step : 0), (max_at ? max_at : "-")
|
||||
}
|
||||
' "$1"
|
||||
}
|
||||
|
||||
if [ "${1:-}" = "--summary" ]; then
|
||||
summarize "$2"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "${1:-}" = "--selftest" ]; then
|
||||
tmp=$(mktemp)
|
||||
# 10 образцов с шагом 1 с: три подряд не-200 (2-я…4-я секунды) → серия ≈ 3 с.
|
||||
printf '1000\tT0\t200\t0.01\n1001\tT1\t502\t0.01\n1002\tT2\t000\t5.00\n1003\tT3\t503\t0.01\n1004\tT4\t200\t0.01\n1005\tT5\t200\t0.01\n1006\tT6\t502\t0.01\n1007\tT7\t200\t0.01\n' >"$tmp"
|
||||
out=$(summarize "$tmp")
|
||||
rm -f "$tmp"
|
||||
echo "$out"
|
||||
echo "$out" | grep -q "максимальная серия не-200: 3 образцов ≈ 3.0 с" || {
|
||||
echo "SELFTEST FAILED: серия посчитана неверно" >&2
|
||||
exit 1
|
||||
}
|
||||
echo "$out" | grep -q " 000: 1" || { echo "SELFTEST FAILED: 000 не выделен отдельно" >&2; exit 1; }
|
||||
echo "SELFTEST OK"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
URL=${1:-https://meraocenka.ru/}
|
||||
INTERVAL=${2:-1}
|
||||
DURATION=${3:-900}
|
||||
OUT=${4:-/tmp/probe-deploy-window.tsv}
|
||||
|
||||
: >"$OUT"
|
||||
echo "проба: $URL каждые ${INTERVAL}с в течение ${DURATION}с → $OUT"
|
||||
end=$(( $(date +%s) + DURATION ))
|
||||
while [ "$(date +%s)" -lt "$end" ]; do
|
||||
# --max-time 5: зависший запрос не должен растягивать шаг пробы на минуты.
|
||||
# -o /dev/null: тело не нужно, важны код и время.
|
||||
line=$(curl -sS -o /dev/null --max-time 5 -w '%{http_code}\t%{time_total}' "$URL" 2>/dev/null || echo "000 0")
|
||||
printf '%s\t%s\t%s\n' "$(date +%s.%N)" "$(date -u +%H:%M:%SZ)" "$line" >>"$OUT"
|
||||
sleep "$INTERVAL"
|
||||
done
|
||||
|
||||
echo "── сводка ─────────────────────────────────────────"
|
||||
summarize "$OUT"
|
||||
Loading…
Add table
Reference in a new issue