- Зачем:
- второй круг ревью нашёл, что текст спорит с собственной приёмкой: окно
держалось доводом «слияния за минуту-две», а замер даёт двадцать секунд;
довод панели ошибок опирался на отказ по памяти, которого 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 в окнах на полчаса, час и
шесть часов; шаг столбика слияний проверен отдельным запросом.
155 lines
13 KiB
Markdown
155 lines
13 KiB
Markdown
# Дашборд «Кластер»
|
||
|
||
`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 generate-batch` поднимает
|
||
`clickhouse-init`, и тот заново прогоняет DDL.
|
||
|
||
Подписи рядов (`${__field.labels.node}`) и правило, по которому пунктирным
|
||
становится только ряд «отведено», сверены по документации Grafana через MCP
|
||
Context7. И то и другое применяется на стороне браузера, через
|
||
`/api/ds/query` их не видно: там проверено только, что нода приезжает меткой
|
||
ряда, а метрика — его именем.
|