Files
sless/doc/thinking/2026-04-09.md
T
Naeel d1d1bffd7c v0.1.13: Strategy Recreate + preStop + liveness fix (ERR-SQS-06 root cause)
- ensureDeployment: Strategy Recreate (not RollingUpdate) — prevents H2 file lock
  when two pods mount same PVC simultaneously during rollout restart
- preStop: sleep 3 — graceful H2 shutdown before SIGTERM
- livenessProbe timeoutSeconds: 3 — prevents false positive on GC pause
- terminationGracePeriodSeconds: 15
- doc: thinking log, ERR-SQS-06, progress.md, architecture SVG schema
2026-04-09 07:49:11 +03:00

5.5 KiB
Raw Blame History

2026-04-09 — Thinking Log

Агент: GitHub Copilot (Claude Opus 4)


Анализ H2 file lock — корневая причина

Симптом

При каждом kubectl rollout restart ElasticMQ стартует, но SendMessage зависает навсегда. В логах: The file is locked: /data/elasticmq.mv.db [2.2.224/7], затем dead letters и AskTimeoutException на SendMessage.

Ошибочная гипотеза (v0.1.10)

Предположил что JVM не освобождает JDBC-level lock при crash → добавил FILE_LOCK=NO в JDBC URI. Это было НЕПРАВИЛЬНО. FILE_LOCK=NO отключает только JDBC soft-lock. H2 MVStore использует java.nio.FileChannel.lock() — это OS-level file lock, не зависящий от JDBC параметров.

Почему "работало" после каждой чистки

После rm /data/elasticmq.mv.db + restart — файл создаётся заново, lock отсутствует. Но при следующем rollout restart проблема возвращается.

Корневая причина (найдена 2026-04-09)

Deployment strategy: RollingUpdate + PVC: ReadWriteOnce

Цепочка событий при kubectl rollout restart:

  1. Kubernetes добавляет аннотацию restartedAt → меняется template → начинается rollout
  2. Стратегия RollingUpdate (maxSurge=25%, maxUnavailable=25%) → для replicas=1:
    • maxSurge=1 (ceil 0.25) → Kubernetes поднимает НОВЫЙ pod
    • maxUnavailable=0 (floor 0.25) → старый pod ЕЩЁ ЖИВА
  3. PVC ReadWriteOnce — допускает mount с нескольких pod на ОДНОЙ НОДЕ (это не ReadWriteOncePod)
  4. Оба pod монтируют один PVC → оба пытаются открыть /data/elasticmq.mv.db
  5. Старый ElasticMQ держит FileChannel.lock() → новый ElasticMQ получает MVStoreException: The file is locked
  6. Persistence actor (SqlQueuePersistenceActor) в новом pod падает → dead letters
  7. Старый pod убивается (readinessProbe eventual fail) → lock освобождается — но поздно
  8. SQS REST server работает (port 9324 слушает), но WRITE-операции (SendMessage) зависают — actor мёртв

Решение — 3 изменения в ensureDeployment

  1. Strategy: Recreate (вместо RollingUpdate)

    • Kubernetes СНАЧАЛА убивает старый pod, ПОТОМ поднимает новый
    • Два pod НИКОГДА не работают одновременно → lock невозможен
    • Downtime ~25-30 секунд (JVM startup) — допустимо для мультитенант SQS
  2. preStop hook: sleep 3

    • При SIGTERM JVM начинает shutdown
    • sleep 3 даёт H2 время на fsync + FileChannel.close()
    • Без preStop: Kubernetes может убить pod раньше чем H2 закончит flush
  3. livenessProbe timeoutSeconds: 1 → 3

    • JVM стартует за 20-23 секунды
    • initialDelaySeconds=5 + failureThreshold=5 × period=10 = 55 сек запас — хватает для старта
    • НО: timeoutSeconds=1 — если GC pause > 1 сек → liveness fail → unnecessary restart → CrashLoopBackOff
    • Поднимаем до 3 секунд. GC pause > 3 сек — это уже реальная проблема которую стоит рестартить

Дополнительные обнаруженные проблемы

/_next/ и /queues/ ingress — глобальные (архитектурная)

Пути /_next/ и /queues/ на хосте sqs.kube5s.ru общие. При двух тенантах с enableUI=true — конфликт ingress. Решение отложено — пока один тенант с UI. При мультитенант UI → нужен отдельный хост per tenant.

imagePullPolicy: Always на UI

softwaremill/elasticmq-ui:latest + Always → upstream может сломать при обновлении. Пока оставляем — будем пинить версию когда стабилизируем.

memory limit 512Mi vs Xmx 384m

-Xmx384m + JVM overhead ~150 МБ = ~534 МБ > limit 512 Mi. OOMKill возможен при нагрузке. Пока оставляем — в idle не стреляет. Учтём при нагрузочном тестировании.

Самоанализ ошибки

Почему неправильно решил в v0.1.10:

  • Увидел The file is locked → сразу искал H2-настройки → нашёл FILE_LOCK=NO
  • НЕ проверил deployment strategy (RollingUpdate — default в Kubernetes)
  • НЕ проверил ReadWriteOnce behavior (допускает multi-pod на одной ноде)
  • НЕ проверил что происходит при rollout (два pod одновременно)
  • Лечил симптом (lock message) вместо причины (concurrent access)

Вывод: при любой ошибке связанной с persistence/lock/state — ПЕРВЫМ делом проверять: кто ещё имеет доступ к файлу? Сколько pod одновременно работают? Какая стратегия деплоя?