Инженеры ClickHouse одержимы производительностью. С каждым релизом мы ищем новые способы сделать работу ClickHouse еще быстрее и обеспечить лучшую масштабируемость на базовом аппаратном обеспечении. Когда компания NVIDIA недавно предоставила нам сервер раннего доступа на базе своей будущей платформы Vera CPU, мы сделали очевидный выбор: запустили на нем ClickBench, наш популярный бенчмарк для аналитики. Мы использовали стандартный бинарный файл ClickHouse с нашего сайта и не выполняли никаких специфичных для NVIDIA Vera оптимизаций или настроек базы данных. Результаты говорят сами за себя:
NVIDIA Vera сразу же с большим отрывом вышла на первое место в аппаратном рейтинге ClickBench.
Любой инженер баз данных признает, что Vera — поистине впечатляющая платформа. Она оснащена 88 ядрами NVIDIA Olympus, полностью совместимыми с архитектурой Arm-v9.2. В двухсокетной системе это дает в общей сложности 176 ядер и 352 аппаратных потока благодаря технологии NVIDIA Spatial Multithreading. Каждое ядро имеет доступ к 96/64 КБ кэша L1 для данных/инструкций, 2 МБ кэша L2 и 164 МБ общего кэша L3. Максимальная пропускная способность памяти составляет от 1,0 ТБ/с до 1,2 ТБ/с в зависимости от объема DRAM в системе.
Можно утверждать, что большее количество ядер процессора, больший объем кэша и более высокая пропускная способность памяти естественным образом ускоряют выполнение любого бенчмарка. И в этом есть доля правды, но главная задача заключается в том, чтобы производительность базы данных масштабировалась линейно вместе с ресурсами машины. Проще говоря: если машина становится в два раза мощнее (в два раза больше ядер процессора, в два раза больше кэша, в два раза выше пропускная способность памяти и т. д.), можно ожидать, что запросы будут выполняться в два раза быстрее, а не просто с коэффициентом ускорения менее двух. То, как данные организованы на диске, как они представлены в памяти и как обрабатываются — все это влияет на способность базы данных масштабироваться. Без тщательной инженерии на каждом уровне база данных не смогла бы задействовать огромное количество ядер и пропускную способность памяти Vera, оставив часть производительности неиспользованной.
Более мощное аппаратное обеспечение помогает решать новые задачи, требующие интенсивной обработки данных и вычислений в реальном времени, аналитике, хранилищах данных, системах наблюдаемости и машинного обучения. Если раньше большинство запросов генерировалось дашбордами (например, Grafana, Superset) или BI-инструментами (например, Tableau и Looker), то теперь мы все чаще видим агентов в ClickHouse Cloud, которые автономно отвечают на вопросы по данным. С точки зрения базы данных, запросы агентов характеризуются тремя свойствами: во-первых, они гораздо более «специфичны» по своей природе, чем обычные запросы (например, они могут иметь сложные структуры, которые также значительно меняются от запроса к запросу); во-вторых, они поступают в непредсказуемое время и часто всплесками; и в-третьих, большое количество запросов агентов обычно выполняется параллельно, так как агенты порождают субагентов, которые сами выполняются параллельно.
Я хотел протестировать масштабируемость ClickHouse на платформе Vera, поэтому разработал три микробенчмарка, которые симулируют основные стандартные операции базы данных в большинстве обычных и агентских запросов.
- оценка фильтров (предложение SQL WHERE),
- агрегация (предложение SQL GROUP BY) и
- соединения (предложение SQL JOIN).
Синтетический набор данных должен был храниться полностью в памяти, чтобы дисковый ввод-вывод (объектное хранилище или локальный SSD) не создавал искусственного узкого места. К счастью, в ClickHouse для этих целей предусмотрен табличный движок Memory. Слой обработки запросов ClickHouse полностью отделен от слоя хранения. При условии достаточной пропускной способности хранилища результаты бенчмарка применимы и к обычной таблице MergeTree в ClickHouse Cloud или ClickHouse Open Source.
Наша тестовая таблица состоит из одного столбца с 62,5 миллиардами случайных строк, каждая из которых содержит от 5 до 10 символов ASCII. Размер таблицы составляет ровно 1 ТБ. Строки хранятся в необработанном виде без сжатия (сжатие в движке таблиц Memory является опциональным).
Первым микробенчмарком, который я запустил, был простой запрос LIKE:
Я измерил время выполнения запроса с различным количеством потоков (установив параметр max_threads):
«time» — это время выполнения запроса. «speedup» показывает, во сколько раз сократилось время выполнения запроса по сравнению с max_threads = 1. «efficiency» — это наблюдаемое ускорение по отношению к ожидаемому. Например, при использовании 32 потоков можно ожидать 32-кратного ускорения. Более низкие показатели ускорения означают эффективность менее 100%.
Время выполнения уменьшается пропорционально количеству потоков запроса… пока не упрется в барьер на 64 потоках. Дальнейшее увеличение количества потоков до 256 не дает значительного дополнительного ускорения. Такое поведение обычно указывает на то, что запрос ограничен пропускной способностью ввода-вывода (I/O-bound), то есть ядра обрабатывают данные быстрее, чем шина памяти успевает читать или записывать их из/в DRAM. Мы можем быстро проверить это, разделив общий объем данных (1 ТБ) на время выполнения запроса.
Действительно, когда запрос перестает масштабироваться, пропускная способность ввода-вывода памяти составляет 813 ГБ/с (64 ядра) и 900 ГБ/с (256 ядер) соответственно. Это значение примерно соответствует физической пропускной способности памяти системы Vera. Это говорит о том, что запрос ограничен памятью — добавление новых ядер не приведет к его дальнейшему ускорению.
Чтобы убедиться, что все ресурсы процессора задействованы в полной мере и за этим пределом, я изменил запрос, сделав его более ресурсоемким для CPU:
В то время как предыдущий запрос выполнял простой поиск подстроки, этот запрос вычисляет сложное регулярное выражение для каждой строки в таблице. Вот результаты:
Теперь время выполнения масштабируется практически идеально в зависимости от количества ядер процессора. Ну, почти всегда… начиная со 176 ядер (числа физических ядер на машине Vera), эффективность начинает постепенно снижаться. После этого числа ядер дальнейшие программные потоки обслуживаются логическими, а не физическими аппаратными потоками. Логические аппаратные потоки (или потоки SMT) разделяют физические ядра с другими логическими аппаратными потоками. Хотя логические аппаратные потоки отлично подходят для маскировки задержек, таких как промахи кэша, неверно предсказанные ветвления или удаленный доступ к данным, это помогает лишь умеренно (например, ускорение на 10-30%) для вычислительно-интенсивных задач, таких как запрос фильтрации. Именно это мы и наблюдаем здесь.
Обработка запросов распараллеливается очевидным образом: ClickHouse делит таблицу на 352 виртуальных фрагмента одинакового размера, которые затем фильтруются одновременно. Это видно по выводу EXPLAIN PIPELINE:
Не вдаваясь в детальное описание операторов плана, запись x 352 после каждого оператора означает, что 352 виртуальных фрагмента таблицы обрабатываются параллельно. Финальный оператор Resize объединяет 352 частичных результата в конечный ответ.
Затем я протестировал запрос на агрегацию:
Запрос группирует строки по их первому символу (т.е. на 255 групп) и возвращает как размер группы, так и сумму значения (sip)hash для каждой строки. 255 групп — это то, что разработчики баз данных называют агрегацией с «низкой кардинальностью». Агрегация с высокой кардинальностью, насчитывающая миллионы групп, представляет большую сложность с точки зрения базы данных, однако агрегация с низкой кардинальностью гораздо чаще встречается на практике, например при группировке наборов данных по часам. Чтобы избежать повторного ограничения по памяти, вычисление хэша делает запрос намеренно более ресурсоемким для процессора.
Вот результаты для разного количества потоков запроса:
Время выполнения снова практически идеально масштабируется с количеством ядер. Нейропредсказание ветвлений и сложная предварительная выборка данных Vera помогают ClickHouse эффективно записывать и читать хэш-таблицу, а также скрывать задержки доступа, вызванные неравномерностью распределения. Как и в предыдущем запросе, эффективность начинает снижаться после превышения числа физических ядер, поскольку запросы, ограниченные производительностью вычислений, могут лишь в ограниченной степени использовать SMT.
Ниже приведен вывод команды EXPLAIN PIPELINE:
План лучше всего понимать снизу вверх. ExpressionTransform вычисляет ascii (ключ группировки) и sipHash (аргумент функции sum) для каждой строки таблицы в 352 параллельных потоках. Затем AggregatingTransform создает хэш-таблицу для каждого потока и, когда последний поток завершает обработку, объединяет их в общую хэш-таблицу. Оба оператора полностью распараллелены и задействуют все аппаратные ресурсы. План содержит еще два оператора: Resize снова разделяет общую хэш-таблицу и распределяет ее элементы по входам следующего оператора — ExpressionTransform. В этом запросе хэш-таблица имеет всего несколько записей (256), однако ClickHouse вставляет оператор resize в план запроса безусловно, поскольку размер хэш-таблицы неизвестен до момента выполнения. ExpressionTransform (в самом конце конвейера) вычисляет дополнительные выражения в списке SELECT. В этом запросе список SELECT ссылается на выражение группировки и агрегаты напрямую — никаких дополнительных вычислений после агрегации не происходит, и оператор практически не выполняет никакой работы. Но даже если бы ему пришлось выполнять какую-то работу, ExpressionTransform также запускался бы с максимальным параллелизмом.
Третий и последний микробенчмарк представляет собой соединение с небольшой таблицей. Это отражает распространенный сценарий использования соединения между таблицей фактов и таблицей измерений. Я выполнил выборку из исходной таблицы, чтобы создать таблицу измерений с 625 тыс. строк (0,001% от исходной таблицы):
Затем я выполнил простой запрос на соединение:
Вот результаты:
Время выполнения уменьшается практически пропорционально количеству потоков запроса. Другими словами, ClickHouse масштабируется идеально.
Инструкция EXPLAIN PIPELINE для этого запроса довольно сложна, поэтому я опускаю ее здесь. В соединении используется алгоритм параллельного хэш-соединения ClickHouse, описанный здесь, что гарантирует возможность построения хэш-таблицы всеми потоками запроса параллельно. Оно также строит фильтр времени выполнения для ускорения зондирования хэш-таблицы. На практике столбец соединения обычно является частью первичного ключа таблицы фактов («соединение по внешнему ключу»). В этом эксперименте ни у одной таблицы не было первичного ключа, но если бы он был, это открыло бы еще одну оптимизацию производительности, которую мы внедрили недавно.
Три упомянутых выше микробенчмарка в первую очередь полагались на многопоточность для ускорения обработки запросов. Ортогональный способ распараллеливания обработки данных — это SIMD (одна инструкция, множество данных). ClickHouse в полной мере использует аппаратные возможности SIMD для ускорения горячих циклов с помощью автовекторизации компилятора или intrinsics. В этих часто повторяющихся участках кода процессор обрабатывает несколько значений одновременно вместо обработки каждого значения по отдельности.
Поэтому в качестве бонуса я выполнил следующий простой запрос с фильтром на машине Vera, чтобы проиллюстрировать преимущества SIMD. В демонстрационных целях я отключил многопоточность:
Этот запрос выполнился за 0,42 с.
Затем я открыл реализацию фильтра WHERE в ClickHouse в коде C++:
a и b представляют левую и правую части условия фильтра, то есть x и 2000000000. Op::apply («применить операцию») вычисляет a < b для пакета входных строк и возвращает результат как c.
, чтобы продемонстрировать, как ClickHouse извлекает выгоду из мощных инструкций SIMD, я поручил компилятору отключить автовекторизацию, вставив Pragma("clang loop vectorize(disable) interleave(disable)") непосредственно перед циклом while. Это позволяет ClickHouse сравнивать входные значения последовательно, без применения инструкций SIMD. В результате запрос теперь выполнялся за 1,48 с, то есть в 3,5 раза медленнее, чем раньше. При включенной автовекторизации ClickHouse использует 128-битные инструкции NEON процессора Vera для одновременного сравнения четырех 32-битных целых чисел (теоретическое ускорение составляет 4 раза). Помимо NEON, Vera также предоставляет мощную 6x128-битную реализацию SVE2, обеспечивающую значительно более высокую пропускную способность векторной обработки на всех вычислительных блоках. Поддержка этих инструкций в ClickHouse в настоящее время добавляется совместно с сообществом с открытым исходным кодом: PR #122860.
«NVIDIA Vera — это невероятно быстрая и мощная серверная платформа, самая быстрая из всех, что мы видели до сих пор. ClickHouse стремится воплотить ее огромный потенциал в производительность аналитики мирового класса». — Алексей Миловидов, технический директор ClickHouse
«NVIDIA Vera — это невероятно быстрая и мощная серверная платформа, самая быстрая из всех, что мы видели до сих пор. ClickHouse стремится воплотить ее огромный потенциал в производительность аналитики мирового класса».
— Алексей Миловидов, технический директор ClickHouse
Хотя ClickHouse уже сегодня отлично работает на машинах с большим количеством ядер, инженерная команда ClickHouse, конечно же, постоянно ставит под сомнение текущее положение дел. Существуют ли неэффективности во время выполнения запроса? Не все ли шаги конвейера запросов пока в полной мере используют аппаратный параллелизм? Можно ли сделать структуры данных более компактными и тем самым более эффективными с точки зрения кэша? Может ли база данных еще лучше использовать сжатие, асинхронную обработку, пакетирование или кэширование? Следите за обновлениями: в следующей статье в блоге, посвященной проектированию производительности в ClickHouse, мы расскажем, как мы анализируем, оптимизируем и тестируем производительность запросов в масштабе в ClickHouse.
Более подробную информацию о NVIDIA Vera можно найти здесь: https://developer.nvidia.com/blog/nvidia-vera-cpu-boosts-ai-factory-throughput-to-accelerate-agentic-workloads/





/)





