
Представьте, что вы спокойно работаете, и тут к вам в чат прилетает сообщение от руководителя:

Вам задали вопрос, нужно постараться побыстрее ответить, к тому же и звучит несложно. Вы вспоминаете, что уже совсем недавно делали похожие запросы в базу, пытаетесь вспомнить, где они. Проходит пара минут, и собирается еще пара вопросов:

Итак, вы всё ещё не дали ответ на первый вопрос, а у вас уже появилась пара новых. Ситуация не очень. На самом деле вопросы по типу таких возникают почти каждый день и они требуют быстрого ответа, получить который должно быть легко и просто. Для того чтобы получать ответы на одни и те же вопросы в любой момент времени, аналитики и разработчики строят дашборды.
Чтобы можно было ответить сразу на несколько разных вопросов, дашборды объединяют в целые системы, которые помогают в комплексном обзоре данных. Это важный инструмент для мониторинга и поддержки, который помогает бизнесу и разработке оставаться в курсе событий и принимать обоснованные решения.

Но иногда могут случаться неприятности по типу таких:

А может быть ещё хуже:

Цель этой статьи — помочь вам, чтобы таких «сори» было как можно меньше. В статье мы рассмотрим проблему производительности и стабильности системы дашбордов на основе связки Grafana + Clickhouse. Обозначим, каким требованиям система должна отвечать и как их достичь.
При чём здесь я?
Меня зовут Валя Борисов, и я — аналитик в команде Ozon. Задача нашей команды — создавать инструменты для мониторинга и анализа скорости.
Наши усилия направлены на то, чтобы в реальном времени следить за тем, как быстро работают наши сервисы и платформа. Благодаря инструментам, которые мы создаём и поддерживаем, команды разработки получают представление о том, как пользователи видят работу нашего сайта или приложения. Мы помогаем выявлять причины деградации скорости и определять узкие места в инфраструктуре.
Если хотите больше подробностей о работе нашей команды, вы можете посмотреть выступление на тему «Скорость как показатель качества» по этой ссылке.
Наши дашборды играют ключевую роль в предоставлении информации о скорости работы платформы. Вместе с командой аналитиков я занимаюсь созданием и поддержкой этой системы в Grafana. Мы стремимся делать её не только информативной, но и быстрой, стабильной и удобной для всех пользователей. В этой статье я хочу поделиться методами и приемами, к которым мы пришли в процессе работы.
Какие инструменты обсудим в статье
Clickhouse — колоночная БД, храним данные распределённо в нескольких дата-центрах
Grafana — система для визуализации данных в реальном времени
Altiity Plugin — плагин для подключения Clickhouse к Grafana
Мы тут
Суммарный объём аналитических данных больше 200Тб. Данные хранятся преимущественно в Clickhouse. На основе этих данных построено более 2.5к графиков. 35 дашбордов, которыми ежедневно пользуются более 50 человек. Днём в пике нагрузка доходит до 300 распределённых запросов в минуту. Иногда трафик доходит до >1к распределённых запросов в минуту. В 95 квантили скорость выполнения запросов не более 500 миллисекунд — иначе говоря, 5% запросов отработали больше чем за полсекунды. Как правило, это запросы для временных рядов в промежутках от часа до нескольких недель в реальном времени. В среднем наши дашборды грузятся за 2 секунды целиком.
Много дашбордов — много графиков — много данных
Этой кучей цифр я сформировал точку отсчёта. Точка отсчёта — это та точка, с которой вы сравниваете свою работу в будущем. Её необходимо обозначить, если в ежедневной аналитике используется больше чем «пара дашбордов». Эти данные помогают увидеть текущее состояние вашей системы. Вы можете взять за шаблон мой абзац и подставить свои инструменты и нагрузку на них.
Каким должен быть дашборд?
Я бы формулировал требования к дашборду следующими:
Графики читаемы и интуитивно понятны — у пользователя не возникает вопросов о том, на какие данные он смотрит.
Дашборд и графики грузятся стабильно и быстро.
Одни и те же данные согласуются между разными дашбордами. Значения одних и тех же показателей не различаются между дашбордами.
Первый пункт больше в сторону UX-дизайна. О методологиях и примерах «как надо» хорошо рассказано тут ПДД: Паттерны Дизайна Дашбордов. Остальное же больше про техническую сторону, именно об этом и поговорим.
Наша команда использует Grafana в качестве источника визуализации. Clickhouse как источник данных. Altinity Plugin как Data Source в Grafana.
Grafana, как правило, используют для timeseries-данных: Prometheus, Influx. С привычной аналитикой это не вяжется, потому Grafana остается, скорее, инструментом инженеров, чем аналитиков. Но моё мнение, что Grafana даёт возможность соответствовать всем требованиям к дашборду, которые я обозначил выше. При этом разработка дашборда проходит быстрее в сравнении с BI-системами.
Решаем проблему производительности
Графики должны грузиться быстро

