awesome-everything EN
↑ Обратно к восхождению

Data engineering

Собираем вместе: система ломается на стыках, а не в хранилищах

Суть Проследи один продукт через OLTP, склад, dbt, MV, лог событий, поиск и vector index. Каждое хранилище корректно; система расходится там, где встречаются два корректных хранилища — поэтому консистентность, свежесть и lineage это сквозные свойства.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на junior-высоте — поверхность
◷ 17 min

Поддержка эскалирует тикет: клиент купил ноутбук, который поиск в каталоге всё ещё показывает «в наличии», AI-ассистент всё ещё его рекомендует, а дашборд аналитики считает его в выручке этой недели дважды. Четыре команды открывают четыре расследования. Postgres корректен — заказ есть, продукт soft-deleted. Склад корректен — для снимка, который он последний раз обновил. Поисковый индекс корректен — для последнего CDC-события, которое до него дошло. Vector index корректен — для эмбеддингов, которые он последний раз пересобрал. Каждое хранилище проходит свои unit-тесты. Баг живёт в зазоре между ними, и им не владеет никто.

Один факт, семь копий

К моменту, когда продукт существует в зрелой системе, одна и та же строка живёт в семи местах. Postgres — система записи (OLTP): транзакция, которая взяла деньги, и есть истина. CDC стримит эту истину в колоночный склад на Parquet/Iceberg. dbt прогоняет её через bronze (сырой landing), silver (очищенный, согласованный) и gold (бизнес-агрегаты). Materialized views и роллапы обслуживают дашборды без пере-сканирования миллиардов строк. Event-sourced поток аудита фиксирует каждое изменение состояния для комплаенса. Полнотекстовый индекс (Elasticsearch/OpenSearch) питает поиск по каталогу. Vector index питает семантический поиск и RAG. Семь хранилищ, один факт: «доступен ли этот продукт и по какой цене?»

Каждый юнит, который ты изучал раньше в этом треке, оптимизировал своё хранилище под одну задачу — и именно эта оптимизация заставляет копии расходиться. Склад построен под throughput, поэтому батчит; батчинг означает lag. Поисковый индекс построен под latency запросов, поэтому денормализует; денормализация означает, что он не видит изменение foreign-key, пока ему кто-то о нём не скажет. Vector index дорого пересобирать, поэтому эмбеддинги пересчитывают по расписанию, а не на каждую запись. Ничего из этого не баг. Это корректный дизайн каждого хранилища. Системный баг в том, что никто не спроектировал контракт между ними.

Стык — это замаскированная проблема dual-write

Посмотри на самый частый источник drift: твой сервис пишет заказ в Postgres, потом публикует «OrderPlaced» в Kafka, чтобы поиск и аналитика среагировали. Две записи, две системы, общей транзакции нет. Если процесс умрёт после коммита в Postgres, но до публикации, в Postgres заказ есть, а остальная система о нём так и не услышит. Это проблема dual-write, и это каноничный отказ стыка — в разборе Confluent прямо сказано, что «нет способа иметь одну атомарную транзакцию» поверх базы и брокера сообщений.

Фикс сеньора — transactional outbox: в той же транзакции БД, что пишет заказ, вставляешь строку в таблицу outbox. Коммит атомарен — заказ и событие живут или умирают вместе. Отдельный relay (или CDC-коннектор вроде Debezium, читающий WAL) отправляет строки outbox в Kafka с at-least-once доставкой. Ты сменил dual-write на одну запись плюс идемпотентного консьюмера, и идемпотентность не обсуждается: at-least-once означает, что то же событие придёт дважды, поэтому каждое хранилище ниже по потоку обязано дедуплицировать по event id, иначе оно дважды посчитает выручку ровно как в Hook.

Стык (хранилище → хранилище)Как расходитсяКонтракт, который чинит
OLTP → Kafka → поисковый индексCDC-событие потеряно; удалённый продукт всё ещё ищетсяTransactional outbox + идемпотентный консьюмер + периодическая полная сверка
OLTP → склад → MV → дашбордRefresh lag: MV отдаёт число, которое источник уже поменялОбъявленный freshness SLA + freshness-проверки, валящие build
OLTP → vector index (RAG)Эмбеддинги пересобираются ночью; возвращает удалённый час назад продуктФильтровать выдачу по живым id из OLTP; tombstone при удалении
поток событий → gold-агрегатСобытия не по порядку / переигранные считаются дваждыEvent id + версия; дедуп и сортировка по sequence, а не по приходу

Свежесть и lineage — свойства пути, а не хранилища

Спроси «дашборд корректен?» — и единственный честный ответ: «по состоянию на когда?» Gold-MV может быть идеальной для снимка, который она обновила в 02:00, но если возврат прошёл в 09:00, число корректно и неверно — корректно для своего снимка, неверно для вопроса, который реально задаёт руководитель. dbt формализует это через source freshness: ты объявляешь окно warn_after и error_after на каждый источник, и freshness-джоб валит пайплайн, когда данные старше твоего SLA. Собственное правило dbt — запускать freshness-проверку как минимум вдвое чаще самого жёсткого SLA, чтобы обнаружить заглохший фид до того, как дашборд проврёт целый час.

