From 2390c655f999788a710989bc7804181afaebd9ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Thu, 9 Jul 2026 00:21:31 +0400 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=BA:=20=D1=80=D0=B0=D0=B7=D0=B1?= =?UTF-8?q?=D0=BE=D1=80=20=D0=B8=D0=BD=D1=86=D0=B8=D0=B4=D0=B5=D0=BD=D1=82?= =?UTF-8?q?=D0=B0=202026-07-08=20(PG=20OOM=20+=20EPERM)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/incident-2026-07-08-pg-oom.md | 88 ++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 docs/incident-2026-07-08-pg-oom.md diff --git a/docs/incident-2026-07-08-pg-oom.md b/docs/incident-2026-07-08-pg-oom.md new file mode 100644 index 0000000..b896408 --- /dev/null +++ b/docs/incident-2026-07-08-pg-oom.md @@ -0,0 +1,88 @@ +# Инцидент 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 этого не было