gendesign/scripts/probe-deploy-window.sh
bot-backend dacd298b21
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
fix(deploy): подменять фронт МЕРЫ отдельной командой — окно 30–90 с уходит
Публичный лендинг 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
2026-09-12 00:44:46 +05:00

99 lines
5.7 KiB
Bash
Executable file
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/bin/sh
# Проба окна недоступности при деплое (#3274).
#
# ЗАЧЕМ. Статус джобы деплоя зелёный независимо от того, видел ли посетитель
# ошибку: подмена контейнера происходит ВНУТРИ успешного прогона. Единственный
# честный замер — непрерывный опрос публичного адреса во время деплоя и подсчёт
# САМОЙ ДЛИННОЙ СЕРИИ подряд идущих не-200. Приёмка #3274 сформулирована именно
# так (комментарий от 05.09): «серия не-2xx/000 на / в окне мержа < 2 с».
#
# ГДЕ ЗАПУСКАТЬ — НА САМОМ ХОСТЕ (ssh poincare), не с ноутбука. Боевой периметр
# отбивает частые серии с одного внешнего адреса: замер 11.09 дал 3050 %
# `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"