#!/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 (epochISO-времякодсекунды): сколько чего, и главное — самая # длинная серия подряд идущих не-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"