Реальное приложение почти никогда не хранит данные в одном месте: рядом с основной базой живут кеш, поисковый индекс, аналитическое хранилище, денормализованные таблицы, 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.