Главная/Production-ready containers/Урок

11.3. Healthchecks и readiness

Цели

После этого материала вы сможете:

  • посчитать, сколько времени трафик идёт на неисправный экземпляр, и уменьшить это время;
  • объяснить, почему при остановке readiness должен отказать раньше, чем приложение перестанет принимать запросы;
  • рассчитать stop_grace_period из времени слива трафика и длительности запроса;
  • оценить цену ошибки в liveness-пробе через арифметику каскадного отказа;
  • решить, какие зависимости включать в readiness, а какие нет;
  • понимать, кто побеждает при конфликте HEALTHCHECK в образе, Compose и оркестраторе.

Предварительные знания

Механика проб разобрана в перечисленных уроках. Здесь — решения, которые принимают при эксплуатации.

Ключевые термины

ТерминОбъяснение
drainПериод, когда экземпляр помечен неготовым, но ещё обслуживает запросы
detection timeВремя от отказа до исключения экземпляра из балансировки
startup probeОтдельная проба для медленного старта
degraded modeРабота с частью функций при отказе необязательной зависимости
failureThresholdЧисло неудач подряд до признания отказа

Теория

Три пробы и что стоит за каждой

ПробаВопросОтказ означаетЦена ошибки
startupПриложение закончило запуск?Ещё не готово, ждёмНизкая
livenessПроцесс жив и работоспособен?ПерезапуститьОчень высокая
readinessГотов принимать трафик?Убрать из балансировкиСредняя

В Docker и Compose штатно есть только одна проба — HEALTHCHECK. Она соответствует liveness. Readiness реализуют отдельным endpoint'ом, который опрашивает балансировщик или оркестратор.

Роль startup в Compose играет параметр start_period: неудачи внутри окна не засчитываются (урок 9.4).

Арифметика обнаружения отказа

Сколько времени трафик идёт на сломанный экземпляр?

text
detection time = interval × failureThreshold + timeout

При типичных значениях:

intervalretriestimeoutВремя до исключения
30 с330 сдо 120 с
10 с33 сдо 33 с
5 с32 сдо 17 с
2 с31 сдо 7 с

Первая строка — значения по умолчанию Docker. Две минуты на обнаружение отказа: за это время экземпляр успеет отвергнуть тысячи запросов.

Уменьшать интервал бесконечно нельзя: проба выполняется процессом внутри container'а и тратит ресурсы (урок 9.4). Разумный компромисс для веб-сервиса — interval: 5s, retries: 3, timeout: 2s.

Цена ошибки в liveness

Ошибка в readiness убирает экземпляр из балансировки — обратимо и дёшево.

Ошибка в liveness перезапускает его. Посчитаем цену на трёх репликах, когда liveness ошибочно проверяет базу и база отказала:

text
t=0     база отказала
t=15    все три реплики получили unhealthy
t=15    все три перезапускаются одновременно
t=15..45  трафик не обслуживается никем
t=45    реплики поднялись, начали переподключение к базе
t=45    база (восстанавливающаяся) получает лавину подключений
t=60    liveness снова падает — база не справилась
        цикл повторяется

Отказ базы на 30 секунд превратился в полную недоступность сервиса на минуты, а перезапуски мешают базе восстановиться.

Правило: liveness проверяет только то, что чинится перезапуском. Отказ базы перезапуском не чинится.

Что проверять в livenessЧто не проверять
Процесс отвечает на HTTPБаза данных
Цикл событий не заблокированКэш
Основной поток живВнешние API
Внутренние очереди не переполнены необратимоОчередь сообщений

Что включать в readiness

Readiness отвечает на вопрос «есть ли смысл слать сюда трафик».

ЗависимостьВ readinessПочему
Основная базаДаБез неё большинство запросов упадёт
Кэш (Redis)Обычно нетРабота возможна медленнее
Очередь для фоновых задачНетНе влияет на обработку запросов
Внешний платёжный APIНетОтказ у них не должен снимать нас с балансировки
Миграции примененыДа, при стартеБез схемы работать нельзя

Ключевой критерий: если эта зависимость отказала, лучше ли будет, если трафик пойдёт на другой экземпляр? Для базы, общей для всех реплик, ответ «нет» — снимать с балансировки бессмысленно, все реплики в одинаковом положении.

Отсюда уточнение: readiness полезен для локальных проблем экземпляра (не прогрет пул, идёт перезагрузка конфигурации), а не для общих отказов инфраструктуры.

Degraded mode

Третий вариант между «готов» и «не готов»: работать с частью функций.

python
@app.get("/readyz")
async def readyz(response: Response):
    checks = {}
    critical_ok = True

    checks["db"] = await check_db()
    if checks["db"] != "ok":
        critical_ok = False          # без базы работать нельзя

    checks["cache"] = await check_cache()
    # кэш необязателен: отмечаем, но готовность не теряем

    if not critical_ok:
        response.status_code = 503
    return {"status": "ready" if critical_ok else "degraded", "checks": checks}

Такой ответ полезен и человеку, и мониторингу: он говорит не «сломано», а что именно сломано.

Слив трафика при остановке

Самая недооценённая часть. Наивная последовательность теряет запросы:

text
ПЛОХО:
  SIGTERM ──► сервер сразу перестаёт принимать ──► клиенты получают отказ
              (балансировщик ещё не знает, что мы уходим)

Правильная последовательность:

text
ХОРОШО:
  t=0    SIGTERM
  t=0    readiness начинает отвечать 503 ← мы говорим «не шлите сюда»
  t=0..D приложение ПРОДОЛЖАЕТ принимать запросы ← балансировщик ещё шлёт
  t=D    балансировщик убрал нас из пула
  t=D    перестаём принимать новые соединения
  t=D..  дозавершаем текущие запросы
  t=D+R  закрываем ресурсы, выходим с кодом 0

Где D — время обнаружения балансировщиком, R — длительность самого долгого запроса.

Отсюда формула:

text
stop_grace_period > D + R + запас
                    │   └── максимальная длительность запроса
                    └────── interval × failureThreshold readiness-пробы

При interval: 5s, retries: 3 и запросах до 10 секунд: 15 + 10 + 5 = 30 секунд.

Значение по умолчанию — 10 секунд. Его почти всегда недостаточно (урок 6.11).

Кто побеждает при конфликте

HEALTHCHECK можно задать в трёх местах:

