Зачем: помочь начинающему инженеру проследить событие и версию заказа от генератора до хранилища, включая ошибки и повторные поступления. Что: обновлены вводная генератора, справочники приема и модели заказа, разделы хранилища и комментарии SQL и дага. Сохранены технические условия; исполняемый код, схемы и поведение стенда не изменены. Проверка: make lint, git diff --check, 142 локальные ссылки и справка CLI. SQL без комментариев и Python AST без строк документации совпали с исходными. Генерация данных и сброс состояния не выполнялись. Refs #250
14 KiB
Прием заказов из Kafka в ODS
Здесь разобран путь одного заказа: сообщение Kafka сохраняется в STG, проверяется и попадает либо в ODS, либо в таблицу ошибок. Следующие слои читают из ODS последнюю принятую версию каждого заказа.
Поток данных
Источник отправляет в топик orders дневной слепок: по одному сообщению
на заказ. Слепок дня D содержит состояния заказов, оформленных в дни
D−6…D, с учетом задержек первого появления.
В начале модельного времени окно короче. Выгрузку за D отправляет
генератор при запуске дня D+1; подробности — в описании слепка.
После отправки шаговый даг модельного времени запускает
orders_ingest и ждет его завершения.
У orders_ingest нет расписания; его два шага выполняются по порядку:
pull_batchчитает порцию изstg.orders_raw_kafkaи записывает ее черезstg.orders_raw_dist. Содержимое сообщения остается строкойraw, рядом сохраняются координаты Kafka,_load_idи_load_ts.parse_batchчитает сохраненные строки с этим_load_id. ПервыйINSERT SELECTпишет годные записи вods.order_dist, второй — остальные вods.order_errors_dist.
_load_id равен run_id Airflow и связывает строки одного запуска.
_load_ts — реальное время записи в STG; при разборе оно переносится
без пересчета. Запросы лежат в
orders_raw_load.sql и
order_parse.sql.
SQLExecuteQueryOperator передает run_id параметром ClickHouse load_id.
Две вставки разбора используют одно условие годности, записанное макросом
Jinja внутри order_parse.sql. Перед выполнением макрос подставляется
в оба запроса; ветвь брака берет отрицание этого условия. Оно возвращает
только истину или ложь и не вызывает исключений на некорректном raw.
Так каждая строка сохраненной порции получает ровно один результат.
Транзакции между вставками нет: первая может завершиться, а вторая упасть.
Повтор parse_batch выполняет обе вставки над тем же срезом STG.
Текущее состояние заказа останется прежним, но таблица ошибок может
получить повторные строки с теми же _load_id и координатами Kafka.
Обе вставки ждут доставки строк на шарды и используют ключ целевой таблицы;
настройки объяснены в справочнике хранилища.
Граница строгого приёма
Проверяется одна строка stg.orders_raw_dist. Корень JSON должен быть
объектом с точным набором из одиннадцати ключей:
order_id, user_id, status, created_at, updated_at,
items_total, discount, delivery, total, items, snapshot_date.
| Поля | Условия приема |
|---|---|
order_id, status |
Строки JSON |
user_id |
Целое число JSON, которое разбирается в UInt64 |
created_at, updated_at |
Строки UTC с миллисекундами, например 2026-06-01T12:34:56.000Z; проверяются форма и разбор времени |
items_total, discount, delivery, total |
Неотрицательные суммы в рублях, записанные строками с двумя знаками после точки; проверяется разбор в Decimal(18, 2) |
snapshot_date |
Строка даты YYYY-MM-DD, которая проходит разбор |
items |
Массив JSON; сохраняется целиком в строковой колонке |
ODS проверяет формат верхнего уровня. Здесь нет проверки допустимых
статусов, их переходов, полей внутри items или равенства денежных сумм.
Позиции разбираются в DDS; проверку их суммы участник
добавляет в задании ступени 5.
Основания выбора формата — в исследовании.
Класс ошибки записывается в error_class по первому нарушенному условию:
not_an_object— корень JSON не объект;keyset_mismatch— набор ключей отличается от ожидаемого;field_invalid— тип, форма или разбор значения не прошли проверку.
Имя поля в класс не входит. Для разбора конкретной ошибки в
ods.order_errors_dist есть исходный raw, координаты Kafka и метки загрузки.
Роль ODS
ods.order_rep использует ReplicatedReplacingMergeTree(snapshot_date).
Строки с одним ключом сортировки order_id считаются версиями одного заказа;
остается строка с максимальной snapshot_date. Партиция —
toDate(created_at) в UTC, ключ шардирования — cityHash64(order_id).
Неизменный created_at и общий ключ направляют все версии заказа
в одну партицию и на один шард.
| Колонка | На какой вопрос отвечает |
|---|---|
snapshot_date |
На конец какого модельного дня источник показал это состояние? |
updated_at |
Когда запись заказа последний раз меняли в магазине? |
_load_id |
Какой запуск Airflow принял сообщение? |
_load_ts |
Когда сообщение записали в STG по реальным часам стенда? |
Например, заказ не менялся с 1 июня, но входит в слепки за 1 и 2 июня.
updated_at у строк одинаков, а snapshot_date различается. ODS оставит
строку за 2 июня вместе с метками ее доставки. Новая дата наблюдения
нужна DDS для определения окна пересборки, даже если состояние не изменилось.
Источник выпускает один слепок на дату. Повторная отправка дает то же
содержимое, а более поздний слепок не возвращает устаревшее состояние.
Эти условия не проверяются на одной строке при приеме. Если источник
пришлет два разных состояния с одной snapshot_date, эта колонка
не позволит определить, какое из них актуально.
Обоснование версии — ADR 0014.
ods.order_v читает ods.order_dist FINAL и возвращает одну текущую строку
на order_id, даже если фоновые слияния еще не убрали старые версии.
Без FINAL физическая таблица может показать несколько строк заказа.
Историю ODS не гарантирует: старые версии постепенно удаляются при слияниях.
Отсутствие заказа в новом слепке не удаляет его из ODS.
DDS читает ods.order_v, переводит времена в пояс счетчика и разбирает
позиции. Для своей загрузки DDS записывает собственные служебные метки —
модель заказа.
Одно чтение Kafka
Один запуск делает один прямой SELECT, без чтения в цикле до пустоты.
Обычный слепок содержит около полутора тысяч строк, а предел порции
на стенде — десятки тысяч сообщений. Это допущение о размере данных:
Kafka не сообщает запросу, что прочитан весь слепок.
Короткая порция оставляет непрочитанные сообщения для следующих запусков.
Поэтому ODS добавляет версии заказов по мере поступления. Замена партиции
по snapshot_date могла бы опубликовать неполный набор заказов
(ADR 0010).
Сбой после чтения опаснее короткой порции. Чтец подтверждает позиции сообщений в Kafka — офсеты — при прямом чтении. Если последующая вставка в STG упадет, эти сообщения уже не будут прочитаны той же группой повторно; часть из них может успеть записаться на шарды. Позиция модельного времени при неуспехе не продвигается. Повтор шагового дага снова отправит слепок того же дня, но не восстановит старые сообщения других слепков, если они попали в неудачную порцию. Граница восстановления описана в ADR 0008.
Что проверено
Это результаты прежних опытов на стенде; при редактуре они заново не выполнялись.
- 18 августа 2026 года (#93, #94) прямое чтение приняло слепок,
а разбор разделил строки на годные и ошибки без потерь и пересечений.
Две версии заказа попали на один шард в одну партицию;
ods.order_vвернул позднюю. Повтор разбора с тем же_load_idсохранил текущую строку и ее_load_ts, но повторил ошибки. Подробности — справочник хранилища и ADR 0008. - 20 августа после исправления
#102
приняты все 1694 строки слепка дня 7 без ошибок. Итог заказа
20260608-0007:5490.00 − 823.50 + 299.00 = 4965.50. - 21 августа (#124) пересборка через
SQLExecuteQueryOperatorзавершилаpull_batchиparse_batch; вsystem.query_logосталось по одному завершенному запросуetlна забор, годные строки и ошибки. - 22 августа (#137) во всех 3034 парах с одинаковым
updated_atделовые поля совпали побайтово; по слепкам время изменения не убывало. - 26 августа (#165) после двух слепков физическая таблица содержала
3356 версий 1917 заказов. В
ods.order_vу всех заказов была дата последнего слепка, а деловые поля совпали сargMaxпоupdated_atиз сырых данных.
На ClickHouse 26.3.17.56 отдельно проверены одна порция при прямом
SELECT Kafka и выполнение FINAL на шардах через Distributed.
При проектировании Context7 был недоступен; чтение сверяли с исходниками
той же версии:
StorageKafka.cpp и
KafkaSource.cpp.
Повторная сверка правил версий с документацией — в
справочнике хранилища.