From 7ffb4606f137d58eb0611a1a09cce9806315354b Mon Sep 17 00:00:00 2001 From: Dmitry Dementev Date: Fri, 21 Aug 2026 14:52:58 -0400 Subject: [PATCH 1/3] =?UTF-8?q?feat(monitoring):=20=D1=81=D0=BE=D0=B1?= =?UTF-8?q?=D1=80=D0=B0=D0=BD=D1=8B=20=D0=B4=D0=B0=D1=88=D0=B1=D0=BE=D1=80?= =?UTF-8?q?=D0=B4=D1=8B=20=C2=AB=D0=9A=D0=BB=D0=B0=D1=81=D1=82=D0=B5=D1=80?= =?UTF-8?q?=C2=BB=20=D0=B8=20=C2=AB=D0=97=D0=B0=D0=BF=D1=80=D0=BE=D1=81?= =?UTF-8?q?=D1=8B=C2=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - ADR 0002 обещает три дашборда; собран был один, а симптом инфраструктуры как следствие решения человека показать было негде. - Что: - cluster.json — семь панелей: процессор, память с пределом коробки, диски, части по таблицам, слияния, keeper, очередь распределённых DDL. - queries.json — четыре панели поверх system.query_log: тяжёлые запросы, поток запросов, аппетит по пользователям, ошибки. - системные таблицы читаются через clusterAllReplicas; очередь DDL — без обёртки, она живёт в keeper и у обеих нод одна. - устройство обоих дашбордов записано в docs/architecture/monitoring/, правило про пороги поднято из data.md в README набора. - Проверка: - make config-test; make smoke 21/21; make check-clickhouse 10/10. - все одиннадцать панелей отвечают через /api/ds/query. --- docs/adr/0002-monitoring-scope.md | 9 + docs/architecture/monitoring/README.md | 15 +- docs/architecture/monitoring/cluster.md | 127 +++++++ docs/architecture/monitoring/data.md | 3 - docs/architecture/monitoring/queries.md | 71 ++++ .../provisioning/dashboards/cluster.json | 316 ++++++++++++++++++ .../provisioning/dashboards/queries.json | 173 ++++++++++ 7 files changed, 708 insertions(+), 6 deletions(-) create mode 100644 docs/architecture/monitoring/cluster.md create mode 100644 docs/architecture/monitoring/queries.md create mode 100644 infra/grafana/provisioning/dashboards/cluster.json create mode 100644 infra/grafana/provisioning/dashboards/queries.json diff --git a/docs/adr/0002-monitoring-scope.md b/docs/adr/0002-monitoring-scope.md index 41c6ee3..fb1f2c2 100644 --- a/docs/adr/0002-monitoring-scope.md +++ b/docs/adr/0002-monitoring-scope.md @@ -121,3 +121,12 @@ ClickHouse, Kafka и Airflow, 17 — в дашборде генератора, 1 пометку, от которой этот же ADR отговаривает выше, — а реальные сутки на стенде не с чем сравнивать. Довод целиком и остальное устройство дашборда — [docs/architecture/monitoring/data.md](../architecture/monitoring/data.md). + +Тогда же собраны «Кластер» и «Запросы». Довод про `clusterAllReplicas` +подтвердился с одной поправкой: очередь распределённых DDL лежит не у ноды, а в +keeper, и обе ноды читают её целиком — запрос вернул с каждой одни и те же 124 +записи. Обёртка там не добавила бы правды, а удвоила бы каждую строку, поэтому +единственная панель «Кластера» без неё — эта. Keeper своих системных таблиц не +имеет вовсе, и панель по нему приходит из Prometheus. Устройство обоих +дашбордов — [cluster.md](../architecture/monitoring/cluster.md) и +[queries.md](../architecture/monitoring/queries.md). diff --git a/docs/architecture/monitoring/README.md b/docs/architecture/monitoring/README.md index e0526af..f671e32 100644 --- a/docs/architecture/monitoring/README.md +++ b/docs/architecture/monitoring/README.md @@ -9,9 +9,9 @@ Prometheus, какие источники данных подготовлены **Что здесь есть и чего ещё нет.** Собран пол: свой образ Grafana с плагином ClickHouse, два подготовленных источника данных, экспортёр метрик Kafka и цель -для него у Prometheus. Поверх пола стоят дашборды из каталога ниже; «Кластер» -и «Запросы» приносят отдельные тикеты. Правил оповещения и серверного журнала -`system.text_log` тоже пока нет. +для него у Prometheus. Поверх пола стоят все три дашборда ADR 0002, они +перечислены ниже. Правил оповещения и серверного журнала `system.text_log` +пока нет. ## Состав @@ -69,11 +69,20 @@ Grafana ходит пользователем `grafana` с ролью `monitorin Файлы каталога разбирает `make config-test`, рядом с файлами дагов и по тому же доводу ([testing.md](../testing.md)). +Порогов и цветовых вердиктов на панелях нет ни на одном дашборде: на них +смотрят глазами, красное и зелёное живёт в проверках и дагах DQ. + Собранные дашборды: - **«Данные»** (`data.json`) — с потоком всё в порядке: свежесть, поток, брак, сходимость Kafka с ClickHouse, отставание чтения и дневной слепок заказов. Устройство и разбор панелей — [data.md](data.md). +- **«Кластер»** (`cluster.json`) — что с машиной: процессор, память и диски + обеих нод, части и слияния, keeper, очередь распределённых DDL. Устройство и + разбор панелей — [cluster.md](cluster.md). +- **«Запросы»** (`queries.json`) — что выполнялось и во что обошлось: тяжёлые + запросы, поток запросов, аппетит по пользователям, ошибки. Устройство и + разбор панелей — [queries.md](queries.md). ## Что проверено diff --git a/docs/architecture/monitoring/cluster.md b/docs/architecture/monitoring/cluster.md new file mode 100644 index 0000000..f0dd258 --- /dev/null +++ b/docs/architecture/monitoring/cluster.md @@ -0,0 +1,127 @@ +# Дашборд «Кластер» + +`infra/grafana/provisioning/dashboards/cluster.json` — семь панелей. Вопрос у +экрана один: что с машиной? Состав панелей и довод за каждую — +[ADR 0002](../../adr/0002-monitoring-scope.md). + +Свой вопрос каждая панель называет заголовком прямо на экране. Здесь — только +источники, о них и речь ниже. + +| Панель | Откуда берёт | +|---|---| +| Процессор | `system.metric_log` | +| Память | `system.asynchronous_metric_log` | +| Диски | `system.metric_log` | +| Части | `system.parts` | +| Слияния | `system.part_log` | +| Keeper | экспортёр keeper через Prometheus | +| Распределённый DDL | `system.distributed_ddl_queue` | + +Окно у дашборда — полчаса, обновление — полминуты, и это выбрано под нагрузку. +Вставка на стенде занимает секунды, а оседание после слияний — минуту-другую; +в часовом окне и то и другое схлопнулось бы в один столбик. Урок здесь — форма +кривой, и окно подобрано так, чтобы форма была видна. + +## Обе ноды на одной панели + +Системная таблица у каждой ноды своя: `system.metric_log` на `clickhouse-01` +знает только про `clickhouse-01`. Запрос без обёртки показал бы половину +правды и ничем бы себя не выдал — вторая нода просто не появилась бы в легенде. +Поэтому запросы идут через `clusterAllReplicas`, а `hostName()` подписывает, +чья это строка. + +Исключение одно — очередь распределённых DDL. Она лежит не у ноды, а в keeper, +и обе ноды читают из него одну и ту же очередь целиком, вместе со строками про +соседа. Обёртка здесь не добавила бы правды, а удвоила бы каждую строку. +Правило поэтому звучит так: `clusterAllReplicas` нужен там, где у каждой ноды +своя правда. + +## Пол на графиках — журналы самого сервера + +ClickHouse ведёт около десятка собственных журналов: `metric_log`, +`trace_log`, `text_log`, `query_log` и соседние. Все они — таблицы MergeTree, и +пишутся они постоянно, даже когда на стенде не происходит ничего. Отсюда три +следствия, которые иначе выглядят поломкой: + +- процессор и диски никогда не падают в ноль; +- новых частей рождается около сотни в минуту на ноду, и большинство из них — + системные; +- в списке слияний системные таблицы стоят рядом со своими, а по потраченному + времени часто и выше. + +Мера отсюда простая: своя нагрузка читается как подъём над полом, а не как +число само по себе. + +Панели этот пол не вычитают, и не по недосмотру. Процессор и диски меряются +счётчиками процесса — в них нет колонки «чья это работа», и вычесть журналы +оттуда нечем. Отфильтруй мы `system` в тех двух панелях, где это технически +возможно, они разошлись бы с остальными, и разбираться пришлось бы уже с +расхождением. + +## Что именно меряют процессор, память и диски + +Процессор считается по времени самого сервера +(`ProfileEvent_OSCPUVirtualTimeMicroseconds`), а не по загрузке машины. +Метрики с именами `OS*` сняты с хоста целиком: обе ноды показывали бы в них +одно и то же число, и панель на два ряда потеряла бы смысл. + +Память показана резидентной (`MemoryResident`), а не той, что сервер считает +своей (`MemoryTracking`). Разница не косметическая: на праздной ноде +`MemoryTracking` — 113 МиБ при 846 МиБ резидентной памяти, а ядро, решая, кого +убить за нехватку памяти, смотрит на вторую величину +([ADR 0004](../../adr/0004-resource-limits.md)). Панель по первой показывала бы +здоровый вид у ноды, которую вот-вот убьют. + +Пунктирная «коробка» рядом — предел памяти контейнера, и он тоже читается +живьём, метрикой `CGroupMemoryTotal`. Впиши мы 4 ГиБ числом в JSON, панель +молча соврала бы в тот день, когда предел поменяют. + +Диски меряются обращениями к файлам (`OSReadChars`, `OSWriteChars`), а не +блоками, дошедшими до устройства (`OSReadBytes`). Блочное чтение на стенде +почти всегда ноль: части лежат в страничном кэше, и слияние перечитывает их +оттуда. Работа при этом делается настоящая, и панель по блокам показывала бы, +что её нет. + +## Keeper и очередь DDL — то, что живёт вне нод + +Keeper — отдельный сервис, системных таблиц у него нет, и панель по нему +единственная на дашборде приходит из Prometheus. + +Показывает она размер дерева. В keeper лежит журнал репликации, а в нём — +запись на каждую часть: пока поток идёт, дерево растёт, а когда части +домержились и старые записи убраны, оседает. Замер за полчаса живого потока: +9 078 узлов на подъёме, 11 512 на пике, 10 308 через десять минут после +остановки потока. + +Очередь распределённых DDL — вторая половина того же сюжета: через keeper +проходит каждый запрос `ON CLUSTER`. Панель показывает последние двадцать +записей независимо от окна дашборда: в спокойные полчаса очередь пуста, и +пустая панель ничего бы не рассказала. Повод заглянуть — не «сколько там +сейчас», а «доехал ли мой `ON CLUSTER` до обеих нод и что ответила каждая». + +## Что проверено + +Дашборд проверен на живом стенде 21 августа 2026 года. + +- Дашборд поднимается из каталога сам, все семь панелей отвечают через + `/api/ds/query` в окне на полчаса. Пустых нет. +- Обе ноды видны на каждой панели по системной таблице. Единственная панель без + `clusterAllReplicas` — очередь DDL, и обёртка ей действительно не нужна: + запрос с ней вернул 124 записи с `clickhouse-01` и те же 124 с + `clickhouse-02`. +- Под нагрузкой. `make generate-batch` (50 626 событий одной пачкой): процессор + поднялся с 0,06–0,07 ядра до 0,37–0,38, запись — с 300 КиБ/с до 1,1 МиБ/с, + активных частей стало 145 против 131. Через полминуты после конца вставки + процессор вернулся к 0,02–0,07 ядра, части осели до 135. +- Панель частей показала `ods.order_rep` по 136 строк в части против 11 171 у + `ods.event_rep` — заказы приезжают дневным слепком, события потоком. +- Панель слияний за полчаса: `stg.hits_raw_rep` съела 430 частей, `ods.event_rep` + — 401, и рядом с ними системные журналы с сопоставимыми числами. +- Очередь DDL показала `CREATE MATERIALIZED VIEW ... ON CLUSTER` в состоянии + `Finished` и временем по каждой ноде: `make generate-batch` поднимает + `clickhouse-init`, и тот заново прогоняет DDL. + +Подписи рядов (`${__field.labels.node}`) и правило, по которому пунктирной +становится только «коробка», сверены по документации Grafana через MCP +Context7. Оба применяются на стороне браузера, и через `/api/ds/query` их не +видно: там проверено, что нода приезжает меткой ряда, а метрика — его именем. diff --git a/docs/architecture/monitoring/data.md b/docs/architecture/monitoring/data.md index d57be99..72e1b1b 100644 --- a/docs/architecture/monitoring/data.md +++ b/docs/architecture/monitoring/data.md @@ -4,9 +4,6 @@ экран. Вопрос у экрана один: с потоком всё в порядке? Состав панелей и довод за каждую — [ADR 0002](../../adr/0002-monitoring-scope.md). -Порогов и цветовых вердиктов на панелях нет: на дашборд смотрят глазами, -красное и зелёное живёт в проверках и дагах DQ. - Свой вопрос каждая панель называет заголовком прямо на экране. Здесь — только источники, о них и речь ниже. diff --git a/docs/architecture/monitoring/queries.md b/docs/architecture/monitoring/queries.md new file mode 100644 index 0000000..43e5bb0 --- /dev/null +++ b/docs/architecture/monitoring/queries.md @@ -0,0 +1,71 @@ +# Дашборд «Запросы» + +`infra/grafana/provisioning/dashboards/queries.json` — четыре панели поверх +одной таблицы, `system.query_log`. Вопрос у экрана один: что выполнялось и во +что это обошлось? Состав панелей и довод за каждую — +[ADR 0002](../../adr/0002-monitoring-scope.md). + +Свой вопрос каждая панель называет заголовком прямо на экране. Здесь — только +то, чего в заголовке не скажешь. + +Обе ноды пишут журнал каждая свой, поэтому все четыре панели читают его через +`clusterAllReplicas` — то же правило, что и на [«Кластере»](cluster.md). + +Окно — час, обновление — полминуты. Час взят потому, что сюда приходят с +вопросом о прошлом: «почему у меня всё стояло десять минут назад». Полминуты — +чтобы только что запущенный тяжёлый запрос не пришлось ждать: в журнал он +попадает через несколько секунд после конца. + +## Первым в списке видно сам мониторинг + +Grafana ходит в ClickHouse обычным пользователем, и её запросы ложатся в +журнал наравне с человеческими. На спокойном стенде верх списка тяжёлых +запросов занимает `grafana`: панель свежести с дашборда «Данные» читает по +полмиллиона строк раз в минуту. + +Это не помеха, а первое, что дашборд рассказывает: мониторинг тоже стоит +денег, и цену видно там же, где и всё остальное. Скрывать свои запросы +фильтром означало бы прятать эту цену — и заодно сделать панель «Кто +спрашивает» неполной ровно на самого частого посетителя. + +Различать пользователей дашборд умеет потому, что у Grafana своя учётка со +своей ролью ([ADR 0007](../../adr/0007-clickhouse-access.md)). Ходи она под +аналитиком, панель «Кто спрашивает» показывала бы одну строку на всех. + +## Что отброшено и почему + +Каждый запрос попадает в журнал дважды: строкой начала и строкой конца. В +строке начала нет ни длительности, ни памяти, ни прочитанных строк — там их +ещё неоткуда взять. Панели считают по строкам конца. + +Распределённый запрос кладёт в журнал не только себя. Инициатор пишет свою +строку, а каждый шард — свою, про доставшийся ему кусок. Три панели из четырёх +считают только запросы инициатора: иначе один запрос человека выглядел бы +тремя, а прочитанные строки сложились бы дважды. + +Панель ошибок, наоборот, куски шардов показывает. Отказ по памяти случается +там, где считали, — на шарде; инициатор в своей строке передаст только текст +ошибки, а нода, на которой всё случилось, останется неназванной. + +## Пустой пользователь + +Часть строк журнала приходит без имени пользователя: так записана собственная +работа сервера — чтение топиков движком Kafka, вставки материализованных +представлений, обслуживание. Панели подписывают такие строки «фоновая работа +сервера»: пустая клетка в таблице читается как поломка, а это не она. + +## Что проверено + +Дашборд проверен на живом стенде 21 августа 2026 года. + +- Дашборд поднимается из каталога сам, все четыре панели отвечают через + `/api/ds/query` в окне на час. Пустых нет. +- Тяжёлый запрос виден. `GROUP BY` по `ods.event_dist` от учётки `analyst` + приехал в панель со своими числами: 741 мс, 53,32 МиБ памяти, 701 994 + прочитанные строки. +- Ошибки видны, и обе стороны различаются: отказ по правам (`bi` без `SELECT` + на `ods`) и ошибка в тексте запроса (несуществующая колонка) легли в панель + разными строками с полным текстом обеих. +- Панель «Кто спрашивает» разделила семь посетителей: `grafana`, `analyst`, + `etl`, `dict`, `bi`, `default` и фоновую работу сервера. Больше всех + прочитала `grafana` — её же панели, 11,5 млн строк за час. diff --git a/infra/grafana/provisioning/dashboards/cluster.json b/infra/grafana/provisioning/dashboards/cluster.json new file mode 100644 index 0000000..8653069 --- /dev/null +++ b/infra/grafana/provisioning/dashboards/cluster.json @@ -0,0 +1,316 @@ +{ + "uid": "cluster", + "title": "Кластер", + "timezone": "utc", + "schemaVersion": 42, + "refresh": "30s", + "time": { + "from": "now-30m", + "to": "now" + }, + "panels": [ + { + "type": "timeseries", + "title": "Процессор: сколько ядер занимает нода", + "datasource": { + "type": "grafana-clickhouse-datasource", + "uid": "clickhouse" + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 0, + "y": 0 + }, + "options": { + "tooltip": { + "mode": "multi" + } + }, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "grafana-clickhouse-datasource", + "uid": "clickhouse" + }, + "editorType": "sql", + "queryType": "timeseries", + "format": 0, + "rawSql": "SELECT\n $__timeInterval(event_time) AS t,\n hostName() AS node,\n -- Процессорное время самого сервера. Метрики с именами OS* сняты с машины\n -- целиком, и в них обе ноды показывали бы одно и то же число.\n sum(ProfileEvent_OSCPUVirtualTimeMicroseconds) / 1e6 / $__interval_s AS \"ядер\"\nFROM clusterAllReplicas('clickstream_cluster', system.metric_log)\nWHERE $__timeFilter(event_time)\nGROUP BY t, node\nORDER BY t, node" + } + ], + "fieldConfig": { + "defaults": { + "displayName": "${__field.labels.node}", + "min": 0, + "decimals": 2 + }, + "overrides": [] + } + }, + { + "type": "timeseries", + "title": "Память: сколько нода занимает и сколько ей отведено", + "datasource": { + "type": "grafana-clickhouse-datasource", + "uid": "clickhouse" + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 8, + "y": 0 + }, + "options": { + "tooltip": { + "mode": "multi" + } + }, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "grafana-clickhouse-datasource", + "uid": "clickhouse" + }, + "editorType": "sql", + "queryType": "timeseries", + "format": 0, + "rawSql": "SELECT\n $__timeInterval(event_time) AS t,\n hostName() AS node,\n -- Занятое меряется резидентной памятью: именно её ядро сравнивает с\n -- пределом контейнера, когда решает, убить ли ноду.\n maxIf(value, metric = 'MemoryResident') AS \"занято\",\n maxIf(value, metric = 'CGroupMemoryTotal') AS \"коробка\"\nFROM clusterAllReplicas('clickstream_cluster', system.asynchronous_metric_log)\nWHERE $__timeFilter(event_time)\n AND metric IN ('MemoryResident', 'CGroupMemoryTotal')\nGROUP BY t, node\nORDER BY t, node" + } + ], + "fieldConfig": { + "defaults": { + "displayName": "${__field.name} · ${__field.labels.node}", + "min": 0, + "unit": "bytes" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "коробка.*" + }, + "properties": [ + { + "id": "custom.lineStyle", + "value": { + "fill": "dash", + "dash": [ + 10, + 10 + ] + } + }, + { + "id": "custom.fillOpacity", + "value": 0 + } + ] + } + ] + } + }, + { + "type": "timeseries", + "title": "Диски: сколько нода читает и пишет", + "datasource": { + "type": "grafana-clickhouse-datasource", + "uid": "clickhouse" + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 16, + "y": 0 + }, + "options": { + "tooltip": { + "mode": "multi" + } + }, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "grafana-clickhouse-datasource", + "uid": "clickhouse" + }, + "editorType": "sql", + "queryType": "timeseries", + "format": 0, + "rawSql": "SELECT\n $__timeInterval(event_time) AS t,\n hostName() AS node,\n -- Байты, прошедшие через обращения к файлам: чтение из страничного кэша\n -- тоже видно. Счётчики блочного устройства на вставке молчат.\n sum(ProfileEvent_OSReadChars) / $__interval_s AS \"чтение\",\n sum(ProfileEvent_OSWriteChars) / $__interval_s AS \"запись\"\nFROM clusterAllReplicas('clickstream_cluster', system.metric_log)\nWHERE $__timeFilter(event_time)\nGROUP BY t, node\nORDER BY t, node" + } + ], + "fieldConfig": { + "defaults": { + "displayName": "${__field.name} · ${__field.labels.node}", + "min": 0, + "unit": "Bps" + }, + "overrides": [] + } + }, + { + "type": "table", + "title": "Части: сколько их у таблицы и по сколько строк в каждой", + "datasource": { + "type": "grafana-clickhouse-datasource", + "uid": "clickhouse" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 8 + }, + "options": {}, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "grafana-clickhouse-datasource", + "uid": "clickhouse" + }, + "editorType": "sql", + "queryType": "table", + "format": 1, + "rawSql": "SELECT\n concat(database, '.', table) AS \"Таблица\",\n hostName() AS \"Нода\",\n count() AS \"Частей\",\n sum(rows) AS \"Строк\",\n round(sum(rows) / count()) AS \"Строк в части\",\n sum(bytes_on_disk) AS \"На диске\"\nFROM clusterAllReplicas('clickstream_cluster', system.parts)\nWHERE active\nGROUP BY \"Таблица\", \"Нода\"\nORDER BY \"Частей\" DESC, \"Таблица\"\nLIMIT 15" + } + ], + "fieldConfig": { + "defaults": {}, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "На диске" + }, + "properties": [ + { + "id": "unit", + "value": "bytes" + } + ] + } + ] + } + }, + { + "type": "table", + "title": "Слияния: сколько частей они съели и чего это стоило", + "datasource": { + "type": "grafana-clickhouse-datasource", + "uid": "clickhouse" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 8 + }, + "options": {}, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "grafana-clickhouse-datasource", + "uid": "clickhouse" + }, + "editorType": "sql", + "queryType": "table", + "format": 1, + "rawSql": "SELECT\n concat(database, '.', table) AS \"Таблица\",\n hostName() AS \"Нода\",\n sum(length(merged_from)) AS \"Частей съело\",\n count() AS \"Слияний\",\n sum(rows) AS \"Строк переварило\",\n round(sum(duration_ms) / 1000, 1) AS \"Заняло, с\",\n max(peak_memory_usage) AS \"Пиковая память\"\nFROM clusterAllReplicas('clickstream_cluster', system.part_log)\nWHERE $__timeFilter(event_time)\n AND event_type = 'MergeParts'\nGROUP BY \"Таблица\", \"Нода\"\nORDER BY \"Частей съело\" DESC, \"Таблица\"\nLIMIT 15" + } + ], + "fieldConfig": { + "defaults": {}, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Пиковая память" + }, + "properties": [ + { + "id": "unit", + "value": "bytes" + } + ] + } + ] + } + }, + { + "type": "timeseries", + "title": "Keeper: сколько узлов в дереве", + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 16 + }, + "options": { + "tooltip": { + "mode": "multi" + } + }, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "ClickHouseAsyncMetrics_KeeperZnodeCount", + "legendFormat": "узлов в дереве", + "range": true + } + ], + "fieldConfig": { + "defaults": { + "min": 0 + }, + "overrides": [] + } + }, + { + "type": "table", + "title": "Распределённый DDL: что прошло по кластеру и чем кончилось", + "datasource": { + "type": "grafana-clickhouse-datasource", + "uid": "clickhouse" + }, + "gridPos": { + "h": 7, + "w": 16, + "x": 8, + "y": 16 + }, + "options": {}, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "grafana-clickhouse-datasource", + "uid": "clickhouse" + }, + "editorType": "sql", + "queryType": "table", + "format": 1, + "rawSql": "SELECT\n query_create_time AS \"Создан\",\n entry AS \"Запись\",\n host AS \"Нода\",\n status AS \"Состояние\",\n query_duration_ms AS \"Заняло, мс\",\n exception_text AS \"Ошибка\",\n query AS \"Запрос\"\nFROM system.distributed_ddl_queue\nORDER BY entry DESC, \"Нода\"\nLIMIT 20" + } + ], + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + ] +} diff --git a/infra/grafana/provisioning/dashboards/queries.json b/infra/grafana/provisioning/dashboards/queries.json new file mode 100644 index 0000000..871cdf3 --- /dev/null +++ b/infra/grafana/provisioning/dashboards/queries.json @@ -0,0 +1,173 @@ +{ + "uid": "queries", + "title": "Запросы", + "timezone": "utc", + "schemaVersion": 42, + "refresh": "30s", + "time": { + "from": "now-1h", + "to": "now" + }, + "panels": [ + { + "type": "table", + "title": "Тяжёлые запросы: что прочитало больше всех", + "datasource": { + "type": "grafana-clickhouse-datasource", + "uid": "clickhouse" + }, + "gridPos": { + "h": 10, + "w": 24, + "x": 0, + "y": 0 + }, + "options": {}, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "grafana-clickhouse-datasource", + "uid": "clickhouse" + }, + "editorType": "sql", + "queryType": "table", + "format": 1, + "rawSql": "SELECT\n event_time AS \"Когда\",\n hostName() AS \"Нода\",\n if(empty(user), 'фоновая работа сервера', user) AS \"Кто\",\n query_duration_ms AS \"Заняло, мс\",\n memory_usage AS \"Память\",\n read_rows AS \"Прочитано строк\",\n result_rows AS \"Отдано строк\",\n query AS \"Запрос\"\nFROM clusterAllReplicas('clickstream_cluster', system.query_log)\nWHERE $__timeFilter(event_time)\n -- Начало запроса записано отдельной строкой, и в ней ещё нет ни времени,\n -- ни памяти. Куски распределённого запроса отброшены: считает их тот, кто\n -- запрос затеял.\n AND type != 'QueryStart'\n AND is_initial_query\nORDER BY \"Прочитано строк\" DESC\nLIMIT 20" + } + ], + "fieldConfig": { + "defaults": {}, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Память" + }, + "properties": [ + { + "id": "unit", + "value": "bytes" + } + ] + } + ] + } + }, + { + "type": "timeseries", + "title": "Поток запросов: сколько выполнено и сколько упало", + "datasource": { + "type": "grafana-clickhouse-datasource", + "uid": "clickhouse" + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 0, + "y": 10 + }, + "options": { + "tooltip": { + "mode": "multi" + } + }, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "grafana-clickhouse-datasource", + "uid": "clickhouse" + }, + "editorType": "sql", + "queryType": "timeseries", + "format": 0, + "rawSql": "SELECT\n $__timeInterval(event_time) AS t,\n countIf(type = 'QueryFinish') AS \"выполнено\",\n countIf(type IN ('ExceptionBeforeStart', 'ExceptionWhileProcessing')) AS \"упало\"\nFROM clusterAllReplicas('clickstream_cluster', system.query_log)\nWHERE $__timeFilter(event_time)\n AND is_initial_query\nGROUP BY t\nORDER BY t" + } + ], + "fieldConfig": { + "defaults": { + "min": 0 + }, + "overrides": [] + } + }, + { + "type": "table", + "title": "Кто спрашивает: сколько запросов и какой у них аппетит", + "datasource": { + "type": "grafana-clickhouse-datasource", + "uid": "clickhouse" + }, + "gridPos": { + "h": 8, + "w": 16, + "x": 8, + "y": 10 + }, + "options": {}, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "grafana-clickhouse-datasource", + "uid": "clickhouse" + }, + "editorType": "sql", + "queryType": "table", + "format": 1, + "rawSql": "SELECT\n if(empty(user), 'фоновая работа сервера', user) AS \"Кто\",\n count() AS \"Запросов\",\n sum(read_rows) AS \"Прочитано строк\",\n max(read_rows) AS \"Самый читающий, строк\",\n max(memory_usage) AS \"Самый прожорливый, память\",\n round(quantile(0.9)(query_duration_ms)) AS \"Девять из десяти быстрее, мс\"\nFROM clusterAllReplicas('clickstream_cluster', system.query_log)\nWHERE $__timeFilter(event_time)\n AND type != 'QueryStart'\n AND is_initial_query\nGROUP BY \"Кто\"\nORDER BY \"Прочитано строк\" DESC" + } + ], + "fieldConfig": { + "defaults": {}, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Самый прожорливый, память" + }, + "properties": [ + { + "id": "unit", + "value": "bytes" + } + ] + } + ] + } + }, + { + "type": "table", + "title": "Ошибки: на чём запросы споткнулись", + "datasource": { + "type": "grafana-clickhouse-datasource", + "uid": "clickhouse" + }, + "gridPos": { + "h": 7, + "w": 24, + "x": 0, + "y": 18 + }, + "options": {}, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "grafana-clickhouse-datasource", + "uid": "clickhouse" + }, + "editorType": "sql", + "queryType": "table", + "format": 1, + "rawSql": "SELECT\n event_time AS \"Когда\",\n hostName() AS \"Нода\",\n if(empty(user), 'фоновая работа сервера', user) AS \"Кто\",\n exception AS \"Чем кончилось\",\n query AS \"Запрос\"\nFROM clusterAllReplicas('clickstream_cluster', system.query_log)\nWHERE $__timeFilter(event_time)\n -- Здесь куски распределённого запроса нужны: отказ по памяти случается\n -- на шарде, а не у того, кто спрашивал.\n AND type IN ('ExceptionBeforeStart', 'ExceptionWhileProcessing')\nORDER BY \"Когда\" DESC\nLIMIT 20" + } + ], + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + ] +} -- 2.54.0 From 42075ff9fb6616f4d29c361525844d21ec43df36 Mon Sep 17 00:00:00 2001 From: Dmitry Dementev Date: Fri, 21 Aug 2026 15:11:21 -0400 Subject: [PATCH 2/3] =?UTF-8?q?fix(monitoring):=20=D0=BF=D0=B0=D0=BD=D0=B5?= =?UTF-8?q?=D0=BB=D0=B8=20=D1=83=D1=82=D0=BE=D1=87=D0=BD=D0=B5=D0=BD=D1=8B?= =?UTF-8?q?=20=D0=BF=D0=BE=20=D1=80=D0=B5=D0=B2=D1=8C=D1=8E,=20=D0=BF?= =?UTF-8?q?=D1=80=D0=B8=D1=91=D0=BC=D0=BA=D0=B0=20=D1=81=D0=BD=D1=8F=D1=82?= =?UTF-8?q?=D0=B0=20=D0=BF=D0=BE=D0=B4=20=D0=BD=D0=B0=D0=B3=D1=80=D1=83?= =?UTF-8?q?=D0=B7=D0=BA=D0=BE=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - слияния в верхних строках показывали только журналы самого сервера, а предел в 15 строк рвал пару нод пополам: панели отвечали не на свой вопрос. - Что: - «Слияния» стали столбиками по слоям: журналы сервера отделены, ряды ods и stg видны своими. - «Части» показывают все таблицы слоёв целиком, без предела и без обрезанных пар. - у «Запросов» убран «Поток запросов» — его пол составляли пробники состояния; у «Кто спрашивает» убрана лишняя колонка. - ряд «коробка» назван «отведено» — как в заголовке панели. - доводы дописаны в cluster.md и queries.md, отступления от ADR 0002 записаны в сам ADR. - Проверка: - make config-test; make smoke 21/21; make check-clickhouse 10/10. - все десять панелей отвечают через /api/ds/query в окнах на полчаса, час и шесть часов. --- docs/adr/0002-monitoring-scope.md | 12 +- docs/architecture/monitoring/README.md | 4 +- docs/architecture/monitoring/cluster.md | 110 ++++++++++-------- docs/architecture/monitoring/queries.md | 41 ++++--- .../provisioning/dashboards/cluster.json | 47 ++++---- .../provisioning/dashboards/queries.json | 46 +------- 6 files changed, 122 insertions(+), 138 deletions(-) diff --git a/docs/adr/0002-monitoring-scope.md b/docs/adr/0002-monitoring-scope.md index fb1f2c2..5bf6767 100644 --- a/docs/adr/0002-monitoring-scope.md +++ b/docs/adr/0002-monitoring-scope.md @@ -127,6 +127,14 @@ ClickHouse, Kafka и Airflow, 17 — в дашборде генератора, 1 keeper, и обе ноды читают её целиком — запрос вернул с каждой одни и те же 124 записи. Обёртка там не добавила бы правды, а удвоила бы каждую строку, поэтому единственная панель «Кластера» без неё — эта. Keeper своих системных таблиц не -имеет вовсе, и панель по нему приходит из Prometheus. Устройство обоих -дашбордов — [cluster.md](../architecture/monitoring/cluster.md) и +имеет вовсе, и панель по нему приходит из Prometheus. + +«Запросы» получили на две панели больше, чем сказано выше. Разбор запросов по +посетителям продиктован [ADR 0007](0007-clickhouse-access.md): своя учётка +Grafana заведена там ровно затем, чтобы её запросы были отличимы от +человеческих. +Отдельная панель ошибок понадобилась под здешний же сюжет про тяжёлый +`GROUP BY`: отказ по памяти случается на шарде, и текст ошибки — единственное +место, где написано, на каком. Устройство обоих дашбордов — +[cluster.md](../architecture/monitoring/cluster.md) и [queries.md](../architecture/monitoring/queries.md). diff --git a/docs/architecture/monitoring/README.md b/docs/architecture/monitoring/README.md index f671e32..69a806d 100644 --- a/docs/architecture/monitoring/README.md +++ b/docs/architecture/monitoring/README.md @@ -81,8 +81,8 @@ Grafana ходит пользователем `grafana` с ролью `monitorin обеих нод, части и слияния, keeper, очередь распределённых DDL. Устройство и разбор панелей — [cluster.md](cluster.md). - **«Запросы»** (`queries.json`) — что выполнялось и во что обошлось: тяжёлые - запросы, поток запросов, аппетит по пользователям, ошибки. Устройство и - разбор панелей — [queries.md](queries.md). + запросы, аппетит по посетителям, ошибки. Устройство и разбор панелей — + [queries.md](queries.md). ## Что проверено diff --git a/docs/architecture/monitoring/cluster.md b/docs/architecture/monitoring/cluster.md index f0dd258..69a263e 100644 --- a/docs/architecture/monitoring/cluster.md +++ b/docs/architecture/monitoring/cluster.md @@ -14,13 +14,18 @@ | Диски | `system.metric_log` | | Части | `system.parts` | | Слияния | `system.part_log` | -| Keeper | экспортёр keeper через Prometheus | +| Keeper | встроенные метрики keeper через Prometheus | | Распределённый DDL | `system.distributed_ddl_queue` | Окно у дашборда — полчаса, обновление — полминуты, и это выбрано под нагрузку. -Вставка на стенде занимает секунды, а оседание после слияний — минуту-другую; -в часовом окне и то и другое схлопнулось бы в один столбик. Урок здесь — форма -кривой, и окно подобрано так, чтобы форма была видна. +Вставка на стенде занимает секунды, а слияния разбирают её следы за минуту-две; +в часовом окне и подъём, и оседание схлопнулись бы в один столбик. Урок здесь — +форма кривой, и окно подобрано так, чтобы форма была видна. + +Две панели окна не знают, и обе по делу. «Части» показывают состояние сейчас: +`system.parts` — опись живых частей, времени в ней нет вовсе. Очередь DDL +показывает последние двадцать записей: в спокойные полчаса она пуста, а пустая +панель ничего бы не рассказала. ## Обе ноды на одной панели @@ -39,24 +44,25 @@ ## Пол на графиках — журналы самого сервера ClickHouse ведёт около десятка собственных журналов: `metric_log`, -`trace_log`, `text_log`, `query_log` и соседние. Все они — таблицы MergeTree, и -пишутся они постоянно, даже когда на стенде не происходит ничего. Отсюда три -следствия, которые иначе выглядят поломкой: +`trace_log`, `text_log`, `query_log` и соседние. Все они — таблицы MergeTree: +их пишут и сливают постоянно, даже когда на стенде не происходит ничего. +Отсюда процессор и диски, которые никогда не падают в ноль. -- процессор и диски никогда не падают в ноль; -- новых частей рождается около сотни в минуту на ноду, и большинство из них — - системные; -- в списке слияний системные таблицы стоят рядом со своими, а по потраченному - времени часто и выше. +Вычесть этот пол на процессоре и дисках нечем: их счётчики знают, сколько +работы сделал процесс, и не знают, ради какой таблицы. Своя нагрузка на этих +двух панелях читается как подъём над полом, а не как число само по себе. -Мера отсюда простая: своя нагрузка читается как подъём над полом, а не как -число само по себе. +У частей и слияний пол отделим: в `system.parts` и `system.part_log` у каждой +строки написано, какой таблице она принадлежит. Обе панели поэтому спрашивают +только про слои стенда. Довод в размере: за спокойные полчаса слияния съели +6 492 части в системных журналах против 219 в `ods` и `stg` вместе. Оставь мы +журналы в кадре, ряды слоёв прижались бы к нулю, и панель рассказывала бы про +уборку за сервером вместо ваших вставок. Сколько эта уборка стоит, видно этажом +выше, на процессоре и дисках. -Панели этот пол не вычитают, и не по недосмотру. Процессор и диски меряются -счётчиками процесса — в них нет колонки «чья это работа», и вычесть журналы -оттуда нечем. Отфильтруй мы `system` в тех двух панелях, где это технически -возможно, они разошлись бы с остальными, и разбираться пришлось бы уже с -расхождением. +Слияния показаны по слоям, а не по нодам и не по таблицам. Шарды сливают +симметрично, и второй ряд на слой удвоил бы легенду, ничего не добавив. Какая +именно таблица держит части, отвечает соседняя панель. ## Что именно меряют процессор, память и диски @@ -69,18 +75,18 @@ ClickHouse ведёт около десятка собственных журн своей (`MemoryTracking`). Разница не косметическая: на праздной ноде `MemoryTracking` — 113 МиБ при 846 МиБ резидентной памяти, а ядро, решая, кого убить за нехватку памяти, смотрит на вторую величину -([ADR 0004](../../adr/0004-resource-limits.md)). Панель по первой показывала бы -здоровый вид у ноды, которую вот-вот убьют. +([ADR 0004](../../adr/0004-resource-limits.md)). По первой нода, которую +вот-вот убьют, выглядит совершенно здоровой. -Пунктирная «коробка» рядом — предел памяти контейнера, и он тоже читается -живьём, метрикой `CGroupMemoryTotal`. Впиши мы 4 ГиБ числом в JSON, панель -молча соврала бы в тот день, когда предел поменяют. +Пунктирный ряд «отведено» — предел памяти контейнера, та самая коробка из +ADR 0004. Он тоже читается живьём, метрикой `CGroupMemoryTotal`: впиши мы +4 ГиБ числом в JSON, панель молча соврала бы в тот день, когда предел поменяют. -Диски меряются обращениями к файлам (`OSReadChars`, `OSWriteChars`), а не -блоками, дошедшими до устройства (`OSReadBytes`). Блочное чтение на стенде -почти всегда ноль: части лежат в страничном кэше, и слияние перечитывает их -оттуда. Работа при этом делается настоящая, и панель по блокам показывала бы, -что её нет. +У дисков считаются обращения к файлам — `OSReadChars` и `OSWriteChars`. +Счётчики блочного устройства (`OSReadBytes`) для той же панели не годятся: +блочное чтение на стенде почти всё время нулевое, части лежат в страничном +кэше, и слияние перечитывает их оттуда. Работа при этом идёт настоящая, и +панель по блокам показывала бы её рваным пунктиром. ## Keeper и очередь DDL — то, что живёт вне нод @@ -90,14 +96,12 @@ Keeper — отдельный сервис, системных таблиц у Показывает она размер дерева. В keeper лежит журнал репликации, а в нём — запись на каждую часть: пока поток идёт, дерево растёт, а когда части домержились и старые записи убраны, оседает. Замер за полчаса живого потока: -9 078 узлов на подъёме, 11 512 на пике, 10 308 через десять минут после -остановки потока. +9 078 узлов на подъёме, 11 512 на пике, 10 308 через десять минут после того, +как поток встал. Очередь распределённых DDL — вторая половина того же сюжета: через keeper -проходит каждый запрос `ON CLUSTER`. Панель показывает последние двадцать -записей независимо от окна дашборда: в спокойные полчаса очередь пуста, и -пустая панель ничего бы не рассказала. Повод заглянуть — не «сколько там -сейчас», а «доехал ли мой `ON CLUSTER` до обеих нод и что ответила каждая». +проходит каждый запрос `ON CLUSTER`. Заглядывают сюда с одним вопросом: доехал +ли он до обеих нод и что ответила каждая. ## Что проверено @@ -105,23 +109,29 @@ Keeper — отдельный сервис, системных таблиц у - Дашборд поднимается из каталога сам, все семь панелей отвечают через `/api/ds/query` в окне на полчаса. Пустых нет. -- Обе ноды видны на каждой панели по системной таблице. Единственная панель без - `clusterAllReplicas` — очередь DDL, и обёртка ей действительно не нужна: - запрос с ней вернул 124 записи с `clickhouse-01` и те же 124 с +- Обе ноды видны на всех панелях по системным таблицам, кроме одной. Без + `clusterAllReplicas` осталась очередь DDL, и обёртка ей действительно не + нужна: запрос с ней вернул 124 записи с `clickhouse-01` и те же 124 с `clickhouse-02`. - Под нагрузкой. `make generate-batch` (50 626 событий одной пачкой): процессор - поднялся с 0,06–0,07 ядра до 0,37–0,38, запись — с 300 КиБ/с до 1,1 МиБ/с, - активных частей стало 145 против 131. Через полминуты после конца вставки - процессор вернулся к 0,02–0,07 ядра, части осели до 135. -- Панель частей показала `ods.order_rep` по 136 строк в части против 11 171 у - `ods.event_rep` — заказы приезжают дневным слепком, события потоком. -- Панель слияний за полчаса: `stg.hits_raw_rep` съела 430 частей, `ods.event_rep` - — 401, и рядом с ними системные журналы с сопоставимыми числами. + поднялся с 0,04–0,18 ядра до 0,54 и 0,64, запись — с 400 КиБ/с до 2,1 и + 4,5 МиБ/с. Следующие десять секунд обе величины уже вернулись к прежним: на + таком объёме подъём и оседание укладываются в полминуты. +- Слияния под той же нагрузкой отозвались столбиком: два слияния в `stg`, + 15 съеденных частей. +- Блочное чтение действительно молчит: за минуту вокруг вставки `OSReadBytes` + был нулевым в десяти десятисекундных окнах из четырнадцати, а обращения к + файлам шли непрерывно во всех. +- Память: около 1 ГиБ занятого против 4 ГиБ отведённого на каждой ноде. +- Панель частей показала `ods.order_rep` по 142 строки в части против 13 046 у + `ods.event_rep` — заказы приезжают дневным слепком, события потоком. Все + таблицы слоёв поместились целиком, каждая обеими нодами: восемь строк. - Очередь DDL показала `CREATE MATERIALIZED VIEW ... ON CLUSTER` в состоянии - `Finished` и временем по каждой ноде: `make generate-batch` поднимает + `Finished` и со временем по каждой ноде: `make generate-batch` поднимает `clickhouse-init`, и тот заново прогоняет DDL. -Подписи рядов (`${__field.labels.node}`) и правило, по которому пунктирной -становится только «коробка», сверены по документации Grafana через MCP -Context7. Оба применяются на стороне браузера, и через `/api/ds/query` их не -видно: там проверено, что нода приезжает меткой ряда, а метрика — его именем. +Подписи рядов (`${__field.labels.node}`) и правило, по которому пунктирным +становится только ряд «отведено», сверены по документации Grafana через MCP +Context7. И то и другое применяется на стороне браузера, через +`/api/ds/query` их не видно: там проверено только, что нода приезжает меткой +ряда, а метрика — его именем. diff --git a/docs/architecture/monitoring/queries.md b/docs/architecture/monitoring/queries.md index 43e5bb0..3db86fe 100644 --- a/docs/architecture/monitoring/queries.md +++ b/docs/architecture/monitoring/queries.md @@ -1,14 +1,14 @@ # Дашборд «Запросы» -`infra/grafana/provisioning/dashboards/queries.json` — четыре панели поверх -одной таблицы, `system.query_log`. Вопрос у экрана один: что выполнялось и во -что это обошлось? Состав панелей и довод за каждую — +`infra/grafana/provisioning/dashboards/queries.json` — три панели поверх одной +таблицы, `system.query_log`. Вопрос у экрана один: что выполнялось и во что это +обошлось? Состав панелей и довод за каждую — [ADR 0002](../../adr/0002-monitoring-scope.md). Свой вопрос каждая панель называет заголовком прямо на экране. Здесь — только то, чего в заголовке не скажешь. -Обе ноды пишут журнал каждая свой, поэтому все четыре панели читают его через +Журнал у каждой ноды свой, поэтому все три панели читают его через `clusterAllReplicas` — то же правило, что и на [«Кластере»](cluster.md). Окно — час, обновление — полминуты. Час взят потому, что сюда приходят с @@ -21,15 +21,18 @@ Grafana ходит в ClickHouse обычным пользователем, и её запросы ложатся в журнал наравне с человеческими. На спокойном стенде верх списка тяжёлых запросов занимает `grafana`: панель свежести с дашборда «Данные» читает по -полмиллиона строк раз в минуту. +полмиллиона строк за каждое обновление. -Это не помеха, а первое, что дашборд рассказывает: мониторинг тоже стоит -денег, и цену видно там же, где и всё остальное. Скрывать свои запросы -фильтром означало бы прятать эту цену — и заодно сделать панель «Кто -спрашивает» неполной ровно на самого частого посетителя. +Так и надо: мониторинг тоже стоит денег, и цену видно там же, где и всё +остальное. Спрячь мы свои запросы фильтром — цена ушла бы с глаз, а панель «Кто +спрашивает» недосчиталась бы самого частого посетителя. -Различать пользователей дашборд умеет потому, что у Grafana своя учётка со -своей ролью ([ADR 0007](../../adr/0007-clickhouse-access.md)). Ходи она под +Там же, в «Кто спрашивает», стоит `default` с сотнями запросов и одной +прочитанной строкой на каждый: это `SELECT 1` от проверки состояния +контейнера, она ходит к каждой ноде раз в несколько секунд. + +Различать посетителей дашборд умеет потому, что у Grafana своя учётка со своей +ролью ([ADR 0007](../../adr/0007-clickhouse-access.md)). Ходи она под аналитиком, панель «Кто спрашивает» показывала бы одну строку на всех. ## Что отброшено и почему @@ -39,7 +42,7 @@ Grafana ходит в ClickHouse обычным пользователем, и ещё неоткуда взять. Панели считают по строкам конца. Распределённый запрос кладёт в журнал не только себя. Инициатор пишет свою -строку, а каждый шард — свою, про доставшийся ему кусок. Три панели из четырёх +строку, а каждый шард — свою, про доставшийся ему кусок. Две панели из трёх считают только запросы инициатора: иначе один запрос человека выглядел бы тремя, а прочитанные строки сложились бы дважды. @@ -58,14 +61,14 @@ Grafana ходит в ClickHouse обычным пользователем, и Дашборд проверен на живом стенде 21 августа 2026 года. -- Дашборд поднимается из каталога сам, все четыре панели отвечают через +- Дашборд поднимается из каталога сам, все три панели отвечают через `/api/ds/query` в окне на час. Пустых нет. - Тяжёлый запрос виден. `GROUP BY` по `ods.event_dist` от учётки `analyst` приехал в панель со своими числами: 741 мс, 53,32 МиБ памяти, 701 994 прочитанные строки. -- Ошибки видны, и обе стороны различаются: отказ по правам (`bi` без `SELECT` - на `ods`) и ошибка в тексте запроса (несуществующая колонка) легли в панель - разными строками с полным текстом обеих. -- Панель «Кто спрашивает» разделила семь посетителей: `grafana`, `analyst`, - `etl`, `dict`, `bi`, `default` и фоновую работу сервера. Больше всех - прочитала `grafana` — её же панели, 11,5 млн строк за час. +- Ошибки видны и различаются по природе: отказ по правам (`bi` без `SELECT` на + `ods`) и ошибка в тексте запроса (несуществующая колонка) легли в панель + разными строками, каждая с полным текстом. +- Панель «Кто спрашивает» разделила шесть учёток — `grafana`, `analyst`, `etl`, + `dict`, `bi`, `default` — и фоновую работу сервера. Больше всех прочитала + `grafana`: 11,5 млн строк за час, своими же панелями. diff --git a/infra/grafana/provisioning/dashboards/cluster.json b/infra/grafana/provisioning/dashboards/cluster.json index 8653069..3e2ada0 100644 --- a/infra/grafana/provisioning/dashboards/cluster.json +++ b/infra/grafana/provisioning/dashboards/cluster.json @@ -77,7 +77,7 @@ "editorType": "sql", "queryType": "timeseries", "format": 0, - "rawSql": "SELECT\n $__timeInterval(event_time) AS t,\n hostName() AS node,\n -- Занятое меряется резидентной памятью: именно её ядро сравнивает с\n -- пределом контейнера, когда решает, убить ли ноду.\n maxIf(value, metric = 'MemoryResident') AS \"занято\",\n maxIf(value, metric = 'CGroupMemoryTotal') AS \"коробка\"\nFROM clusterAllReplicas('clickstream_cluster', system.asynchronous_metric_log)\nWHERE $__timeFilter(event_time)\n AND metric IN ('MemoryResident', 'CGroupMemoryTotal')\nGROUP BY t, node\nORDER BY t, node" + "rawSql": "SELECT\n $__timeInterval(event_time) AS t,\n hostName() AS node,\n -- Занятое меряется резидентной памятью: именно её ядро сравнивает с\n -- пределом контейнера, когда решает, убить ли ноду.\n maxIf(value, metric = 'MemoryResident') AS \"занято\",\n maxIf(value, metric = 'CGroupMemoryTotal') AS \"отведено\"\nFROM clusterAllReplicas('clickstream_cluster', system.asynchronous_metric_log)\nWHERE $__timeFilter(event_time)\n AND metric IN ('MemoryResident', 'CGroupMemoryTotal')\nGROUP BY t, node\nORDER BY t, node" } ], "fieldConfig": { @@ -90,7 +90,7 @@ { "matcher": { "id": "byRegexp", - "options": "коробка.*" + "options": "отведено.*" }, "properties": [ { @@ -176,7 +176,7 @@ "editorType": "sql", "queryType": "table", "format": 1, - "rawSql": "SELECT\n concat(database, '.', table) AS \"Таблица\",\n hostName() AS \"Нода\",\n count() AS \"Частей\",\n sum(rows) AS \"Строк\",\n round(sum(rows) / count()) AS \"Строк в части\",\n sum(bytes_on_disk) AS \"На диске\"\nFROM clusterAllReplicas('clickstream_cluster', system.parts)\nWHERE active\nGROUP BY \"Таблица\", \"Нода\"\nORDER BY \"Частей\" DESC, \"Таблица\"\nLIMIT 15" + "rawSql": "SELECT\n concat(database, '.', table) AS \"Таблица\",\n hostName() AS \"Нода\",\n count() AS \"Частей\",\n sum(rows) AS \"Строк\",\n round(sum(rows) / count()) AS \"Строк в части\",\n sum(bytes_on_disk) AS \"На диске\"\nFROM clusterAllReplicas('clickstream_cluster', system.parts)\nWHERE active\n AND database != 'system'\nGROUP BY \"Таблица\", \"Нода\"\nORDER BY \"Частей\" DESC, \"Таблица\", \"Нода\"\n" } ], "fieldConfig": { @@ -198,8 +198,8 @@ } }, { - "type": "table", - "title": "Слияния: сколько частей они съели и чего это стоило", + "type": "timeseries", + "title": "Слияния: сколько частей они съедают в каждом слое", "datasource": { "type": "grafana-clickhouse-datasource", "uid": "clickhouse" @@ -210,7 +210,11 @@ "x": 12, "y": 8 }, - "options": {}, + "options": { + "tooltip": { + "mode": "multi" + } + }, "targets": [ { "refId": "A", @@ -219,27 +223,24 @@ "uid": "clickhouse" }, "editorType": "sql", - "queryType": "table", - "format": 1, - "rawSql": "SELECT\n concat(database, '.', table) AS \"Таблица\",\n hostName() AS \"Нода\",\n sum(length(merged_from)) AS \"Частей съело\",\n count() AS \"Слияний\",\n sum(rows) AS \"Строк переварило\",\n round(sum(duration_ms) / 1000, 1) AS \"Заняло, с\",\n max(peak_memory_usage) AS \"Пиковая память\"\nFROM clusterAllReplicas('clickstream_cluster', system.part_log)\nWHERE $__timeFilter(event_time)\n AND event_type = 'MergeParts'\nGROUP BY \"Таблица\", \"Нода\"\nORDER BY \"Частей съело\" DESC, \"Таблица\"\nLIMIT 15" + "queryType": "timeseries", + "format": 0, + "rawSql": "SELECT\n $__timeInterval(event_time) AS t,\n database AS db,\n sum(length(merged_from)) AS \"частей съедено\"\nFROM clusterAllReplicas('clickstream_cluster', system.part_log)\nWHERE $__timeFilter(event_time)\n AND event_type = 'MergeParts'\n AND database != 'system'\nGROUP BY t, db\nORDER BY t, db" } ], "fieldConfig": { - "defaults": {}, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "Пиковая память" - }, - "properties": [ - { - "id": "unit", - "value": "bytes" - } - ] + "defaults": { + "displayName": "${__field.labels.db}", + "min": 0, + "custom": { + "drawStyle": "bars", + "fillOpacity": 60, + "stacking": { + "mode": "normal" + } } - ] + }, + "overrides": [] } }, { diff --git a/infra/grafana/provisioning/dashboards/queries.json b/infra/grafana/provisioning/dashboards/queries.json index 871cdf3..daf9e23 100644 --- a/infra/grafana/provisioning/dashboards/queries.json +++ b/infra/grafana/provisioning/dashboards/queries.json @@ -54,44 +54,6 @@ ] } }, - { - "type": "timeseries", - "title": "Поток запросов: сколько выполнено и сколько упало", - "datasource": { - "type": "grafana-clickhouse-datasource", - "uid": "clickhouse" - }, - "gridPos": { - "h": 8, - "w": 8, - "x": 0, - "y": 10 - }, - "options": { - "tooltip": { - "mode": "multi" - } - }, - "targets": [ - { - "refId": "A", - "datasource": { - "type": "grafana-clickhouse-datasource", - "uid": "clickhouse" - }, - "editorType": "sql", - "queryType": "timeseries", - "format": 0, - "rawSql": "SELECT\n $__timeInterval(event_time) AS t,\n countIf(type = 'QueryFinish') AS \"выполнено\",\n countIf(type IN ('ExceptionBeforeStart', 'ExceptionWhileProcessing')) AS \"упало\"\nFROM clusterAllReplicas('clickstream_cluster', system.query_log)\nWHERE $__timeFilter(event_time)\n AND is_initial_query\nGROUP BY t\nORDER BY t" - } - ], - "fieldConfig": { - "defaults": { - "min": 0 - }, - "overrides": [] - } - }, { "type": "table", "title": "Кто спрашивает: сколько запросов и какой у них аппетит", @@ -101,8 +63,8 @@ }, "gridPos": { "h": 8, - "w": 16, - "x": 8, + "w": 24, + "x": 0, "y": 10 }, "options": {}, @@ -116,7 +78,7 @@ "editorType": "sql", "queryType": "table", "format": 1, - "rawSql": "SELECT\n if(empty(user), 'фоновая работа сервера', user) AS \"Кто\",\n count() AS \"Запросов\",\n sum(read_rows) AS \"Прочитано строк\",\n max(read_rows) AS \"Самый читающий, строк\",\n max(memory_usage) AS \"Самый прожорливый, память\",\n round(quantile(0.9)(query_duration_ms)) AS \"Девять из десяти быстрее, мс\"\nFROM clusterAllReplicas('clickstream_cluster', system.query_log)\nWHERE $__timeFilter(event_time)\n AND type != 'QueryStart'\n AND is_initial_query\nGROUP BY \"Кто\"\nORDER BY \"Прочитано строк\" DESC" + "rawSql": "SELECT\n if(empty(user), 'фоновая работа сервера', user) AS \"Кто\",\n count() AS \"Запросов\",\n sum(read_rows) AS \"Прочитано строк\",\n max(memory_usage) AS \"Самый прожорливый, память\",\n round(quantile(0.9)(query_duration_ms)) AS \"Девять из десяти быстрее, мс\"\nFROM clusterAllReplicas('clickstream_cluster', system.query_log)\nWHERE $__timeFilter(event_time)\n AND type != 'QueryStart'\n AND is_initial_query\nGROUP BY \"Кто\"\nORDER BY \"Прочитано строк\" DESC" } ], "fieldConfig": { @@ -145,7 +107,7 @@ "uid": "clickhouse" }, "gridPos": { - "h": 7, + "h": 8, "w": 24, "x": 0, "y": 18 -- 2.54.0 From 48bec014cf98717a50fec41d9776638e78f7a727 Mon Sep 17 00:00:00 2001 From: Dmitry Dementev Date: Fri, 21 Aug 2026 15:28:03 -0400 Subject: [PATCH 3/3] =?UTF-8?q?fix(monitoring):=20=D0=B4=D0=BE=D0=B2=D0=BE?= =?UTF-8?q?=D0=B4=D1=8B=20=D0=BF=D1=80=D0=B8=D0=B2=D0=B5=D0=B4=D0=B5=D0=BD?= =?UTF-8?q?=D1=8B=20=D0=BA=20=D0=B7=D0=B0=D0=BC=D0=B5=D1=80=D0=B0=D0=BC,?= =?UTF-8?q?=20=D0=BF=D0=B0=D1=80=D1=8B=20=D0=BD=D0=BE=D0=B4=20=D0=B1=D0=BE?= =?UTF-8?q?=D0=BB=D1=8C=D1=88=D0=B5=20=D0=BD=D0=B5=20=D1=80=D0=B0=D1=81?= =?UTF-8?q?=D1=85=D0=BE=D0=B4=D1=8F=D1=82=D1=81=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - второй круг ревью нашёл, что текст спорит с собственной приёмкой: окно держалось доводом «слияния за минуту-две», а замер даёт двадцать секунд; довод панели ошибок опирался на отказ по памяти, которого ADR 0004 на этом стенде не обещает. - Что: - «Части» сортируются по таблице: сортировка по числу частей разводила две ноды одной таблицы, как только между ними вставала чужая. - у «Слияний» столбик шириной в минуту, иначе редкие события выходили волосяной линией; из заголовка убран «слой» — фильтр отсекает журналы сервера, а не выбирает слои. - в cluster.md исправлены довод за окно, ложное «времени в system.parts нет», счёт панелей без нод (их две, не одна) и разрешение, в котором снят замер процессора. - панель ошибок и её запись в ADR 0002 переписаны под проверенное: отказ на шарде воспроизведён принудительным пределом памяти. - из queries.md убран абзац про SELECT 1 — он объяснял пол удалённой панели. - Проверка: - make config-test; make smoke 21/21; make check-clickhouse 10/10. - все десять панелей отвечают через /api/ds/query в окнах на полчаса, час и шесть часов; шаг столбика слияний проверен отдельным запросом. --- docs/adr/0002-monitoring-scope.md | 9 ++- docs/architecture/monitoring/cluster.md | 77 +++++++++++-------- docs/architecture/monitoring/queries.md | 16 ++-- .../provisioning/dashboards/cluster.json | 7 +- 4 files changed, 66 insertions(+), 43 deletions(-) diff --git a/docs/adr/0002-monitoring-scope.md b/docs/adr/0002-monitoring-scope.md index 5bf6767..5256ad3 100644 --- a/docs/adr/0002-monitoring-scope.md +++ b/docs/adr/0002-monitoring-scope.md @@ -133,8 +133,11 @@ keeper, и обе ноды читают её целиком — запрос в посетителям продиктован [ADR 0007](0007-clickhouse-access.md): своя учётка Grafana заведена там ровно затем, чтобы её запросы были отличимы от человеческих. -Отдельная панель ошибок понадобилась под здешний же сюжет про тяжёлый -`GROUP BY`: отказ по памяти случается на шарде, и текст ошибки — единственное -место, где написано, на каком. Устройство обоих дашбордов — +Отдельная панель ошибок понадобилась потому, что отказ достаётся шарду: в +замере с принудительным пределом памяти каждая нода записала свой, и строка +шарда видна только без фильтра по инициатору. Здешний сюжет про тяжёлый +`GROUP BY` при этом кончается не отказом, а сбросом на диск +([ADR 0004](0004-resource-limits.md)) — отказ на стенде приходится вызывать +настройкой. Устройство обоих дашбордов — [cluster.md](../architecture/monitoring/cluster.md) и [queries.md](../architecture/monitoring/queries.md). diff --git a/docs/architecture/monitoring/cluster.md b/docs/architecture/monitoring/cluster.md index 69a263e..5fe9547 100644 --- a/docs/architecture/monitoring/cluster.md +++ b/docs/architecture/monitoring/cluster.md @@ -17,14 +17,17 @@ | Keeper | встроенные метрики keeper через Prometheus | | Распределённый DDL | `system.distributed_ddl_queue` | -Окно у дашборда — полчаса, обновление — полминуты, и это выбрано под нагрузку. -Вставка на стенде занимает секунды, а слияния разбирают её следы за минуту-две; -в часовом окне и подъём, и оседание схлопнулись бы в один столбик. Урок здесь — -форма кривой, и окно подобрано так, чтобы форма была видна. +Окно у дашборда — полчаса, обновление — полминуты. Пачка из +`make generate-batch` проходит за двадцать секунд, и на получасовой оси это +узкий всплеск в пару столбиков. Шире окно не берут, чтобы всплеск не выродился +в точку; уже — чтобы не промахнуться мимо него, заглянув через минуту, и чтобы +рядом осталось спокойное время: подъём читается только над полом. Кто хочет +разглядеть форму вплотную, растягивает окно мышью по самому всплеску. -Две панели окна не знают, и обе по делу. «Части» показывают состояние сейчас: -`system.parts` — опись живых частей, времени в ней нет вовсе. Очередь DDL -показывает последние двадцать записей: в спокойные полчаса она пуста, а пустая +Панель частей окна не знает вовсе. `system.parts` — опись живых частей, и +вопрос к ней про сейчас: колонки со временем в ней есть, но ни одна не годится +в ось. Очередь DDL окна тоже не знает, но по другой причине: она показывает +последние двадцать записей, потому что в спокойные полчаса пуста, а пустая панель ничего бы не рассказала. ## Обе ноды на одной панели @@ -35,11 +38,15 @@ Поэтому запросы идут через `clusterAllReplicas`, а `hostName()` подписывает, чья это строка. -Исключение одно — очередь распределённых DDL. Она лежит не у ноды, а в keeper, -и обе ноды читают из него одну и ту же очередь целиком, вместе со строками про -соседа. Обёртка здесь не добавила бы правды, а удвоила бы каждую строку. -Правило поэтому звучит так: `clusterAllReplicas` нужен там, где у каждой ноды -своя правда. +Исключений два, и они разной природы. Очередь распределённых DDL лежит не у +ноды, а в keeper, и обе ноды читают из него одну и ту же очередь целиком, +вместе со строками про соседа. Обёртка здесь не добавила бы правды, а удвоила +бы каждую строку — это единственная панель дашборда без неё. Правило поэтому +звучит так: `clusterAllReplicas` нужен там, где у каждой ноды своя правда. + +Второе исключение мягче. Панель слияний читает обе ноды той же обёрткой, но +складывает их в один ряд: шарды сливают симметрично, и разделение удвоило бы +легенду ради двух одинаковых кривых. ## Пол на графиках — журналы самого сервера @@ -53,16 +60,22 @@ ClickHouse ведёт около десятка собственных журн двух панелях читается как подъём над полом, а не как число само по себе. У частей и слияний пол отделим: в `system.parts` и `system.part_log` у каждой -строки написано, какой таблице она принадлежит. Обе панели поэтому спрашивают -только про слои стенда. Довод в размере: за спокойные полчаса слияния съели +строки написано, какой таблице она принадлежит. Обе панели поэтому показывают +всё, кроме журналов сервера, — и у каждой на это своя причина. + +Столбик у слияний шириной в минуту, а не в шаг панели. Слияния — события +редкие: на своём шаге столбик выродился бы в волосяную линию, и четыре события +за полчаса пришлось бы искать глазами. + +Слияниям фильтр иначе не пережить по размеру: за спокойные полчаса они съели 6 492 части в системных журналах против 219 в `ods` и `stg` вместе. Оставь мы журналы в кадре, ряды слоёв прижались бы к нулю, и панель рассказывала бы про -уборку за сервером вместо ваших вставок. Сколько эта уборка стоит, видно этажом -выше, на процессоре и дисках. +уборку за сервером вместо приехавших данных. Сколько эта уборка стоит, видно +этажом выше, на процессоре и дисках. -Слияния показаны по слоям, а не по нодам и не по таблицам. Шарды сливают -симметрично, и второй ряд на слой удвоил бы легенду, ничего не добавив. Какая -именно таблица держит части, отвечает соседняя панель. +Частям фильтр нужен для другого: без него в таблице десятки строк, а значит +нужен предел — и предел рвёт пару нод пополам, оставляя таблицу с одной нодой в +кадре. С фильтром помещаются все, и предел не нужен. ## Что именно меряют процессор, память и диски @@ -109,23 +122,27 @@ Keeper — отдельный сервис, системных таблиц у - Дашборд поднимается из каталога сам, все семь панелей отвечают через `/api/ds/query` в окне на полчаса. Пустых нет. -- Обе ноды видны на всех панелях по системным таблицам, кроме одной. Без - `clusterAllReplicas` осталась очередь DDL, и обёртка ей действительно не - нужна: запрос с ней вернул 124 записи с `clickhouse-01` и те же 124 с - `clickhouse-02`. -- Под нагрузкой. `make generate-batch` (50 626 событий одной пачкой): процессор - поднялся с 0,04–0,18 ядра до 0,54 и 0,64, запись — с 400 КиБ/с до 2,1 и - 4,5 МиБ/с. Следующие десять секунд обе величины уже вернулись к прежним: на - таком объёме подъём и оседание укладываются в полминуты. -- Слияния под той же нагрузкой отозвались столбиком: два слияния в `stg`, +- Обе ноды видны на четырёх панелях из пяти, что читают системные таблицы нод. + Пятая — слияния, там ноды сложены намеренно. Без `clusterAllReplicas` + осталась только очередь DDL, и обёртка ей действительно не нужна: запрос с + ней вернул с `clickhouse-01` и с `clickhouse-02` одно и то же число записей — + на день замера 124 и 124. +- Под нагрузкой. `make generate-batch` (50 626 событий одной пачкой), замер в + десятисекундных вёдрах: процессор поднялся с 0,04–0,18 ядра до 0,54 на одной + ноде и 0,64 на другой, запись — с 400 КиБ/с до 2,1 и 4,5 МиБ/с. Ведро тут + часть ответа: в тридцатисекундном тот же прогон даёт 0,29 и 0,21 ядра. + Следующие десять секунд обе величины уже вернулись к прежним: на таком объёме + подъём и оседание укладываются в полминуты. +- Слияния под той же нагрузкой отозвались столбиком в ряду `stg` высотой в 15 съеденных частей. - Блочное чтение действительно молчит: за минуту вокруг вставки `OSReadBytes` был нулевым в десяти десятисекундных окнах из четырнадцати, а обращения к файлам шли непрерывно во всех. - Память: около 1 ГиБ занятого против 4 ГиБ отведённого на каждой ноде. - Панель частей показала `ods.order_rep` по 142 строки в части против 13 046 у - `ods.event_rep` — заказы приезжают дневным слепком, события потоком. Все - таблицы слоёв поместились целиком, каждая обеими нодами: восемь строк. + `ods.event_rep` — заказы приезжают дневным слепком, события потоком. Строк + вышло восемь: четыре таблицы, каждая обеими нодами. Таблиц ошибок среди них + нет, и это правда состояния — частей у них пока не появилось. - Очередь DDL показала `CREATE MATERIALIZED VIEW ... ON CLUSTER` в состоянии `Finished` и со временем по каждой ноде: `make generate-batch` поднимает `clickhouse-init`, и тот заново прогоняет DDL. diff --git a/docs/architecture/monitoring/queries.md b/docs/architecture/monitoring/queries.md index 3db86fe..cb84e52 100644 --- a/docs/architecture/monitoring/queries.md +++ b/docs/architecture/monitoring/queries.md @@ -27,10 +27,6 @@ Grafana ходит в ClickHouse обычным пользователем, и остальное. Спрячь мы свои запросы фильтром — цена ушла бы с глаз, а панель «Кто спрашивает» недосчиталась бы самого частого посетителя. -Там же, в «Кто спрашивает», стоит `default` с сотнями запросов и одной -прочитанной строкой на каждый: это `SELECT 1` от проверки состояния -контейнера, она ходит к каждой ноде раз в несколько секунд. - Различать посетителей дашборд умеет потому, что у Grafana своя учётка со своей ролью ([ADR 0007](../../adr/0007-clickhouse-access.md)). Ходи она под аналитиком, панель «Кто спрашивает» показывала бы одну строку на всех. @@ -46,9 +42,11 @@ Grafana ходит в ClickHouse обычным пользователем, и считают только запросы инициатора: иначе один запрос человека выглядел бы тремя, а прочитанные строки сложились бы дважды. -Панель ошибок, наоборот, куски шардов показывает. Отказ по памяти случается -там, где считали, — на шарде; инициатор в своей строке передаст только текст -ошибки, а нода, на которой всё случилось, останется неназванной. +Панель ошибок, наоборот, куски шардов показывает: отказ случается там, где +считали, и строка шарда — единственное место, где эта нода названа. В замере с +принудительным пределом памяти `clickhouse-01` записал свой отказ, а +`clickhouse-02` — свой, и вторая строка видна только без фильтра по +инициатору. ## Пустой пользователь @@ -69,6 +67,10 @@ Grafana ходит в ClickHouse обычным пользователем, и - Ошибки видны и различаются по природе: отказ по правам (`bi` без `SELECT` на `ods`) и ошибка в тексте запроса (несуществующая колонка) легли в панель разными строками, каждая с полным текстом. +- Строка шарда в панели ошибок настоящая. Принудительный предел памяти на + распределённом `GROUP BY` дал две записи: `clickhouse-01` с + `is_initial_query = 1` и `clickhouse-02` с нулём. Вторая пропала бы, стой на + панели тот же фильтр, что на соседних. - Панель «Кто спрашивает» разделила шесть учёток — `grafana`, `analyst`, `etl`, `dict`, `bi`, `default` — и фоновую работу сервера. Больше всех прочитала `grafana`: 11,5 млн строк за час, своими же панелями. diff --git a/infra/grafana/provisioning/dashboards/cluster.json b/infra/grafana/provisioning/dashboards/cluster.json index 3e2ada0..0593e7d 100644 --- a/infra/grafana/provisioning/dashboards/cluster.json +++ b/infra/grafana/provisioning/dashboards/cluster.json @@ -176,7 +176,7 @@ "editorType": "sql", "queryType": "table", "format": 1, - "rawSql": "SELECT\n concat(database, '.', table) AS \"Таблица\",\n hostName() AS \"Нода\",\n count() AS \"Частей\",\n sum(rows) AS \"Строк\",\n round(sum(rows) / count()) AS \"Строк в части\",\n sum(bytes_on_disk) AS \"На диске\"\nFROM clusterAllReplicas('clickstream_cluster', system.parts)\nWHERE active\n AND database != 'system'\nGROUP BY \"Таблица\", \"Нода\"\nORDER BY \"Частей\" DESC, \"Таблица\", \"Нода\"\n" + "rawSql": "SELECT\n concat(database, '.', table) AS \"Таблица\",\n hostName() AS \"Нода\",\n count() AS \"Частей\",\n sum(rows) AS \"Строк\",\n round(sum(rows) / count()) AS \"Строк в части\",\n sum(bytes_on_disk) AS \"На диске\"\nFROM clusterAllReplicas('clickstream_cluster', system.parts)\nWHERE active\n AND database != 'system'\nGROUP BY \"Таблица\", \"Нода\"\nORDER BY \"Таблица\", \"Нода\"\n" } ], "fieldConfig": { @@ -199,7 +199,7 @@ }, { "type": "timeseries", - "title": "Слияния: сколько частей они съедают в каждом слое", + "title": "Слияния: сколько частей они съедают", "datasource": { "type": "grafana-clickhouse-datasource", "uid": "clickhouse" @@ -241,7 +241,8 @@ } }, "overrides": [] - } + }, + "interval": "1m" }, { "type": "timeseries", -- 2.54.0