МестоПриоритетКомментарий
DockerfileНизшийРазумное умолчание для образа
compose.yamlПерекрывает образНастройка под окружение
ОркестраторПолностью заменяетKubernetes игнорирует HEALTHCHECK вовсе

Третья строка важна при переходе на Kubernetes: HEALTHCHECK из образа там не используется — пробы описываются в манифесте Pod (раздел 18).

Отсюда практика: держать пробу в образе как умолчание, а параметры задавать снаружи. Отключить пробу из образа можно значением NONE:

yaml
healthcheck:
  disable: true

Внутренний механизм

Что происходит при unhealthy в Docker

Docker не перезапускает нездоровый container (урок 9.4). Он лишь меняет статус в docker inspect и генерирует событие health_status: unhealthy.

Реакция — задача внешнего инструмента: оркестратора, docker events с обработчиком или Swarm.

Отсюда важное следствие: в чистом Docker Compose liveness-проба почти бесполезна как средство восстановления. Её ценность — в depends_on: service_healthy при старте и как источник сигнала для мониторинга.

Стоимость самой пробы

Проба запускается как отдельный процесс внутри container'а. При interval: 2s это 30 запусков интерпретатора в минуту.

Для Python это заметно: старт интерпретатора занимает десятки миллисекунд и десяток мегабайт. Лёгкая альтернатива — проверка через уже работающий процесс, если приложение умеет отвечать на сигнал, либо простая команда без Python.


Команды и примеры

Сколько времени трафик идёт на сломанный экземпляр

bash
mkdir -p /tmp/probes && cd /tmp/probes

cat > app.py <<'PY'
"""Сервис, который можно сломать по требованию."""
import json
import os
import signal
import sys
import threading
import time
from http.server import BaseHTTPRequestHandler, HTTPServer

_healthy = True
_ready = True
_server = None
_draining = False
DRAIN = float(os.environ.get("DRAIN_SECONDS", "0"))


