Files
SQS-service/doc/thinking/step3-state-extraction-design-2026-10-10.md
T

6.8 KiB
Raw Blame History

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