Один образ на 18 клубов
Как устроена мультитенантность в PokerBot Online, почему у каждого клуба своя база и почему бесплатный таймер сделан наоборот.
PokerBot Online обслуживает 18 клубов спортивного покера. У каждого свой бот, свой домен, свой вид приложения и свои игроки. Код при этом один, и Docker-образы тоже одни на всех.
Что такое клуб с точки зрения системы
Клуб - это строка TENANT_ID и файл с настройками. Все остальное выводится из него:
| Что | Как разделено |
|---|---|
| База данных | отдельная база Postgres на клуб в общем контейнере |
| Кэш и очереди | префикс ключей в Redis |
| Живые обновления | свои каналы в Centrifugo |
| Домен | маршрут в Traefik |
| Внешний вид | тема клуба в настройках |
Три образа - бэкенд, бот и фронтенд - собираются один раз в CI и раскатываются на все клубы одним скриптом.
Фронтенд без пересборки
Самое неочевидное место - фронтенд. Vite при сборке вшивает переменные окружения прямо в бандл, и тогда на каждый клуб нужен свой образ. Вместо этого настройки читаются при запуске: контейнер пишет env.js, страница подключает его раньше приложения. Образ один, а адрес API, тема и имя клуба у каждого свои. Как это сделать - в фишке «Один фронтенд на все окружения».
Почему отдельная база, а не колонка tenant_id
Колонка дешевле: одна база, одна миграция, один бэкап. Но цена ошибки другая. Забытое условие WHERE tenant_id = ... в одном запросе показывает игрокам одного клуба чужой рейтинг или чужие деньги. С отдельной базой такой ошибки просто не может быть: подключение уже смотрит в базу нужного клуба.
Плюсы отдельной базы для клубов:
- данные клуба можно выгрузить, восстановить или удалить целиком, не трогая остальных;
- тяжелый отчет одного клуба не тормозит другие;
- миграции катятся по клубам по очереди, и если что-то пошло не так, задет один клуб, а не все.
Минус - миграций и бэкапов столько, сколько клубов. Это решается автоматизацией: миграции Alembic выполняются на старте контейнера, бэкапы идут по списку клубов.
Когда я сделал наоборот
Бесплатный таймер - тоже для клубов, но там один экземпляр на всех, одна база и колонка club_id. Почему:
- данных мало и они не про деньги: структура блайндов и список игроков на один вечер;
- клубов может быть сотни, и контейнер на каждый - перебор;
- утечка между клубами тут неприятна, но не страшна.
Правило получилось простое. Где деньги и персональные данные - изоляция на уровне базы. Где нет - достаточно колонки и тестов на нее.