Перейти к содержанию

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.