6.8 KiB
6.8 KiB
Шаг 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 сразу:
- Прежде всего — C (active/passive): даёт HA за малые сроки, без переписывания ядра. Убирает главную боль «одна ВМ = SPOF».
- Потом A — когда реально упрёмся в потолок нагрузки/числа тенантов (и когда есть средства на новый движок + замеры латентности).
- B — рассматривать только если появится требование «много независимых тенантов на разных инстансах» без общей нагрузки.
Обоснование: A — самая дорогая и рискованная; делать её «на всякий случай» рано (сервис ещё не под нагрузкой). C снимает SPOF дешевле.
5. Что замерить/решить ПЕРЕД реализацией
- Нагрузка: сколько SQS-вызовов/с ожидается? Какая латентность допустима? (определяет, переживёт ли вариант A сетевые round-trip'ы).
- Объём состояния: число очередей × сообщений сейчас и на горизонте — влезает ли в Redis (память/лимиты), сколько стоит снапшот.
- Требование к HA: окно даунтайма при переключении (для C) — допустимо ли?
- Порог переключения (для 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.