From b3e66034fc82f54d1f49dcdabf0449c738c354f2 Mon Sep 17 00:00:00 2001 From: Dmitry Dementiev Date: Thu, 20 Aug 2026 11:42:26 +0300 Subject: [PATCH] =?UTF-8?q?docs(storage):=20=D1=83=D1=81=D1=82=D1=80=D0=BE?= =?UTF-8?q?=D0=B9=D1=81=D1=82=D0=B2=D0=BE=20=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BE=D1=87=D0=BD=D0=B8=D0=BA=D0=BE=D0=B2=20=D0=BE=D1=81=D1=82?= =?UTF-8?q?=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=BE=20=D0=B2=20=D0=BE=D0=B4?= =?UTF-8?q?=D0=BD=D0=BE=D0=BC=20=D0=BC=D0=B5=D1=81=D1=82=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Зачем: каждое утверждение о словаре было записано трижды — в доке хранилища, в комментариях DDL и в ADR 0012. Правка поведения стоила бы трёх согласованных правок, а расхождение между копиями обнаружилось бы не сразу. Дока хранилища — карта, а не пересказ реализации. Что: раздел «Справочники» сведён к своему уровню — правило принадлежности зоны, топология на кластере, единицы денег и указатели. Механика (движок, форма пути, пользователь, окно обновления) осталась только в sql/ddl/05-dic-catalog.sql, рядом с кодом, который она объясняет. Блок «Что проверено» отправлен к замерам ADR 0012 вместо их пересказа — так же, как он уже поступает с ADR 0005. Проверка: минус 30 строк; ссылок на убранный подзаголовок в репозитории нет, три относительные ссылки нового текста разрешаются в существующие файлы. Co-Authored-By: Claude Opus 5 --- docs/architecture/storage.md | 78 +++++++++++------------------------- 1 file changed, 24 insertions(+), 54 deletions(-) diff --git a/docs/architecture/storage.md b/docs/architecture/storage.md index 05d676a..f932cbd 100644 --- a/docs/architecture/storage.md +++ b/docs/architecture/storage.md @@ -424,48 +424,27 @@ kafka_offset)`: смотрят такую таблицу от класса, а Справочные данные живут в базе `dic` — вне цепочки STG → ODS → DDS → DM. В хранилище их никто не производит: содержимое приходит из файла репозитория, а -читают его несколько слоёв сразу. Поэтому зона своя, читать её вправе любой -слой, писать — никто. Почему так, а не пропиской в слое модели, — -[ADR 0012](../adr/0012-dictionary-home.md). +читают его несколько слоёв сразу. Отсюда правило принадлежности: читать зону +вправе любой слой, писать — никто. Почему зона, а не прописка в слое модели, и +почему словарь читает подложку, а не файл, — [ADR +0012](../adr/0012-dictionary-home.md). -### Словарь товаров +Первый и пока единственный справочник — каталог товаров: подложка +`dic.products_file` над CSV репозитория и словарь `dic.products` поверх неё. +Реплик у зоны нет: обе ноды читают свой смонтированный файл и держат свою копию +словаря в памяти. Словарь обновляется сам, окном `LIFETIME`, и потому после +правки каталога ноды какое-то время отвечают по-разному. Разогнать его раньше +срока — `SYSTEM RELOAD DICTIONARY ON CLUSTER`; это административная операция, +роль `etl` права на неё не получает. -Под словарём лежит подложка `dic.products_file` на движке `File`. Она ничего не -хранит и перечитывает `data/catalog/products.csv` на каждом запросе. Compose -монтирует каталог только для чтения в `user_files` обеих нод, DDL создаёт оба -объекта `ON CLUSTER`: имя и форма одни, но каждая нода читает свой файл и держит -свою копию словаря в памяти. +Деньги каталога стоит держать в голове отдельно от остальных: в CSV лежат целые +копейки, словарь отдаёт `Decimal(18, 2)`, а `productPrice` события — уже целые +рубли. Разрыв намеренный, на нём стоит урок про `Float64` ([описание +выгрузки](../formats/clickstream-event.md)). -Путь у движка `File` считается **от `user_files`**, а не от корня данных. -Форма `./user_files/catalog/products.csv`, которой требовал прежний файловый -источник словаря, даёт `FILE_DOESNT_EXIST` — легко принять за пропавший монтаж. - -Сам `dic.products` берёт подложку источником `CLICKHOUSE`, причём формой -`query`, а не `table`: словарь нормализует данные на входе, а не зеркалит -подложку. Пользователя надо называть явно — без него словарь идёт как `default` -с пустым паролем и падает с `AUTHENTICATION_FAILED`. Ходит он беспарольным -`dict`, у которого одно право: чтение `dic`. Хост локальный, поэтому запрос -идёт без сети. - -Строковый ключ `sku` требует `COMPLEX_KEY_HASHED`; числовой `FLAT` здесь -неприменим. - -**Цена: три единицы, и их не надо путать.** В файле каталога лежат целые -копейки — так их пишет генератор, и часть цен несёт копейки намеренно. Словарь -приводит их к `Decimal(18, 2)`, как у денег бэкенда: единица становится видна в -самом числе, `129000` против `1290.00`. А в контракте события `productPrice` — -целые **рубли**, округление формата. Разрыв между ценой каталога и ценой в -событии заложен специально: на нём держится урок про `Float64` и расхождение -представлений денег. - -`LIFETIME(MIN 60 MAX 90)` включает фоновое обновление: правка каталога доезжает -до словаря сама, без команды. Момент внутри окна случаен — так разводят -обращения разных серверов к источнику, чтобы они не шли разом. Цена у этого -заявленная: ноды обновляются вразнобой, и до тридцати секунд одна отвечает по -новому каталогу, а вторая по старому. Разогнать словари вручную можно штатной -командой — `SYSTEM RELOAD DICTIONARY ON CLUSTER clickstream_cluster -dic.products`; это административная операция, её выполняют под `default`, роль -`etl` права перезагрузки словарей не получает. +Устройство обоих объектов — почему такой движок, такая форма пути, такой +пользователь и такое окно — расписано в +[`sql/ddl/05-dic-catalog.sql`](../../sql/ddl/05-dic-catalog.sql). ## Раскладка SQL @@ -592,21 +571,12 @@ DDL-словарь с файловым источником внутри `user_f изменилась на обеих нодах; после отката и повторной команды вернулась обратно. **Проверка зоны справочников 20 августа 2026 года (#105).** Подложка на движке -`File` работает `ON CLUSTER` и перечитывает файл на каждом запросе: строка, -дописанная снаружи, меняет счёт без команд. Словарь поверх неё обновляется сам -внутри окна `LIFETIME` — замер поймал изменение через 116 секунд, без -`SYSTEM RELOAD DICTIONARY`. Относительный путь у движка считается от -`user_files`, а не от корня данных. Источник `CLICKHOUSE` без явного `user` -идёт как `default` с пустым паролем и падает с `AUTHENTICATION_FAILED`; -беспарольный пользователь из файла настройки принимается молча. Приведение -`toDecimal64(price, 2) / 100` даёт `Decimal(18, 2)` и точно на ценах с -копейками: `188990 → 1889.90`. Собранный с нуля стенд поднял оба объекта, `bi` -читает словарь `dictGet`-ом со второй ноды, `analyst` — подложку соединением. - -Отдельно про право на движок: отказ называет `TABLE ENGINE ON File`, но права -с таким именем не хватает. Замер тремя пользователями показал, что достаточно -`GRANT FILE ON *.*` — привилегии на источник, как у Kafka, — а `TABLE ENGINE ON -File` не нужен вовсе. Текст ошибки здесь уводит в сторону. +`File` перечитывает файл на каждом запросе, словарь поверх неё обновляется сам +внутри окна `LIFETIME`, а собранный с нуля стенд поднимает оба объекта и отдаёт +словарь читателям. Замеры целиком — в +[ADR 0012](../adr/0012-dictionary-home.md), раздел «Что проверено»; там же +разобрано, почему отказ в праве на движок называет не то право, которого не +хватает. **Проверка версий заказов 18 августа 2026 года (#94).** MCP Context7 подтвердил, что `JSONType` возвращает имя типа значения JSON, — на нём стоит проверка типов