From 7ffb4606f137d58eb0611a1a09cce9806315354b Mon Sep 17 00:00:00 2001 From: Dmitry Dementev Date: Fri, 21 Aug 2026 14:52:58 -0400 Subject: [PATCH] =?UTF-8?q?feat(monitoring):=20=D1=81=D0=BE=D0=B1=D1=80?= =?UTF-8?q?=D0=B0=D0=BD=D1=8B=20=D0=B4=D0=B0=D1=88=D0=B1=D0=BE=D1=80=D0=B4?= =?UTF-8?q?=D1=8B=20=C2=AB=D0=9A=D0=BB=D0=B0=D1=81=D1=82=D0=B5=D1=80=C2=BB?= =?UTF-8?q?=20=D0=B8=20=C2=AB=D0=97=D0=B0=D0=BF=D1=80=D0=BE=D1=81=D1=8B?= =?UTF-8?q?=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": [] + } + } + ] +}