Можно бесконечно проводить работы по оптимизации, не сравнивая с тем, как было. Но однажды к вам придут с вопросами, например, почему всё работает так медленно. Такие вопросы могут загонять в депрессию, тем более, если на них нет ответа. Вы должны знать, где и когда стало лучше, — это мотивирует продолжать работы. А если стало хуже, то важно понимать почему.
Решение проблемы медленных графиков — многосоставная задача. Поделю работу на пункты:
Мониторинг за мониторингом — научитесь видеть картину стабильности работы вашей системы.
Правильно проектируйте дашборд — не совершайте ошибки, которые тормозят первое открытие дашборда.
Разделение ответственности — разделите дашборды на общие и внутренние. Введите квоты на запросы.
Отдельно выделю пункты, которые требуют плотной работы с хранилищем данных Clickhouse.
Делайте оптимальные запросы — определите, какие запросы тормозят графики и как это исправить.
Оптимально храните данные — научитесь хранить данные так, чтобы запросы были быстрыми.
Кэшируйте результаты запросов — если вы грузите одни и те же данные, то можно не выполнять лишней работы.
Мониторинг за мониторингом
Научитесь работать с системными таблицами
А именно — с таблицей логов запросов system.query_log. Здесь есть вся нужная информация по всем запросам в Clickhouse:
query_duration_ms— время выполнения запроса в миллисекундах;read_bytes— объём прочитанных данных в байтах;initial_user— инициатор запроса — пользователь, от которого пошли распределённые;query— полный SQL-текст запроса;normalized_query_hash— нормализованный запрос (в запросе остались только поля и операторы) в хешированном виде.
Это полезно при поиске проблемных или зависших запросов. Они могут аффектить общую производительность кластера.
Динамика по времени выполнения запросов:
select toStartOfInterval(event_time, interval 1 day) as t,
quantileTDigest(0.95)(query_duration_ms) as "q0.95",
quantileTDigest(0.9)(query_duration_ms) as "q0.9",
quantileTDigest(0.75)(query_duration_ms) as "q0.75",
quantileTDigest(0.5)(query_duration_ms) as "q0.5",
quantileTDigest(0.25)(query_duration_ms) as "q0.25"
from clusterAllReplicas('cluster_name', system.query_log)
where read_bytes > 0
and type == 'QueryFinish'
and query_kind == 'Select'
and initial_user in ('grafana')
group by t
order by tКоличество запросов, которые отправляли не меньше 10 раз:
select uniq(h)
from (
select normalized_query_hash as h
from clusterAllReplicas('cluster_name', system.query_log)
where user in ('grafana')
and type in ('QueryFinish')
and is_initial_query = 1
group by h
having count() > 10
);Настройте алерты на скорость выполнения запросов
Определите значения для порогов, которые нельзя превышать. Настройте алерты о превышении порогов. Деградация метрики должна стать проблемой, на которую стоит реагировать или, по крайней мере, иметь её в виду. Особенно это важно при постоянной нагрузке на кластер.

Размечайте запросы в Grafana и ловите их в логах
Необходимо уметь разделять запросы из разных дашбордов. Если есть проблема, то важно понимать, где исправлять. Есть несколько глобальных стандартных переменных, которые вы можете использовать:
$__dashboard— название дашборда${__user.login}— логин пользователя, который открыл дашборд
Будет полезно разделить разные открытия дашборда от одного пользователя. Придумать своего рода view_id. Для решения этой задачки создайте переменную в Grafana, допустим, dashboard_view_id, которая подключается к Clickhouse, обновляется только при открытии борды и выполняет:
select generateUUIDv4()На каждое открытие будет генерироваться уникальный UUID

Наш типичный запрос из Grafana выглядит примерно так:
WITH '$__dashboard' as dashboard,
'${__user.login}' as user,
'$dashboard_view_id' as view_id
SELECT $timeSeries as t,
quantileTDigestIf($q)(metric, metric>0) as value
FROM table
WHERE $timeFilter
GROUP BY t
ORDER BY t
-- Generated SQL ----------------------------------------------------
WITH 'Base Alerts Dashboard' as dashboard,
'napitok' as user,
'd3b4779e-8184-4e4f-ba7f-aba7060426e6' as view_id
SELECT $timeSeries as t,
quantileTDigestIf($q)(metric, metric>0) as value
FROM table
WHERE $timeFilter
GROUP BY t
ORDER BY tВ логах его можно будет поймать регулярками. Например, вот так:
select extract(query, '([^\'|\)]*)\' as dashboard') as name_dashboard,
extract(query, '([^\'|\)]*)\' as user') as user_login
from clusterAllReplicas('cluster_name', system.query_log)Важно держать структуру именования этих переменных одинаковой, иначе статистику собрать не получится.
Используя такой подход, можно вывести метрику Error Rate по загрузке дашбордов:
select name_dashboard,
uniq(view_id) as uniq_loads,
100 * avg(error) as error_rate,
100 * sum(bad_requests) / sum(all_requests) as bad_requests_rate
from (
with extract(query, '([^\'|\)]*)\' as dashboard') as name_dashboard,
extract(query, '([^\'|\)]*)\' as user') as user_login,
extract(query, '([^\'|\)]*)\' as view_id') as view_id
select name_dashboard,
view_id,
max(if(exception_code != 0 or query_duration_ms > 25000, 1, 0)) as error,
countIf(exception_code != 0 or query_duration_ms > 25000) as bad_requests,
count() as all_requests
from clusterAllReplicas('cluster_name', system.query_log)
where query like '% as dashboard%'
and user in ('grafana')
group by name_dashboard, view_id
)
group by name_dashboard
order by sum(all_requests) descИспользуйте инструменты внешнего мониторинга
Графики должны грузиться не только быстро, но и стабильно. Нужно, чтобы кто-то или что-то открывало ваши дашборды регулярно и фиксировало скорость открытия. Для решения этой задачи в нашем случае отлично подошёл sitespeed. Для нас это был наиболее простой вариант, так как sitespeed — один из инструментов, который поддерживает наша команда.
Выберите эталонный дашборд и настройте регулярный запуск сценария по его открытию.