Lineage — другое сквозное свойство. Когда число неверно, ты дебажишь не дашборд — ты идёшь по пути назад: gold → silver → bronze → CDC offset → OLTP. Поэтому важны и разделение medallion (bronze/silver/gold), и неизменяемый формат таблиц. Снимки Iceberg делают каждое изменение таблицы полной версионированной точкой во времени, поэтому можно выполнить time-travel запрос на ровно том снимке, который прочитал дашборд, и доказать, склад ли был устаревшим или трансформ был неверным. Задержка в bronze каскадит в silver и gold, поэтому один заглохший батч проявляется как «вчерашняя выручка» тремя слоями ниже — и только lineage даёт найти, какой слой заглох.

Почему это работает

Reconciliation-джоб — неэффектная часть, которую никто не закладывает в бюджет и которая нужна всем. CDC рано или поздно потеряет или продублирует событие — рестарт коннектора, заполнившийся и сброшенный WAL-слот, пропущенное poison-сообщение. Фикс не в том, чтобы «сделать CDC идеальным»; это периодическая полная пере-синхронизация, которая считает чексумму (количество строк + хеш ключевых колонок) на партицию в OLTP и в индексе, а потом чинит разницу. Считай живой CDC быстрым путём, а reconciliation — страховочной сеткой; и то, и другое, всегда.

Ты проектируешь контракт, а не только хранилища

Сдвиг от мидла к сеньору здесь — в том, чья проблема этот стык. Мидл владеет хранилищем и делает его корректным. Сеньор владеет контрактом данных: схемой, event id и версией, гарантией доставки, freshness SLA и reconciliation, которая замыкает петлю. Контракт — это то, что позволяет семи независимо-корректным хранилищам сложиться в одну систему, которая согласуется сама с собой.

Конкретно контракт отвечает на четыре вопроса для каждого стыка: какова каноничная схема и кто может её менять (ломающее переименование колонки в OLTP молча занулит колонку ниже по потоку)? Какова гарантия доставки и, значит, должны ли консьюмеры быть идемпотентными (at-least-once → да)? Каков freshness SLA и что валится при его нарушении? И что сверяет drift, как часто и кого зовут по пейджеру? Пропусти любой — и получишь Hook: четыре корректных хранилища, одна некорректная система, владельца нет.

Выбери лучший вариант

Сервис должен обновить Postgres, а затем сделать изменение видимым в поисковом индексе и на складе. Выбери дизайн интеграции.

Викторина

Продукт soft-deleted в Postgres в 14:00. В 14:30 RAG-ассистент всё ещё его рекомендует. Vector index пересобирает эмбеддинги ночью. Какой фикс сеньора?

Викторина

Дашборд выручки расходится с ручным SQL-подсчётом по Postgres. Оба «корректны». Какова самая вероятная причина?

Расставь шаги по порядку

Число на дашборде неверно. Расставь обход lineage, который делает сеньор, чтобы локализовать drift:

  1. 1 Спроси «по состоянию на когда?» — установи время снимка/обновления, которое прочитал дашборд
  2. 2 Проверь историю обновления gold-MV / агрегата против freshness SLA
  3. 3 Поднимись по medallion: silver устарел, потому что bronze приземлился поздно?
  4. 4 Проверь CDC offset / лаг outbox между OLTP и bronze
  5. 5 Сравни чексумму (количество строк + хеш ключей) OLTP против партиции склада, чтобы подтвердить, где они расходятся
Вспомните перед уходом
  1. 01
    Объясни коллеге, почему поисковый индекс показывает продукт, который Postgres удалил, хотя оба хранилища проходят свои тесты, и как ты закроешь стык.
  2. 02
    Объясни, почему «дашборд корректен?» — неверный вопрос, и при чём тут свежесть и lineage.
Итог

К моменту, когда продукт в проде, один факт живёт в семи хранилищах: OLTP как система записи, колоночный склад, питаемый CDC, слои bronze/silver/gold в dbt, materialized views для дашбордов, event-sourced поток аудита, полнотекстовый поисковый индекс и vector index для RAG. Каждое хранилище корректно оптимизировано под свою задачу — и именно эта оптимизация заставляет копии расходиться. Система ломается не внутри хранилища, а на стыке между двумя корректными: потерянное CDC-событие оставляет удалённый продукт в поиске, refresh lag заставляет MV расходиться с OLTP-источником, ночью пересобранный vector index рекомендует удалённый час назад продукт. Ход сеньора — перестать владеть хранилищами и начать владеть контрактами: transactional outbox, чтобы убить dual-write, идемпотентные консьюмеры для at-least-once доставки, объявленный freshness SLA, валящий build, lineage через версионированные слои medallion, чтобы идти по drift назад, и периодический reconciliation, который сверяет OLTP с каждой копией и чинит разницу. Консистентность, свежесть и lineage — сквозные свойства: ты проектируешь их на стыках, иначе система расходится сама с собой, пока каждая её часть проходит тесты.

Продолжить восхождение ↑Дата-платформа: тест с выбором ответа
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources4
expand
  1. 01
  2. 02
  3. 03
  4. 04

Trademarks belong to their respective owners. Editorial reference only.