Представьте, что вы спокойно работаете, и тут к вам в чат прилетает сообщение от руководителя: Вам задали вопрос, нужно постараться побыстрее ответить, к тому же и звучит несложно. Вы вспоминаете, что уже совсем недавно делали похожие запросы в базу, пытаетесь вспомнить, где они. Проходит пара минут, и собирается еще пара вопросов: Итак, вы всё ещё не дали ответ на первый вопрос, а у вас уже появилась пара новых. Ситуация не очень. На самом деле вопросы по типу таких возникают почти каждый день и они требуют быстрого ответа, получить который должно быть легко и просто. Для того чтобы получать ответы на одни и те же вопросы в любой момент времени, аналитики и разработчики строят дашборды.  Чтобы можно было ответить сразу на несколько разных вопросов, дашборды объединяют в целые системы, которые помогают в комплексном обзоре данных. Это важный инструмент для мониторинга и поддержки, который помогает бизнесу и разработке оставаться в курсе событий и принимать обоснованные решения. Но иногда могут случаться неприятности по типу таких: А может быть ещё хуже: Цель этой статьи — помочь вам, чтобы таких «сори» было как можно меньше. В статье мы рассмотрим проблему производительности и стабильности системы дашбордов на основе связки 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 — нормализованный запрос (в запросе остались только поля и операторы) в хешированном виде. Это полезно при поиске проблемных или зависших запросов. Они могут аффектить общую производительность кластера. Динамика по времени выполнения запросов: Количество запросов, которые отправляли не меньше 10 раз: Определите значения для порогов, которые нельзя превышать. Настройте алерты о превышении порогов. Деградация метрики должна стать проблемой, на которую стоит реагировать или, по крайней мере, иметь её в виду. Особенно это важно при постоянной нагрузке на кластер. Необходимо уметь разделять запросы из разных дашбордов. Если есть проблема, то важно понимать, где исправлять. Есть несколько глобальных стандартных переменных, которые вы можете использовать: $__dashboard — название дашборда ${__user.login} — логин пользователя, который открыл дашборд Будет полезно разделить разные открытия дашборда от одного пользователя. Придумать своего рода view_id. Для решения этой задачки создайте переменную в Grafana, допустим, dashboard_view_id, которая подключается к Clickhouse, обновляется только при открытии борды и выполняет: На каждое открытие будет генерироваться уникальный UUID Наш типичный запрос из Grafana выглядит примерно так: В логах его можно будет поймать регулярками. Например, вот так: Важно держать структуру именования этих переменных одинаковой, иначе статистику собрать не получится. Используя такой подход, можно вывести метрику Error Rate по загрузке дашбордов: Графики должны грузиться не только быстро, но и стабильно. Нужно, чтобы кто-то или что-то открывало ваши дашборды регулярно и фиксировало скорость открытия. Для решения этой задачи в нашем случае отлично подошёл sitespeed. Для нас это был наиболее простой вариант, так как sitespeed — один из инструментов, который поддерживает наша команда. Выберите эталонный дашборд и настройте регулярный запуск сценария по его открытию. Допустим, мы стали замечать, что дашборды стали открываться медленнее. Лишь этого факта недостаточно, чтобы делать выводы о причинах. Как и в любом анализе, важно отделить причину, почему это произошло. Вполне возможно, что проблема тормозов на уровне самой Grafana, а не источника БД или сети. Возьмите за ориентир время получения данных дашборда, метод GET dashboards/uid. Несколько раз нас это спасало от долгих исследований в сторону проблем с БД. Используйте ту же Grafana для мониторинга, соберите удобный дашборд. Это ваша текущая картина стабильности всей системы. Возможно, странно, что это не первый пункт, но всё же. Ошибку можно совершить уже на этапе проектирования дашборда. Часто используется тип Query, где переменная — это запрос в базу. Пока не загрузятся все переменные, дашборд не откроется. Если что-то можно вынести в запросы графиков без потери скорости, то лучше так и сделать. Раскрывая строку, вы отправите много запросов сразу. Grafana ограничивает количество параллельно отправляемых запросов, по умолчанию их 5. То есть в один момент времени может выполняться не более 5 запросов. Даже со скоростью менее секунды на запрос это может привести к задержкам, если их больше 20 штук. Открытие дашборда должно быть лёгким. Если строки раскрыты, то при каждом открытии создаётся бесполезная нагрузка на БД, получаем ощущение, что вся система работает медленно. Это не критично, но за этим нужно следить. Панели в Grafana позволяют использовать сразу несколько источников для отрисовки графиков. Иногда стоит разделить один большой запрос на несколько маленьких, которые будут вызываться параллельно. Это может быть актуально, например, когда вы используете UNION в запросе. Дашборды можно поделить на условные 2 категории Общие. Здесь собраны основные показатели — на них смотрят в первую очередь, в том числе на инцидентах. К этим дашбордам есть строгое требование к стабильности — графики должны открываться быстро и всегда. Внутренние. Эти дашборды нужны для deep-dive-исследований. Их используют, когда нужны дополнительные разрезы. Графики здесь также должны грузиться быстро, но нет строгого требования к стабильности. Чтобы подключить Clickhouse к Grafana так или иначе необходимо завести сервисного пользователя, от имени которого Grafana будет отправлять запросы. Чтобы контролировать потребление данных, учётку нужно ограничить настройками профиля и квотами на чтение. Именно квоты и профиль могут оказаться узким горлышком, если в системе с более 2к графиков. Если кто-то будет исследовать проблему, используя внутренние дашборды, то это может аффектить открытие общих. Мы выйдем из квот, пользователи получат ошибку вместо данных. Отсутствие квот и ограничений = хаос В качестве решения проблемы можно разделить сервисных пользователей по тому же принципу, что и дашборды: critical — учётка для критически важных (общих) дашбордов, для неё можно дать увеличенную квоту. base — учётка для всех остальных (внутренних) дашбордов с базовыми ограничениями. Как это поможет? Допустим, инцидент — деградирует какой-то показатель. Дежурные и ответственные команд подключаются на разбор проблемы, разом открывают дашборды, какие могут, грузят данные за разный промежуток времени. С точки зрения БД это резкое увеличение нагрузки. При таком разделении ответственности, одна группа (base) не влияет на другую (critical) в плане квот и ограничений. Только если не положить весь кластер одним запросом, и такое может быть🙂 Я выделил наиболее важные ограничения, список можно (стоит) расширить. Значения показателей зависят от ваших мощностей. Наш основной паттерн — это построение и исследование временных рядов в реальном времени и у этого есть своя специфика. Тем не менее, для Clickhouse есть несколько универсальных советов: 1. Чем меньше колонок в запросе, тем лучше. Причина — колоночное хранение данных в Clickhouse. 2. Старайтесь использовать колонки из ключа сортировки таблицы (секция ORDER BY при создании таблицы). Идеальная ситуация — когда в запросе используются все колонки из сортировки, и только они. 3. Используйте динамическое семплирование. Чем больший промежуток смотрите, тем меньше данных читаете. Можно сделать следующим образом: создаём колонку sample типа UInt16 и используем её в ключе сортировки; заполняем случайными значениями в диапазоне от 0 до 99. В каждом запросе ограничиваем sample пропорционально количеству прочитанных дней Таким образом, при чтении данных за 2 дня мы прочитаем случайную половину от всего объёма, при чтении 10 дней прочитаем 1/10 и так далее. 4. Используйте макрос $unescape() Макрос позволяет скрывать часть запроса, если переменная в состоянии All, то есть не использовать колонки, которые не нужны в запросе.Например, есть фильтр по регионам. Если выбрать все регионы, то условие на регион становится бессмысленным. При этом данные из колонки мы читаем. Тут макрос и пригодится. 5. Выполняйте приближенные вычисления на больших данных В основном мы используем квантили, потому для нас это особенно критично. Например, мы используем quantileTDigest вместо наиболее точного варианта quantileExact 6. Агрегируйте данные за долгий период заранее, используйте в графиках агрегаты и сырые данные вместе Используйте Materialized View или внешние системы по типу Airflow для вычисления агрегатов в фоне.При чтении данных более чем за пару дней подключайте агрегаты, иначе используйте сырые данные.Пример запроса: Есть три основных показателя оптимальности работы запроса: elapsed — время выполнения read_rows — количество прочитанных строк read_bytes — количество прочитанных байт Есть несколько способов получить эти данные быстро: Выполните запрос и найдите его в логах по query_id. Вы можете получить id своего запроса прямо во время его выполнения, используя функцию query_id() 123 queryID() 123 50550ffe-0654-409c-bc81-44e3185b0cca read_rows read_bytes query_duration_ms query 1 1 1 SELECT 123, query_id() 2. В Grafana каждый запрос отправляется с клиента по отдельности. В devtools в Chrome можно найти нужную информацию по запросам, не переключаясь от дашборда. Фиксируйте эти показатели, экспериментируйте с запросами и сравнивайте результаты с прошлыми замерами. По моим наблюдениям, если запрос читает более 10-20 Гб, то начинаются визуальные тормоза графиков. Мы ставим ограничение в 600 Гб на один запрос, иначе он будет отбиваться с ошибкой. Как-то раз мы переделали структуру таблицы и получили профит производительности запросов больше чем в 100 раз на тех же данных (ни капли преувеличения). Как же хранить данные в Clickhouse оптимально? Обозначу пункты, по которым стоит пройтись. Для наглядности примера будем отталкиваться от следующих данных: timestamp — время измерения platform — название платформы url — урл страницы metric_name — название метрики flag — какой-то флаг разметки metric_value — значение метрики в миллисекундах timestamp platform url metric_name flag metric_value 2023-09-29 00:00 WEB https://{domain}/{path}/?{params} browsertime.statistics.timings.navigationTiming.domInteractive 0 6902 2023-09-29 00:01 MOBILE_WEB https://{domain}/{path}/?{params} browsertime.statistics.timings.navigationTiming.domainLookupEnd 0 2936 2023-09-29 00:02 WEB https://{domain}/{path}/?{params} browsertime.statistics.timings.navigationTiming.domainLookupStart 1 1231 … Если это флаг, то используйте UInt8. Если это число и точность значения не важна, то используйте Int вместо Float. Если это строка и она низкокардинальная, то используйте LowCardinality(String). Если строка длинная, а вам нужна только ее часть, то разделите строку на несколько частей и храните отдельно — например урл поделите на домен, путь и get-параметры. platform — низкокардинальный признак. Мы заранее знаем список платформ. url — высококардинальный признак. После обсуждения с заказчиком узнали, что от урла содержательно нужен только домен, остальное нужно для точечных исследований. Выделим домен в отдельную колонку, которая будет низкокардинальна. metric_name — низкокардинальный признак. Мы заранее знаем список метрик. flag — число со значениями 0/1. Для хранения достаточно UInt8. metric_value — для миллисекунд вполне достаточно UInt32. Выберите только те поля, которые регулярно используются в запросах. Отсортируйте поля от низко- до высококардинальных. Например, начните с платформы устройства, закончите временем. По умолчанию по ключу сортировки строится первичный ключ. В нашем примере основной паттерн использования — построение временного ряда, потому timestamp обязательно должен быть частью сортировки. Интерпретируйте партицию как отдельный файл. На открытие и чтение каждого файла система тратит время. Чем меньше партиций используется в запросе, тем лучше. При этом размер одной партиции должен быть не больше 100-150 Гб — найдите баланс. В моем случае партицирование, как правило, по дню, в редком случае — по неделям и месяцам. Храним немного данных, но читаем за большой промежуток времени. Партицируем по месяцам: Если данных много, то дефолтного значения в 8192 достаточно. Если используете готовую витрину, то порежьте индекс до 1024 и меньше. Гранула — количество строк между двумя метками из индекса. Мы не можем прочитать строк меньше, чем в одной грануле. С нашей вариативностью в данных экспериментально подобрали гранулярность в 1024: Храните свежие данные на SSD, старые — на HDD. По нашим замерам, разница между дисками доходила до 20 раз. Настройка storage_policy. Наши данные используются редко, будем хранить их на HDD, чтобы оставить больше места для SSD: Ничего из того, что перечислено, не получится сделать с уже готовой таблицей, разве только типы изменить, что долго и затратно. Структуру таблицы менять нельзя. Так или иначе, спроектируйте таблицу со структурой, которую считаете правильной. Далее есть два пути: Перекопировать данные из старой таблицы в новую. Примерно так: Если данных много, то стоит автоматизировать процесс или использовать clickhouse-copier https://clickhouse.com/docs/en/operations/utilities/clickhouse-copier  Сделать Materialized View со вставкой в новую таблицу. В таком случае будет дубликация данных, но как быстрый вариант вполне ок. А вообще, о том, что делать, если необходимо оптимизировать старую таблицу, описано здесь: https://kb.altinity.com/altinity-kb-schema-design/change-order-by/ Используйте настройку use_query_cache = True. Возможность кэшировать результаты запроса появилась в версии 23.1. https://clickhouse.com/docs/en/operations/query-cache Подробнее про применение тут: https://clickhouse.com/blog/introduction-to-the-clickhouse-query-cache-and-design В нашем случае был реализован собственный сервис, который хранит результаты в Memcache. Заодно реализовали прокси, который направляет запросы в нужный кластер. Получилась единая точка входа сразу на несколько кластеров и в целом интересный инструмент. Данные должны быть одинаковыми Итак, мы спроектировали правильные таблицы, дашборды, научились делать оптимальные запросы и привязали мониторинг. Остаётся проблема — согласованность данных. Каждый день приходится добавлять/удалять графики, а иногда и создавать новые дашборды. Как не допустить ошибку? Что делать, если на одном дашборде метрика с одним значением, а на другом дашборде та же метрика с другим значением? Для нас это оказалось большой проблемой, так как мы используем множество условий и ограничений на данные, которые попадают в конечный мониторинг. Например, в метриках скорости мы не учитываем заходы через VPN. Проблемы, с которыми столкнулись: Большое количество условий тормозит запросы. Когда графиков много, то легко что-то забыть. Проведите системную работу. Определите набор условий и метрик, который участвует в большей части визуализаций. Определите количество трафика, которого достаточно для репрезентативной картины. Мы создали еще одну таблицу, в которую приходят анонимные события (без урлов, идентификаторов пользователя). Только метрики скорости и основные атрибуты. Таблицу наполняют Materialized View, которые триггерятся на вставку в основные таблицы. В Materialized View уже используются все наши собранные условия. Тем самым в итоговую таблицу попадают события, которые не нужно фильтровать. Эту таблицу мы назвали «горячей», основные таблицы — «холодными». Этим решением мы покрыли большую часть дашбордов. Но остались графики для deep-dive-исследований, графики по новым метрикам и атрибутам, которых еще нет в горячей таблице и так далее. Создайте View, в котором строкой хранили набор условий: Создайте переменную в Grafana, в которой обращаетесь к View. Сделайте мультивыбор, включая All. Это нужно чтобы при копировании ссылки на дашборд пользователь не видел SQL-код в гет-параметрах урла. Вызывайте эту переменную в запросах, используя csv-форматирование. Это нужно, чтобы убрать внешние кавычки, но не убирать внутренние со строковых значений. Теперь изменение во View изменит все дашборды, на которых она завязана. Я собрал краткую выжимку по тем решениям, которые помогли решить проблему медленных и нестабильно работающих графиков. Использовал как очевидные и обобщенные советы, например, «храните данные оптимально, а неоптимально не храните», так и рекомендации по внедрению динамического семплирования, которые могут подойти не для всех ситуаций. А история с внешним мониторингом за дашбордами с использованием sitespeed может показаться вовсе перебором. Нет единственного решения всех проблем для каждого случая, но собрать универсальные советы попробовать можно: Следите за стабильностью и скоростью загрузки графиков. Смотрите за системными таблицами. Делайте быстрые запросы, а при проектировании хранилища учитывайте специфичную нагрузку. При возможности кэшируйте результаты запросов. Ограничивайте запросы квотами и другими лимитами. Чем меньше различных источников данных, тем лучше. Делайте дашборды не только красивыми, но и быстрыми  Аналитик данных