Алексей, разработчик ClickHouse.
Профилирование на разных нагрузках.
Оптимизация всего, что вылезает.
Про тестирование производительности — смотрите доклад Александра Кузьменкова завтра в 10 утра.
https://www.techdesignforums.com/practice/technique/
winning-at-whac-a-mole-redesigning-an-rf-transceiver/
В ClickHouse есть разные «движки таблиц».
MergeTree таблицы хранят данные на диске.
Memory таблицы хранят данные в оперативке.
Память быстрее, чем диски*.
Значит Memory таблицы быстрее, чем MergeTree?
* Что значит «быстрее»?. Скорость последовательного чтения и записи. Задержки случайных чтений и записи. IOPS при заданном параллелизме и распределении нагрузки.
Конечно память может быть медленнее, чем дисковая подсистема,
например одноканальная память vs. 10x PCIe 4.0 SSDs.
Memory таблицы хранят данные в оперативке.
MergeTree таблицы хранят данные на диске,
точнее в файловой системе.
Но данные из файловой системы попадают в page cache.
И затем читаются уже из оперативки.
Значит нет разницы между Memory и MergeTree таблицами
в случае наличия данных в page cache?
Очевидные случаи, когда MergeTree быстрее, чем Memory.
MergeTree таблицы имеют первичный ключ и вторичные индексы,
и позволяют читать только нужные диапазоны данных.
Memory таблицы позволяют только full scan.
Но этот случай не интересен.
А при full scan может MergeTree быть быстрее, чем Memory?
Неочевидные случаи, когда MergeTree быстрее, чем Memory.
MergeTree таблицы хранят данные в сортированном порядке
по первичному ключу.
Некоторые алгоритмы в ClickHouse эксплуатируют
преимущества локальности данных, если она есть (fast path).
Например, если при GROUP BY подряд дважды встретилось
одно и то же значение, то мы не делаем повторный поиск в хэш-таблице.
Про хэш-таблицы в ClickHouse смотрите доклад Максима Киты завтра в 12:50.
А если данные в таблицах находятся в одинаковом порядке,
может ли MergeTree быть быстрее, чем Memory?
Данные в ClickHouse хранятся по столбцам
и обрабатываются тоже по столбцам.
Array of Structures | Structure of Arrays |
---|---|
struct Point3d { float x; float y; float z; }; std::vector<Point3d> points; |
struct Points { std::vector<float> x; std::vector<float> y; std::vector<float> z; }; |
Данные в ClickHouse хранятся по столбцам
и обрабатываются тоже по столбцам. По кусочкам столбцов.
struct Chunk { std::vector<float> x; std::vector<float> y; std::vector<float> z; }; std::vector<Chunk> chunks;
— Morsel-based processing.
В случае MergeTree:
— читаем сжатые файлы из файловой системы;
— вычисляем и сверяем чексуммы;
— разжимаем сжатые блоки;
— десериализуем кусочки столбцов;
— обрабатываем их;
В случае Memory:
— в оперативке уже находятся готовые
кусочки столбцов,
обрабатываем их;
В случае MergeTree:
1. Читаем сжатые файлы из файловой системы:
— читать можно с помощью синхронного (read/pread, mmap)
или асинхронного (AIO, uring) ввода-вывода;
— в случае синхронного ввода-вывода, можно
использовать (обычный read или mmap)
или не использовать page cache (O_DIRECT);
— если читать из page cache без mmap,
то будет копирование из page cache в userspace;
— читаем сжатые данные — если коэффициент сжатия большой,
то доля времени в обработке запроса маленькая;
В случае MergeTree:
2. Разжимаем сжатые блоки:
— по-умолчанию используется LZ4*;
— можно выбрать как более сильный метод сжатия (ZSTD),
так и более слабый, например вообще без сжатия (NONE);
— иногда NONE внезапно работает медленнее, с чего бы это?
— а блоками какого размера были сжаты данные?
и как это влияет на скорость?
* Смотрите доклад «Как ускорить разжатие LZ4» с HighLoad++ Siberia 2018.
В случае MergeTree:
3. Десериализуем кусочки столбцов:
— десериализации как таковой нет;
— это просто перекладывание данных (memcpy);
— а зачем вообще нужно перекладывать данные?
В случае Memory:
— готовые кусочки столбцов в оперативке.
В случае MergeTree:
— кусочки столбцов формируются динамически при чтении.
MergeTree делает больше работы,
но может ли это иногда быть оптимальнее?
В случае MergeTree:
— кусочки столбцов формируются динамически при чтении,
и их размер в числе строк может выбираться адаптивно
для кэш-локальности!
С какой скоростью работает оперативка?
— какая оперативка, на какой машине?
С какой скоростью работает кэш?
— кэш какого уровня, на каком CPU?
— один или все вместе?
С какой скоростью чего?
— throughput, latency?..
В ClickHouse данные по-умолчанию хранятся сжатыми.
При записи сжимаются, при чтении — разжимаются.
Профилируем запросы...
В топе по CPU — функция LZ4_decomress_safe.
🤔 Чтобы всё ускорить, надо просто убрать сжатие данных?
Megg, Mogg & Owl Series by Simon Hanselmann
Но ничего хорошего из этого не выходит:
1. Убрали сжатие данных и теперь они не помещаются на диск.
2. Убрали сжатие данных и теперь чтение с диска тормозит.
3. Убрали сжатие данных и теперь
меньше данных помещается в page cache.
...
Но даже если несжатые данные помещаются целиком
в оперативку — имеет ли смысл не сжимать их?
Функцию memcpy используют как baseline
самого слабого сжатия или разжатия в бенчмарках.
Конечно, это самый быстрый эталон для сравнения.
Пример:
— memcpy: 12 ГБ в секунду.
— LZ4 decompression: 2..4 ГБ разжатых данных в секунду.
Вывод: memcpy быстрее, чем разжатие LZ4?
Рассмотрим сценарий:
— данные хранятся в оперативке;
— данные обрабатываются по блокам;
— каждый блок достаточно небольшой и помещается в кэш CPU;
— обработка каждого блока помещается в кэш CPU;
— данные обрабатываются в несколько потоков;
Данные читаются из оперативки, дальше используется только кэш CPU.
Пример: Ryzen 3950 (16 ядер)
— memcpy: 16×12 ГБ = 192 ГБ в секунду.
— LZ4 decompression: 16×2..4 ГБ = 32..48 ГБ разжатых данных в секунду.
— скорость чтения из памяти: 30 ГБ* в секунду.
В случае memcpy чтение упирается в скорость памяти.
Но если используется сжатие, то из памяти читается меньше данных.
Память работает как диск. Разжатие LZ4 быстрее, чем memcpy?
* память двухканальная, но работает не на максимальной частоте.
По спецификации для этого CPU до 48 ГБ в секунду.
Пример: 2 × AMD EPYC 7742 (128 ядер)
8 channel memory, max throughput 190 GiB/s
Для этого сервера работа с данными,
сжатыми LZ4, также будет быстрее.
Но если ядер меньше — уже не всё однозначно.
Если данные хорошо сжаты, то разжатие всё-таки упирается в CPU,
а значит, его можно ускорить!
Для Memory таблиц:
— Уменьшили размер блока при записи
для лучшей кэш-локальности обработки данных #20169.
— Возможность сжатия Memory таблиц #20168.
Для MergeTree таблиц:
— Убрали лишнее копирование для режима сжатия NONE #22145.
— Возможность отключить чексуммы при чтении #19588,
но использовать эту возможность не надо.
— Возможность чтения с помощью mmap #8520, чтобы убрать
лишнее копирование из page cache а также кэш memory mappings #22206.
Чтобы оптимизировать производительность, нужно всего лишь:
— точно знать, что делает ваш код;
— профилировать систему на реалистичных сценариях нагрузки;
— представлять возможности железа;
...
— не забывать что в системе много ядер, а у процессора есть кэш;
не путать latency и throughput :)