27 KiB
27 KiB
type, work_item_id, title, version, status, created_at, authors
| type | work_item_id | title | version | status | created_at | authors | |
|---|---|---|---|---|---|---|---|
| tech-risks | ET-015 | Технические риски — ET-015: Healthcheck enduro-trails-app через python urllib | 1 | approved | 2026-06-05 |
|
Технические риски — ET-015
Технические риски замены curl на python urllib.request-one-liner
в healthcheck сервиса app. Бизнес-риски — в BRD §3 (ложные алёрты,
эрозия доверия, невозможность SLO). Шкала: вероятность (Н/С/В) ×
влияние (Н/С/В).
R-T-1 — Python one-liner крэшится из-за квотинга / парсинга YAML
- Описание: YAML-парсер Compose может неожиданно интерпретировать
кавычки внутри python-строки. Например,
'http://...'внутри"... urlopen('http://...', timeout=3) ..."— двойные кавычки снаружи, одинарные внутри. Если строка где-то будет некорректно «склеена», команда попадёт в Docker в покорёженном виде иpython -cсвалится с SyntaxError. - Вероятность / Влияние: Н / С.
- Митигация:
- Архитектурное решение (ADR-020 §«Решение»): YAML-массив
["CMD", "python", "-c", "..."], неCMD-SHELL. Docker передаёт аргументы напрямую черезexec(), без/bin/sh -c, без двойного парсинга. Кавычки внутри 4-го элемента — обычные символы строки, YAML их не трогает. - Acceptance гейт: ST-03 —
python -c 'import yaml,sys; print(yaml.safe_load(...)["services"]["app"]["healthcheck"]["test"])'проверяет, что массив парсится корректно и 4-й элемент содержитurllib.requestиsys.exit. - Integration гейт: IT-01 —
docker compose up -d appи переход вhealthyза ≤ 120 с подтверждает, что строка дошла до Docker в правильном виде.
- Архитектурное решение (ADR-020 §«Решение»): YAML-массив
R-T-2 — Стоимость fork+exec python каждые 30 секунд
- Описание: Каждый цикл healthcheck поднимает отдельный процесс
python(~80–150 мс старт интерпретатора). На фоне нагруженного uvicorn это может создавать заметный CPU-spike. - Вероятность / Влияние: Н / Н.
- Митигация:
- Архитектурное решение: интервал 30 с → накладные расходы
~150 мс / 30 с = 0.5% от одного ядра в пике, в среднем —
~0.005% CPU. На mva154 (BRD: idle ~1–2% CPU
app) это пренебрежимо. - Оценка: даже при росте интервала до
interval: 10s(что не планируется) overhead остался бы < 2% от одного ядра в пике. - Мониторинг: наблюдение
docker stats enduro-trails-app-1в течение суток после деплоя (см.07-infra-requirements.md§7.1).
- Архитектурное решение: интервал 30 с → накладные расходы
~150 мс / 30 с = 0.5% от одного ядра в пике, в среднем —
~0.005% CPU. На mva154 (BRD: idle ~1–2% CPU
R-T-3 — Внутренний timeout=3 короче, чем фактическое время ответа /api/health
- Описание: Сейчас
/api/healthотвечает за ~7 мс (BRD §1). Но при высокой нагрузке uvicorn (например, медленный SELECT в другом запросе блокирует event loop)/api/healthможет отвечать за3 с, healthcheck свалится в
unhealthy. - Вероятность / Влияние: Н / С.
- Митигация:
- Архитектурное решение:
/api/health— лёгкий sync handler (async def health()отдаёт сразу JSON, без IO в БД). FastAPI/uvicorn обслуживает его очень быстро. ~7 мс — стабильно. - Запас: внутренний 3 с — это 430× медленнее текущего среднего ответа. Чтобы попасть в timeout, нужно 430-кратное замедление endpoint'а — это уже не «загруженность», а инцидент.
- Контракт: если healthcheck начинает фейлиться из-за timeout — это корректный сигнал, что приложение деградировало. То, что нужно от healthcheck'а.
- TD (ADR-020 TD-1): если
/api/healthстанет «дорогим» (расширят проверками OSRM/тайлов), нужно будет либо увеличить timeout, либо разнести «liveness» и «readiness» — отдельный work-item.
- Архитектурное решение:
R-T-4 — urllib.request.urlopen бросает разные exception'ы на разные ошибки → разный exit code
- Описание: При connection refused —
URLError; при HTTP 4xx/5xx —HTTPError; при timeout —socket.timeout(илиTimeoutErrorв 3.10+). Все они приведут к ненулевому exit code, но конкретное значение зависит от Python. Если в будущем кто-то напишет логику «если exit code 1 — это connection refused, если 2 — это timeout», она не сработает. - Вероятность / Влияние: Н / Н.
- Митигация:
- Архитектурное решение: Docker трактует «exit code 0» как healthy, «всё остальное» как unhealthy. Семантика binary, различать конкретные коды не нужно (TRZ §3.1).
- Документация: ADR-020 §«Решение» явно фиксирует «status != 200 → exit 1; любой raise → ненулевой код».
- Будущее: если когда-нибудь захочется различать «приложение
отвалилось» vs «приложение тормозит», нужно переходить на
scripts/healthcheck.py(TD-3 в ADR-020) с явнымtry/exceptиsys.exit(2)/sys.exit(3). YAGNI.
R-T-5 — start_period: 20s слишком короткий или слишком длинный
- Описание: Если uvicorn будет грузиться > 20 с (например, при
холодном кэше БД или большой инициализации), первые проверки
выпадут как
unhealthyуже до окончания start_period. Противоположно — если задать слишком большой start_period (например, 120 с), новый деплой будет долго «висеть в starting», что в CI/CD затянет проверку. - Вероятность / Влияние: Н / Н.
- Митигация:
- Архитектурное решение (ADR-020 §«Решение»): uvicorn в этом
проекте поднимается за < 2 с (вне холодного docker pull). 20 с —
10× запас, комфортный для редких холодных стартов после
docker compose up. - Контракт start_period в Docker: в течение start_period
проваленный healthcheck не увеличивает
FailingStreak. Если проверка пройдёт хотя бы раз в start_period, контейнер сразу переходит вhealthy. То есть слишком длинный start_period «безопасен» (просто отложит признаниеunhealthyпри реальном отказе), а слишком короткий — приведёт к ложномуunhealthyпри первом запуске. - Acceptance гейт: AC-01 (≤ 120 с до healthy) включает start_period в окно проверки.
- Архитектурное решение (ADR-020 §«Решение»): uvicorn в этом
проекте поднимается за < 2 с (вне холодного docker pull). 20 с —
10× запас, комфортный для редких холодных стартов после
R-T-6 — localhost:5556 внутри контейнера резолвится не туда
- Описание: В некоторых конфигурациях
localhostможет резолвиться в IPv6::1, а uvicorn слушает только IPv40.0.0.0(см. Dockerfile CMD). Тогда healthcheck-запрос пойдёт на v6 и не достучится. - Вероятность / Влияние: Н / С.
- Митигация:
- Архитектурное решение: в
python:3.12-slimдефолтныйgetaddrinfoдляlocalhostвозвращает обе семьи,urllibпробует их по порядку. На IPv4-host (mva154)127.0.0.1доступен первым. - Fallback при провале: если на каком-то Docker Engine начнут
наблюдаться проблемы, переписать URL на явный
http://127.0.0.1:5556/api/health(правка ~10 символов). - Acceptance гейт: IT-01..IT-04 на dev-машине + E2E-01 на mva154 проверяют реальное поведение.
- Архитектурное решение: в
R-T-7 — Эндпоинт /api/health в будущем переедет/переименуется
- Описание: Сейчас
/api/healthживёт вsrc/api/main.py:1224. Если кто-то рефакторит API под APIRouter и сменит путь (например, на/api/v1/healthили/healthz), healthcheck сломается. - Вероятность / Влияние: Н / С.
- Митигация:
- AC-08: диф
src/api/main.pyпротив main по/api/healthпроверяется — изменения контракта блокируют merge ET-015. - Долгосрочная: ADR-020 фиксирует, что путь зашит в YAML.
При будущей миграции на APIRouter (если случится) разработчик
увидит ADR-индекс, найдёт упоминание
/api/healthи обновит healthcheck-команду одной правкой YAML. - CHANGELOG/ADR трейл: будущая правка пути сама по себе должна породить ADR (это change cross-cutting).
- AC-08: диф
R-T-8 — Python urllib SSL/TLS-проверка прокинется на loopback
- Описание: Мы делаем
http://...(не HTTPS), но если кто-то в будущем перенесёт healthcheck наhttps://localhost, потребуется валидный сертификат илиssl._create_unverified_context(). - Вероятность / Влияние: Н / Н.
- Митигация:
- Архитектурное решение: на loopback HTTPS не нужен. TLS терминирует nginx на хосте, не внутри контейнера.
- Если когда-нибудь uvicorn получит TLS прямо в контейнере (вряд ли — текущий деплой не предполагает), нужно будет либо обходить проверку, либо ставить самоподписанный CA в образ. Это уже серьёзная архитектурная смена → новый ADR.
R-T-9 — python alias исчезнет в будущих базовых образах
- Описание:
python:3.12-slimсейчас имеет/usr/local/bin/pythonи/usr/local/bin/python3(оба ведут наpython3.12). Если апстрим решит оставить толькоpython3, healthcheck-команда сломается. - Вероятность / Влияние: Н / Н.
- Митигация:
- Архитектурное решение: Python Docker official images
поддерживают alias
pythonв slim/full тегах как минимум до 3.13. Никаких признаков deprecation. - Тривиальная правка: при необходимости — заменить
pythonнаpython3в YAML (1 символ). - Тест: UT-01..UT-03 запускают
python -cна dev-машине; если alias сломан локально, заметим до деплоя.
- Архитектурное решение: Python Docker official images
поддерживают alias
R-T-10 — Поломка YAML-формата при ручной правке (длинная строка)
- Описание: YAML-строка ~135 символов читается тяжело. Будущий
редактор может случайно разорвать её newline'ом без
\, получится невалидный YAML. - Вероятность / Влияние: С / Н.
- Митигация:
- Архитектурное решение (TRZ R-5): строка лежит внутри
YAML-массива, длина не ограничена. При желании можно перейти
на block-scalar (
>-или|) — позволит разнести по строкам. Сейчас оставлено в одну строку для read-as-one-blob. - Acceptance гейт: ST-03 — YAML-парсер на CI поймает поломку.
make lint: валидирует compose YAML.
- Архитектурное решение (TRZ R-5): строка лежит внутри
YAML-массива, длина не ограничена. При желании можно перейти
на block-scalar (
R-T-11 — Гонка между docker compose up -d app и healthcheck'ом во время деплоя
- Описание: В момент пересоздания контейнера старый
enduro-trails-app-1останавливается, новый запускается. Если пайплайн деплоя сразу же опрашиваетState.Health.Status, он может прочитатьstartingили даже краткийunhealthyи принять это за провал. - Вероятность / Влияние: С / Н.
- Митигация:
- Архитектурное решение (
07-infra-requirements.md§6.2): процедура деплоя описывает waiting-loop с тайм-аутом 120 с (AC-01). Никакой immediate-fail policy. start_period: 20sбуферизирует холодный старт: первые ~20 с проваленные проверки не учитываются вFailingStreak.
- Архитектурное решение (
R-T-12 — Healthcheck помечает контейнер healthy, но БД недоступна
- Описание:
/api/healthсейчас возвращает 200 даже еслиdb_exists == false. Healthcheck скажетhealthy, хотя приложение не сможет отдать/api/trails. Ложно-положительный сигнал. - Вероятность / Влияние: Н / С.
- Митигация:
- Out of scope ET-015 (BRD §7): углубление содержимого
/api/health— отдельный work-item. ADR-020 это явно фиксирует как TD-1. - Текущее поведение:
/api/healthотдаётdb_existsв JSON, но HTTP-статус — 200. Healthcheck смотрит только на статус. - Не делает хуже: до ET-015 healthcheck был всегда ложным
(
unhealthyпри работающем приложении). После — healthcheck станет частично достоверным (фиксирует «uvicorn слушает порт и роутер жив»). Это улучшение, не «новая дыра». - Будущее: при появлении мониторинга на базе
State.Health.Status— можно ввести более глубокий/api/health(с проверкой БД/OSRM), и поведение healthcheck'а «бесплатно» углубится. ADR-020 это не блокирует.
- Out of scope ET-015 (BRD §7): углубление содержимого
R-T-13 — Ложное срабатывание AC-05 (переход в unhealthy при остановке uvicorn)
- Описание: AC-05 / IT-03 требуют, чтобы при остановке uvicorn
внутри контейнера healthcheck перешёл в
unhealthyза ≤ 120 с. Способ «kill -STOP 1» из ТЗ останавливает init-процесс, но при этом останавливается и сам healthcheck-процесс (он же child от 1). Возможны странные эффекты на Docker'е. - Вероятность / Влияние: Н / Н.
- Митигация:
- Альтернативная методика теста (test-plan IT-03): допустимо
делать
docker stopконтейнера и проверять, что Docker сам помечаетunhealthy(или простоexited). - Реальный сценарий отказа: uvicorn вернёт 500 на
/api/healthпри сбое внутреннего state, либо вообще не примет соединение (process aborted). В обоих случаях pythonurlopenподнимет исключение → ненулевой exit code → Docker фиксируетunhealthy. - Это покрывается AC-05 семантически, не buchstäblich на kill.
- Альтернативная методика теста (test-plan IT-03): допустимо
делать
R-T-14 — make lint падает на длинной строке в YAML
- Описание: Если в проекте настроен
yamllintс правиломline-length: max 120, наша 135-символьная строка не пройдёт. - Вероятность / Влияние: Н / Н.
- Митигация:
- Архитектурное решение:
yamllint-конфиг можно либо не настраивать наline-lengthдля значений multi-line массивов, либо переписать строку через block-scalar>-. - Проверка:
make lint— часть DoD. Если падает — на этапе реализации решает реализатор (например, отключает rule для конкретной строки# yamllint disable-line rule:line-length). - Не блокирует ADR.
- Архитектурное решение:
R-T-15 — Кто-то в будущем добавит restart: unless-stopped + restart_policy.condition: unhealthy
- Описание: Сейчас compose не указывает
restart_policy. Если кто-то добавит «контейнер автоматически перезапускается при unhealthy», ET-015 (правильный healthcheck) внезапно станет частью retry-логики. Любой реальный кратковременный сбой будет крутить контейнер в цикле перезапусков. - Вероятность / Влияние: Н / С.
- Митигация:
- Архитектурное решение: ET-015 такую политику не вводит
(out of scope).
restart_policyсейчас отсутствует — Docker использует дефолт «no restart on unhealthy». - Будущее: при появлении
restart_policy.condition: unhealthyнужно проверить, что start_period достаточен для всех валидных стартов, и чтоinterval × retriesне складывается в шторм перезапусков. Это будет тема отдельного ADR.
- Архитектурное решение: ET-015 такую политику не вводит
(out of scope).
R-T-16 — Образ не получает curl, и кто-то будет через docker exec пытаться отлаживать API curl-ом
- Описание: Оператор зайдёт в контейнер на mva154 для отладки и
обнаружит, что
curlнет. Привычка проверятьcurl localhost:5556не сработает. - Вероятность / Влияние: С / Н.
- Митигация:
- Архитектурное решение: в slim-образе curl никогда не было. Это не регрессия ET-015 — это уже было до фикса.
- Альтернатива для оператора: тот же
python -c "import urllib.request; print(urllib.request.urlopen('http://localhost:5556/api/health').read())"из контейнера, либоcurlс хоста против externalного URL. - Будущее (если боль): добавить debug-образ с
curl(отдельный Dockerfile + tag) — out of scope ET-015.
R-T-17 — В будущем Compose v3 будет deprecated, перейдём на Compose Spec — структура healthcheck изменится
- Описание: Docker Compose Spec (v2024+) уже унифицировал
формат healthcheck. Если проект мигрирует на новый формат, нужно
будет переписать
test:поле. - Вероятность / Влияние: Н / Н.
- Митигация:
- Архитектурное решение: Compose Spec сохраняет обратную
совместимость с массивной формой
test: ["CMD", ...]. Никакой обязательной миграции healthcheck-секции при апгрейде Compose не предвидится. - Если миграция понадобится: перенесённая секция останется идентичной по смыслу.
- Архитектурное решение: Compose Spec сохраняет обратную
совместимость с массивной формой
R-T-18 — Прочие пути дёргают /api/health для своих целей и трафик растёт
- Описание: Если в будущем кто-то добавит внешний мониторинг
(Uptime Robot, Prometheus blackbox), который тоже бьёт по
/api/health, плюс наш healthcheck — нагрузка пойдёт дважды. Сейчас 2 req/min — пренебрежимо, но при росте уровней может стать заметным. - Вероятность / Влияние: Н / Н.
- Митигация:
- Архитектурное решение:
/api/healthдешёвый (~7 мс, in-memory). Линейный рост источников выдержит много порядков. - Если действительно станет проблемой: ввести rate-limit на
/api/health(отдельный work-item) или разнести liveness (internal-only) и readiness (external) — TD-1 ADR-020.
- Архитектурное решение:
Сводная таблица
| # | Риск | Вер | Влиян | Митигация (тип) |
|---|---|---|---|---|
| R-T-1 | Поломка квотинга YAML/shell | Н | С | CMD-массив без shell; ST-03 валидирует |
| R-T-2 | CPU/RAM overhead fork+exec python | Н | Н | 30 с интервал → ~0.005% CPU; docker stats мониторинг |
| R-T-3 | /api/health отвечает > 3 с под нагрузкой |
Н | С | Endpoint лёгкий; 3 с = 430× запас; деградация = валидный сигнал |
| R-T-4 | Разные exit code при разных ошибках | Н | Н | Docker — binary семантика; различение не нужно |
| R-T-5 | start_period: 20s неподходящий |
Н | Н | uvicorn стартует < 2 с; 10× запас |
| R-T-6 | localhost резолвится в IPv6 |
Н | С | Дефолт IPv4 в python:3.12-slim; fallback 127.0.0.1 |
| R-T-7 | /api/health сменит путь |
Н | С | AC-08 блокирует merge; ADR-020 трейл для будущего |
| R-T-8 | TLS на loopback | Н | Н | HTTP loopback, HTTPS только на nginx |
| R-T-9 | python alias исчезнет |
Н | Н | Долгосрочно стабилен; правка 1 символ |
| R-T-10 | Поломка YAML при ручной правке | С | Н | ST-03 + make lint; block-scalar при необходимости |
| R-T-11 | Гонка при деплое | С | Н | Waiting-loop 120 с + start_period: 20s |
| R-T-12 | Healthy при недоступной БД | Н | С | Out of scope (BRD §7); не делает хуже; TD-1 |
| R-T-13 | AC-05 не воспроизводится через kill -STOP 1 |
Н | Н | Альтернатива через docker stop; покрывает семантику |
| R-T-14 | yamllint падает на длинной строке |
Н | Н | Конфигурация yamllint или block-scalar |
| R-T-15 | restart_policy.condition: unhealthy в будущем |
Н | С | Не вводится в ET-015; новый ADR при добавлении |
| R-T-16 | Оператор привык к curl для отладки |
С | Н | curl и раньше не было; альтернативы есть |
| R-T-17 | Compose v3 deprecation | Н | Н | Спека сохраняет совместимость массивной формы |
| R-T-18 | Внешний мониторинг + healthcheck = 2× нагрузка | Н | Н | Endpoint дешёвый; rate-limit при росте |
Связанные документы
01-brd.md§3 R1..R3 (бизнес-риски — ложные алёрты, эрозия доверия, SLO), §8 (сценарий «как должно стать»)02-trz.md§3 (целевое состояние), §4 (альтернативы), §8 (риски ТЗ — частично пересекаются с этим документом, но фокусируются на имплементации; здесь — архитектурный взгляд)03-acceptance-criteria.mdAC-01..AC-10 (все гейты)04-test-plan.yamlST-01..ST-07, UT-01..UT-03, IT-01..IT-04, E2E-01..E2E-0206-adr/ADR-020-healthcheck-via-python-urllib.md§«Решение», §«Последствия», §«Технический долг», §«Альтернативы для будущего»07-infra-requirements.md§6 (deploy procedure), §7 (observability)08-data-requirements.mddocs/architecture/README.md§«Деплой», §«Компоненты»docs/work-items/ET-014/10-tech-risks.md— образец «UI calibration risks» документа (наследие)docs/work-items/ET-013/10-tech-risks.md— образец «layer calibration risks» документа (наследие)