doc: Шаг 3 — дизайн выноса состояния + журнал (read-through тенантов)
This commit is contained in:
@@ -114,6 +114,14 @@
|
||||
- Примечание: пока один процесс с двумя листенерами (общее in-memory состояние); два процесса —
|
||||
после Шага 3.
|
||||
|
||||
### 2.14. Шаг 3 — начало: дизайн + тенанты read-through Redis
|
||||
- Дизайн: `doc/thinking/step3-state-extraction-design-2026-10-10.md` (варианты A/B/C, рекомендация
|
||||
C→A, что замерить).
|
||||
- Код (коммит `779a71d`): Redis-индекс `ssq:tenant_ak` (accessKey → JSON тенанта) + бэкфилл при старте;
|
||||
auth-middleware читает тенанта из Redis при промахе локальной памяти.
|
||||
- Нужно для ЛЮБОГО multi-instance варианта; в одном процессе — страховка.
|
||||
- Проверено: индекс заполнен (`hlen ssq:tenant_ak` = 1), регрессий нет.
|
||||
|
||||
## 3. Грабли (для повторного использования)
|
||||
|
||||
1. **`proxy_set_header Host $host` теряет порт** → SigV4 `SignatureDoesNotMatch`.
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
# Шаг 3: вынос состояния — дизайн (2026-10-10)
|
||||
|
||||
> Стратегический пункт. Определяет потолок сервиса: пока очередь живёт в памяти одного
|
||||
> процесса — HA/масштабирования нет. Файл — анализ вариантов и план; перед реализацией
|
||||
> нужны решения/замеры (см. §5).
|
||||
|
||||
## 1. Проблема (факты)
|
||||
|
||||
- **Источник истины — память**: `models.SyncQueues` (map очередей + индексы tombstone) и
|
||||
`tenant.TenantStore`. Redis (`app/persistence/redis.go`) — **write-through**: записи
|
||||
асинхронны (`asyncWrite`), чтение — **только при старте** (`LoadAllQueues`, `LoadAllTenantsRaw`).
|
||||
- Следствия:
|
||||
- две реплики разъедутся (каждая со своей map; last-write-wins затирает);
|
||||
- падение процесса ИЛИ ВМ = полный простой;
|
||||
- сервис принципиально single-instance → нет HA, нет масштабирования.
|
||||
- Замеренный ранее риск (P1.1): глобальный лок `SyncQueues` + сетевой Redis-I/O **под локом**
|
||||
давал таймауты до 3–5с. При выносе в Redis локально-атомарные операции станут сетевыми —
|
||||
это ключевой вопрос латентности.
|
||||
|
||||
## 2. Варианты
|
||||
|
||||
### A. Redis как источник истины (полный вынос)
|
||||
- Все операции читают/пишут Redis; память — только кэш (или ничего).
|
||||
- Атомарность — **Lua-скрипты** (dequeue, visibility, dedup, DLQ) поверх LIST/ZSET/HASH,
|
||||
либо Redis Streams.
|
||||
- **Плюс:** несколько инстансов data-plane, HA, потолок снят, «настоящий облачный» SQS.
|
||||
- **Минус:** переписывание ядра очередей (по сути новый движок); сетевой round-trip на КАЖДЫЙ
|
||||
SQS-вызов (латентность!); сложность гонок/транзакций/FIFO/dedup/DLQ на Lua.
|
||||
- **Объём:** крупный.
|
||||
|
||||
### B. Шардирование по тенантам
|
||||
- Тенанты распределяются по инстансам (consistent hashing); каждый держит СВОИ очереди в памяти.
|
||||
- **Плюс:** ядро очередей не переписываем; масштабирование по числу тенантов.
|
||||
- **Минус:** HA нет (шард живёт на одном инстансе — его падение = часть тенантов недоступна);
|
||||
нужен роутинг/ребаланс (LB или сам сервис); состояние всё так же в памяти.
|
||||
|
||||
### C. Активный/пассивный (failover)
|
||||
- Один активный инстанс; состояние непрерывно реплицируется в Redis; пассивный подхватывает
|
||||
при падении (переключение по health/leader-election).
|
||||
- **Плюс:** HA **без переписывания ядра** (as-is); быстрее всего по срокам.
|
||||
- **Минус:** горизонтального масштабирования нет (один активный); окно даунтайма при
|
||||
переключении; надо решить рассинхрон (last-write-wins, потеря «подтверждённых» сообщений
|
||||
в async-окне).
|
||||
|
||||
## 3. Сравнение
|
||||
|
||||
| | A (Redis = истина) | B (шарды) | C (active/passive) |
|
||||
|---|---|---|---|
|
||||
| HA (падение инстанса) | да | частично | да (с окном) |
|
||||
| Масштабирование (нагрузка) | да | да (по тенантам) | нет |
|
||||
| Переписывание ядра | **да (крупно)** | нет | нет |
|
||||
| Латентность на вызов | растёт (сеть) | как сейчас | как сейчас |
|
||||
| Сроки | большие | средние | малые |
|
||||
|
||||
## 4. Предварительная рекомендация
|
||||
|
||||
Идти **поэтапно**, не прыгая в A сразу:
|
||||
|
||||
1. **Прежде всего — C** (active/passive): даёт HA за малые сроки, без переписывания ядра.
|
||||
Убирает главную боль «одна ВМ = SPOF».
|
||||
2. **Потом A** — когда реально упрёмся в потолок нагрузки/числа тенантов (и когда есть
|
||||
средства на новый движок + замеры латентности).
|
||||
3. B — рассматривать только если появится требование «много независимых тенантов на разных
|
||||
инстансах» без общей нагрузки.
|
||||
|
||||
> Обоснование: A — самая дорогая и рискованная; делать её «на всякий случай» рано (сервис
|
||||
> ещё не под нагрузкой). C снимает SPOF дешевле.
|
||||
|
||||
## 5. Что замерить/решить ПЕРЕД реализацией
|
||||
|
||||
1. **Нагрузка**: сколько SQS-вызовов/с ожидается? Какая латентность допустима? (определяет,
|
||||
переживёт ли вариант A сетевые round-trip'ы).
|
||||
2. **Объём состояния**: число очередей × сообщений сейчас и на горизонте — влезает ли в Redis
|
||||
(память/лимиты), сколько стоит снапшот.
|
||||
3. **Требование к HA**: окно даунтайма при переключении (для C) — допустимо ли?
|
||||
4. **Порог переключения** (для C): health-check + leader-election (Redis SETNX/lock).
|
||||
|
||||
## 6. Ближайшие безопасные шаги (независимо от варианта)
|
||||
|
||||
- **Тенанты read-through Redis**: сейчас тенант, созданный control-plane, не виден data-plane
|
||||
до рестарта. Добавить Redis-индекс `accessKey → tenant` и fallback в auth-middleware —
|
||||
нужно для ЛЮБОГО multi-instance варианта и снимает текущее ограничение.
|
||||
- **Метрики/health** для переключения (готово: `/health`, `/metrics`).
|
||||
- **Разнести процессы data/control** (листенеры уже разделены, коммит `c1520c0`) — после
|
||||
read-through тенантов возможен полноценный процесс-split.
|
||||
Reference in New Issue
Block a user