ClickHouse — это колоночная база данных с открытым исходным кодом. Это великолепная среда, где сотни аналитиков могут быстро запрашивать развернутые данные, даже когда вводятся десятки миллиардов новых записей в день. Расходы на инфраструктуру для поддержки такой системы могут достигать 100 тыс. долларов США в год, и потенциально вдвое меньше, в зависимости от использования. В какой-то момент инсталяция ClickHouse от Яндекс Метрики содержала 10 триллионов записей. Помимо Яндекса, ClickHouse также снискала успех у Bloomberg и Cloudflare.
Два года назад я провел сравнительный анализ баз данных с использованием одной машины, и она стала самым быстрым бесплатным программным обеспечением для баз данных, которое я когда-либо видел. С тех пор разработчики не переставали добавлять фичи, включая поддержку Kafka, HDFS и ZStandard сжатия. В прошлом году они добавили поддержку каскадирования методов сжатия, и дельта-от-дельты кодирование стало возможным. При сжатии данных временных рядов gauge-значения могут хорошо сжиматься с помощью дельта-кодирования, но для счетчиков (counter) будет лучше использовать дельта-от-дельты-кодирование. Хорошее сжатие стало ключом к производительности ClickHouse.
ClickHouse состоит из 170 тысяч строк C++ кода, за исключением сторонних библиотек, и является одной из наименьших кодовых баз для распределенных баз данных. Для сравнения, SQLite не поддерживает распределение и состоит из 235 тысяч строк кода на языке С. На момент написания этой статьи свой вклад в ClickHouse внесли 207 инженеров, и интенсивность коммитов в последнее время увеличивается.
В марте 2017 года ClickHouse начал вести журнал изменений в качестве простого способа отслеживать разработку. Они также разбили монолитный файл документации на иерархию файлов на основе Markdown. Проблемы и фичи отслеживаются через GitHub, и в целом это программное обеспечение стало намного более доступным в последние несколько лет.
В этой статье я собираюсь взглянуть на производительность кластера ClickHouse на AWS EC2 с использованием 36-ядерных процессоров и NVMe-накопителя.
АПДЕЙТ: Через неделю после первоначальной публикации этого поста я повторно запустил тест с улучшенной конфигурацией и достиг гораздо лучших результатов. Этот пост был обновлен, чтобы отразить эти изменения.
Запуск кластера AWS EC2
Я буду использовать три экземпляра c5d.9xlarge EC2 для этого поста. Каждый из них содержит 36 виртуальных ЦП, 72 ГБ ОЗУ, 900 ГБ накопителя NVMe SSD и поддерживает 10-гигабитную сеть. Они стоят $1,962/час каждый в регионе eu-west-1 при запуске по требованию. Я буду использовать Ubuntu Server 16.04 LTS в качестве операционной системы.
Фаервол настроен так, что каждая машина может связываться друг с другом без ограничений, и только мой IPv4-адрес занесен в белый список SSH в кластере.
NVMe-накопитель в состоянии рабочей готовности
Для работы ClickHouse я создам в NVMe-накопителе файловую систему в формате EXT4 на каждом из серверов.
$ sudo mkfs -t ext4 /dev/nvme1n1
$ sudo mkdir /ch
$ sudo mount /dev/nvme1n1 /ch
После того как все настроено, вы можете увидеть точку монтирования и 783 ГБ пространства, доступного в каждой из систем.
$ lsblk
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
loop0 7:0 0 87.9M 1 loop /snap/core/5742
loop1 7:1 0 16.5M 1 loop /snap/amazon-ssm-agent/784
nvme0n1 259:1 0 8G 0 disk
L-nvme0n1p1 259:2 0 8G 0 part /
nvme1n1 259:0 0 838.2G 0 disk /ch
$ df -h
Filesystem Size Used Avail Use% Mounted on
udev 35G 0 35G 0% /dev
tmpfs 6.9G 8.8M 6.9G 1% /run
/dev/nvme0n1p1 7.7G 967M 6.8G 13% /
tmpfs 35G 0 35G 0% /dev/shm
tmpfs 5.0M 0 5.0M 0% /run/lock
tmpfs 35G 0 35G 0% /sys/fs/cgroup
/dev/loop0 88M 88M 0 100% /snap/core/5742
/dev/loop1 17M 17M 0 100% /snap/amazon-ssm-agent/784
tmpfs 6.9G 0 6.9G 0% /run/user/1000
/dev/nvme1n1 825G 73M 783G 1% /ch
Набор данных, который я буду использовать в этом тесте, представляет собой дамп данных, который я сформировал из 1.1 миллиарда поездок на такси, произведенных в Нью-Йорке за шесть лет. В блоге Миллиард поездок на такси в Redshift подробно рассказывается о том, как я собрал этот набор данных. Они хранятся в AWS S3, поэтому я настрою интерфейс командной строки AWS с помощью моего доступа и секретных ключей.
$ sudo apt update
$ sudo apt install awscli
$ aws configure
Я установлю ограничение количества одновременных запросов клиента на 100, чтобы файлы загружались быстрее, чем при стандартных настройках.
$ aws configure set default.s3.max_concurrent_requests 100
Я скачаю набор данных поездок на такси с AWS S3 и сохраню его на диске NVMe на первом сервере. Этот набор данных составляет ~ 104 ГБ в GZIP-сжатом CSV-формате.
$ sudo mkdir -p /ch/csv
$ sudo chown -R ubuntu /ch/csv
$ aws s3 sync s3://<bucket>/csv /ch/csv
Установка ClickHouse
Я установлю дистрибутив OpenJDK для Java 8, так как он необходим для запуска Apache ZooKeeper, необходимого для распределенной установки ClickHouse на всех трех машинах.
$ sudo apt update
$ sudo apt install openjdk-8-jre openjdk-8-jdk-headless
Затем я устанавливаю переменную среды
JAVA_HOME
.$ sudo vi /etc/profile
export JAVA_HOME=/usr
$ source /etc/profile
Затем я буду использовать систему управления пакетами в Ubuntu для установки ClickHouse 18.16.1, glances и ZooKeeper на все три машины.
$ sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv E0C56BD4
$ echo "deb http://repo.yandex.ru/clickhouse/deb/stable/ main/" | sudo tee /etc/apt/sources.list.d/clickhouse.list
$ sudo apt-get update
$ sudo apt install clickhouse-client clickhouse-server glances zookeeperd
Я создам каталог для ClickHouse, а также совершу некоторые переопределения конфигурации на всех трех серверах.
$ sudo mkdir /ch/clickhouse
$ sudo chown -R clickhouse /ch/clickhouse
$ sudo mkdir -p /etc/clickhouse-server/conf.d
$ sudo vi /etc/clickhouse-server/conf.d/taxis.conf
Это переопределения конфигурации, которые я буду использовать.
<?xml version="1.0"?>
<yandex>
<listen_host>0.0.0.0</listen_host>
<path>/ch/clickhouse/</path>
<remote_servers>
<perftest_3shards>
<shard>
<replica>
<host>172.30.2.192</host>
<port>9000</port>
</replica>
</shard>
<shard>
<replica>
<host>172.30.2.162</host>
<port>9000</port>
</replica>
</shard>
<shard>
<replica>
<host>172.30.2.36</host>
<port>9000</port>
</replica>
</shard>
</perftest_3shards>
</remote_servers>
<zookeeper-servers>
<node>
<host>172.30.2.192</host>
<port>2181</port>
</node>
<node>
<host>172.30.2.162</host>
<port>2181</port>
</node>
<node>
<host>172.30.2.36</host>
<port>2181</port>
</node>
</zookeeper-servers>
<macros>
<shard>03</shard>
<replica>01</replica>
</macros>
</yandex>
Затем я запущу ZooKeeper и сервер ClickHouse на всех трех машинах.
$ sudo /etc/init.d/zookeeper start
$ sudo service clickhouse-server start
Загрузка данных в ClickHouse
На первом сервере я создам таблицу поездок (
trips
), в которой будет храниться набор данных поездок в такси с использованием движка Log.$ clickhouse-client --host=0.0.0.0
CREATE TABLE trips (
trip_id UInt32,
vendor_id String,
pickup_datetime DateTime,
dropoff_datetime Nullable(DateTime),
store_and_fwd_flag Nullable(FixedString(1)),
rate_code_id Nullable(UInt8),
pickup_longitude Nullable(Float64),
pickup_latitude Nullable(Float64),
dropoff_longitude Nullable(Float64),
dropoff_latitude Nullable(Float64),
passenger_count Nullable(UInt8),
trip_distance Nullable(Float64),
fare_amount Nullable(Float32),
extra Nullable(Float32),
mta_tax Nullable(Float32),
tip_amount Nullable(Float32),
tolls_amount Nullable(Float32),
ehail_fee Nullable(Float32),
improvement_surcharge Nullable(Float32),
total_amount Nullable(Float32),
payment_type Nullable(String),
trip_type Nullable(UInt8),
pickup Nullable(String),
dropoff Nullable(String),
cab_type Nullable(String),
precipitation Nullable(Int8),
snow_depth Nullable(Int8),
snowfall Nullable(Int8),
max_temperature Nullable(Int8),
min_temperature Nullable(Int8),
average_wind_speed Nullable(Int8),
pickup_nyct2010_gid Nullable(Int8),
pickup_ctlabel Nullable(String),
pickup_borocode Nullable(Int8),
pickup_boroname Nullable(String),
pickup_ct2010 Nullable(String),
pickup_boroct2010 Nullable(String),
pickup_cdeligibil Nullable(FixedString(1)),
pickup_ntacode Nullable(String),
pickup_ntaname Nullable(String),
pickup_puma Nullable(String),
dropoff_nyct2010_gid Nullable(UInt8),
dropoff_ctlabel Nullable(String),
dropoff_borocode Nullable(UInt8),
dropoff_boroname Nullable(String),
dropoff_ct2010 Nullable(String),
dropoff_boroct2010 Nullable(String),
dropoff_cdeligibil Nullable(String),
dropoff_ntacode Nullable(String),
dropoff_ntaname Nullable(String),
dropoff_puma Nullable(String)
) ENGINE = Log;
Затем я распаковываю и загружаю каждый из CSV-файлов в таблицу поездок (
trips
). Следующее выполнено за 55 минут и 10 секунд. После этой операции размер каталога данных составил 134 ГБ.$ time (for FILENAME in /ch/csv/trips_x*.csv.gz; do
echo $FILENAME
gunzip -c $FILENAME | clickhouse-client --host=0.0.0.0 --query="INSERT INTO trips FORMAT CSV"
done)
Скорость импорта составляла 155 МБ несжатого CSV-контента в секунду. Я подозреваю, что это было связано с узким местом в GZIP-декомпрессии. Возможно, быстрее было распаковать все файлы gzip параллельно, используя xargs, а затем загрузить распакованные данные. Ниже приведено описание того, что сообщалось в процессе импорта CSV.
$ sudo glances
ip-172-30-2-200 (Ubuntu 16.04 64bit / Linux 4.4.0-1072-aws) Uptime: 0:11:42
CPU 8.2% nice: 0.0% LOAD 36-core MEM 9.8% active: 5.20G SWAP 0.0%
user: 6.0% irq: 0.0% 1 min: 2.24 total: 68.7G inactive: 61.0G total: 0
system: 0.9% iowait: 1.3% 5 min: 1.83 used: 6.71G buffers: 66.4M used: 0
idle: 91.8% steal: 0.0% 15 min: 1.01 free: 62.0G cached: 61.6G free: 0
NETWORK Rx/s Tx/s TASKS 370 (507 thr), 2 run, 368 slp, 0 oth sorted automatically by cpu_percent, flat view
ens5 136b 2Kb
lo 343Mb 343Mb CPU% MEM% VIRT RES PID USER NI S TIME+ IOR/s IOW/s Command
100.4 1.5 1.65G 1.06G 9909 ubuntu 0 S 1:01.33 0 0 clickhouse-client --host=0.0.0.0 --query=INSERT INTO trips FORMAT CSV
DISK I/O R/s W/s 85.1 0.0 4.65M 708K 9908 ubuntu 0 R 0:50.60 32M 0 gzip -d -c /ch/csv/trips_xac.csv.gz
loop0 0 0 54.9 5.1 8.14G 3.49G 8091 clickhous 0 S 1:44.23 0 45M /usr/bin/clickhouse-server --config=/etc/clickhouse-server/config.xml
loop1 0 0 4.5 0.0 0 0 319 root 0 S 0:07.50 1K 0 kworker/u72:2
nvme0n1 0 3K 2.3 0.0 91.1M 28.9M 9912 root 0 R 0:01.56 0 0 /usr/bin/python3 /usr/bin/glances
nvme0n1p1 0 3K 0.3 0.0 0 0 960 root -20 S 0:00.10 0 0 kworker/28:1H
nvme1n1 32.1M 495M 0.3 0.0 0 0 1058 root -20 S 0:00.90 0 0 kworker/23:1H
Я освобожу место на NVMe-приводе, удалив исходные CSV-файлы, прежде чем продолжить.
$ sudo rm -fr /ch/csv
Преобразование в колоночную форму
Движок Log ClickHouse будет хранить данные в строко-ориентированном формате. Чтобы быстрее запрашивать данные, я конвертирую их в колоночный формат с помощью движка MergeTree.
$ clickhouse-client --host=0.0.0.0
Следующее выполнено за 34 минуты и 50 секунд. После этой операции размер каталога данных составил 237 ГБ.
CREATE TABLE trips_mergetree
ENGINE = MergeTree(pickup_date, pickup_datetime, 8192)
AS SELECT
trip_id,
CAST(vendor_id AS Enum8('1' = 1,
'2' = 2,
'CMT' = 3,
'VTS' = 4,
'DDS' = 5,
'B02512' = 10,
'B02598' = 11,
'B02617' = 12,
'B02682' = 13,
'B02764' = 14)) AS vendor_id,
toDate(pickup_datetime) AS pickup_date,
ifNull(pickup_datetime, toDateTime(0)) AS pickup_datetime,
toDate(dropoff_datetime) AS dropoff_date,
ifNull(dropoff_datetime, toDateTime(0)) AS dropoff_datetime,
assumeNotNull(store_and_fwd_flag) AS store_and_fwd_flag,
assumeNotNull(rate_code_id) AS rate_code_id,
assumeNotNull(pickup_longitude) AS pickup_longitude,
assumeNotNull(pickup_latitude) AS pickup_latitude,
assumeNotNull(dropoff_longitude) AS dropoff_longitude,
assumeNotNull(dropoff_latitude) AS dropoff_latitude,
assumeNotNull(passenger_count) AS passenger_count,
assumeNotNull(trip_distance) AS trip_distance,
assumeNotNull(fare_amount) AS fare_amount,
assumeNotNull(extra) AS extra,
assumeNotNull(mta_tax) AS mta_tax,
assumeNotNull(tip_amount) AS tip_amount,
assumeNotNull(tolls_amount) AS tolls_amount,
assumeNotNull(ehail_fee) AS ehail_fee,
assumeNotNull(improvement_surcharge) AS improvement_surcharge,
assumeNotNull(total_amount) AS total_amount,
assumeNotNull(payment_type) AS payment_type_,
assumeNotNull(trip_type) AS trip_type,
pickup AS pickup,
pickup AS dropoff,
CAST(assumeNotNull(cab_type)
AS Enum8('yellow' = 1, 'green' = 2))
AS cab_type,
precipitation AS precipitation,
snow_depth AS snow_depth,
snowfall AS snowfall,
max_temperature AS max_temperature,
min_temperature AS min_temperature,
average_wind_speed AS average_wind_speed,
pickup_nyct2010_gid AS pickup_nyct2010_gid,
pickup_ctlabel AS pickup_ctlabel,
pickup_borocode AS pickup_borocode,
pickup_boroname AS pickup_boroname,
pickup_ct2010 AS pickup_ct2010,
pickup_boroct2010 AS pickup_boroct2010,
pickup_cdeligibil AS pickup_cdeligibil,
pickup_ntacode AS pickup_ntacode,
pickup_ntaname AS pickup_ntaname,
pickup_puma AS pickup_puma,
dropoff_nyct2010_gid AS dropoff_nyct2010_gid,
dropoff_ctlabel AS dropoff_ctlabel,
dropoff_borocode AS dropoff_borocode,
dropoff_boroname AS dropoff_boroname,
dropoff_ct2010 AS dropoff_ct2010,
dropoff_boroct2010 AS dropoff_boroct2010,
dropoff_cdeligibil AS dropoff_cdeligibil,
dropoff_ntacode AS dropoff_ntacode,
dropoff_ntaname AS dropoff_ntaname,
dropoff_puma AS dropoff_puma
FROM trips;
Вот как выглядел glance-вывод во время операции:
ip-172-30-2-200 (Ubuntu 16.04 64bit / Linux 4.4.0-1072-aws) Uptime: 1:06:09
CPU 10.3% nice: 0.0% LOAD 36-core MEM 16.1% active: 13.3G SWAP 0.0%
user: 7.9% irq: 0.0% 1 min: 1.87 total: 68.7G inactive: 52.8G total: 0
system: 1.6% iowait: 0.8% 5 min: 1.76 used: 11.1G buffers: 71.8M used: 0
idle: 89.7% steal: 0.0% 15 min: 1.95 free: 57.6G cached: 57.2G free: 0
NETWORK Rx/s Tx/s TASKS 367 (523 thr), 1 run, 366 slp, 0 oth sorted automatically by cpu_percent, flat view
ens5 1Kb 8Kb
lo 2Kb 2Kb CPU% MEM% VIRT RES PID USER NI S TIME+ IOR/s IOW/s Command
241.9 12.8 20.7G 8.78G 8091 clickhous 0 S 30:36.73 34M 125M /usr/bin/clickhouse-server --config=/etc/clickhouse-server/config.xml
DISK I/O R/s W/s 2.6 0.0 90.4M 28.3M 9948 root 0 R 1:18.53 0 0 /usr/bin/python3 /usr/bin/glances
loop0 0 0 1.3 0.0 0 0 203 root 0 S 0:09.82 0 0 kswapd0
loop1 0 0 0.3 0.1 315M 61.3M 15701 ubuntu 0 S 0:00.40 0 0 clickhouse-client --host=0.0.0.0
nvme0n1 0 3K 0.3 0.0 0 0 7 root 0 S 0:00.83 0 0 rcu_sched
nvme0n1p1 0 3K 0.0 0.0 0 0 142 root 0 S 0:00.22 0 0 migration/27
nvme1n1 25.8M 330M 0.0 0.0 59.7M 1.79M 2764 ubuntu 0 S 0:00.00 0 0 (sd-pam)
В последнем тесте несколько столбцов были преобразованы и пересчитаны. Я обнаружил, что некоторые из этих функций больше не работают должным образом в этом наборе данных. Для решения этой проблемы я удалил неподходящие функции и загрузил данные без преобразования в более детализированные типы.
Распределение данных по кластеру
Я буду распределять данные по всем трем узлам кластера. Для начала ниже я создам таблицу на всех трех машинах.
$ clickhouse-client --host=0.0.0.0
CREATE TABLE trips_mergetree_third (
trip_id UInt32,
vendor_id String,
pickup_date Date,
pickup_datetime DateTime,
dropoff_date Date,
dropoff_datetime Nullable(DateTime),
store_and_fwd_flag Nullable(FixedString(1)),
rate_code_id Nullable(UInt8),
pickup_longitude Nullable(Float64),
pickup_latitude Nullable(Float64),
dropoff_longitude Nullable(Float64),
dropoff_latitude Nullable(Float64),
passenger_count Nullable(UInt8),
trip_distance Nullable(Float64),
fare_amount Nullable(Float32),
extra Nullable(Float32),
mta_tax Nullable(Float32),
tip_amount Nullable(Float32),
tolls_amount Nullable(Float32),
ehail_fee Nullable(Float32),
improvement_surcharge Nullable(Float32),
total_amount Nullable(Float32),
payment_type Nullable(String),
trip_type Nullable(UInt8),
pickup Nullable(String),
dropoff Nullable(String),
cab_type Nullable(String),
precipitation Nullable(Int8),
snow_depth Nullable(Int8),
snowfall Nullable(Int8),
max_temperature Nullable(Int8),
min_temperature Nullable(Int8),
average_wind_speed Nullable(Int8),
pickup_nyct2010_gid Nullable(Int8),
pickup_ctlabel Nullable(String),
pickup_borocode Nullable(Int8),
pickup_boroname Nullable(String),
pickup_ct2010 Nullable(String),
pickup_boroct2010 Nullable(String),
pickup_cdeligibil Nullable(FixedString(1)),
pickup_ntacode Nullable(String),
pickup_ntaname Nullable(String),
pickup_puma Nullable(String),
dropoff_nyct2010_gid Nullable(UInt8),
dropoff_ctlabel Nullable(String),
dropoff_borocode Nullable(UInt8),
dropoff_boroname Nullable(String),
dropoff_ct2010 Nullable(String),
dropoff_boroct2010 Nullable(String),
dropoff_cdeligibil Nullable(String),
dropoff_ntacode Nullable(String),
dropoff_ntaname Nullable(String),
dropoff_puma Nullable(String)
) ENGINE = MergeTree(pickup_date, pickup_datetime, 8192);
Затем я позабочусь о том, чтобы первый сервер мог видеть все три узла в кластере.
SELECT *
FROM system.clusters
WHERE cluster = 'perftest_3shards'
FORMAT Vertical;
Row 1:
------
cluster: perftest_3shards
shard_num: 1
shard_weight: 1
replica_num: 1
host_name: 172.30.2.192
host_address: 172.30.2.192
port: 9000
is_local: 1
user: default
default_database:
Row 2:
------
cluster: perftest_3shards
shard_num: 2
shard_weight: 1
replica_num: 1
host_name: 172.30.2.162
host_address: 172.30.2.162
port: 9000
is_local: 0
user: default
default_database:
Row 3:
------
cluster: perftest_3shards
shard_num: 3
shard_weight: 1
replica_num: 1
host_name: 172.30.2.36
host_address: 172.30.2.36
port: 9000
is_local: 0
user: default
default_database:
Затем я определю новую таблицу на первом сервере, которая основана на схеме
trips_mergetree_third
и использует движок Distributed.CREATE TABLE trips_mergetree_x3
AS trips_mergetree_third
ENGINE = Distributed(perftest_3shards,
default,
trips_mergetree_third,
rand());
Затем я скопирую данные из таблицы на основе MergeTree на все три сервера. Следующее выполнено за 34 минуты и 44 секунды.
INSERT INTO trips_mergetree_x3
SELECT * FROM trips_mergetree;
После приведенной выше операции я дал ClickHouse 15 минут, чтобы отойти от отметки максимального уровня хранилища. Каталоги данных в конечном итоге составляли 264 ГБ, 34 ГБ и 33 ГБ соответственно на каждом из трех серверов.
Оценка производительности кластера ClickHouse
То, что я увидел дальше, было самым быстрым временем, которое я видел при многократном выполнении каждого запроса в таблице
trips_mergetree_x3
.$ clickhouse-client --host=0.0.0.0
Следующее выполнено за 2.449 секунды.
SELECT cab_type, count(*)
FROM trips_mergetree_x3
GROUP BY cab_type;
Следующее выполнено за 0.691 секунды.
SELECT passenger_count,
avg(total_amount)
FROM trips_mergetree_x3
GROUP BY passenger_count;
Следующее выполнено за 0.,582 секунды.
SELECT passenger_count,
toYear(pickup_date) AS year,
count(*)
FROM trips_mergetree_x3
GROUP BY passenger_count,
year;
Следующее выполнено за 0.983 секунды.
SELECT passenger_count,
toYear(pickup_date) AS year,
round(trip_distance) AS distance,
count(*)
FROM trips_mergetree_x3
GROUP BY passenger_count,
year,
distance
ORDER BY year,
count(*) DESC;
Для сравнения я выполнил те же запросы в таблице на основе MergeTree, которая находится исключительно на первом сервере.
Оценка производительности одного узла ClickHouse
То, что я увидел дальше, было самым быстрым временем, которое я видел при многократном выполнении каждого запроса в таблице
trips_mergetree_x3
.Следующее выполнено за 0.241 секунды.
SELECT cab_type, count(*)
FROM trips_mergetree
GROUP BY cab_type;
Следующее выполнено за 0.826 секунды.
SELECT passenger_count,
avg(total_amount)
FROM trips_mergetree
GROUP BY passenger_count;
Следующее выполнено за 1.209 секунды.
SELECT passenger_count,
toYear(pickup_date) AS year,
count(*)
FROM trips_mergetree
GROUP BY passenger_count,
year;
Следующее выполнено за 1.781 секунды.
SELECT passenger_count,
toYear(pickup_date) AS year,
round(trip_distance) AS distance,
count(*)
FROM trips_mergetree
GROUP BY passenger_count,
year,
distance
ORDER BY year,
count(*) DESC;
Размышления о результатах
Это первый раз, когда бесплатная база данных на базе процессора смогла превзойти базу данных на основе GPU в моих тестах. Та база данных на основе GPU с тех пор подверглась двум ревизиям, но, тем не менее, производительность, которую ClickHouse показал на одном узле, очень впечатляет.
При этом при выполнении Query 1 на распределенном движке накладные расходы оказываются на порядок выше. Я надеюсь, что что-то пропустил в своем исследовании для этого поста, потому что было бы хорошо увидеть, как время запросов снижается, когда я добавляю больше узлов в кластер. Однако это замечательно, что при выполнении других запросов производительность выросла примерно в 2 раза.
Было бы неплохо, если бы ClickHouse развивался в направлении того, чтобы можно было отделить хранилище и вычисления, чтобы они могли масштабироваться независимо. Поддержка HDFS, которая была добавлена в прошлом году, может стать шагом к этому. Что касается вычислений, если один запрос может быть ускорен при добавлении большего количества узлов в кластер, то будущее этого программного обеспечения будет очень безоблачным.
Спасибо, что нашли время, чтобы прочитать этот пост. Я предлагаю консалтинг, архитектуру и услуги по практическому развитию для клиентов в Северной Америке и Европе. Если вы хотите обсудить, как мои предложения могут помочь вашему бизнесу, свяжитесь со мной через LinkedIn.
Комментарии (9)
youROCK
14.08.2019 17:49Строго говоря, это нетипичная конфигурация для ClickHouse — мало данных (помещаются целиком в оперативку) и использование NVMe SSD. Особенно в случае, если в результате GROUP BY получается очень много различных значений, которые нужно мержить на принимающей стороне, такая конфигурация не имеет особого смысла. То есть, я не утверждаю, что так делать вообще нельзя, но действительно выгоды от ClickHouse в этом случае меньше. Плюс, насколько я помню, есть настройка, которая позволяет отключить финальный Merge результатов на принимающей стороне, и тогда latency тоже будет лучше. В любом случае, почти всегда при использовании Distributed даже если latency не уменьшается в несколько раз, нагрузка на сервера падает, что позволяет выдерживать большую нагрузку на запись и на чтение в кластере.
algotrader2013
14.08.2019 23:32Эх… прочитав подобную поверхностную статью, я тоже сильно воодушевился кликхаусом пару месяцев назад, решив использовав ch, как историческую базу для нескольких миллиардов транзакций в денормализованном виде, которые до этого хранились в партиционированной таблице с clustered columnstore index в MSSQL.
В итоге мы провели тесты на сервере с 72 железными ядрами (которые отображаются, как 144 логических), почти топовым NVMe диском, 512ГБ памяти.
Первое впечатление, что ch это очень круто. Да, агрегационные запросы сразу задействуют все ядра с первой секунды выполнения. Тупые запросы типа тех, что есть в статье, летают.
Но вот стоит написать что-то типа a join a on..., как потребление процессора падает до 1 ядра, и ch пытается в лоб в 1 поток это все просчитать. Более того, даже килл долгоотрабатывающих запросов отрабатывает как-то рандомно, и не с первого раза. То есть, если (применительно к домену примера) считать вещи типа "найти кластер пассажиров, которые имеют как минимум 10 одинаковых поездок с одним из членов кластера", или "вычислить семейные пары — люди, которые с одного района выезжают, едут на разные работы, но потом могут оттуда поехать в условный ресторан вместе (и, понятное дело, найти не конкретные кейсы, а принять решение статистически)", то ch показывает просто отвратительные результаты там, где mssql без проблем делает честный map-reduce под капотом с репартишенгом, где это надо. Хотя, кто знает, может есть какие-то неизвестные и неописанные в доке тонкости настройки или хинтов, которые включают магию… А такие задачи реально возникают, когда надо заготовить сложные фичи для ML, или искать кейсы фрода.
Из плюсов. Диалект SQL поражает некой правильностью и очевидностью мысли (видя вещи вроде приблизительного каунта, самого встречаемого значения, семплирования из коробки, неумножающего джойна, понимаешь что да, эти люди прямо залезли тебе в голову, и сделали то, чего давно подсознательно хотел). Но при этом, он лишен многих привычных вещей, и перестроить голову под сн — это непросто. Также есть прикольные административные фичи вроде ограничения памяти на юзера. Позволяет быстро отбивать бред типа джойнов с always true условием за несколько секунд.
Ну и, подводя итог, мы остались на mssql, принимая за ограничение, что о масштабировании выполнения больших аналитических запросов придется забыть (вертикально от 4 сокетов расти особо некуда — даже 8 сокетные системы на Xeon platinum уже неадекватно дороги, не говоря уже о монстрах вроде hp superdome x, а горизонтально не позволит mssql). Говоря же о сн, масштабирование тупых групбай по трем столбцам не сильно интересно в принципе. Тот же ms это делает и так очень быстро на одном сервере, и замедление даже на порядок вполне терпимо. И да, Google big query в свое время произвел куда лучше впечатление. Хотя, уверен, что есть масса примеров бизнесов, где надо считать примитивные аггрегации на кластерах на десятки тысяч ядер, и для них сн намного проще и эффективнее, чем любые другие решения (hadoop, написание шардинга на клиенте, сверхдорогие проприетарные решения)...
Jedi_PHP
15.08.2019 17:48> стоит написать что-то типа join a on
> колоночная база данных
Вы уверены, что у вас был денормализованный вид?
Для избежания join-ов в наш DWH на ClickHouse приходилось ради приемлемой денормализации докидывать данные, иногда полученные join'ами из боевой базы, иногда парсить логи, иногда даже лазить за данными в API к партнерам — еще до вставки в CH. Благо наполнение DWH было фоновой, относительно неприоритетной задачей, real-time там был не нужен.
Зато у аналитиков tableau просто летал.algotrader2013
15.08.2019 18:48Разумеется, что уверен)
a join a — это не опечатка. В данном случае цель не дополнить таблицу дополнительными полями, так, как все уже предварительно дополнено. Цель — работать с сильно отфильтрованным декартовым произведением всех данных на все данные.
youROCK
15.08.2019 20:05Всё верно, ClickHouse не умеет в merge join (пока что?), но, при некотором желании, обычно всё же можно организовать данные таким образом, чтобы можно было их эффективно использовать в ClickHouse (т.е. учитывая, что например join всегда означает hash join с выполнением подзапроса в первую очередь). См. например habr.com/ru/company/vk/blog/445284 — изначально тоже было требование иметь полноценные джойны, но после реструктуризации схемы базы получилось обойтись без них и получить хорошую производительность.
Но да, ClickHouse это тоже лишь инструмент и нужно понимать, когда его можно использовать, а когда не стоит (например джойнить 2 больших таблицы по сути ClickHouse не может).
Если у вас данных действительно много и переделка структуры данных под ClickHouse будет дешевле, чем покупка железа (и софта), то оно того стоит. Иначе действительно, возможно, имеет смысл использовать другие решения вроде Exasol, BigQuery и т.д.
youROCK
15.08.2019 20:18+1Что касается следующих кейсов:
вещи типа «найти кластер пассажиров, которые имеют как минимум 10 одинаковых поездок с одним из членов кластера», или «вычислить семейные пары — люди, которые с одного района выезжают, едут на разные работы, но потом могут оттуда поехать в условный ресторан вместе (и, понятное дело, найти не конкретные кейсы, а принять решение статистически)»
Здесь вы, вероятно, хотите делать джойн таблицы сам на себя на лету, и это действительно ClickHouse не умеет делать. Но это не значит, что задачу нельзя решить в рамках ClickHouse. Для этого зачастую можно обойтись GROUP BY и правильной вставкой данных. Например, есть схожая задача — найти взаимные переписки пользователей (переписка пользователей это условно кортеж <from_user_id, to_user_id, message>). В наивном решении нам нужно делать что-то вродеSELECT ... FROM messages AS m JOIN messages AS m2 ON m1.from_user_id = m2.to_user_id GROUP BY ...
. Это работать в ClickHouse будет очень плохо (действительно нужен map reduce в этом случае). Но если немного поменять условия задачи и учитывать то, как ClickHouse хранит данные, то неожиданно можно получить намного лучшее решение:
Для каждой переписки мы вставляем по две записи, добавляя новое поле («тип»: исходящее или входящее сообщение):
<from_user_id, to_user_id, message, «out»>
<to_user_id, from_user_id, message, «in»>
Теперь, чтобы получить взаимные переписки (при условии сортировки по <user_id1, user_id2>), достаточно написать что-то вроде такого:
SELECT user_id1, user_id2 FROM messages GROUP BY user_id1, user_id2 WHERE uniqUpTo(2)(message_type) = 2
Где 2 — это 2 разных типа message_type: «out» и «in».
Похожим способом можно группировать другие сущности. Безусловно, это работает не для 100% случаев, но часто этого достаточно.
vlanko
16.08.2019 22:54Скажите, в чем проблема цены 8-сокетника, если лицензия SQL Server под него будет 1,5млн $?
time2rfc
Надеюсь что он не кэш ch проверял
ainu
Не, кэш ch не может быть 2.449 секунды