Skip to main content
Статус: принято.

Контекст

Omniflow устанавливает один оператор на один сервер, чтобы вести один VPN-сервис. Работа, которую он выполняет, охватывает поверхности, которые принято разделять: HTTP API, бот Telegram и фоновые задания. Разнести их по отдельно разворачиваемым сервисам — рефлекс, и он был отвергнут. Решающее ограничение в том, что покупка — это одна транзакция. Принять деньги, записать заказ, зафиксировать использование пробного периода, погасить промокод и зафиксировать задание, которое выдаст доступ в Remnawave, — это либо одно атомарное изменение, либо класс ошибок, о котором оператор узнаёт из жалоб. Разнесённая по сервисам, эта атомарность собирается заново из саг, компенсаций и брокера — механики, чьи режимы отказа строго хуже тех, что она заменяет, и за которую каждая установка платит эксплуатационную цену независимо от того, есть ли у неё оправдывающий это масштаб. Второе ограничение — кто это эксплуатирует. У целевой установки нет платформенной команды. Три контейнера, одна база и один .env — это система, в которой один человек разберётся в три часа ночи.

Решение

Один модуль Go. Три точки входа — cmd/api, cmd/bot, cmd/worker, — которые используют одни и те же доменные пакеты, одну базу данных и одну конфигурацию и разворачиваются как три контейнера. Модульность держат четыре правила, а не благие намерения, и пул-реквест, нарушающий любое из них, неверен независимо от того, чего он достигает:
  • Доменные пакеты не импортируют транспорт, генерацию доступа к базе, Telegram и пакеты конкретных провайдеров. Стрелки зависимостей направлены в одну сторону.
  • Внешние системы находятся за небольшими интерфейсами, поэтому типы формы провайдера существуют ровно в одном пакете.
  • Доменные события идут через транзакционный outbox. Брокер сообщений не вводится без записи, заменяющей эту.
  • Сборка зависимостей явная: обычные конструкторы, вызываемые из каждого main, а общая для всех трёх часть — коммерческое ядро, которое принимает деньги и ставит выдачу доступа в очередь, — собирается один раз в internal/app, и его же использует интеграционная обвязка тестов. Никакого фреймворка внедрения зависимостей.
Процессы разделены потому, что у них разные характеристики отказа и масштабирования, а не потому, что разный код. Ожидание после флуд-лимита Telegram не должно останавливать оформление покупки; долгая сверка не должна удерживать HTTP-воркер.

Последствия

Перезапуск одного процесса ради изменения кода означает выкатку одной и той же версии образа на все три, потому что это один артефакт. Выкатка исправления в API переустанавливает и бота. Горизонтальное масштабирование ограничено тем, что обслуживает один экземпляр PostgreSQL. Поддерживается сколько угодно реплик api за прокси и сколько угодно реплик worker: проходы по всей таблице — это периодические задачи River, которые выполняются один раз за интервал на всю установку, а не один раз на процесс. bot работает ровно в одном экземпляре, потому что Telegram доставляет обновления одному потребителю. Пул подключений каждой реплики расходует max_connections PostgreSQL. Истории шардирования нет и не планируется — см. Развёртывание. Участник не может закрепить границу модуля сетевым вызовом, поэтому её держат ревью и дисциплина импортов. Это слабее границы процесса и является ценой атомарности выше. Взамен изменение, охватывающее покупку, право на услугу и выдачу доступа, — это одна транзакция и один тест, а не распределённый протокол, а обновление для оператора — это docker compose pull, а не упорядоченная выкатка.

Когда пересматривать

  • Один экземпляр PostgreSQL доказуемо является узким местом реальной установки, а не предполагаемой.
  • Omniflow должен работать как управляющий слой над установками, которыми он не владеет; тогда у outbox появляется потребитель вне этого репозитория — см. пункты об управлении парком и NATS JetStream среди кандидатов после 1.0 в ROADMAP.md.
  • Появляется возможность, которую действительно нельзя зафиксировать в одной транзакции с изменением состояния, которое её требует. Пока такой не было: ближе всех подошёл шлюз цифровых товаров, и он разрешён тем, что неоднозначная выдача откладывается для человека.

Смежное