feat(stg): DDL-бутстрап, топик hits и приём сырья обеими нодами
Зачем: стенду нужен воспроизводимый холодный старт, при котором схема хранилища и топик появляются сами, а сырьё из Kafka доезжает в STG обеими нодами кластера — без ручных шагов между `make clean` и рабочим приёмом. Что: - `sql/ddl/` — три файла, применяются по порядку имён: базы `stg` и `ods`, Kafka-чтец `hits_raw_kafka` формата RawBLOB, реплицируемая `hits_raw_rep` с окном TTL в трое суток, распределённая `hits_raw_dist` и матвью `hits_raw_mv`, переносящая сырьё вместе с метаданными доставки. - `compose.yaml` — службы `kafka-init` (топик `hits` на две партиции, с ремонтом уже созданного однопартиционного) и `clickhouse-init` (применяет `/ddl/*.sql`); `hostname:` у обеих нод, чтобы `hostName()` отдавал имя узла, а не идентификатор контейнера; `airflow-init` зависит от `clickhouse-init` — без зависимого успешный одноразовый сервис считается упавшим для `--wait`. - Доки: конвенции и раздел «Что проверено» в справочнике хранилища, указатели и границы обещаний в ADR 0005, снятые пункты в разделе 11 спеки. Проверка: `make lint`, `make typecheck`, `make config-test`, `make smoke` (25 проверок), `make smoke-guards` — зелёные. Приёмочный прогон с чистого тома подтвердил все пять критериев #37: холодный старт и идемпотентный повтор, две партиции у `hits`, метаданные доставки у доехавшего сообщения, обе партиции на обеих потребляющих нодах в одном прогоне, некорректный JSON лежит сырым и приём не встаёт. Известная граница: RawBLOB молча теряет запись с пустым значением и запись-надгробие; принято как свойство, замер и довод — в справочнике хранилища. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,14 +4,18 @@
|
||||
|
||||
## Решение
|
||||
|
||||
Топик `hits` читает одна Kafka-таблица формата `RawBLOB`: сообщение ложится в
|
||||
`stg.hits_raw_dist` строкой, как пришло, рядом с метаданными доставки. Ни
|
||||
типизации, ни проверки на этом шаге нет — слой сырья ничего не интерпретирует.
|
||||
Топик `hits` читает одна Kafka-таблица формата `RawBLOB`: непустое сообщение
|
||||
ложится в `stg.hits_raw_dist` строкой, как пришло, рядом с метаданными доставки.
|
||||
Ни типизации, ни проверки на этом шаге нет — слой сырья ничего не интерпретирует.
|
||||
У формата есть следствие для DDL: он читает вход в одно значение и рассчитан на
|
||||
таблицу с единственной колонкой, поэтому у чтеца она ровно одна — `raw`, а
|
||||
метаданные доставки берутся только из виртуальных колонок и добавить к чтецу
|
||||
что-либо своё нельзя.
|
||||
|
||||
Слово «непустое» приписано позже самого решения: у обещания нашлась измеренная
|
||||
граница, и она датирована ниже, в разделе «Что проверено». Решения она не
|
||||
меняет — от того, рождает ли пустая запись строку, выбор формата не зависит.
|
||||
|
||||
Типизированный слой наполняют две матвью, привязанные к `stg.hits_raw_dist`.
|
||||
Поля достаются `JSONExtract`. В таблицу ошибок уходят три класса брака:
|
||||
сообщение, не являющееся объектом JSON; объект, чей набор ключей разошёлся с
|
||||
@@ -99,8 +103,9 @@ STG сырьё исключительно от брака: слой сырых
|
||||
грязные записи не должны валить пайплайн. Лечится это двумя способами — включить
|
||||
на чтеце режим обработки ошибок или не проверять на входе вовсе. Второе честнее:
|
||||
частичная проверка в слое, чья работа — не проверять, спорит сама с собой, а
|
||||
настоящий разбор всё равно идёт ниже. При `RawBLOB` ломаться нечему, доезжают
|
||||
любые байты, и оба класса брака разбираются в одном месте.
|
||||
настоящий разбор всё равно идёт ниже. При `RawBLOB` ломаться нечему, доезжает
|
||||
любое непустое сообщение, каким бы мусором оно ни было (про «непустое» — там же,
|
||||
в «Что проверено»), и оба класса брака разбираются в одном месте.
|
||||
|
||||
Архив нужен буквальный и читаемый глазами. `RawBLOB` — это про способ чтения, а
|
||||
не про тип колонки: на диске лежит обычный `String` с текстом события, и менти
|
||||
@@ -162,18 +167,20 @@ contract-тест из #43 сюда не дотягивается — он ср
|
||||
|
||||
Матвью с источником-`Distributed` срабатывает на вставку именно в эту
|
||||
распределённую таблицу — блок она видит до разрезания по шардам. Проверено
|
||||
владельцем на рабочих проектах; на стенде подтверждается заодно с приёмкой #37.
|
||||
владельцем на рабочих проектах; на стенде проверяется вместе с матвью разбора,
|
||||
то есть при исполнении #43.
|
||||
|
||||
На живом стенде проверяется при исполнении #37. Первые два внесены в раздел 11
|
||||
спеки; остальные — однострочные `SELECT`, их довольно прогнать заодно:
|
||||
Пункт про `RawBLOB`, стоявший в списке ниже первым, закрыт при исполнении #37,
|
||||
на стенде 5 августа 2026 года: формат даёт ровно одну строку на каждое непустое
|
||||
сообщение, пачка продюсера границы сообщений не стирает, запасной `LineAsString`
|
||||
не понадобился.
|
||||
Тогда же нашлась и граница обещания — запись с пустым значением и
|
||||
запись-надгробие не дают строки вовсе; из-за неё в «Решении» и в «Почему»
|
||||
приписано слово «непустое». Замер целиком — в [доке
|
||||
хранилища](../architecture/storage.md), раздел «Что проверено». Остальные три
|
||||
по-прежнему ждут живого стенда; это однострочные `SELECT`, их довольно прогнать
|
||||
заодно:
|
||||
|
||||
- `RawBLOB` в Kafka-движке даёт ровно одну строку на сообщение. Проверять это
|
||||
нужно первым и до написания DDL: формулировка «читает вход в одно значение»
|
||||
про файл понятна, а про пачку сообщений из топика — нет, и если сообщения
|
||||
склеятся, переделывать придётся решение целиком, а не DDL. Опыт стоит трёх
|
||||
сообщений и одного `count()`. Запасной вариант — `LineAsString`: он режет по
|
||||
переводу строки, а события у нас однострочные; цена запасного — сообщение с
|
||||
переводом строки внутри даст две строки вместо одной;
|
||||
- форма именованного кортежа в `JSONExtract` с `Nullable`-членами — нужна для
|
||||
свёртки сорока семи вызовов в один, если разбор окажется дорогим;
|
||||
- `isValidJSON('123')` возвращает единицу, а `JSONExtractKeys` от скаляра —
|
||||
|
||||
Reference in New Issue
Block a user