fix(tradein/ops): health-check валит деплой + mem-лимиты + log-rotation (#2214) (#2231)
All checks were successful
Deploy Trade-In / build-browser (push) Successful in 32s
Deploy Trade-In / changes (push) Successful in 9s
Deploy Trade-In / build-frontend (push) Successful in 34s
Deploy Trade-In / test (push) Successful in 1m40s
Deploy Trade-In / build-backend (push) Successful in 26s
Deploy Trade-In / deploy (push) Successful in 45s
All checks were successful
Deploy Trade-In / build-browser (push) Successful in 32s
Deploy Trade-In / changes (push) Successful in 9s
Deploy Trade-In / build-frontend (push) Successful in 34s
Deploy Trade-In / test (push) Successful in 1m40s
Deploy Trade-In / build-backend (push) Successful in 26s
Deploy Trade-In / deploy (push) Successful in 45s
This commit is contained in:
parent
9f28092356
commit
8ea52b9b7a
2 changed files with 75 additions and 6 deletions
|
|
@ -479,12 +479,28 @@ jobs:
|
||||||
docker compose -p gendesign -f docker-compose.prod.yml exec -T caddy \
|
docker compose -p gendesign -f docker-compose.prod.yml exec -T caddy \
|
||||||
caddy reload --config /etc/caddy/Caddyfile || true
|
caddy reload --config /etc/caddy/Caddyfile || true
|
||||||
|
|
||||||
# Health check
|
# Health check — деплой ВАЛИТСЯ, если backend не поднялся (#2214).
|
||||||
|
# Раньше цикл после 30 неуспешных попыток молча продолжал скрипт и
|
||||||
|
# доходил до записи success-маркера → мёртвый backend помечался
|
||||||
|
# «задеплоено». Теперь: флаг healthy выставляется ТОЛЬКО при HTTP 200
|
||||||
|
# на /health; после цикла — hard exit 1, если флаг пуст. exit 1
|
||||||
|
# происходит ДО записи .tradein-deployed-sha (маркер пишется последним,
|
||||||
|
# ниже) → следующий прогон changes-job возьмёт корректную базу.
|
||||||
|
# NB set -e: curl стоит в условии `if` (exempt из errexit) — неуспешная
|
||||||
|
# попытка НЕ фатальна, а лишь провоцирует следующую итерацию цикла.
|
||||||
|
healthy=""
|
||||||
for i in $(seq 1 30); do
|
for i in $(seq 1 30); do
|
||||||
docker compose -p gendesign-tradein -f /opt/gendesign/tradein-mvp/docker-compose.prod.yml \
|
if docker compose -p gendesign-tradein -f /opt/gendesign/tradein-mvp/docker-compose.prod.yml \
|
||||||
exec -T backend curl -fsS http://localhost:8000/health && break
|
exec -T backend curl -fsS http://localhost:8000/health >/dev/null 2>&1; then
|
||||||
|
healthy="yes"; break
|
||||||
|
fi
|
||||||
sleep 1
|
sleep 1
|
||||||
done
|
done
|
||||||
|
if [ -z "$healthy" ]; then
|
||||||
|
echo "ERROR: backend не ответил на /health за 30s — деплой FAILED"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "→ backend healthy на /health."
|
||||||
|
|
||||||
# Cleanup старых образов
|
# Cleanup старых образов
|
||||||
for repo in ghcr.io/lekss361/gendesign-tradein-backend \
|
for repo in ghcr.io/lekss361/gendesign-tradein-backend \
|
||||||
|
|
|
||||||
|
|
@ -11,14 +11,40 @@
|
||||||
# - Backend/frontend подключены к network gendesign_shared чтобы основной
|
# - Backend/frontend подключены к network gendesign_shared чтобы основной
|
||||||
# Caddy мог проксировать /trade-in/* → tradein-frontend
|
# Caddy мог проксировать /trade-in/* → tradein-frontend
|
||||||
# - Frontend строится с basePath=/trade-in (см. next.config.ts)
|
# - Frontend строится с basePath=/trade-in (см. next.config.ts)
|
||||||
|
#
|
||||||
|
# ── Observability hardening (#2214) ───────────────────────────────────────────
|
||||||
|
# mem_limit/memswap_limit: до этого лимитов НЕ было — при runaway любой tradein-
|
||||||
|
# контейнер мог съесть весь хост (замер 2026-07-03: коробка 11.96G, ~1.7G в свопе).
|
||||||
|
# memswap_limit == mem_limit ⇒ контейнеру запрещён свап → tradein перестаёт
|
||||||
|
# докидывать в общий свап хоста (у browser оставлен небольшой swap-cushion).
|
||||||
|
# Лимиты — ПОТОЛКИ безопасности (×2-3 от фактического рабочего набора), а не
|
||||||
|
# ожидаемое потребление. Обоснование каждого — рядом с сервисом (замер docker stats).
|
||||||
|
# ⚠️ ПЕРВЫЙ деплой после мержа этого файла (infra-путь → compose up -d)
|
||||||
|
# ПЕРЕСОЗДАСТ ВСЕ tradein-контейнеры, включая scraper — прервёт бегущий sweep
|
||||||
|
# (stop_grace_period 120s даёт unit'у до-checkpoint'иться). Допустимо, разово.
|
||||||
|
#
|
||||||
|
# logging: json-file с ротацией (20m × 3 = ≤60M на сервис). До этого драйвер по
|
||||||
|
# умолчанию рос без границ. Общий anchor ниже.
|
||||||
|
x-logging: &default-logging
|
||||||
|
driver: json-file
|
||||||
|
options:
|
||||||
|
max-size: "20m"
|
||||||
|
max-file: "3"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
browser:
|
browser:
|
||||||
image: ghcr.io/lekss361/gendesign-tradein-browser:${IMAGE_TAG:-latest}
|
image: ghcr.io/lekss361/gendesign-tradein-browser:${IMAGE_TAG:-latest}
|
||||||
container_name: tradein-browser
|
container_name: tradein-browser
|
||||||
# 5g: N параллельных headless-Firefox страниц (BROWSER_CONCURRENCY, default 4)
|
# mem: каждая headless-Firefox страница ~0.8-1.5G. Прод-замер 2026-07-03:
|
||||||
# — каждая ~0.8-1.5GB. VPS имеет ~6.9G свободно. Было 2.5g под 1-за-раз (#884/#905).
|
# BROWSER_CONCURRENCY=1 (ops выставил в .env.runtime), idle 29MiB. 2.5g —
|
||||||
mem_limit: 5g
|
# исторически валидированный потолок для «1-за-раз» (#884/#905); было 5g под
|
||||||
|
# рассчёт на concurrency=4, но прод работает 1 → 5g избыточно. memswap +0.5g:
|
||||||
|
# единственный сервис со swap-cushion, чтобы не OOM-killнуть карточку mid-flight.
|
||||||
|
# ⚠️ Поднимаешь BROWSER_CONCURRENCY в .env.runtime — подними и mem_limit
|
||||||
|
# (грубо ~2.5g на каждую доп. параллельную страницу).
|
||||||
|
mem_limit: 2560m
|
||||||
|
memswap_limit: 3g
|
||||||
|
logging: *default-logging
|
||||||
env_file:
|
env_file:
|
||||||
- path: ./backend/.env.runtime
|
- path: ./backend/.env.runtime
|
||||||
required: false
|
required: false
|
||||||
|
|
@ -36,6 +62,14 @@ services:
|
||||||
postgres:
|
postgres:
|
||||||
image: postgis/postgis:16-3.4
|
image: postgis/postgis:16-3.4
|
||||||
container_name: tradein-postgres
|
container_name: tradein-postgres
|
||||||
|
# mem: прод-замер 2026-07-03 = 656MiB. Самый большой относительный запас (×3):
|
||||||
|
# OOM-kill БД — худший исход (порванные коннекты, recovery). memswap==mem ⇒
|
||||||
|
# без свапа (shared_buffers не должны уходить в swap-thrash).
|
||||||
|
# ревью #2214: 3g (idle 656MiB — БД дороже всего при ложном OOM; work_mem-спайки
|
||||||
|
# тяжёлых PostGIS-сортировок бэктеста/эстиматора + autovacuum)
|
||||||
|
mem_limit: 3g
|
||||||
|
memswap_limit: 3g
|
||||||
|
logging: *default-logging
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_DB: tradein
|
POSTGRES_DB: tradein
|
||||||
POSTGRES_USER: ${TRADEIN_POSTGRES_USER:-tradein}
|
POSTGRES_USER: ${TRADEIN_POSTGRES_USER:-tradein}
|
||||||
|
|
@ -56,6 +90,14 @@ services:
|
||||||
backend:
|
backend:
|
||||||
image: ghcr.io/lekss361/gendesign-tradein-backend:${IMAGE_TAG:-latest}
|
image: ghcr.io/lekss361/gendesign-tradein-backend:${IMAGE_TAG:-latest}
|
||||||
container_name: tradein-backend
|
container_name: tradein-backend
|
||||||
|
# mem: прод-замер 2026-07-03 = 100MiB. 640m (×6) — запас под спайки экспорта
|
||||||
|
# (WeasyPrint PDF / ezdxf / openpyxl держат документ в памяти). memswap==mem ⇒
|
||||||
|
# без свапа (single-worker uvicorn, предсказуемый event-loop).
|
||||||
|
# ревью #2214: 768m (WeasyPrint PDF разово берёт 200-500MB поверх ~100MiB idle;
|
||||||
|
# наложение export + бэкфилл при 640m было бы впритык)
|
||||||
|
mem_limit: 768m
|
||||||
|
memswap_limit: 768m
|
||||||
|
logging: *default-logging
|
||||||
# Prod: uvicorn БЕЗ --reload (Dockerfile CMD несёт --reload только для dev hot-reload,
|
# Prod: uvicorn БЕЗ --reload (Dockerfile CMD несёт --reload только для dev hot-reload,
|
||||||
# где app/ bind-mount'ится). В prod --reload = лишний WatchFiles-наблюдатель + риск
|
# где app/ bind-mount'ится). В prod --reload = лишний WatchFiles-наблюдатель + риск
|
||||||
# reload в середине запроса. Single worker сохраняется для предсказуемости
|
# reload в середине запроса. Single worker сохраняется для предсказуемости
|
||||||
|
|
@ -118,6 +160,12 @@ services:
|
||||||
scraper:
|
scraper:
|
||||||
image: ghcr.io/lekss361/gendesign-tradein-backend:${IMAGE_TAG:-latest}
|
image: ghcr.io/lekss361/gendesign-tradein-backend:${IMAGE_TAG:-latest}
|
||||||
container_name: tradein-scraper
|
container_name: tradein-scraper
|
||||||
|
# mem: прод-замер 2026-07-03 = 11MiB idle. 640m (×~1.5 от backend-профиля) —
|
||||||
|
# тяжёлый парсинг делает browser-контейнер, здесь только оркестрация + JSON-
|
||||||
|
# payload'ы sweep'ов. memswap==mem ⇒ без свапа.
|
||||||
|
mem_limit: 640m
|
||||||
|
memswap_limit: 640m
|
||||||
|
logging: *default-logging
|
||||||
command: ["python", "-m", "app.scheduler_main"]
|
command: ["python", "-m", "app.scheduler_main"]
|
||||||
env_file:
|
env_file:
|
||||||
- path: ./backend/.env.runtime
|
- path: ./backend/.env.runtime
|
||||||
|
|
@ -146,6 +194,11 @@ services:
|
||||||
frontend:
|
frontend:
|
||||||
image: ghcr.io/lekss361/gendesign-tradein-frontend:${IMAGE_TAG:-latest}
|
image: ghcr.io/lekss361/gendesign-tradein-frontend:${IMAGE_TAG:-latest}
|
||||||
container_name: tradein-frontend
|
container_name: tradein-frontend
|
||||||
|
# mem: прод-замер 2026-07-03 = 32MiB. 384m (×12) — Next.js standalone SSR,
|
||||||
|
# лёгкий; потолок с большим запасом. memswap==mem ⇒ без свапа.
|
||||||
|
mem_limit: 384m
|
||||||
|
memswap_limit: 384m
|
||||||
|
logging: *default-logging
|
||||||
environment:
|
environment:
|
||||||
NODE_ENV: production
|
NODE_ENV: production
|
||||||
# Next.js standalone server.js читает HOSTNAME для bind-адреса.
|
# Next.js standalone server.js читает HOSTNAME для bind-адреса.
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue