# Шаг 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.