Files
clickstream-data-platform/docs/architecture/monitoring/cluster.md
T
ddadmin 48bec014cf 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 в окнах на полчаса, час и
    шесть часов; шаг столбика слияний проверен отдельным запросом.
2026-08-21 15:28:03 -04:00

155 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Дашборд «Кластер»
`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` их не видно: там проверено только, что нода приезжает меткой
ряда, а метрика — его именем.