← назад к разделу

Реальное приложение почти никогда не хранит данные в одном месте: рядом с основной базой живут кеш, поисковый индекс, аналитическое хранилище, денормализованные таблицы, read-модели. Легко утонуть в этом зоопарке и не понимать, кто кому хозяин. Есть простая рамка, которая наводит порядок — деление всех хранилищ на систему записи и производные данные. Это концептуальная статья «для кругозора»: она не про конкретную технологию, а про то, как перестать путаться в собственной архитектуре.

Источник правды против его отражений

Система записи (system of record, источник правды) — хранилище, где факт живёт ровно один раз в авторитетном виде. Обычно это нормализованная база: пользователь завёл заказ — заказ записан сюда, и при любом расхождении с остальными хранилищами правым по определению считается источник правды.

Производные данные — результат преобразования данных из системы записи: их всегда можно пересчитать заново из источника. Сюда попадает почти всё остальное:

  • кеш — быстрая копия части данных;
  • поисковый индекс (Elasticsearch, GIN) — переупакованные данные под запросы по тексту;
  • материализованное представление и денормализованное поле — предвычисленный результат;
  • read-модель в CQRS — данные, разложенные под конкретный экран;
  • аналитическое хранилище (ClickHouse) — те же события под сканы.

Ключевой признак производных данных — избыточность: они дублируют то, что уже есть в источнике, ради скорости чтения. Одно и то же можно отразить многими способами, поэтому производных наборов бывает несколько на один источник.

Главное свойство: производное можно выбросить

Из определения следует практический вывод, который меняет отношение к данным: производные данные одноразовы. Потеряли кеш, индекс, read-модель — не беда: пересобрали из источника правды, и система как новенькая. Потеряли систему записи — беда настоящая, восстанавливать неоткуда.

Это диктует всё остальное: бэкапить и защищать нужно в первую очередь источник правды; любой производный набор должен иметь процедуру пересборки с нуля (переиндексация, прогрев кеша, перестроение проекции); а «схема потерялась после выката» перестаёт быть катастрофой, если это была всего лишь проекция. Понимание, что где источник, а что производное, — половина ясной архитектуры.

Три способа получать производные данные

Как именно данные текут из источника в производные наборы? Есть три режима обработки, и различать их полезно:

  • Online (сервис). Ждёт запрос и отвечает как можно быстрее; главная мера — время отклика и доступность. Так работает само приложение, но для деривации больших наборов online не годится.
  • Batch (пакетная обработка). Берёт большой набор на вход, гоняет задачу минуты-часы, выдаёт результат; главная мера — пропускная способность, запускается по расписанию (ночная переиндексация, пересчёт витрины). Классика big-data — MapReduce/Spark; в бэкенде попроще — фоновый обработчик на очереди.
  • Stream (потоковая обработка). Нечто среднее: реагирует на события по мере поступления, с низкой задержкой, но обрабатывает не весь набор, а поток изменений. Так производные данные обновляют почти в реальном времени.

Batch пересобирает всё и надёжен, но с задержкой; stream держит производные свежими, но сложнее. Выбор между ними — отдельная большая тема (глава 11 у Клеппмана).

Дисциплина: деривировать, а не дублировать записью

Главная ошибка при работе с производными данными — писать в них напрямую, в обход источника. Классический пример — двойная запись: приложение пишет и в базу, и в кеш (или в поисковый индекс) двумя отдельными операциями. Рано или поздно одна пройдёт, а вторая — нет, и данные разъедутся молча.

Правильный принцип: писать только в систему записи, а всё производное деривировать из неё — единым потоком изменений. Тогда у производных наборов один общий порядок обновлений, они согласованы и пересобираемы. Технически это реализуют событиями и outbox-паттерном или через CDC (change data capture — чтение журнала базы). Тот же принцип лежит в основе event sourcing: источник правды — поток событий, а всё остальное (проекции, read-модели) из него выводится.

Где это применяется

Как только рядом с базой появляется кеш, поисковый индекс или отдельная витрина — вы уже управляете производными данными, даже если не называли их так. Практическая рамка: явно назовите, что у вас источник правды, а что производное; для каждого производного набора заведите процедуру пересборки; и никогда не пишите в производное в обход источника — только деривация из единого потока.

Где спотыкаются начинающие:

  • Двойная запись в базу и кеш/индекс — две операции без общей транзакции расходятся. Пишем в источник, производное деривируем (события/outbox/CDC).
  • Нет процедуры пересборки производного набора — «переиндексировать с нуля» оказывается неосуществимо, и потеря индекса превращается в инцидент на ровном месте.
  • Бэкапят всё одинаково — не отделяя незаменимый источник правды от одноразовых производных, тратят силы не туда.
  • Считают read-модель или денормализованное поле «второй правдой» — при расхождении начинают гадать, кто прав; прав всегда источник.

Что почитать дальше: read-модель в CQRS — производные данные под конкретный экран; инвалидация кеша — как держать производную копию свежей; event sourcing — поток событий как источник правды; двойная запись — почему нельзя писать в два места сразу. Первоисточник — Мартин Клеппман, «Высоконагруженные приложения», часть III и глава 10.