# Дашборд «Кластер» `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` | Окно у дашборда — полчаса, обновление — полминуты. Пачка из `make generate-batch` проходит за двадцать секунд, и на получасовой оси это узкий всплеск в пару столбиков. Шире окно не берут, чтобы всплеск не выродился в точку; уже — чтобы не промахнуться мимо него, заглянув через минуту, и чтобы рядом осталось спокойное время: подъём читается только над полом. Кто хочет разглядеть форму вплотную, растягивает окно мышью по самому всплеску. Панель частей окна не знает вовсе. `system.parts` — опись живых частей, и вопрос к ней про сейчас: колонки со временем в ней есть, но ни одна не годится в ось. Очередь DDL окна тоже не знает, но по другой причине: она показывает последние двадцать записей, потому что в спокойные полчаса пуста, а пустая панель ничего бы не рассказала. ## Обе ноды на одной панели Системная таблица у каждой ноды своя: `system.metric_log` на `clickhouse-01` знает только про `clickhouse-01`. Запрос без обёртки показал бы половину правды и ничем бы себя не выдал — вторая нода просто не появилась бы в легенде. Поэтому запросы идут через `clusterAllReplicas`, а `hostName()` подписывает, чья это строка. Исключений два, и они разной природы. Очередь распределённых DDL лежит не у ноды, а в keeper, и обе ноды читают из него одну и ту же очередь целиком, вместе со строками про соседа. Обёртка здесь не добавила бы правды, а удвоила бы каждую строку — это единственная панель дашборда без неё. Правило поэтому звучит так: `clusterAllReplicas` нужен там, где у каждой ноды своя правда. Второе исключение мягче. Панель слияний читает обе ноды той же обёрткой, но складывает их в один ряд: шарды сливают симметрично, и разделение удвоило бы легенду ради двух одинаковых кривых. ## Пол на графиках — журналы самого сервера ClickHouse ведёт около десятка собственных журналов: `metric_log`, `trace_log`, `text_log`, `query_log` и соседние. Все они — таблицы MergeTree: их пишут и сливают постоянно, даже когда на стенде не происходит ничего. Отсюда процессор и диски, которые никогда не падают в ноль. Вычесть этот пол на процессоре и дисках нечем: их счётчики знают, сколько работы сделал процесс, и не знают, ради какой таблицы. Своя нагрузка на этих двух панелях читается как подъём над полом, а не как число само по себе. У частей и слияний пол отделим: в `system.parts` и `system.part_log` у каждой строки написано, какой таблице она принадлежит. Обе панели поэтому показывают всё, кроме журналов сервера, — и у каждой на это своя причина. Столбик у слияний шириной в минуту, а не в шаг панели. Слияния — события редкие: на своём шаге столбик выродился бы в волосяную линию, и четыре события за полчаса пришлось бы искать глазами. Слияниям фильтр иначе не пережить по размеру: за спокойные полчаса они съели 6 492 части в системных журналах против 219 в `ods` и `stg` вместе. Оставь мы журналы в кадре, ряды слоёв прижались бы к нулю, и панель рассказывала бы про уборку за сервером вместо приехавших данных. Сколько эта уборка стоит, видно этажом выше, на процессоре и дисках. Частям фильтр нужен для другого: без него в таблице десятки строк, а значит нужен предел — и предел рвёт пару нод пополам, оставляя таблицу с одной нодой в кадре. С фильтром помещаются все, и предел не нужен. ## Что именно меряют процессор, память и диски Процессор считается по времени самого сервера (`ProfileEvent_OSCPUVirtualTimeMicroseconds`), а не по загрузке машины. Метрики с именами `OS*` сняты с хоста целиком: обе ноды показывали бы в них одно и то же число, и панель на два ряда потеряла бы смысл. Память показана резидентной (`MemoryResident`), а не той, что сервер считает своей (`MemoryTracking`). Разница не косметическая: на праздной ноде `MemoryTracking` — 113 МиБ при 846 МиБ резидентной памяти, а ядро, решая, кого убить за нехватку памяти, смотрит на вторую величину ([ADR 0004](../../adr/0004-resource-limits.md)). По первой нода, которую вот-вот убьют, выглядит совершенно здоровой. Пунктирный ряд «отведено» — предел памяти контейнера, та самая коробка из ADR 0004. Он тоже читается живьём, метрикой `CGroupMemoryTotal`: впиши мы 4 ГиБ числом в JSON, панель молча соврала бы в тот день, когда предел поменяют. У дисков считаются обращения к файлам — `OSReadChars` и `OSWriteChars`. Счётчики блочного устройства (`OSReadBytes`) для той же панели не годятся: блочное чтение на стенде почти всё время нулевое, части лежат в страничном кэше, и слияние перечитывает их оттуда. Работа при этом идёт настоящая, и панель по блокам показывала бы её рваным пунктиром. ## Keeper и очередь DDL — то, что живёт вне нод Keeper — отдельный сервис, системных таблиц у него нет, и панель по нему единственная на дашборде приходит из Prometheus. Показывает она размер дерева. В keeper лежит журнал репликации, а в нём — запись на каждую часть: пока поток идёт, дерево растёт, а когда части домержились и старые записи убраны, оседает. Замер за полчаса живого потока: 9 078 узлов на подъёме, 11 512 на пике, 10 308 через десять минут после того, как поток встал. Очередь распределённых DDL — вторая половина того же сюжета: через keeper проходит каждый запрос `ON CLUSTER`. Заглядывают сюда с одним вопросом: доехал ли он до обеих нод и что ответила каждая. ## Что проверено Дашборд проверен на живом стенде 21 августа 2026 года. - Дашборд поднимается из каталога сам, все семь панелей отвечают через `/api/ds/query` в окне на полчаса. Пустых нет. - Обе ноды видны на четырёх панелях из пяти, что читают системные таблицы нод. Пятая — слияния, там ноды сложены намеренно. Без `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` — заказы приезжают дневным слепком, события потоком. Строк вышло восемь: четыре таблицы, каждая обеими нодами. Таблиц ошибок среди них нет, и это правда состояния — частей у них пока не появилось. - Очередь DDL показала `CREATE MATERIALIZED VIEW ... ON CLUSTER` в состоянии `Finished` и со временем по каждой ноде. Новые безопасные записи для показа оставляет `make check-clickhouse`: цель создаёт и удаляет временные таблицы `ON CLUSTER`. Канонический DDL появляется в очереди при создании мира дагом `world_initialize`. Подписи рядов (`${__field.labels.node}`) и правило, по которому пунктирным становится только ряд «отведено», сверены по документации Grafana через MCP Context7. И то и другое применяется на стороне браузера, через `/api/ds/query` их не видно: там проверено только, что нода приезжает меткой ряда, а метрика — его именем.