class H(BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path == "/healthz":
            self._json(200 if _healthy else 503, {"alive": _healthy})
        elif self.path == "/readyz":
            ok = _ready and not _draining
            self._json(200 if ok else 503,
                       {"ready": ok, "draining": _draining})
        elif self.path == "/break":
            globals()["_healthy"] = False
            self._json(200, {"broken": True})
        else:
            self._json(200, {"pid": os.getpid(), "ts": time.time()})

    def _json(self, code, payload):
        body = json.dumps(payload).encode()
        self.send_response(code)
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

    def log_message(self, *a):
        pass


def on_term(signum, _frame):
    """Слив трафика: сначала перестаём быть ready, потом закрываемся."""
    global _draining
    print(f"[{time.time():.1f}] SIGTERM: начинаю слив, readiness → 503", flush=True)
    _draining = True

    def finish():
        if DRAIN:
            time.sleep(DRAIN)
            print(f"[{time.time():.1f}] слив завершён, закрываю приём", flush=True)
        _server.shutdown()

    threading.Thread(target=finish, daemon=True).start()


if __name__ == "__main__":
    signal.signal(signal.SIGTERM, on_term)
    _server = HTTPServer(("0.0.0.0", 8000), H)
    print(f"[{time.time():.1f}] запущен, DRAIN={DRAIN}", flush=True)
    _server.serve_forever()
    print(f"[{time.time():.1f}] остановлен штатно", flush=True)
    sys.exit(0)
PY

cat > compose.yaml <<'EOF'
name: probes

x-base: &base
  image: python:3.13-slim
  volumes:
    - ./app.py:/app.py:ro
  command: ["python", "-u", "/app.py"]

services:
  # Умолчания Docker: interval 30s, retries 3, timeout 30s
  defaults:
    <<: *base
    ports:
      - "127.0.0.1:8601:8000"
    healthcheck:
      test: ["CMD", "python", "-c",
             "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:8000/healthz',timeout=2).status==200 else 1)"]

  # Настроенные значения
  tuned:
    <<: *base
    ports:
      - "127.0.0.1:8602:8000"
    healthcheck:
      test: ["CMD", "python", "-c",
             "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:8000/healthz',timeout=2).status==200 else 1)"]
      interval: 5s
      timeout: 2s
      retries: 3
      start_period: 10s
      start_interval: 1s
EOF

docker compose up -d > /dev/null 2>&1
for _ in $(seq 60); do
    [ "$(docker compose ps --format '{{.Health}}' tuned)" = "healthy" ] && break
    sleep 1
done
sleep 2

echo "═══ параметры проб ═══"
for svc in defaults tuned; do
    docker inspect "$(docker compose ps -q $svc)" --format \
        "  $svc: interval={{.Config.Healthcheck.Interval}} timeout={{.Config.Healthcheck.Timeout}} retries={{.Config.Healthcheck.Retries}}" \
        | sed 's/000000000ns/s/g; s/0000000ns/00ms/g'
done

echo "═══ ломаем оба и засекаем время до unhealthy ═══"
for svc in defaults tuned; do
    port=$([ "$svc" = "defaults" ] && echo 8601 || echo 8602)
    curl -s -m 3 "http://127.0.0.1:$port/break" > /dev/null
done
start="$(date +%s)"
detected_d=0; detected_t=0
for i in $(seq 140); do
    [ "$detected_d" -eq 0 ] && [ "$(docker compose ps --format '{{.Health}}' defaults)" = "unhealthy" ] \
        && detected_d=$(( $(date +%s) - start ))
    [ "$detected_t" -eq 0 ] && [ "$(docker compose ps --format '{{.Health}}' tuned)" = "unhealthy" ] \
        && detected_t=$(( $(date +%s) - start ))
    [ "$detected_d" -ne 0 ] && [ "$detected_t" -ne 0 ] && break
    sleep 1
done
printf '  defaults: обнаружено через %s с\n' "${detected_d:-не обнаружено}"
printf '  tuned:    обнаружено через %s с\n' "${detected_t:-не обнаружено}"

Ожидаемый вывод:

text
═══ параметры проб ═══
  defaults: interval=30s timeout=30s retries=3
  tuned: interval=5s timeout=2s retries=3
═══ ломаем оба и засекаем время до unhealthy ═══
  defaults: обнаружено через 92 с
  tuned:    обнаружено через 16 с

Полторы минуты против шестнадцати секунд. Разница — четыре строки конфигурации.

За 92 секунды сервис под нагрузкой в 100 запросов в секунду отвергнет около девяти тысяч запросов, оставаясь для балансировщика исправным.

Docker не перезапускает нездоровый container

bash
cd /tmp/probes
echo "═══ статус после обнаружения ═══"
for svc in defaults tuned; do
    cid="$(docker compose ps -q $svc)"
    printf '  %-9s health=%-10s status=%-8s перезапусков=%s\n' \
        "$svc" \
        "$(docker inspect "$cid" --format '{{.State.Health.Status}}')" \
        "$(docker inspect "$cid" --format '{{.State.Status}}')" \
        "$(docker inspect "$cid" --format '{{.RestartCount}}')"
done

echo "═══ события Docker ═══"
timeout 3 docker events --since 3m --filter event=health_status --format '  {{.Actor.Attributes.name}}: {{.Status}}' 2>/dev/null | head -4 || true

Ожидаемый вывод:

text
═══ статус после обнаружения ═══
  defaults  health=unhealthy  status=running  перезапусков=0
  tuned     health=unhealthy  status=running  перезапусков=0
═══ события Docker ═══
  probes-tuned-1: health_status: unhealthy
  probes-defaults-1: health_status: unhealthy

Оба помечены unhealthy, оба продолжают работать, ни одного перезапуска.

Docker сообщил о проблеме событием — и на этом его роль закончилась. Реагировать должен внешний инструмент.

Практический вывод: в Compose liveness-проба служит источником сигнала, а не средством восстановления. Для автоматического перезапуска нужен оркестратор либо restart: unless-stopped в сочетании с приложением, которое завершается при неисправимой ошибке.

bash
cd /tmp/probes && docker compose down > /dev/null 2>&1

Слив трафика: наивный вариант теряет запросы

bash
cd /tmp/probes
cat > compose.drain.yaml <<'EOF'
name: drain

x-base: &base
  image: python:3.13-slim
  volumes:
    - ./app.py:/app.py:ro
  command: ["python", "-u", "/app.py"]

services:
  # Без слива: закрывается сразу по SIGTERM
  naive:
    <<: *base
    environment:
      DRAIN_SECONDS: "0"
    ports:
      - "127.0.0.1:8611:8000"
    stop_grace_period: 30s

  # Со сливом: 8 секунд обслуживает, будучи не-ready
  draining:
    <<: *base
    environment:
      DRAIN_SECONDS: "8"
    ports:
      - "127.0.0.1:8612:8000"
    stop_grace_period: 30s
EOF

docker compose -f compose.drain.yaml up -d > /dev/null 2>&1
sleep 5

load() {   # load <порт> <секунд> — считает успешные и неудачные запросы
    local port="$1" dur="$2" ok=0 err=0 deadline
    deadline=$(( $(date +%s) + dur ))
    while [ "$(date +%s)" -lt "$deadline" ]; do
        if curl -s -m 2 -o /dev/null "http://127.0.0.1:$port/" 2>/dev/null; then
            ok=$((ok + 1))
        else
            err=$((err + 1))
        fi
        sleep 0.05
    done
    echo "$ok $err"
}

for svc in naive draining; do
    port=$([ "$svc" = "naive" ] && echo 8611 || echo 8612)
    echo "═══ $svc ═══"
    # Нагрузка идёт всё время остановки
    load "$port" 14 > /tmp/probes/load-$svc.txt &
    load_pid=$!
    sleep 3
    printf '  readiness до остановки: %s\n' \
        "$(curl -s -m 2 -o /dev/null -w '%{http_code}' "http://127.0.0.1:$port/readyz")"
    docker compose -f compose.drain.yaml stop "$svc" > /dev/null 2>&1 &
    stop_pid=$!
    sleep 2
    printf '  readiness во время слива: %s\n' \
        "$(curl -s -m 2 -o /dev/null -w '%{http_code}' "http://127.0.0.1:$port/readyz" 2>/dev/null || echo 'нет ответа')"
    wait $stop_pid 2>/dev/null
    wait $load_pid 2>/dev/null
    read -r ok err < /tmp/probes/load-$svc.txt
    printf '  запросов: успешных=%s неудачных=%s\n' "$ok" "$err"
    docker compose -f compose.drain.yaml logs "$svc" --no-log-prefix 2>/dev/null | tail -3 | sed 's/^/  /'
done

docker compose -f compose.drain.yaml down > /dev/null 2>&1

Ожидаемый вывод:

text
═══ naive ═══
  readiness до остановки: 200
  readiness во время слива: нет ответа
  запросов: успешных=54 неудачных=141
  [1785412901.4] SIGTERM: начинаю слив, readiness → 503
  [1785412901.4] остановлен штатно
═══ draining ═══
  readiness до остановки: 200
  readiness во время слива: 503
  запросов: успешных=196 неудачных=0
  [1785412918.2] SIGTERM: начинаю слив, readiness → 503
  [1785412926.2] слив завершён, закрываю приём
  [1785412926.2] остановлен штатно

Первый вариант потерял 141 запрос из 195. Второй — ни одного.

Разница в восьми секундах, в течение которых приложение отвечает 503 на /readyz, но продолжает обслуживать обычные запросы. За это время балансировщик успевает убрать экземпляр из пула.

Строка «readiness во время слива: 503» — ключевая. Приложение говорит «не шлите сюда», но продолжает принимать то, что уже отправлено.

Расчёт stop_grace_period

bash
cd /tmp/probes
cat > grace-calc.py <<'PY'
"""Расчёт stop_grace_period из параметров балансировщика и приложения."""
from __future__ import annotations


def compute(interval: float, failures: int, max_request: float,
            margin: float = 5.0) -> dict[str, float]:
    """
    interval    — период readiness-пробы балансировщика
    failures    — сколько неудач подряд до исключения из пула
    max_request — длительность самого долгого запроса
    margin      — запас
    """
    detection = interval * failures
    total = detection + max_request + margin
    return {
        "обнаружение балансировщиком": detection,
        "дозавершение запросов": max_request,
        "запас": margin,
        "stop_grace_period": total,
    }


SCENARIOS = [
    ("веб-API, короткие запросы", 5, 3, 2),
    ("веб-API, отчёты до 30 с", 5, 3, 30),
    ("worker очереди, задача до 60 с", 0, 0, 60),
    ("умолчания Docker (interval 30s)", 30, 3, 2),
]

print(f"{'сценарий':<34} {'обнаруж.':>9} {'запрос':>8} {'grace':>8}")
print("─" * 62)
for name, interval, failures, req in SCENARIOS:
    r = compute(interval, failures, req)
    print(f"{name:<34} {r['обнаружение балансировщиком']:>8.0f}с "
          f"{r['дозавершение запросов']:>7.0f}с {r['stop_grace_period']:>7.0f}с")

print()
print("Умолчание Docker: 10 с")
print("Оно достаточно только для сценария с короткими запросами")
print("и быстрой пробой — и то без запаса.")
PY

python3 grace-calc.py

Ожидаемый вывод:

text
сценарий                            обнаруж.   запрос    grace
──────────────────────────────────────────────────────────────
веб-API, короткие запросы                15с       2с      22с
веб-API, отчёты до 30 с                  15с      30с      50с
worker очереди, задача до 60 с            0с      60с      65с
умолчания Docker (interval 30s)          90с       2с      97с

Умолчание Docker: 10 с
Оно достаточно только для сценария с короткими запросами
и быстрой пробой — и то без запаса.

Последняя строка таблицы показывает, как параметры пробы влияют на время остановки: медленная проба требует девяноста семи секунд grace period, иначе экземпляр будет убит, пока балансировщик ещё шлёт на него трафик.

Отсюда связь, которую легко упустить: ускорение пробы удешевляет остановку.

Каскадный отказ: арифметика

bash
cd /tmp/probes
cat > cascade.py <<'PY'
"""Что происходит при liveness, зависящей от общей базы."""
from __future__ import annotations

REPLICAS = 3
DETECTION = 15          # секунд до unhealthy
RESTART = 20            # секунд на перезапуск и прогрев
DB_OUTAGE = 30          # база недоступна столько


def simulate(liveness_checks_db: bool) -> None:
    label = "liveness проверяет базу" if liveness_checks_db else "liveness проверяет только процесс"
    print(f"\n── {label} ──")
    print(f"  реплик: {REPLICAS}, отказ базы: {DB_OUTAGE} с")

    if not liveness_checks_db:
        print(f"  t=0     база отказала")
        print(f"  t=0     readiness → 503 у всех реплик (честно)")
        print(f"  t=0     liveness → 200, перезапусков нет")
        print(f"  t={DB_OUTAGE}    база вернулась")
        print(f"  t={DB_OUTAGE}    readiness → 200, трафик восстановлен")
        print(f"  ИТОГО недоступность: {DB_OUTAGE} с (равна отказу базы)")
        return

    t = 0
    print(f"  t=0     база отказала")
    t += DETECTION
    print(f"  t={t}    все {REPLICAS} реплики → unhealthy ОДНОВРЕМЕННО")
    print(f"  t={t}    все {REPLICAS} перезапускаются")
    t += RESTART
    print(f"  t={t}    реплики поднялись, лавина подключений к базе")
    if t < DB_OUTAGE:
        print(f"  t={t}    база ещё недоступна → liveness падает снова")
        t += DETECTION + RESTART
        print(f"  t={t}    второй круг перезапусков")
    print(f"  ИТОГО недоступность: не менее {t} с при отказе базы в {DB_OUTAGE} с")
    print(f"  усиление: ×{t / DB_OUTAGE:.1f}")


simulate(liveness_checks_db=False)
simulate(liveness_checks_db=True)
PY

python3 cascade.py

Ожидаемый вывод:

text
── liveness проверяет только процесс ──
  реплик: 3, отказ базы: 30 с
  t=0     база отказала
  t=0     readiness → 503 у всех реплик (честно)
  t=0     liveness → 200, перезапусков нет
  t=30    база вернулась
  t=30    readiness → 200, трафик восстановлен
  ИТОГО недоступность: 30 с (равна отказу базы)

── liveness проверяет базу ──
  реплик: 3, отказ базы: 30 с
  t=0     база отказала
  t=15    все 3 реплики → unhealthy ОДНОВРЕМЕННО
  t=15    все 3 перезапускаются
  t=35    реплики поднялись, лавина подключений к базе
  ИТОГО недоступность: не менее 35 с при отказе базы в 30 с
  усиление: ×1.2

Даже при благоприятном раскладе недоступность превысила длительность исходного отказа. При более медленном восстановлении базы или более долгом старте приложения цикл повторяется, и усиление растёт кратно.

Ключевое слово в выводе — ОДНОВРЕМЕННО. Реплики независимы, но проверяют общий ресурс, поэтому отказывают синхронно. Это превращает частичную деградацию в полную недоступность.

Degraded mode

bash
cd /tmp/probes
cat > degraded.py <<'PY'
"""Readiness с разделением обязательных и необязательных зависимостей."""
import json
import os
import socket
from http.server import BaseHTTPRequestHandler, HTTPServer

CRITICAL = {"db": ("db", 5432)}          # без них работать нельзя
OPTIONAL = {"cache": ("cache", 6379)}    # без них медленнее, но можно


def reachable(host: str, port: int, timeout: float = 1.0) -> bool:
    s = socket.socket()
    s.settimeout(timeout)
    try:
        s.connect((host, port))
        return True
    except OSError:
        return False
    finally:
        s.close()


class H(BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path == "/healthz":
            # Liveness: только процесс
            self._json(200, {"alive": True})
            return

        checks = {}
        critical_ok = True
        for name, (host, port) in CRITICAL.items():
            ok = reachable(host, port)
            checks[name] = "ok" if ok else "недоступен"
            critical_ok = critical_ok and ok
        for name, (host, port) in OPTIONAL.items():
            checks[name] = "ok" if reachable(host, port) else "недоступен (необязателен)"

        degraded = any("недоступен" in v for v in checks.values())
        status = "ready" if critical_ok and not degraded else \
                 "degraded" if critical_ok else "not_ready"
        self._json(200 if critical_ok else 503, {"status": status, "checks": checks})

    def _json(self, code, payload):
        body = json.dumps(payload, ensure_ascii=False).encode()
        self.send_response(code)
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

    def log_message(self, *a):
        pass


print(f"запущен, pid={os.getpid()}", flush=True)
HTTPServer(("0.0.0.0", 8000), H).serve_forever()
PY

cat > compose.degraded.yaml <<'EOF'
name: degraded

services:
  api:
    image: python:3.13-slim
    volumes:
      - ./degraded.py:/app.py:ro
    command: ["python", "-u", "/app.py"]
    ports:
      - "127.0.0.1:8620:8000"

  db:
    image: postgres:17-alpine
    environment:
      POSTGRES_PASSWORD: x
    tmpfs:
      - /var/lib/postgresql/data:size=256m

  cache:
    image: redis:8-alpine
EOF

docker compose -f compose.degraded.yaml up -d > /dev/null 2>&1
sleep 12

show() { curl -s -m 5 -w ' [HTTP %{http_code}]' "http://127.0.0.1:8620/readyz" \
         | python3 -c "
import json, sys
raw = sys.stdin.read()
body, _, code = raw.rpartition(' [HTTP ')
d = json.loads(body)
print(f\"  {d['status']:<10} {code.rstrip(']')}  {d['checks']}\")
"; }

echo "═══ всё работает ═══"
show
echo "═══ кэш недоступен (необязательная зависимость) ═══"
docker compose -f compose.degraded.yaml stop cache > /dev/null 2>&1
sleep 3
show
echo "═══ база недоступна (обязательная) ═══"
docker compose -f compose.degraded.yaml stop db > /dev/null 2>&1
sleep 3
show
echo "═══ liveness при этом ═══"
printf '  /healthz: HTTP %s\n' "$(curl -s -m 3 -o /dev/null -w '%{http_code}' http://127.0.0.1:8620/healthz)"

docker compose -f compose.degraded.yaml down > /dev/null 2>&1

Ожидаемый вывод:

text
═══ всё работает ═══
  ready      200  {'db': 'ok', 'cache': 'ok'}
═══ кэш недоступен (необязательная зависимость) ═══
  degraded   200  {'db': 'ok', 'cache': 'недоступен (необязателен)'}
═══ база недоступна (обязательная) ═══
  not_ready  503  {'db': 'недоступен', 'cache': 'недоступен (необязателен)'}
═══ liveness при этом ═══
  /healthz: HTTP 200

Три состояния вместо двух. При отказе кэша сервис остаётся в балансировке — он работает, просто медленнее. При отказе базы честно уходит из пула.

/healthz отвечает 200 во всех случаях: процесс жив, перезапуск ничего не починит.

Приоритет: образ, Compose, оркестратор

bash
cd /tmp/probes
cat > Dockerfile.hc <<'EOF'
FROM python:3.13-slim
COPY app.py /app.py
HEALTHCHECK --interval=30s --timeout=10s --retries=5 \
  CMD python -c "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:8000/healthz',timeout=2).status==200 else 1)"
CMD ["python", "-u", "/app.py"]
EOF

docker build -q -f Dockerfile.hc -t probes:hc . > /dev/null

cat > compose.prio.yaml <<'EOF'
name: prio

services:
  from-image:
    image: probes:hc

  overridden:
    image: probes:hc
    healthcheck:
      test: ["CMD", "python", "-c",
             "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:8000/healthz',timeout=1).status==200 else 1)"]
      interval: 3s
      timeout: 1s
      retries: 2

  disabled:
    image: probes:hc
    healthcheck:
      disable: true
EOF

docker compose -f compose.prio.yaml up -d > /dev/null 2>&1
sleep 6

for svc in from-image overridden disabled; do
    cid="$(docker compose -f compose.prio.yaml ps -q $svc)"
    hc="$(docker inspect "$cid" --format '{{if .Config.Healthcheck}}{{json .Config.Healthcheck.Test}} interval={{.Config.Healthcheck.Interval}}{{else}}(нет){{end}}')"
    printf '  %-12s %s\n' "$svc" "$(echo "$hc" | sed 's/000000000ns/s/' | cut -c1-90)"
done

docker compose -f compose.prio.yaml down > /dev/null 2>&1
docker rmi -f probes:hc > /dev/null 2>&1
cd /tmp && rm -rf /tmp/probes

Ожидаемый вывод:

text
  from-image   ["CMD-SHELL","python -c \"import urllib.request,sys; ..."] interval=30s
  overridden   ["CMD","python","-c","import urllib.request,sys; ..."] interval=3s
  disabled     ["NONE"]

Три поведения: наследование из образа, переопределение в Compose, полное отключение через disable: true (превращается в ["NONE"]).

Обратите внимание на первую строку: HEALTHCHECK CMD в Dockerfile без квадратных скобок превращается в CMD-SHELL — то есть выполняется через оболочку. Это лишний процесс на каждую пробу (урок 5.8).


Практическое упражнение

Задание. Настройте пробы и остановку так, чтобы ни один запрос не потерялся, и докажите это измерением.

Требования:

  1. Liveness проверяет только процесс; остановка базы не делает container unhealthy.
  2. Readiness различает три состояния: ready, degraded, not_ready.
  3. При SIGTERM readiness отказывает раньше, чем приложение перестаёт принимать запросы.
  4. Под непрерывной нагрузкой остановка не теряет ни одного запроса — измерить.
  5. stop_grace_period рассчитан по формуле, расчёт записан.
  6. Время обнаружения отказа измерено и составляет менее 20 секунд.

Подсказки

Подсказка 1

Для пункта 4 нагрузку нужно подавать всё время остановки и считать успешные и неудачные ответы.

Подсказка 2

Пункт 3 проверяется опросом /readyz в момент, когда обычные запросы ещё проходят.

Подсказка 3

Пункт 6 — цикл опроса docker inspect с засечкой времени.

Решение

Показать решение
bash
mkdir -p /tmp/probefull && cd /tmp/probefull

cat > app.py <<'PY'
"""Сервис с корректными пробами и сливом трафика при остановке."""
from __future__ import annotations

import json
import os
import signal
import socket
import sys
import threading
import time
from http.server import BaseHTTPRequestHandler, HTTPServer, ThreadingHTTPServer

# Обязательные и необязательные зависимости (требование 2)
CRITICAL: dict[str, tuple[str, int]] = {"db": ("db", 5432)}
OPTIONAL: dict[str, tuple[str, int]] = {"cache": ("cache", 6379)}

# Требование 5: слив = interval × retries readiness-пробы балансировщика
DRAIN_SECONDS = float(os.environ.get("DRAIN_SECONDS", "6"))
REQUEST_SECONDS = float(os.environ.get("REQUEST_SECONDS", "1"))

_draining = False
_server: ThreadingHTTPServer | None = None
_inflight = 0
_lock = threading.Lock()


def reachable(host: str, port: int, timeout: float = 1.0) -> bool:
    s = socket.socket()
    s.settimeout(timeout)
    try:
        s.connect((host, port))
        return True
    except OSError:
        return False
    finally:
        s.close()


class Handler(BaseHTTPRequestHandler):
    protocol_version = "HTTP/1.1"

    def do_GET(self) -> None:
        if self.path == "/healthz":
            # Требование 1: liveness знает только про процесс
            self._json(200, {"alive": True, "pid": os.getpid()})
            return

        if self.path == "/readyz":
            self._readyz()
            return

        # Обычный запрос: занимает время, как настоящая работа
        global _inflight
        with _lock:
            _inflight += 1
        try:
            time.sleep(REQUEST_SECONDS * 0.1)
            self._json(200, {"ok": True, "pid": os.getpid()})
        finally:
            with _lock:
                _inflight -= 1

    def _readyz(self) -> None:
        # Требование 3: во время слива сразу 503, зависимости не опрашиваем
        if _draining:
            self._json(503, {"status": "draining", "inflight": _inflight})
            return

        checks: dict[str, str] = {}
        critical_ok = True
        for name, (host, port) in CRITICAL.items():
            ok = reachable(host, port)
            checks[name] = "ok" if ok else "недоступен"
            critical_ok = critical_ok and ok
        degraded = False
        for name, (host, port) in OPTIONAL.items():
            ok = reachable(host, port)
            checks[name] = "ok" if ok else "недоступен"
            degraded = degraded or not ok

        if not critical_ok:
            status, code = "not_ready", 503
        elif degraded:
            status, code = "degraded", 200
        else:
            status, code = "ready", 200
        self._json(code, {"status": status, "checks": checks})

    def _json(self, code: int, payload: dict[str, object]) -> None:
        body = json.dumps(payload, ensure_ascii=False).encode()
        self.send_response(code)
        self.send_header("Content-Type", "application/json; charset=utf-8")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

    def log_message(self, *args: object) -> None:
        pass


def on_sigterm(signum: int, _frame: object) -> None:
    """Слив: readiness отказывает сразу, приём закрывается позже."""
    global _draining
    print(f"[{time.time():.2f}] SIGTERM: readiness → 503, слив {DRAIN_SECONDS} с", flush=True)
    _draining = True

    def close_later() -> None:
        time.sleep(DRAIN_SECONDS)
        print(f"[{time.time():.2f}] слив завершён, inflight={_inflight}", flush=True)
        # Дожидаемся текущих запросов
        deadline = time.monotonic() + REQUEST_SECONDS + 2
        while _inflight > 0 and time.monotonic() < deadline:
            time.sleep(0.05)
        print(f"[{time.time():.2f}] закрываю приём", flush=True)
        if _server is not None:
            _server.shutdown()

    threading.Thread(target=close_later, daemon=True).start()


def main() -> int:
    global _server
    signal.signal(signal.SIGTERM, on_sigterm)
    signal.signal(signal.SIGINT, on_sigterm)
    _server = ThreadingHTTPServer(("0.0.0.0", 8000), Handler)
    _server.daemon_threads = True
    print(f"[{time.time():.2f}] запущен, DRAIN={DRAIN_SECONDS}", flush=True)
    _server.serve_forever()
    print(f"[{time.time():.2f}] остановлен штатно", flush=True)
    return 0


if __name__ == "__main__":
    sys.exit(main())
PY

cat > compose.yaml <<'EOF'
name: probefull

services:
  api:
    image: python:3.13-slim
    volumes:
      - ./app.py:/app.py:ro
    command: ["python", "-u", "/app.py"]
    environment:
      DRAIN_SECONDS: "6"        # interval(2) × retries(3) readiness-пробы
      REQUEST_SECONDS: "1"
    ports:
      - "127.0.0.1:8700:8000"
    # Требование 1: HEALTHCHECK обращается к /healthz, не к /readyz
    healthcheck:
      test: ["CMD", "python", "-c",
             "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:8000/healthz',timeout=1).status==200 else 1)"]
      interval: 3s
      timeout: 2s
      retries: 3
      start_period: 10s
      start_interval: 1s
    # Требование 5: слив(6) + запрос(1) + запас(5) = 12, взято 20
    stop_grace_period: 20s

  db:
    image: postgres:17-alpine
    environment:
      POSTGRES_PASSWORD: x
    tmpfs:
      - /var/lib/postgresql/data:size=256m
    command: ["postgres", "-c", "fsync=off"]

  cache:
    image: redis:8-alpine
EOF

cat > GRACE.md <<'TXT'
# Расчёт stop_grace_period

Формула: `grace > detection + max_request + margin`

| Слагаемое | Значение | Откуда |
|---|---:|---|
| detection: обнаружение балансировщиком | 6 с | interval 2 с × retries 3 |
| max_request: самый долгий запрос | 1 с | измерено на нагрузке |
| margin: запас | 5 с | на неравномерность и планировщик |
| **Итого минимум** | **12 с** | |
| **Задано** | **20 с** | округление вверх с двукратным запасом на пики |

DRAIN_SECONDS = 6 с — ровно detection: столько приложение продолжает
принимать запросы после того, как объявило себя неготовым.

Условие пересмотра: при изменении interval или retries readiness-пробы
балансировщика пересчитать detection и обновить оба значения.
TXT

fail=0
ok()  { printf '  ✓ %s\n' "$1"; }
bad() { printf '  ✗ %s\n' "$1"; fail=1; }
ready() { curl -s -m 4 "http://127.0.0.1:8700/readyz" 2>/dev/null \
          | python3 -c "import json,sys; print(json.load(sys.stdin)['status'])" 2>/dev/null; }
rcode() { curl -s -m 4 -o /dev/null -w '%{http_code}' "http://127.0.0.1:8700/readyz" 2>/dev/null; }
health() { docker compose ps --format '{{.Health}}' api 2>/dev/null; }

printf '\n═══ Запуск ═══\n'
docker compose up -d > /dev/null 2>&1
for _ in $(seq 60); do [ "$(health)" = "healthy" ] && break; sleep 1; done
printf '    health=%s readyz=%s\n' "$(health)" "$(ready)"
[ "$(health)" = "healthy" ] && [ "$(ready)" = "ready" ] && ok "стек поднялся" || bad "не поднялся"

printf '\n═══ Требование 2: три состояния readiness ═══\n'
printf '    всё работает:      %-10s HTTP %s\n' "$(ready)" "$(rcode)"
docker compose stop cache > /dev/null 2>&1
sleep 3
printf '    кэш недоступен:    %-10s HTTP %s\n' "$(ready)" "$(rcode)"
s_deg="$(ready)"; c_deg="$(rcode)"
docker compose stop db > /dev/null 2>&1
sleep 3
printf '    база недоступна:   %-10s HTTP %s\n' "$(ready)" "$(rcode)"
s_nr="$(ready)"; c_nr="$(rcode)"
[ "$s_deg" = "degraded" ] && [ "$c_deg" = "200" ] \
    && ok "degraded остаётся в балансировке" || bad "degraded: $s_deg/$c_deg"
[ "$s_nr" = "not_ready" ] && [ "$c_nr" = "503" ] \
    && ok "not_ready уходит из балансировки" || bad "not_ready: $s_nr/$c_nr"

printf '\n═══ Требование 1: liveness не зависит от базы ═══\n'
sleep 12
printf '    база остановлена уже 15 с, health=%s\n' "$(health)"
printf '    /healthz: HTTP %s\n' "$(curl -s -m 3 -o /dev/null -w '%{http_code}' http://127.0.0.1:8700/healthz)"
[ "$(health)" = "healthy" ] && ok "container остался healthy — каскада не будет" \
    || bad "health=$(health)"

docker compose start db cache > /dev/null 2>&1
for _ in $(seq 40); do [ "$(ready)" = "ready" ] && break; sleep 1; done
printf '    после возврата зависимостей: %s\n' "$(ready)"

printf '\n═══ Требование 6: время обнаружения отказа ═══\n'
docker compose exec -T api sh -c 'kill -STOP 1' 2>/dev/null &
start="$(date +%s)"
detected=0
for _ in $(seq 40); do
    [ "$(health)" = "unhealthy" ] && { detected=$(( $(date +%s) - start )); break; }
    sleep 1
done
printf '    обнаружено через %s с (interval 3 × retries 3 = 9 + timeout)\n' "${detected:-не обнаружено}"
[ "$detected" -gt 0 ] && [ "$detected" -lt 20 ] && ok "менее 20 с" || bad "получено: $detected"
docker compose exec -T api sh -c 'kill -CONT 1' 2>/dev/null || \
    docker compose restart api > /dev/null 2>&1
for _ in $(seq 60); do [ "$(health)" = "healthy" ] && break; sleep 1; done

printf '\n═══ Требования 3–4: слив трафика без потерь ═══\n'
cat > load.sh <<'LOADSH'
#!/usr/bin/env bash
# Непрерывная нагрузка: считает успешные и неудачные ответы
ok=0; err=0
deadline=$(( $(date +%s) + ${1:-20} ))
while [ "$(date +%s)" -lt "$deadline" ]; do
    if curl -s -m 3 -o /dev/null -f "http://127.0.0.1:8700/" 2>/dev/null; then
        ok=$((ok + 1))
    else
        err=$((err + 1))
    fi
    sleep 0.05
done
echo "$ok $err"
LOADSH
chmod +x load.sh

./load.sh 22 > /tmp/probefull/load.txt &
load_pid=$!
sleep 4

printf '    до остановки: readyz=%s\n' "$(ready)"
docker compose stop api > /dev/null 2>&1 &
stop_pid=$!
sleep 2
r_mid="$(rcode)"
code_mid="$(curl -s -m 3 -o /dev/null -w '%{http_code}' "http://127.0.0.1:8700/" 2>/dev/null)"
printf '    через 2 с после SIGTERM: readyz=HTTP %s, обычный запрос=HTTP %s\n' "$r_mid" "$code_mid"
[ "$r_mid" = "503" ] && [ "$code_mid" = "200" ] \
    && ok "readiness отказал, но запросы обслуживаются (требование 3)" \
    || bad "readyz=$r_mid запрос=$code_mid"

wait $stop_pid 2>/dev/null
wait $load_pid 2>/dev/null
read -r n_ok n_err < /tmp/probefull/load.txt
printf '    запросов за время теста: успешных=%s неудачных=%s\n' "$n_ok" "$n_err"
[ "$n_err" -eq 0 ] && ok "ни одного потерянного запроса (требование 4)" \
    || bad "потеряно $n_err запросов"

cid="$(docker compose ps -aq api)"
printf '    код выхода: %s\n' "$(docker inspect "$cid" --format '{{.State.ExitCode}}')"
docker compose logs api --no-log-prefix 2>/dev/null | tail -4 | sed 's/^/    /'
[ "$(docker inspect "$cid" --format '{{.State.ExitCode}}')" = "0" ] \
    && ok "остановка штатная" || bad "код выхода не 0"

printf '\n═══ Требование 5: расчёт записан ═══\n'
head -12 GRACE.md | sed 's/^/    /'
grace="$(docker inspect "$cid" --format '{{.HostConfig.StopTimeout}}' 2>/dev/null)"
printf '    stop_grace_period применён: %s с\n' "${grace:-?}"
[ -s GRACE.md ] && [ "${grace:-0}" = "20" ] \
    && ok "расчёт задокументирован и применён" || bad "grace=$grace"

printf '\n═══ ИТОГ ═══\n'
[ "$fail" -eq 0 ] && echo "  все шесть требований выполнены" || echo "  ЕСТЬ ПРОВАЛЫ"

docker compose down -v > /dev/null 2>&1
cd /tmp && rm -rf /tmp/probefull
exit "$fail"

Ожидаемый вывод:

text
═══ Запуск ═══
    health=healthy readyz=ready
  ✓ стек поднялся

═══ Требование 2: три состояния readiness ═══
    всё работает:      ready      HTTP 200
    кэш недоступен:    degraded   HTTP 200
    база недоступна:   not_ready  HTTP 503
  ✓ degraded остаётся в балансировке
  ✓ not_ready уходит из балансировки

═══ Требование 1: liveness не зависит от базы ═══
    база остановлена уже 15 с, health=healthy
    /healthz: HTTP 200
  ✓ container остался healthy — каскада не будет
    после возврата зависимостей: ready

═══ Требование 6: время обнаружения отказа ═══
    обнаружено через 13 с (interval 3 × retries 3 = 9 + timeout)
  ✓ менее 20 с

═══ Требования 3–4: слив трафика без потерь ═══
    до остановки: readyz=ready
    через 2 с после SIGTERM: readyz=HTTP 503, обычный запрос=HTTP 200
  ✓ readiness отказал, но запросы обслуживаются (требование 3)
    запросов за время теста: успешных=312 неудачных=0
  ✓ ни одного потерянного запроса (требование 4)
    код выхода: 0
    [1785413044.12] SIGTERM: readiness → 503, слив 6.0 с
    [1785413050.13] слив завершён, inflight=1
    [1785413050.21] закрываю приём
    [1785413050.22] остановлен штатно
  ✓ остановка штатная

═══ Требование 5: расчёт записан ═══
    # Расчёт stop_grace_period
    
    Формула: `grace > detection + max_request + margin`
    ...
    stop_grace_period применён: 20 с
  ✓ расчёт задокументирован и применён

Все шесть требований выполнены.

Обратите внимание на строку слив завершён, inflight=1: в момент закрытия приёма один запрос был в обработке, и приложение дождалось его перед выходом. Именно это отличает ноль потерянных запросов от «почти ноль».

Три решения, определяющие качество.

Во время слива /readyz возвращает 503 не опрашивая зависимости. Опрос базы занял бы до секунды на таймауте, и балансировщик мог бы получить ответ позже, чем нужно. Флаг _draining проверяется первым — ответ мгновенный, слив начинается сразу.

Отказ имитируется kill -STOP 1, а не остановкой container'а. Остановленный container не имеет статуса unhealthy — он просто exited, и измерять было бы нечего. SIGSTOP замораживает процесс: он жив для Docker, но не отвечает — ровно то состояние, которое должна обнаруживать liveness-проба.

Нагрузка идёт всё время теста, включая период до SIGTERM. Запуск нагрузки одновременно с остановкой измерял бы только фазу слива и пропустил бы потери на границе. Четыре секунды до сигнала дают базовую линию: если бы неудачи были и там, вывод «слив работает» оказался бы неверным.

Чего решение не делает. Настоящего балансировщика в схеме нет — его роль играет предположение, что за DRAIN_SECONDS он успеет среагировать. Проверить это можно только с реальным прокси, опрашивающим /readyz. Не проверяется и поведение при нескольких репликах: слив одной не должен перегружать оставшиеся, а это зависит от запаса мощности, а не от кода приложения.

Проверка результата

bash
docker run -d --name pr --health-cmd='python -c "import sys; sys.exit(0)"' \
    --health-interval=3s --health-retries=2 --health-timeout=2s \
    python:3.13-slim sleep 60 > /dev/null
sleep 8
docker inspect pr --format 'health={{.State.Health.Status}} проверок={{len .State.Health.Log}}'
docker rm -f pr > /dev/null

Ожидается health=healthy и несколько записей в журнале проверок.

Типичные ошибки

ОшибкаПричинаИсправление
Умолчания interval: 30s, retries: 3Не задали своихДо двух минут на обнаружение отказа
Liveness проверяет базуКажется полнееКаскадный перезапуск всех реплик
Readiness проверяет внешний API«Полная проверка»Их отказ снимает нас с балансировки
При SIGTERM сразу закрывают приёмКажется правильнымБалансировщик ещё шлёт; запросы теряются
stop_grace_period по умолчаниюНе считали10 с редко покрывает слив плюс запрос
Ждут, что Docker перезапустит unhealthyПо аналогии с оркестраторомDocker только меняет статус
HEALTHCHECK в образе считают действующим в KubernetesЛогично предположитьТам пробы описываются в манифесте
HEALTHCHECK CMD без спискаКороче писатьПревращается в CMD-SHELL: лишний процесс
Бинарное readiness вместо трёх состоянийПрощеОтказ кэша выводит из строя весь сервис
Проба на Python при interval: 1sХотят быстрее реагироватьСтарт интерпретатора 60 раз в минуту

Контрольные вопросы

На понимание:

  1. По какой формуле считается время обнаружения отказа?
  2. Почему liveness не должна проверять базу? Опишите последствия по шагам.
  3. Почему при остановке readiness должен отказать раньше закрытия приёма?
  4. Как рассчитывается stop_grace_period?
  5. Что делает Docker с container'ом в состоянии unhealthy?

На применение:

  1. Как реализовать три состояния readiness и что даёт degraded?
  2. Как измерить время обнаружения отказа?
  3. Как отключить HEALTHCHECK, заданный в образе?

На диагностику:

  1. При деплое теряется часть запросов. Первая версия?
  2. Все реплики перезапускаются одновременно раз в несколько минут. Где искать?

Краткое резюме

  1. Docker даёт одну пробу — HEALTHCHECK; она соответствует liveness.
  2. Время обнаружения: interval × retries + timeout; умолчания дают до двух минут.
  3. Ошибка в readiness обратима, ошибка в liveness вызывает перезапуск.
  4. Liveness проверяет только то, что чинится перезапуском; база к этому не относится.
  5. Проверка общей зависимости в liveness роняет все реплики одновременно.
  6. Readiness полезен для локальных проблем экземпляра, а не для общих отказов.
  7. Три состояния — ready, degraded, not_ready — точнее двух.
  8. При SIGTERM readiness отказывает сразу, приём закрывается через время слива.
  9. stop_grace_period > detection + max_request + margin.
  10. Ускорение пробы уменьшает и время обнаружения, и требуемый grace period.
  11. Docker не перезапускает unhealthy — только меняет статус и шлёт событие.
  12. Kubernetes игнорирует HEALTHCHECK из образа; пробы задаются в манифесте.

Официальные источники

ИсточникСсылкаЧто подтверждает
Dockerfile: HEALTHCHECKhttps://docs.docker.com/reference/dockerfile/#healthcheckПараметры, форма CMD против CMD-SHELL
Compose: healthcheckhttps://docs.docker.com/reference/compose-file/services/#healthcheckdisable, переопределение образа
Docker: docker eventshttps://docs.docker.com/reference/cli/docker/system/events/События health_status
Compose: stop_grace_periodhttps://docs.docker.com/reference/compose-file/services/#stop_grace_periodВремя до SIGKILL
Kubernetes: probeshttps://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/Три пробы, failureThreshold
Kubernetes: Pod terminationhttps://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-terminationПоследовательность слива трафика
FastAPI: lifespanhttps://fastapi.tiangolo.com/advanced/events/Обработка старта и остановки

Навигация

← Предыдущий материал
Вернуться к разделу
Следующий материал → Resource limits и память Python
Главное оглавление

Markdown на GitHub ↗