fix(monitoring): доводы приведены к замерам, пары нод больше не расходятся

- Зачем:
  - второй круг ревью нашёл, что текст спорит с собственной приёмкой: окно
    держалось доводом «слияния за минуту-две», а замер даёт двадцать секунд;
    довод панели ошибок опирался на отказ по памяти, которого 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 в окнах на полчаса, час и
    шесть часов; шаг столбика слияний проверен отдельным запросом.
This commit is contained in:
ddadmin committed 2026-08-21 15:28:03 -04:00
1 parent 42075ff9fb
commit 48bec014cf
4 files changed
+66 -43

No files matched your search

+6 -3
View File
@@ -133,8 +133,11 @@ keeper, и обе ноды читают её целиком — запрос в
посетителям продиктован [ADR 0007](0007-clickhouse-access.md): своя учётка посетителям продиктован [ADR 0007](0007-clickhouse-access.md): своя учётка
Grafana заведена там ровно затем, чтобы её запросы были отличимы от Grafana заведена там ровно затем, чтобы её запросы были отличимы от
человеческих. человеческих.
Отдельная панель ошибок понадобилась под здешний же сюжет про тяжёлый Отдельная панель ошибок понадобилась потому, что отказ достаётся шарду: в
`GROUP BY`: отказ по памяти случается на шарде, и текст ошибки — единственное замере с принудительным пределом памяти каждая нода записала свой, и строка
место, где написано, на каком. Устройство обоих дашбордов — шарда видна только без фильтра по инициатору. Здешний сюжет про тяжёлый
`GROUP BY` при этом кончается не отказом, а сбросом на диск
([ADR 0004](0004-resource-limits.md)) — отказ на стенде приходится вызывать
настройкой. Устройство обоих дашбордов —
[cluster.md](../architecture/monitoring/cluster.md) и [cluster.md](../architecture/monitoring/cluster.md) и
[queries.md](../architecture/monitoring/queries.md). [queries.md](../architecture/monitoring/queries.md).
+47 -30
View File
@@ -17,14 +17,17 @@
| Keeper | встроенные метрики keeper через Prometheus | | Keeper | встроенные метрики keeper через Prometheus |
| Распределённый DDL | `system.distributed_ddl_queue` | | Распределённый DDL | `system.distributed_ddl_queue` |
Окно у дашборда — полчаса, обновление — полминуты, и это выбрано под нагрузку. Окно у дашборда — полчаса, обновление — полминуты. Пачка из
Вставка на стенде занимает секунды, а слияния разбирают её следы за минуту-две; `make generate-batch` проходит за двадцать секунд, и на получасовой оси это
в часовом окне и подъём, и оседание схлопнулись бы в один столбик. Урок здесь — узкий всплеск в пару столбиков. Шире окно не берут, чтобы всплеск не выродился
форма кривой, и окно подобрано так, чтобы форма была видна. в точку; уже — чтобы не промахнуться мимо него, заглянув через минуту, и чтобы
рядом осталось спокойное время: подъём читается только над полом. Кто хочет
разглядеть форму вплотную, растягивает окно мышью по самому всплеску.
Две панели окна не знают, и обе по делу. «Части» показывают состояние сейчас: Панель частей окна не знает вовсе. `system.parts` — опись живых частей, и
`system.parts` — опись живых частей, времени в ней нет вовсе. Очередь DDL вопрос к ней про сейчас: колонки со временем в ней есть, но ни одна не годится
показывает последние двадцать записей: в спокойные полчаса она пуста, а пустая в ось. Очередь DDL окна тоже не знает, но по другой причине: она показывает
последние двадцать записей, потому что в спокойные полчаса пуста, а пустая
панель ничего бы не рассказала. панель ничего бы не рассказала.
## Обе ноды на одной панели ## Обе ноды на одной панели
@@ -35,11 +38,15 @@
Поэтому запросы идут через `clusterAllReplicas`, а `hostName()` подписывает, Поэтому запросы идут через `clusterAllReplicas`, а `hostName()` подписывает,
чья это строка. чья это строка.
Исключение одно — очередь распределённых DDL. Она лежит не у ноды, а в keeper, Исключений два, и они разной природы. Очередь распределённых DDL лежит не у
и обе ноды читают из него одну и ту же очередь целиком, вместе со строками про ноды, а в keeper, и обе ноды читают из него одну и ту же очередь целиком,
соседа. Обёртка здесь не добавила бы правды, а удвоила бы каждую строку. вместе со строками про соседа. Обёртка здесь не добавила бы правды, а удвоила
Правило поэтому звучит так: `clusterAllReplicas` нужен там, где у каждой ноды бы каждую строку — это единственная панель дашборда без неё. Правило поэтому
своя правда. звучит так: `clusterAllReplicas` нужен там, где у каждой ноды своя правда.
Второе исключение мягче. Панель слияний читает обе ноды той же обёрткой, но
складывает их в один ряд: шарды сливают симметрично, и разделение удвоило бы
легенду ради двух одинаковых кривых.
## Пол на графиках — журналы самого сервера ## Пол на графиках — журналы самого сервера
@@ -53,16 +60,22 @@ ClickHouse ведёт около десятка собственных журн
двух панелях читается как подъём над полом, а не как число само по себе. двух панелях читается как подъём над полом, а не как число само по себе.
У частей и слияний пол отделим: в `system.parts` и `system.part_log` у каждой У частей и слияний пол отделим: в `system.parts` и `system.part_log` у каждой
строки написано, какой таблице она принадлежит. Обе панели поэтому спрашивают строки написано, какой таблице она принадлежит. Обе панели поэтому показывают
только про слои стенда. Довод в размере: за спокойные полчаса слияния съели всё, кроме журналов сервера, — и у каждой на это своя причина.
Столбик у слияний шириной в минуту, а не в шаг панели. Слияния — события
редкие: на своём шаге столбик выродился бы в волосяную линию, и четыре события
за полчаса пришлось бы искать глазами.
Слияниям фильтр иначе не пережить по размеру: за спокойные полчаса они съели
6 492 части в системных журналах против 219 в `ods` и `stg` вместе. Оставь мы 6 492 части в системных журналах против 219 в `ods` и `stg` вместе. Оставь мы
журналы в кадре, ряды слоёв прижались бы к нулю, и панель рассказывала бы про журналы в кадре, ряды слоёв прижались бы к нулю, и панель рассказывала бы про
уборку за сервером вместо ваших вставок. Сколько эта уборка стоит, видно этажом уборку за сервером вместо приехавших данных. Сколько эта уборка стоит, видно
выше, на процессоре и дисках. этажом выше, на процессоре и дисках.
Слияния показаны по слоям, а не по нодам и не по таблицам. Шарды сливают Частям фильтр нужен для другого: без него в таблице десятки строк, а значит
симметрично, и второй ряд на слой удвоил бы легенду, ничего не добавив. Какая нужен предел — и предел рвёт пару нод пополам, оставляя таблицу с одной нодой в
именно таблица держит части, отвечает соседняя панель. кадре. С фильтром помещаются все, и предел не нужен.
## Что именно меряют процессор, память и диски ## Что именно меряют процессор, память и диски
@@ -109,23 +122,27 @@ Keeper — отдельный сервис, системных таблиц у
- Дашборд поднимается из каталога сам, все семь панелей отвечают через - Дашборд поднимается из каталога сам, все семь панелей отвечают через
`/api/ds/query` в окне на полчаса. Пустых нет. `/api/ds/query` в окне на полчаса. Пустых нет.
- Обе ноды видны на всех панелях по системным таблицам, кроме одной. Без - Обе ноды видны на четырёх панелях из пяти, что читают системные таблицы нод.
`clusterAllReplicas` осталась очередь DDL, и обёртка ей действительно не Пятая — слияния, там ноды сложены намеренно. Без `clusterAllReplicas`
нужна: запрос с ней вернул 124 записи с `clickhouse-01` и те же 124 с осталась только очередь DDL, и обёртка ей действительно не нужна: запрос с
`clickhouse-02`. ней вернул с `clickhouse-01` и с `clickhouse-02` одно и то же число записей —
- Под нагрузкой. `make generate-batch` (50 626 событий одной пачкой): процессор на день замера 124 и 124.
поднялся с 0,04–0,18 ядра до 0,54 и 0,64, запись — с 400 КиБ/с до 2,1 и - Под нагрузкой. `make generate-batch` (50 626 событий одной пачкой), замер в
4,5 МиБ/с. Следующие десять секунд обе величины уже вернулись к прежним: на десятисекундных вёдрах: процессор поднялся с 0,04–0,18 ядра до 0,54 на одной
таком объёме подъём и оседание укладываются в полминуты. ноде и 0,64 на другой, запись — с 400 КиБ/с до 2,1 и 4,5 МиБ/с. Ведро тут
- Слияния под той же нагрузкой отозвались столбиком: два слияния в `stg`, часть ответа: в тридцатисекундном тот же прогон даёт 0,29 и 0,21 ядра.
Следующие десять секунд обе величины уже вернулись к прежним: на таком объёме
подъём и оседание укладываются в полминуты.
- Слияния под той же нагрузкой отозвались столбиком в ряду `stg` высотой в
15 съеденных частей. 15 съеденных частей.
- Блочное чтение действительно молчит: за минуту вокруг вставки `OSReadBytes` - Блочное чтение действительно молчит: за минуту вокруг вставки `OSReadBytes`
был нулевым в десяти десятисекундных окнах из четырнадцати, а обращения к был нулевым в десяти десятисекундных окнах из четырнадцати, а обращения к
файлам шли непрерывно во всех. файлам шли непрерывно во всех.
- Память: около 1 ГиБ занятого против 4 ГиБ отведённого на каждой ноде. - Память: около 1 ГиБ занятого против 4 ГиБ отведённого на каждой ноде.
- Панель частей показала `ods.order_rep` по 142 строки в части против 13 046 у - Панель частей показала `ods.order_rep` по 142 строки в части против 13 046 у
`ods.event_rep` — заказы приезжают дневным слепком, события потоком. Все `ods.event_rep` — заказы приезжают дневным слепком, события потоком. Строк
таблицы слоёв поместились целиком, каждая обеими нодами: восемь строк. вышло восемь: четыре таблицы, каждая обеими нодами. Таблиц ошибок среди них
нет, и это правда состояния — частей у них пока не появилось.
- Очередь DDL показала `CREATE MATERIALIZED VIEW ... ON CLUSTER` в состоянии - Очередь DDL показала `CREATE MATERIALIZED VIEW ... ON CLUSTER` в состоянии
`Finished` и со временем по каждой ноде: `make generate-batch` поднимает `Finished` и со временем по каждой ноде: `make generate-batch` поднимает
`clickhouse-init`, и тот заново прогоняет DDL. `clickhouse-init`, и тот заново прогоняет DDL.
+9 -7
View File
@@ -27,10 +27,6 @@ Grafana ходит в ClickHouse обычным пользователем, и
остальное. Спрячь мы свои запросы фильтром — цена ушла бы с глаз, а панель «Кто остальное. Спрячь мы свои запросы фильтром — цена ушла бы с глаз, а панель «Кто
спрашивает» недосчиталась бы самого частого посетителя. спрашивает» недосчиталась бы самого частого посетителя.
Там же, в «Кто спрашивает», стоит `default` с сотнями запросов и одной
прочитанной строкой на каждый: это `SELECT 1` от проверки состояния
контейнера, она ходит к каждой ноде раз в несколько секунд.
Различать посетителей дашборд умеет потому, что у Grafana своя учётка со своей Различать посетителей дашборд умеет потому, что у Grafana своя учётка со своей
ролью ([ADR 0007](../../adr/0007-clickhouse-access.md)). Ходи она под ролью ([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` на - Ошибки видны и различаются по природе: отказ по правам (`bi` без `SELECT` на
`ods`) и ошибка в тексте запроса (несуществующая колонка) легли в панель `ods`) и ошибка в тексте запроса (несуществующая колонка) легли в панель
разными строками, каждая с полным текстом. разными строками, каждая с полным текстом.
- Строка шарда в панели ошибок настоящая. Принудительный предел памяти на
распределённом `GROUP BY` дал две записи: `clickhouse-01` с
`is_initial_query = 1` и `clickhouse-02` с нулём. Вторая пропала бы, стой на
панели тот же фильтр, что на соседних.
- Панель «Кто спрашивает» разделила шесть учёток — `grafana`, `analyst`, `etl`, - Панель «Кто спрашивает» разделила шесть учёток — `grafana`, `analyst`, `etl`,
`dict`, `bi`, `default` — и фоновую работу сервера. Больше всех прочитала `dict`, `bi`, `default` — и фоновую работу сервера. Больше всех прочитала
`grafana`: 11,5 млн строк за час, своими же панелями. `grafana`: 11,5 млн строк за час, своими же панелями.
@@ -176,7 +176,7 @@
"editorType": "sql", "editorType": "sql",
"queryType": "table", "queryType": "table",
"format": 1, "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": { "fieldConfig": {
@@ -199,7 +199,7 @@
}, },
{ {
"type": "timeseries", "type": "timeseries",
"title": "Слияния: сколько частей они съедают в каждом слое", "title": "Слияния: сколько частей они съедают",
"datasource": { "datasource": {
"type": "grafana-clickhouse-datasource", "type": "grafana-clickhouse-datasource",
"uid": "clickhouse" "uid": "clickhouse"
@@ -241,7 +241,8 @@
} }
}, },
"overrides": [] "overrides": []
} },
"interval": "1m"
}, },
{ {
"type": "timeseries", "type": "timeseries",