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
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
# 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 одновременно работают? Какая стратегия деплоя?
|
||||
Reference in New Issue
Block a user