Логируйте метрики с самой Grafana, используйте их в анализе
Допустим, мы стали замечать, что дашборды стали открываться медленнее. Лишь этого факта недостаточно, чтобы делать выводы о причинах. Как и в любом анализе, важно отделить причину, почему это произошло. Вполне возможно, что проблема тормозов на уровне самой Grafana, а не источника БД или сети. Возьмите за ориентир время получения данных дашборда, метод GET dashboards/uid. Несколько раз нас это спасало от долгих исследований в сторону проблем с БД.

Соберите весь мониторинг в одном месте
Используйте ту же Grafana для мониторинга, соберите удобный дашборд. Это ваша текущая картина стабильности всей системы.

Правильно проектируйте дашборд
Возможно, странно, что это не первый пункт, но всё же. Ошибку можно совершить уже на этапе проектирования дашборда.
Внимательно относитесь к переменным в дашборде
Часто используется тип Query, где переменная — это запрос в базу. Пока не загрузятся все переменные, дашборд не откроется. Если что-то можно вынести в запросы графиков без потери скорости, то лучше так и сделать.
Не храните все графики в одной строке (Row)
Раскрывая строку, вы отправите много запросов сразу. Grafana ограничивает количество параллельно отправляемых запросов, по умолчанию их 5. То есть в один момент времени может выполняться не более 5 запросов. Даже со скоростью менее секунды на запрос это может привести к задержкам, если их больше 20 штук.

По умолчанию все строки (Row) стоит сворачивать
Открытие дашборда должно быть лёгким. Если строки раскрыты, то при каждом открытии создаётся бесполезная нагрузка на БД, получаем ощущение, что вся система работает медленно. Это не критично, но за этим нужно следить.
Несколько быстрых запросов лучше, чем один медленный
Панели в Grafana позволяют использовать сразу несколько источников для отрисовки графиков. Иногда стоит разделить один большой запрос на несколько маленьких, которые будут вызываться параллельно. Это может быть актуально, например, когда вы используете UNION в запросе.
Разделение ответственности
Дашборды можно поделить на условные 2 категории
Общие. Здесь собраны основные показатели — на них смотрят в первую очередь, в том числе на инцидентах. К этим дашбордам есть строгое требование к стабильности — графики должны открываться быстро и всегда.
Внутренние. Эти дашборды нужны для deep-dive-исследований. Их используют, когда нужны дополнительные разрезы. Графики здесь также должны грузиться быстро, но нет строгого требования к стабильности.
Чтобы подключить Clickhouse к Grafana так или иначе необходимо завести сервисного пользователя, от имени которого Grafana будет отправлять запросы.
Чтобы контролировать потребление данных, учётку нужно ограничить настройками профиля и квотами на чтение. Именно квоты и профиль могут оказаться узким горлышком, если в системе с более 2к графиков. Если кто-то будет исследовать проблему, используя внутренние дашборды, то это может аффектить открытие общих. Мы выйдем из квот, пользователи получат ошибку вместо данных.
Отсутствие квот и ограничений = хаос
В качестве решения проблемы можно разделить сервисных пользователей по тому же принципу, что и дашборды:
critical — учётка для критически важных (общих) дашбордов, для неё можно дать увеличенную квоту.
base — учётка для всех остальных (внутренних) дашбордов с базовыми ограничениями.
Как это поможет? Допустим, инцидент — деградирует какой-то показатель. Дежурные и ответственные команд подключаются на разбор проблемы, разом открывают дашборды, какие могут, грузят данные за разный промежуток времени. С точки зрения БД это резкое увеличение нагрузки.
При таком разделении ответственности, одна группа (base) не влияет на другую (critical) в плане квот и ограничений. Только если не положить весь кластер одним запросом, и такое может быть
