ADR-0001 · Хранилище источника истины#
Статус: принят · 2026-09-16
Метод: D-01 v1.0 · рыночный снимок 2026-09-16 · шаблон ADR v1.1
Участники: команда разработки (4 человека)
Учебная запись: часть прогона E-01, система вымышлена. Форма и рассуждение настоящие.
Контекст#
Вход — 00-context.md, шаги W0 и W1. Ответы на семь вопросов D-01:
Основная нагрузка — пять запросов:
| Запрос | Ключ | Возвращает | Пик | Цель p99 | |
|---|---|---|---|---|---|
| Q1 | Схема зала с занятостью мест | event_id |
до 10 тыс. строк | 8 000/с | 200 мс |
| Q2 | Резерв конкретного места | (event_id, seat_id) |
1 строка, атомарно | 300/с | 300 мс |
| Q3 | Подтверждение резерва оплатой | reservation_id |
1 строка + вставка билета | 30/с | 500 мс |
| Q4 | Освобождение истёкших резервов | expires_at < now() |
пачками, фоново | — | — |
| Q5 | Билеты покупателя | user_id |
десятки строк | редко | 500 мс |
Соотношение и профиль. 5:1 в среднем, 27:1 в пике [расчёт W1]. Профиль пиковый с коэффициентом ≈ 1000×, но пики известны заранее по расписанию продаж — это редкий случай, когда всплеск можно готовить, а не переживать.
Объём. 26 ГБ в год, ≈ 130 ГБ за пять лет хранения [расчёт W1].
Что обязано быть атомарным. И1: резерв места — одна строка, одна транзакция. И2 и И4: подтверждение резерва — билет вставляется и резерв закрывается вместе. И3 касается внешнего провайдера и в границу нашей БД не попадает.
Допустимая свежесть. Q1 — до 5 секунд устаревания: интерфейс всё равно обязан обрабатывать «место только что заняли» как штатный исход, значит устаревшая схема не создаёт нового класса ошибок. Q2, Q3 — строго актуально.
Эксплуатация. Четыре разработчика, платформенной команды нет, дежурство по очереди. Управляемая версия обязательна.
Ограничения. Персональные данные — в юрисдикции компании. Один облачный провайдер, два AZ, второго региона нет.
Оговорка о пометках#
У системы, которой ещё нет, не существует пометки [замер]: измерять нечего. Вместо неё в этой записи используется [расчёт W1] — оценка на салфетке с указанием шага. Это слабее замера, и ячейки с ней подлежат пересмотру после первого крупного старта продаж. Словарь пометок объявляется здесь, как требует мастер-шаблон.
Дисквалификаторы#
Проверены до сравнения. Все сработавшие — организационные, ни одного технического.
| Вариант | Отпал по условию |
|---|---|
| Apache Cassandra | Команда не умеет эксплуатировать и не планирует учиться в горизонте проекта; выделенного дежурства нет |
| Управляемые сервисы других облаков | Один провайдер по решению компании; второй не добавляем |
| Любое самостоятельно разворачиваемое хранилище | Нет управляемой версии там, где размещаемся, а эксплуатировать некому |
Технические дисквалификаторы D-01 не сработали: записи мелкие, лимиты платформ не задеты, транзакционную границу на две-три строки дают все оставшиеся кандидаты. Это стоит отметить отдельно — ожидалось обратное.
Рассмотренные варианты#
Управляемый PostgreSQL · управляемый MySQL 8.0 · управляемое документное хранилище. Нулевой вариант «оставить как есть» неприменим — система новая; его роль играет реляционная БД, от которой W3 требует обосновывать отклонение.
| Ось | Управляемый PostgreSQL | Управляемый MySQL 8.0 | Управляемое документное |
|---|---|---|---|
| Форма данных | Событие, место, резерв, билет — плоские связанные сущности [расчёт W1] |
То же | Место и резерв естественно лежат в одном документе события, но документ события растёт до 10 тыс. мест |
| Паттерны доступа | Все пять выражаются; Q4 закрывается SELECT … FOR UPDATE SKIP LOCKED [док] |
То же, SKIP LOCKED есть с 8.0 [док] |
Q1 и Q2 выражаются; Q4 требует своего механизма очереди |
| Профиль нагрузки | 300 записей/с на одном узле — заведомо в пределах; 8 000 чтений/с на источник истины не планируются, см. последствия | То же | То же |
| Объём и рост | 130 ГБ за 5 лет — один узел с многократным запасом [расчёт W1] |
То же | То же |
| Транзакционные границы | Билет и закрытие резерва — одна транзакция [док] |
То же [док] |
Достижимо многодокументной транзакцией, но это не тот режим, ради которого выбирают документное |
| Требуемая согласованность | Строгая на первичном узле; реплики отстают — для Q1 допустимо | То же | По умолчанию настраиваемая; для Q2 требует явного усиления |
| Гибкость запросов | Произвольный SQL: отчёты организатору без миграций [док] |
То же | Новый паттерн — новый индекс |
| Операционная стоимость | Управляемая версия у нашего провайдера; команда умеет [орг] |
Управляемая версия есть; команда умеет хуже [орг] |
Управляемая версия есть; опыта у команды нет [орг] |
| Стоимость владения на единицу нагрузки | [нет данных] — тариф провайдера не запрашивали |
[нет данных] |
[нет данных] |
Ячейки [нет данных] не блокируют решение. Стоимость владения различает варианты одного класса, а класс уже определён предыдущими восемью строками; к тому же при 130 ГБ разница в тарифе на порядки меньше стоимости часа разработчика. Записано, каким замером закрывается: запрос тарифа и расчёт на реальный объём после первого месяца.
Решение#
Управляемый PostgreSQL как единственный источник истины. Все пять запросов, все четыре инварианта и хранение на пять лет — в нём.
Обоснование#
Перевесили две оси: операционная стоимость и транзакционные границы.
Между PostgreSQL и MySQL 8.0 по существу ничья: SKIP LOCKED для Q4 есть в обоих [док], транзакция на две строки есть в обоих, объём и нагрузка обоим безразличны. Применена эвристика D-01 «при ничьей — то, что уже есть в инфраструктуре и в головах»: опыт команды с PostgreSQL выше, а второй тип хранилища стоит дороже, чем кажется при выборе.
Документное отпало не по дисквалификатору, а по сравнению: выигрыша ни по одной оси нет, проигрыш по операционной стоимости и гибкости запросов есть.
Отдельно: выбор не решает проблему чтений. 8 000 запросов/с на схему зала в источник истории не пойдут — это задача уровня W5 и отдельного решения по кэшированию. Здесь зафиксировано только то, что источник истины один и он реляционный. Попытка закрыть пиковые чтения выбором хранилища была бы ошибкой порядка: узкое место в конкуренции за строки, а не в способности БД отдавать чтения.
Последствия#
- Путь чтения придётся строить отдельно — кэш или реплика. Это порождает решение по D-03 на этапе 5 прогона и вопрос устаревания на W6.
- Один узел записи. При 300 записях/с это с запасом, но отказ основного узла в окно старта продаж стоит выручки события: нужен автоматический переход на резервный узел во второй зоне и проверка времени переключения на учениях, а не в бою.
- Конкуренция за популярные ряды упирается в блокировки строк, а не в мощность узла. Двукратный запас бюджета задержки на Q2 существует именно под эту очередь; если её не хватит — оптимизация идёт по пути «резерв через уникальный индекс вместо блокировки», а не по пути смены БД.
- Фоновое освобождение резервов обязано быть идемпотентным (И4) и брать пачки через
SKIP LOCKED, иначе два экземпляра задачи начнут спорить. - Отчётность организаторам со временем начнёт мешать боевой нагрузке — это ожидаемо и закрывается репликой для чтения, а не отдельной аналитической системой на старте.
Условия пересмотра#
- Объём вырос на порядок относительно оценки W1 — то есть до сотен гигабайт в год вместо десятков.
- Профиль сменился: записи перестали помещаться в один узел.
- Появилось требование, невыразимое в реляционной модели.
- Изменилась лицензия или условия поставщика — для этого сегмента пункт не теоретический.
- Изменился состав команды или появилась платформенная команда: часть организационных дисквалификаторов перестанет действовать.
Связанные ADR#
Зависимые: ADR-0002 (границы сервисов) · ADR-0003 (связь в процессе «резерв → оплата») · короткая запись по кэшу схемы зала.
Отвергнутые варианты и условия их пересмотра вынесены в constraints.md.