# Инцидент 2026-07-08: PostgreSQL OOM + EPERM ## Итоговая цепочка ``` 0.1.13 деплой → fs.readdirSync throws ENOENT (sql/migrations/ не в образе) → process.exit(1) → k8s CrashLoopBackoff, rapid restarts → каждый рестарт: новый pool + тяжёлый DO$$ блок на PG → 20-30 concurrent connections + ALTER locks → PG 512 MB → 134% OOM → PG крашится → NodeJS теряет PG → дальнейшие рестарты уже без PG → после recovery PG: stale iptables на ноде NodeJS → EPERM до полного обновления kube-proxy → редеплой NodeJS → чистый pod → работает ``` --- ## Детальный разбор ### 1. Почему PG ушёл в OOM? **512 MB = катастрофически мало для PG 17 с этой нагрузкой.** Zalando operator выставляет `shared_buffers = 25% RAM = 128 MB`. Каждое подключение стоит ~5–10 MB. Триггеры при деплое 0.1.13: **a) Rolling update → двойное число подключений.** Пока k8s поднимал новый под, старый ещё жил. Новый под открывает pool (10 соединений) плюс старый не успел корректно закрыть свой (`process.exit(1)` без `pool.end()`). PG видел 20–30 соединений → +100-200 MB. **b) DO $$ блок в `schema.js`.** При каждом старте делал `ALTER TABLE ... DROP CONSTRAINT` + `ADD CONSTRAINT` (AccessExclusiveLock + rewrite). Несколько подов подряд → очередь блокировок → спайк памяти. **c) connect-pg-simple + `saveUninitialized: true`.** Health-check'и создавали пустые сессии (вторичный фактор). ### 2. EPERM после recovery `EPERM` на TCP-коннекте — ошибка **ядра** на уровне iptables/netfilter. После recovery PG-под получил новый Pod IP. k8s Service IP (`10.102.125.70`) не менялся, но `kube-proxy` должен был обновить iptables-правила — процесс асинхронный. NodeJS делал `connect(10.102.125.70:5432)`, iptables пытался перенаправить на старый Pod IP PG (уже не существующий) → ядро возвращало `EPERM`. DNS здесь ни при чём — `svc.cluster.local` резолвился правильно. ### 3. Как правильно делать readiness probe? **`/ready` всегда 200 — правильное решение.** Если `/ready` возвращает 503: 1. Все поды → NotReady 2. nginx → 503 3. k8s перезапускает поды 4. Новые поды создают соединения к нагруженному PG 5. **Каскадный сбой** Модель: ``` /healthz → всегда 200 (liveness) /ready → всегда 200 (readiness) + страницы показывают user-friendly ошибку ``` ### 4. 6202 записей session — нормально ли? Да. 6202 / 8 часов = ~775/час = ~13/минута. Для многокомпанийного сервиса разумно. `saveUninitialized: true` создаёт сессии от ботов/сканеров. Для PG не критично: 6202 × ~5 KB ≈ 31 MB. ### 5. `fs.readdirSync` в managed k8s `fs` доступен, но SQL-файлы должны быть в Docker-образе. Если `.dockerignore` их исключил → `ENOENT` → crash → k8s рестарты → OOM PG. --- ## Что починили 1. `process.exit(1)` убран везде — приложение не падает 2. `/ready` всегда 200 — nginx не блокирует трафик 3. Ошибки показываются на странице 4. PG: 512 MB → 2048 MB --- ## Рекомендации 1. **DO $$ DROP+ADD CONSTRAINT** — запускать один раз, проверять через `pg_constraint` 2. **PG memory** — минимум 1 GB для production 3. **`saveUninitialized: false`** для публичных роутов 4. **SQL-миграции** — включить в образ явно или встроить в js-строки 5. **`pool.end()`** — уже реализован в SIGTERM, но при `process.exit(1)` без SIGTERM этого не было