fix(monitoring): панели уточнены по ревью, приёмка снята на стенде
- Зачем:
- две линии ревью нашли, что колонки панели брака читаются как
непересекающиеся кучи, «вовремя ли» из ADR 0002 не считается вовсе, а
отказ от ряда «против вчерашнего» нигде не записан рядом с решением.
- Что:
- в панели брака столбец назван «Из них в брак»: брак — часть приехавшего,
а не соседняя куча.
- панель слепка считает паузу после прошлого слепка — это и есть «вовремя
ли» из ADR 0002; пустая пауза у самого раннего слепка держится
обнуляемым типом.
- отказ от ряда «против вчерашнего» записан в ADR 0002 и в monitoring.md с
доводом: сдвиг на модельные сутки прописал бы ускорение мира в JSON.
- в monitoring.md сказано, почему свежесть читает физическую _dist, а не
ods.*_v, и почему панель слепка не знает окна; довод про сломанный JSON
оставлен у кода, а не задвоен в доке.
- README больше не утверждает, что панелей нет.
- у разбора дашбордов убран флаг -e: он обещал утверждение, которого нет.
- Проверка:
- make config-test (краснеет на нарочно сломанном JSON дашборда), make lint,
make smoke 21/21, make check-clickhouse 10/10; все шесть панелей отвечают
через /api/ds/query. Опыты приёмки и их следы — monitoring.md,
«Что проверено».
This commit is contained in:
1 parent
180ef5b94f
commit
e5c573bb25
6 files changed
+73
-30
No files matched your search
@@ -351,9 +351,11 @@ docker compose up --force-recreate --wait clickhouse-01 clickhouse-02
|
|||||||
[официальным описанием автоматической настройки](https://grafana.com/docs/grafana/latest/administration/provisioning/)
|
[официальным описанием автоматической настройки](https://grafana.com/docs/grafana/latest/administration/provisioning/)
|
||||||
и [документацией плагина ClickHouse](https://grafana.com/docs/plugins/grafana-clickhouse-datasource/latest/configure/).
|
и [документацией плагина ClickHouse](https://grafana.com/docs/plugins/grafana-clickhouse-datasource/latest/configure/).
|
||||||
Prometheus собирает встроенные метрики двух серверов и keeper, а через
|
Prometheus собирает встроенные метрики двух серверов и keeper, а через
|
||||||
`kafka-exporter` — состояние Kafka. Устройство зоны описано в
|
`kafka-exporter` — состояние Kafka. Поверх этого в Grafana поднимается дашборд
|
||||||
[справочнике мониторинга](docs/architecture/monitoring.md); панелей и правил
|
«Данные»: свежесть, поток, брак, сходимость Kafka с ClickHouse, отставание
|
||||||
оповещения пока нет.
|
чтения и дневной слепок заказов. Устройство зоны и разбор панелей — в
|
||||||
|
[справочнике мониторинга](docs/architecture/monitoring.md); правил оповещения
|
||||||
|
пока нет.
|
||||||
|
|
||||||
Airflow закреплён на 3.3.0. Состав обязательных процессов, LocalExecutor,
|
Airflow закреплён на 3.3.0. Состав обязательных процессов, LocalExecutor,
|
||||||
публичный `airflow.sdk`, API здоровья и SimpleAuthManager сверены с
|
публичный `airflow.sdk`, API здоровья и SimpleAuthManager сверены с
|
||||||
|
|||||||
@@ -114,3 +114,10 @@ ClickHouse, Kafka и Airflow, 17 — в дашборде генератора, 1
|
|||||||
(спека, раздел 10), но рассказать урок можно только про то, что дашборд
|
(спека, раздел 10), но рассказать урок можно только про то, что дашборд
|
||||||
показывает. Скопируй мы состав предшественника — урок неизбежно получился бы
|
показывает. Скопируй мы состав предшественника — урок неизбежно получился бы
|
||||||
про пульс планировщика.
|
про пульс планировщика.
|
||||||
|
|
||||||
|
21 августа 2026 года дашборд «Данные» собран. Один пункт решения исполнен не
|
||||||
|
буквально: у панели потока нет ряда «против вчерашнего». Сдвинуть ряд на
|
||||||
|
модельные сутки значит записать ускорение мира в JSON дашборда — ровно ту
|
||||||
|
пометку, от которой этот же ADR отговаривает выше, — а реальные сутки на стенде
|
||||||
|
не с чем сравнивать. Довод целиком и остальное устройство дашборда —
|
||||||
|
[docs/architecture/monitoring.md](../architecture/monitoring.md).
|
||||||
@@ -66,10 +66,8 @@ Grafana ходит пользователем `grafana` с ролью `monitorin
|
|||||||
новый файл в каталоге и больше ничего: в `compose.yaml` заходить не нужно, ради
|
новый файл в каталоге и больше ничего: в `compose.yaml` заходить не нужно, ради
|
||||||
этого каталог и заведён заранее.
|
этого каталог и заведён заранее.
|
||||||
|
|
||||||
Сломанный JSON Grafana не выносит сообщением: она пишет строку в свой журнал и
|
Файлы каталога разбирает `make config-test`, рядом с файлами дагов и по тому же
|
||||||
оставляет прежнюю версию дашборда, то есть правка молча не появляется. Поэтому
|
доводу ([testing.md](testing.md)).
|
||||||
файлы каталога разбирает `make config-test` — тем же доводом, что и файлы дагов
|
|
||||||
([testing.md](testing.md)).
|
|
||||||
|
|
||||||
## Дашборд «Данные»
|
## Дашборд «Данные»
|
||||||
|
|
||||||
@@ -87,7 +85,7 @@ Grafana ходит пользователем `grafana` с ролью `monitorin
|
|||||||
| Брак | те же и `ods.*_errors_dist` | сколько приехавшего не разобралось и какая это доля |
|
| Брак | те же и `ods.*_errors_dist` | сколько приехавшего не разобралось и какая это доля |
|
||||||
| Сходимость | экспортёр Kafka и `stg.hits_raw_dist` | произведённое против приземлившегося |
|
| Сходимость | экспортёр Kafka и `stg.hits_raw_dist` | произведённое против приземлившегося |
|
||||||
| Отставание | `kafka_consumergroup_lag` | сколько сообщений не прочитано |
|
| Отставание | `kafka_consumergroup_lag` | сколько сообщений не прочитано |
|
||||||
| Дневной слепок | `stg.orders_raw_dist` | какой слепок доехал и когда |
|
| Дневной слепок | `stg.orders_raw_dist` | какой слепок доехал, когда и после какой паузы |
|
||||||
|
|
||||||
### Почему слои у панелей разные
|
### Почему слои у панелей разные
|
||||||
|
|
||||||
@@ -95,6 +93,11 @@ Grafana ходит пользователем `grafana` с ролью `monitorin
|
|||||||
длиннее: сырьё привозит забор, а типизированную строку кладёт разбор. Встань
|
длиннее: сырьё привозит забор, а типизированную строку кладёт разбор. Встань
|
||||||
разбор — свежесть ODS это покажет, свежесть сырья нет.
|
разбор — свежесть ODS это покажет, свежесть сырья нет.
|
||||||
|
|
||||||
|
Читает свежесть физическую `_dist`, а не каноническое `ods.*_v`. Представление
|
||||||
|
прячет за собой `FINAL` и отвечает про текущее состояние сущностей, а вопрос
|
||||||
|
здесь про доставку: местом диагностики доставленных версий физическая таблица
|
||||||
|
названа там же, где заведено представление (`sql/ddl/30-ods-views.sql`).
|
||||||
|
|
||||||
Поток и брак считаются по слою сырья. Сырьё записывает каждую доставку и ничего
|
Поток и брак считаются по слою сырья. Сырьё записывает каждую доставку и ничего
|
||||||
не схлопывает, а `ReplacingMergeTree` в ODS схлопнет и повтор доставки события,
|
не схлопывает, а `ReplacingMergeTree` в ODS схлопнет и повтор доставки события,
|
||||||
и все версии одного заказа. Считай мы поток по ODS — он занизился бы, и тем
|
и все версии одного заказа. Считай мы поток по ODS — он занизился бы, и тем
|
||||||
@@ -103,22 +106,29 @@ Grafana ходит пользователем `grafana` с ролью `monitorin
|
|||||||
Доля брака берётся от приехавшего, а не от суммы «годные плюс брак»: брак не
|
Доля брака берётся от приехавшего, а не от суммы «годные плюс брак»: брак не
|
||||||
отдельная куча, а часть сырья, которая не прошла разбор.
|
отдельная куча, а часть сырья, которая не прошла разбор.
|
||||||
|
|
||||||
Окно у панели брака — окно дашборда, и это не косметика. У `ods.*_errors_dist`
|
Окно у панели брака — окно дашборда, и это не косметика. Сырьё живёт трое
|
||||||
срок жизни месяц, а у `ods.event_dist` его нет вовсе: доля за всё время
|
суток, а брак — месяц: доля за всё время делила бы месяц брака на трое суток
|
||||||
сравнивала бы разные сроки хранения.
|
приехавшего и однажды перевалила бы за сотню процентов.
|
||||||
|
|
||||||
Отдельного ряда «против вчерашнего» на панели потока нет. Модельные сутки при
|
Панель дневного слепка, наоборот, окна не знает. Её вопрос про состояние, а не
|
||||||
ускорении ×60 укладываются примерно в двадцать четыре реальные минуты, так что
|
про отрезок, и последние десять слепков она показывает всегда.
|
||||||
соседние дни мира и без того помещаются в одно окно панели, — а реальное «вчера»
|
|
||||||
на стенде чаще всего приходится на время, когда стенда не было.
|
Отдельного ряда «против вчерашнего» на панели потока нет, и это решение, а не
|
||||||
|
пропуск. Реальное «вчера» на стенде чаще всего приходится на время, когда
|
||||||
|
стенда ещё не было. А сдвинуть ряд на модельные сутки значит записать в JSON
|
||||||
|
дашборда ускорение мира — ровно ту пометку про тик, которую [ADR
|
||||||
|
0002](../adr/0002-monitoring-scope.md) в дашборде держать не велит: поменяют
|
||||||
|
ускорение, и панель молча соврёт. Сравнение остаётся глазу: модельные сутки
|
||||||
|
укладываются в минуты реального времени, и соседние дни мира помещаются в одно
|
||||||
|
окно панели сами.
|
||||||
|
|
||||||
### Чтецов Kafka панели не читают
|
### Чтецов Kafka панели не читают
|
||||||
|
|
||||||
`stg.hits_raw_kafka` и `stg.orders_raw_kafka` — таблицы движка Kafka, а не
|
`stg.hits_raw_kafka` и `stg.orders_raw_kafka` — таблицы движка Kafka, а не
|
||||||
хранилище. У второго стоит `kafka_commit_on_select = 1`: запрос из панели забрал
|
хранилище: запрос из панели у второго забрал бы слепок себе и сдвинул офсеты
|
||||||
бы слепок себе и сдвинул офсеты, а даг приёма приехал бы к пустому топику.
|
(`kafka_commit_on_select`, см. `sql/ddl/10-stg-tables.sql`), а даг приёма
|
||||||
Приземлившееся панели считают по `stg.*_raw_dist`, произведённое — по метрикам
|
приехал бы к пустому топику. Приземлившееся панели считают по `stg.*_raw_dist`,
|
||||||
экспортёра.
|
произведённое — по метрикам экспортёра.
|
||||||
|
|
||||||
### Как считается сходимость
|
### Как считается сходимость
|
||||||
|
|
||||||
@@ -132,8 +142,9 @@ Kafka» приходит из Prometheus, ряд «приземлилось в C
|
|||||||
Два соседних способа отвергнуты, и оба по замеру на живом стенде. `increase`
|
Два соседних способа отвергнуты, и оба по замеру на живом стенде. `increase`
|
||||||
растягивает приращение на всё окно и на минутном интервале промахивается на
|
растягивает приращение на всё окно и на минутном интервале промахивается на
|
||||||
проценты: здоровый стенд выглядел бы расходящимся. Накопленный итог от начала
|
проценты: здоровый стенд выглядел бы расходящимся. Накопленный итог от начала
|
||||||
окна точнее их обоих, но отсчитывать его надо от края окна — а на свежем стенде
|
окна точнее обоих приростов, но отсчитывать его надо от края окна — а на
|
||||||
окно начинается раньше, чем появился экспортёр, и ряд пропадает целиком.
|
свежем стенде окно начинается раньше, чем появился экспортёр, и ряд пропадает
|
||||||
|
целиком.
|
||||||
|
|
||||||
Столбцы ClickHouse подписаны правым краем, а не левым. Точка Prometheus — это
|
Столбцы ClickHouse подписаны правым краем, а не левым. Точка Prometheus — это
|
||||||
прирост к моменту времени, а `count()` по интервалу — сумма, которая набирается
|
прирост к моменту времени, а `count()` по интервалу — сумма, которая набирается
|
||||||
@@ -149,7 +160,7 @@ Kafka идёт чуть выше. Читается устойчивый разр
|
|||||||
|
|
||||||
Панель смотрит на топик событий. У него приём непрерывный, и разрыв виден
|
Панель смотрит на топик событий. У него приём непрерывный, и разрыв виден
|
||||||
сразу. Слепок заказов уезжает пачкой раз в модельные сутки, и вопрос к нему
|
сразу. Слепок заказов уезжает пачкой раз в модельные сутки, и вопрос к нему
|
||||||
другой — не «сходится ли поток», а «доехала ли выгрузка и когда». На него
|
другой — не «сходится ли поток», а «доехала ли выгрузка и вовремя ли». На него
|
||||||
отвечает своя панель, и модельный день она берёт из самого сообщения: в сырьё
|
отвечает своя панель, и модельный день она берёт из самого сообщения: в сырьё
|
||||||
он приезжает полем `snapshot_date`.
|
он приезжает полем `snapshot_date`.
|
||||||
|
|
||||||
@@ -169,3 +180,26 @@ ClickHouse через MCP Context7; разбор — в
|
|||||||
- Prometheus видит ровно четыре цели, все со значением `up = 1`; запрос
|
- Prometheus видит ровно четыре цели, все со значением `up = 1`; запрос
|
||||||
`kafka_consumergroup_lag` вернул ряды по группам `clickstream_*`.
|
`kafka_consumergroup_lag` вернул ряды по группам `clickstream_*`.
|
||||||
- `make smoke` 21/21, `make check-clickhouse` 10/10, `make check-services` 7/7.
|
- `make smoke` 21/21, `make check-clickhouse` 10/10, `make check-services` 7/7.
|
||||||
|
|
||||||
|
Дашборд «Данные» проверен на живом стенде в тот же день, `world_live` снят с
|
||||||
|
паузы.
|
||||||
|
|
||||||
|
- Дашборд поднимается из каталога сам: файл положен, панели появились без
|
||||||
|
ручного импорта. Сохранение и удаление через API отвергнуты («Cannot save
|
||||||
|
provisioned dashboard», «provisioned dashboard cannot be deleted»).
|
||||||
|
- Все шесть панелей отвечают через `/api/ds/query` — и в окне часа по
|
||||||
|
умолчанию, и в окне на шесть часов, которое начинается раньше, чем поднялся
|
||||||
|
экспортёр. Пустых нет ни при идущем потоке, ни при остановленном.
|
||||||
|
- Брак: подсунутое в топик `hits` негодное сообщение легло в
|
||||||
|
`ods.event_errors` с классом `keyset_mismatch`; панель показала единицу и
|
||||||
|
долю 0,039 % в десятиминутном окне. Строка убрана мутациями из
|
||||||
|
`stg.hits_raw_rep` и `ods.event_errors_rep`, обе таблицы вернулись к нулю.
|
||||||
|
- Сходимость: на живом потоке за двадцать пять минут медиана расхождения между
|
||||||
|
рядами 3 %, худший столбец 11 % — на переломе потока, как и описано выше.
|
||||||
|
Отказы тоже по замеру: `increase` расходился сильнее, а накопленный итог
|
||||||
|
пропадал целиком, стоило окну начаться раньше экспортёра.
|
||||||
|
- Свежесть реагирует на остановленный поток и возвращается. Через пять минут
|
||||||
|
после конца модельного дня панель показала 305 с у событий и 288 с у
|
||||||
|
заказов; стоило пустить следующий день, события вернулись к секунде.
|
||||||
|
Свежесть заказов при этом растёт до конца дня и садится, когда доедет его
|
||||||
|
слепок, — так и устроен второй источник.
|
||||||
@@ -34,9 +34,9 @@
|
|||||||
спрашивают его отдельно.
|
спрашивают его отдельно.
|
||||||
|
|
||||||
Стенд нужен трём целям из восьми. Цена — замер, см. «Что проверено».
|
Стенд нужен трём целям из восьми. Цена — замер, см. «Что проверено».
|
||||||
`make smoke` и `make check-services` перемерены 21 августа 2026 года,
|
`make config-test`, `make smoke` и `make check-services` перемерены 21 августа
|
||||||
`make check-clickhouse` — 19 августа, корневой `make lint` — 9 августа,
|
2026 года, `make check-clickhouse` — 19 августа, корневой `make lint` —
|
||||||
`make test` — 7 августа; остальные стоят с замера 6 августа.
|
9 августа, `make test` — 7 августа; остальные стоят с замера 6 августа.
|
||||||
|
|
||||||
| Цель | Откуда | Что утверждает | Стенд | Цена |
|
| Цель | Откуда | Что утверждает | Стенд | Цена |
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
|
|||||||
@@ -81,13 +81,13 @@
|
|||||||
"editorType": "sql",
|
"editorType": "sql",
|
||||||
"queryType": "table",
|
"queryType": "table",
|
||||||
"format": 1,
|
"format": 1,
|
||||||
"rawSql": "SELECT\n source AS \"Источник\",\n countIf(NOT is_error) AS \"Приехало\",\n countIf(is_error) AS \"В брак\",\n 100 * countIf(is_error) / countIf(NOT is_error) AS \"Доля брака, %\"\nFROM\n(\n SELECT 'события' AS source, 0 AS is_error, _load_ts FROM stg.hits_raw_dist\n UNION ALL\n SELECT 'события', 1, _load_ts FROM ods.event_errors_dist\n UNION ALL\n SELECT 'заказы', 0, _load_ts FROM stg.orders_raw_dist\n UNION ALL\n SELECT 'заказы', 1, _load_ts FROM ods.order_errors_dist\n)\nWHERE $__timeFilter(_load_ts)\nGROUP BY source\nORDER BY source"
|
"rawSql": "SELECT\n source AS \"Источник\",\n countIf(NOT is_error) AS \"Приехало\",\n countIf(is_error) AS \"Из них в брак\",\n 100 * countIf(is_error) / countIf(NOT is_error) AS \"Доля брака, %\"\nFROM\n(\n SELECT 'события' AS source, 0 AS is_error, _load_ts FROM stg.hits_raw_dist\n UNION ALL\n SELECT 'события', 1, _load_ts FROM ods.event_errors_dist\n UNION ALL\n SELECT 'заказы', 0, _load_ts FROM stg.orders_raw_dist\n UNION ALL\n SELECT 'заказы', 1, _load_ts FROM ods.order_errors_dist\n)\nWHERE $__timeFilter(_load_ts)\nGROUP BY source\nORDER BY source"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "table",
|
"type": "table",
|
||||||
"title": "Дневной слепок заказов: какой доехал и когда",
|
"title": "Дневной слепок заказов: какой доехал, когда и после какой паузы",
|
||||||
"datasource": {
|
"datasource": {
|
||||||
"type": "grafana-clickhouse-datasource",
|
"type": "grafana-clickhouse-datasource",
|
||||||
"uid": "clickhouse"
|
"uid": "clickhouse"
|
||||||
@@ -109,7 +109,7 @@
|
|||||||
"editorType": "sql",
|
"editorType": "sql",
|
||||||
"queryType": "table",
|
"queryType": "table",
|
||||||
"format": 1,
|
"format": 1,
|
||||||
"rawSql": "SELECT\n JSONExtractString(raw, 'snapshot_date') AS \"Слепок\",\n max(_load_ts) AS \"Доехал\",\n count() AS \"Строк\"\nFROM stg.orders_raw_dist\nGROUP BY \"Слепок\"\nORDER BY \"Слепок\" DESC\nLIMIT 10"
|
"rawSql": "SELECT\n \"Слепок\",\n \"Доехал\",\n dateDiff('second', prev, \"Доехал\") AS \"После прошлого, с\",\n \"Строк\"\nFROM\n(\n SELECT\n JSONExtractString(raw, 'snapshot_date') AS \"Слепок\",\n max(_load_ts) AS \"Доехал\",\n count() AS \"Строк\",\n -- Обнуляемый тип нужен, чтобы у самого раннего слепка пауза осталась\n -- пустой, а не превратилась в разницу с началом эпохи.\n lagInFrame(toNullable(max(_load_ts))) OVER (ORDER BY \"Слепок\") AS prev\n FROM stg.orders_raw_dist\n GROUP BY \"Слепок\"\n)\nORDER BY \"Слепок\" DESC\nLIMIT 10"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -195,7 +195,7 @@
|
|||||||
"editorType": "sql",
|
"editorType": "sql",
|
||||||
"queryType": "timeseries",
|
"queryType": "timeseries",
|
||||||
"format": 0,
|
"format": 0,
|
||||||
"rawSql": "SELECT\n -- Столбец подписан правым краем: ряд Kafka в этой точке — прирост офсета\n -- за тот же интервал, а он набирается к его концу.\n $__timeInterval(_load_ts) + INTERVAL $__interval_s SECOND AS t,\n count() AS \"приземлилось в ClickHouse\"\nFROM stg.hits_raw_dist\nWHERE $__timeFilter(_load_ts)\nGROUP BY t\nORDER BY t"
|
"rawSql": "SELECT\n -- Правый край, а не левый: ряд Kafka в этой точке — прирост за тот же\n -- интервал, а он набирается к его концу.\n $__timeInterval(_load_ts) + INTERVAL $__interval_s SECOND AS t,\n count() AS \"приземлилось в ClickHouse\"\nFROM stg.hits_raw_dist\nWHERE $__timeFilter(_load_ts)\nGROUP BY t\nORDER BY t"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"interval": "1m"
|
"interval": "1m"
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ jq -e '
|
|||||||
PYTHONPYCACHEPREFIX="$CACHE_DIR" uv run --no-project python -m compileall -q "$ROOT_DIR/dags"
|
PYTHONPYCACHEPREFIX="$CACHE_DIR" uv run --no-project python -m compileall -q "$ROOT_DIR/dags"
|
||||||
# Дашборды Grafana — тот же случай и тот же довод: сломанный JSON не сообщает о
|
# Дашборды Grafana — тот же случай и тот же довод: сломанный JSON не сообщает о
|
||||||
# себе, а молча оставляет в интерфейсе прежнюю версию дашборда.
|
# себе, а молча оставляет в интерфейсе прежнюю версию дашборда.
|
||||||
jq -e . "$ROOT_DIR"/infra/grafana/provisioning/dashboards/*.json >/dev/null
|
jq . "$ROOT_DIR"/infra/grafana/provisioning/dashboards/*.json >/dev/null
|
||||||
git -C "$ROOT_DIR" diff --check
|
git -C "$ROOT_DIR" diff --check
|
||||||
|
|
||||||
printf 'ЗЕЛЁНО: Compose, Python, дашборды и пробельные ошибки diff проверены.\n'
|
printf 'ЗЕЛЁНО: Compose, Python, дашборды и пробельные ошибки diff проверены.\n'
|
||||||
Reference in new issue
Block a user