From 13abc58d982a19956e7668bf0e67da27f70709c2 Mon Sep 17 00:00:00 2001 From: Dmitry Dementiev Date: Wed, 19 Aug 2026 00:17:01 +0300 Subject: [PATCH] =?UTF-8?q?feat(clickhouse):=20=D0=B4=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=20=D1=81=D0=BB=D0=BE=D0=B2=D0=B0=D1=80?= =?UTF-8?q?=D1=8C=20=D1=82=D0=BE=D0=B2=D0=B0=D1=80=D0=BE=D0=B2=20=D0=B8?= =?UTF-8?q?=D0=B7=20=D0=BA=D0=B0=D1=82=D0=B0=D0=BB=D0=BE=D0=B3=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - витринам нужен общий с генератором каталог, а менти — пример штатного обновления словаря. - Что: - каталог смонтирован read-only в обе ноды и создан как dds.products ON CLUSTER. - роли получили dictGet, а check-clickhouse сверяет ответ под etl с исходным CSV. - источник, раскладка и явная политика обновления записаны в документации. - Проверка: - make up с чистыми томами; make lint; make config-test; make smoke; make check-clickhouse. - в generator: make lint typecheck test — 427 тестов прошли. --- README.md | 8 ++--- compose.yaml | 5 +++ docs/architecture/storage.md | 32 ++++++++++++++++-- docs/architecture/testing.md | 6 +++- infra/clickhouse/users.d/access.xml | 6 ++-- scripts/check-clickhouse.sh | 52 ++++++++++++++++++++++------- sql/ddl/00-databases.sql | 3 ++ sql/ddl/25-dds-dictionaries.sql | 16 +++++++++ 8 files changed, 106 insertions(+), 22 deletions(-) create mode 100644 sql/ddl/25-dds-dictionaries.sql diff --git a/README.md b/README.md index d0368ee..3a9a1b4 100644 --- a/README.md +++ b/README.md @@ -105,10 +105,10 @@ Kafka по той же причине спрашивают снаружи. Её `make check-clickhouse` запускает отдельную глубокую проверку ClickHouse: описание кластера, макросы, связь с keeper, `ReplicatedMergeTree`, -`Distributed`, очередь распределённых DDL и очистку временных таблиц. Последняя -из девяти проверок — единственная на настоящих данных: она подневно сверяет -события стартового мира с описью и при расхождении говорит, где искать — -в событиях или в браке. +`Distributed`, очередь распределённых DDL и очистку временных таблиц. Девятая +из десяти проверок спрашивает словарь товаров на обеих нодах. Последняя — +единственная на настоящих событиях: она подневно сверяет стартовый мир с описью +и при расхождении говорит, где искать — в событиях или в браке. `make config-test` проверяет Compose, синтаксис файлов DAG и пробельные ошибки в diff без запуска стенда. diff --git a/compose.yaml b/compose.yaml index 6e2944b..a8b2218 100644 --- a/compose.yaml +++ b/compose.yaml @@ -11,6 +11,9 @@ x-clickhouse-common: &clickhouse-common environment: # Пользователей объявляет access.xml; настройка учётки образом не нужна. CLICKHOUSE_SKIP_USER_SETUP: "1" + # Каталог товаров смонтирован в user_files только для чтения. Entrypoint + # иначе пытается сделать ему chown и первый запуск ноды завершается ошибкой. + CLICKHOUSE_DO_NOT_CHOWN: "1" CLICKHOUSE_CLUSTER_SECRET: ${CLICKHOUSE_CLUSTER_SECRET:?Скопируйте .env.example в .env} CLICKHOUSE_DEFAULT_PASSWORD: ${CLICKHOUSE_DEFAULT_PASSWORD:?Скопируйте .env.example в .env} CLICKHOUSE_ETL_PASSWORD: ${CLICKHOUSE_ETL_PASSWORD:?Скопируйте .env.example в .env} @@ -149,6 +152,7 @@ services: - ./infra/clickhouse/config.d/prometheus.xml:/etc/clickhouse-server/config.d/prometheus.xml:ro - ./infra/clickhouse/config.d/macros-01.xml:/etc/clickhouse-server/config.d/macros.xml:ro - ./infra/clickhouse/users.d/access.xml:/etc/clickhouse-server/users.d/access.xml:ro + - ./data/catalog:/var/lib/clickhouse/user_files/catalog:ro - clickhouse_01_data:/var/lib/clickhouse # Точка подключения Superset. @@ -163,6 +167,7 @@ services: - ./infra/clickhouse/config.d/prometheus.xml:/etc/clickhouse-server/config.d/prometheus.xml:ro - ./infra/clickhouse/config.d/macros-02.xml:/etc/clickhouse-server/config.d/macros.xml:ro - ./infra/clickhouse/users.d/access.xml:/etc/clickhouse-server/users.d/access.xml:ro + - ./data/catalog:/var/lib/clickhouse/user_files/catalog:ro - clickhouse_02_data:/var/lib/clickhouse kafka: diff --git a/docs/architecture/storage.md b/docs/architecture/storage.md index 7523bb2..9c8aca5 100644 --- a/docs/architecture/storage.md +++ b/docs/architecture/storage.md @@ -12,6 +12,8 @@ keeper, Kafka, каркас сервисов. Этап 2 идёт: в `sql/ddl/` Этап 3 добавил вход второго источника и довёл его до ODS: топик `orders`, свой чтец, своё сырьё, версии заказов с таблицей ошибок и поверхность текущего состояния. Наполняет всю цепочку даг `orders_ingest` двумя шагами, а не матвью. +Тот же этап принёс первый объект DDS — словарь товаров из общего с генератором +CSV-файла. Дальше по тексту устройство описано так, как оно проектируется; построенное от заложенного отличает карта таблиц в конце. @@ -414,6 +416,23 @@ kafka_offset)`: смотрят такую таблицу от класса, а разрастается до имени отдельного поля. Точная граница приёма — в [спецификации заказов](orders/ingestion.md). +## Словарь товаров + +`dds.products` читает `data/catalog/products.csv` напрямую. Compose монтирует +каталог только для чтения в `user_files` обеих нод, а DDL создаёт словарь +`ON CLUSTER`: имя и форма одни, но каждая нода держит свою копию в памяти. + +Источник `FILE` с форматом `CSVWithNames` читает заголовок файла. Строковый ключ +`sku` требует `COMPLEX_KEY_HASHED`; числовой `FLAT` здесь неприменим. Цена +остаётся целым числом копеек типа `Int64`, как в контракте события. + +`LIFETIME(0)` отключает фоновое обновление. Каталог меняется только явной +правкой репозитория, а независимый опрос двух нод позволил бы им временно +отвечать разными версиями. Изменение применяют к обеим нодам штатной командой: +`SYSTEM RELOAD DICTIONARY ON CLUSTER clickstream_cluster dds.products`. +Это административная операция: её выполняют под `default`; роль `etl` права +перезагрузки словарей не получает. + ## Раскладка SQL Исполняемые дагами запросы лежат по правилу @@ -439,6 +458,7 @@ Airflow читает и собирает эти файлы штатным шаб | `00-databases.sql` | базы слоёв | | `10-stg-tables.sql` | чтецы топиков `hits` и `orders`, локальные и распределённые таблицы сырья обоих источников | | `20-ods-tables.sql` | типизированное событие, версии заказа и обе таблицы ошибок | +| `25-dds-dictionaries.sql` | словарь товаров из общего CSV-каталога | | `30-ods-views.sql` | актуальные события, текущие заказы и матвью разбора в ODS | | `40-stg-views.sql` | матвью приёма: чтец в сырьё | @@ -501,13 +521,14 @@ ODS. Второе: матвью приёма создаётся последне | ODS | `ods.order_snapshot_rep` / `_dist` | типизированные версии заказа | | ODS | `ods.order_v` | текущая версия заказа на языке источника | | ODS | `ods.order_snapshot_errors_rep` / `_dist` | строки слепка, не прошедшие строгий приём | +| DDS | `dds.products` | словарь товаров из общего с генератором CSV-каталога | Матвью разбора у заказов нет: срез сырья раскладывают по этим двум целям два `INSERT SELECT` шага `parse_batch` из файлов `sql/ods/order_snapshot_load.sql` и `sql/ods/order_snapshot_errors_load.sql`. -Слои DDS и DM появляются на следующих этапах; их состав задан разделом 7 -мастер-спеки и переносится сюда по мере постройки. +Таблицы DDS и слой DM появляются на следующих этапах; их состав задан разделом +7 мастер-спеки и переносится сюда по мере постройки. ## Что проверено @@ -525,6 +546,13 @@ MCP Context7 подтвердила обычное представление с `ods.event_v` остался равен `ods.event_dist FINAL`. Имена и типы всех колонок представления совпали с распределённой таблицей. +**Проверка словаря товаров 19 августа 2026 года (#95).** Context7 подтвердил +DDL-словарь с файловым источником внутри `user_files`, хешированную раскладку +для строкового ключа и отключение фонового обновления через `LIFETIME(0)`. +На закреплённом ClickHouse 26.3 цена известного `sku` после правки CSV и +`SYSTEM RELOAD DICTIONARY ON CLUSTER` +изменилась на обеих нодах; после отката и повторной команды вернулась обратно. + **Проверка версий заказов 18 августа 2026 года (#94).** MCP Context7 подтвердил, что `JSONType` возвращает имя типа значения JSON, — на нём стоит проверка типов в предикате приёма заказов. Остальное снято на закреплённом ClickHouse 26.3. diff --git a/docs/architecture/testing.md b/docs/architecture/testing.md index 97c28c3..65ad23f 100644 --- a/docs/architecture/testing.md +++ b/docs/architecture/testing.md @@ -43,7 +43,7 @@ | `make config-test` | корень | Compose разбирается, файлы DAG синтаксически целы, в diff нет пробельных ошибок. О работоспособности не говорит ничего | не нужен | 1 с | | `make lint` | корень | Код стенда — `dags/` и `infra/superset/` — отформатирован и проходит ruff | не нужен | 0,4 с | | `make smoke` | корень | Стенд **собран**: службы живы, порты отвечают, подключения настроены друг на друга. Вширь и по касательной к каждой службе. Единственная цель, которая здесь правда смоук | нужен | 9 с | -| `make check-clickhouse` | корень | Всё, что спрашивают **у ClickHouse** и он отвечает сам: макросы, шарды, реплики, путь в keeper, ключ шардирования, очередь распределённых DDL, счёт событий стартового мира против описи | нужен | 8 с | +| `make check-clickhouse` | корень | Всё, что спрашивают **у ClickHouse** и он отвечает сам: макросы, шарды, реплики, путь в keeper, ключ шардирования, очередь распределённых DDL, ответ словаря товаров, счёт событий стартового мира против описи | нужен | 24 с | | `make check-services` | корень | **Службы работают**: DAG запускается и доходит, топик создаётся и удаляется, Superset логинится и ходит в базу | нужен | 59 с | | `make lint` | `generator/` | Код генератора отформатирован и проходит ruff | не нужен | 0,4 с | | `make typecheck` | `generator/` | Типы генератора сходятся (ty) | не нужен | 0,5 с | @@ -221,6 +221,10 @@ Kafka → STG → ODS, и живёт он в `make check-clickhouse`. ## Что проверено +**Перезамер 19 августа 2026 года при исполнении #95.** После добавления десятой +проверки словаря `make check-clickhouse` на чисто поднятом стенде прошёл за +24 секунды. Это цена всей цели, а не одной новой строки. + **Замер 9 августа 2026 года при появлении корневого `lint` (#65).** Три прогона подряд — 0,39, 0,36 и 0,39 секунды на трёх файлах стенда; в таблице 0,4 с. На машине с непрогретым кешем первый прогон дороже: `uvx` сначала скачивает ruff. diff --git a/infra/clickhouse/users.d/access.xml b/infra/clickhouse/users.d/access.xml index c66c711..a744933 100644 --- a/infra/clickhouse/users.d/access.xml +++ b/infra/clickhouse/users.d/access.xml @@ -9,7 +9,7 @@ GRANT SELECT, INSERT, ALTER, CREATE TABLE, CREATE VIEW, DROP TABLE, DROP VIEW ON default.* GRANT SELECT, INSERT, ALTER, CREATE TABLE, CREATE VIEW, DROP TABLE, DROP VIEW, CREATE DATABASE ON stg.* GRANT SELECT, INSERT, ALTER, CREATE TABLE, CREATE VIEW, DROP TABLE, DROP VIEW, CREATE DATABASE ON ods.* - GRANT SELECT, INSERT, ALTER, CREATE TABLE, CREATE VIEW, DROP TABLE, DROP VIEW, CREATE DATABASE ON dds.* + GRANT SELECT, INSERT, ALTER, CREATE TABLE, CREATE VIEW, CREATE DICTIONARY, dictGet, DROP TABLE, DROP VIEW, CREATE DATABASE ON dds.* GRANT SELECT, INSERT, ALTER, CREATE TABLE, CREATE VIEW, DROP TABLE, DROP VIEW, CREATE DATABASE ON dm.* GRANT SELECT ON system.* GRANT CLUSTER ON *.* @@ -21,7 +21,7 @@ - GRANT SELECT ON dds.* + GRANT SELECT, dictGet ON dds.* GRANT SELECT ON dm.* GRANT SELECT ON system.settings @@ -30,7 +30,7 @@ GRANT SELECT ON stg.* GRANT SELECT ON ods.* - GRANT SELECT ON dds.* + GRANT SELECT, dictGet ON dds.* GRANT SELECT ON dm.* GRANT SELECT ON system.settings diff --git a/scripts/check-clickhouse.sh b/scripts/check-clickhouse.sh index 4a2acef..71cca65 100755 --- a/scripts/check-clickhouse.sh +++ b/scripts/check-clickhouse.sh @@ -2,6 +2,7 @@ set -Eeuo pipefail readonly ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +readonly CATALOG="${ROOT_DIR}/data/catalog/products.csv" readonly INVENTORY="${ROOT_DIR}/data/world-inventory.json" readonly CLUSTER="clickstream_cluster" readonly LOCAL_TABLE="smoke_replicated_local" @@ -26,6 +27,17 @@ query() { ' _ "$sql" /dev/null query clickhouse-01 "DROP TABLE default.${LOCAL_TABLE} ON CLUSTER ${CLUSTER} SYNC" >/dev/null remaining_sql="SELECT count() FROM system.tables WHERE database = 'default' AND name IN ('${LOCAL_TABLE}', '${DISTRIBUTED_TABLE}') FORMAT TSVRaw" @@ -249,9 +261,25 @@ assert_ddl_queue_completed clickhouse-02 'после DROP' trap - EXIT INT TERM printf 'ЗЕЛЁНО: временные таблицы удалены; проверены завершённые задания CREATE и DROP.\n' -# После снятия ловушек: своих объектов эта проверка не заводит и прибирать за -# собой ей нечего — она только смотрит на то, что стенд произвёл сам. -printf 'Проверка 9/9: стартовый мир в ods.event сходится с описью...\n' +# После снятия ловушек: своих объектов эти проверки не заводят и прибирать за +# собой им нечего — они только смотрят на то, что стенд произвёл сам. +printf 'Проверка 9/10: словарь товаров отвечает на обеих нодах...\n' +product_sql="SELECT + dictGet('dds.products', 'name', tuple('HOME-0001')), + dictGet('dds.products', 'category', tuple('HOME-0001')), + dictGet('dds.products', 'price', tuple('HOME-0001')) +FORMAT TSV" +expected_product="$( + awk -F, \ + '$1 == "HOME-0001" { print $2 "\t" $3 "\t" $5; exit }' \ + "$CATALOG" +)" +[[ -n "$expected_product" ]] || fail 'в каталоге нет эталонного sku HOME-0001' +assert_equal "$expected_product" "$(query_as_etl clickhouse-01 "$product_sql")" "словарь товаров не ответил на первой ноде" +assert_equal "$expected_product" "$(query_as_etl clickhouse-02 "$product_sql")" "словарь товаров не ответил на второй ноде" +printf 'ЗЕЛЁНО: известный sku вернул имя, категорию и цену на обеих нодах.\n' + +printf 'Проверка 10/10: стартовый мир в ods.event сходится с описью...\n' check_starting_world -printf 'ИТОГ: все 9 проверок кластера ClickHouse прошли.\n' +printf 'ИТОГ: все 10 проверок кластера ClickHouse прошли.\n' diff --git a/sql/ddl/00-databases.sql b/sql/ddl/00-databases.sql index 8a15c8d..4d35ddd 100644 --- a/sql/ddl/00-databases.sql +++ b/sql/ddl/00-databases.sql @@ -13,3 +13,6 @@ CREATE DATABASE IF NOT EXISTS stg ON CLUSTER clickstream_cluster; -- База ODS заводится здесь же, хотя её объекты приносит #43: базы дёшевы, а -- порядок файлов от этого не зависит. CREATE DATABASE IF NOT EXISTS ods ON CLUSTER clickstream_cluster; + +-- DDS начинается со словаря товаров; таблицы слоя появятся на следующем этапе. +CREATE DATABASE IF NOT EXISTS dds ON CLUSTER clickstream_cluster; diff --git a/sql/ddl/25-dds-dictionaries.sql b/sql/ddl/25-dds-dictionaries.sql new file mode 100644 index 0000000..ce70161 --- /dev/null +++ b/sql/ddl/25-dds-dictionaries.sql @@ -0,0 +1,16 @@ +-- Общий с генератором каталог товаров. ClickHouse разрешает файловому +-- источнику читать только из user_files; Compose монтирует сюда один и тот же +-- файл на обе ноды. Цена хранится в копейках, как и в контракте события. +CREATE DICTIONARY IF NOT EXISTS dds.products ON CLUSTER clickstream_cluster +( + sku String, + name String, + category String, + brand String, + price Int64, + demand String +) +PRIMARY KEY sku +SOURCE(FILE(PATH './user_files/catalog/products.csv' FORMAT 'CSVWithNames')) +LAYOUT(COMPLEX_KEY_HASHED()) +LIFETIME(0);