Всем привет! В этой статье я расскажу свой опыт установки БД Clickhouse на пару с zookeeper`ом.
Установка
Для начала скачаем пакеты для установки
wget curl https://packages.clickhouse.com/tgz/stable/clickhouse-client-22.9.3.18-amd64.tgz && \
wget curl https://packages.clickhouse.com/tgz/stable/clickhouse-common-static-22.9.3.18-amd64.tgz && \
wget curl https://packages.clickhouse.com/tgz/stable/clickhouse-common-static-dbg-22.9.3.18-amd64.tgz && \
wget curl https://packages.clickhouse.com/tgz/stable/clickhouse-server-22.9.3.18-amd64.tgz
Список версий можно посмотреть здесь.
Далее я использую скрипт, в котором собраны все этапы установки от распаковки архивов, до создание дефолтного пользователя с последующим запуском сервиса.
Скрипт имеет следующий вид:
LATEST_VERSION=22.9.3.18
export LATEST_VERSION
case $(uname -m) in
x86_64) ARCH=amd64 ;;
aarch64) ARCH=arm64 ;;
*) echo "Unknown architecture $(uname -m)"; exit 1 ;;
esac
tar -xzvf "clickhouse-common-static-$LATEST_VERSION-${ARCH}.tgz" \
|| tar -xzvf "clickhouse-common-static-$LATEST_VERSION.tgz"
sudo "clickhouse-common-static-$LATEST_VERSION/install/doinst.sh"
tar -xzvf "clickhouse-common-static-dbg-$LATEST_VERSION-${ARCH}.tgz" \
|| tar -xzvf "clickhouse-common-static-dbg-$LATEST_VERSION.tgz"
sudo "clickhouse-common-static-dbg-$LATEST_VERSION/install/doinst.sh"
tar -xzvf "clickhouse-server-$LATEST_VERSION-${ARCH}.tgz" \
|| tar -xzvf "clickhouse-server-$LATEST_VERSION.tgz"
sudo "clickhouse-server-$LATEST_VERSION/install/doinst.sh" configure
sudo /etc/init.d/clickhouse-server start
tar -xzvf "clickhouse-client-$LATEST_VERSION-${ARCH}.tgz" \
|| tar -xzvf "clickhouse-client-$LATEST_VERSION.tgz"
sudo "clickhouse-client-$LATEST_VERSION/install/doinst.sh"
Важное напоминание, если вы проводите установку на нескольких ВМ, не забудьте указан одинаковый пароль для default user, как это сделал я и пришлось запускать установку заново)
После окончания установки вы увидите, что сервис стартанул и можно зайти под дефолтным юзером в систему:
clickhouse-client --password
select 1

Настройка zookeer и ingress-inginx
Так как у меня клик установлен на нескольких машинах, мне нужно как то их согласовывать между собой и использовать единую точку входа в кластер. Для этой работы я и буду использовать zookeeper.
По мимо вышеупомянутого, zookeeper также нужен для:
Хранение метаданных о таблицах и кластере.
В ClickHouse есть типы таблиц ReplicatedMergeTree, ReplicatedReplacingMergeTree и др.
Все они используют ZooKeeper для хранения:
списка реплик,
информации о партициях
состояния мерджей,
флагов "эта партиция уже загружена" и т.д.
Координация реплик.
Когда одна реплика получает новые данные, она должна синхронизироваться с другими
ZooKeeper фиксирует, что данные загружены, и указывает другим репликам подтянуть изменения.
Распределение задач (scheduling).
Например, две реплики могут попытаться одновременно сделать
merge
одного и того же куска данных.Через ZooKeeper они "договариваются", кто будет делать merge, а кто потом просто скопирует результат.
Failover и согласованность.
ZooKeeper хранит список активных реплик.
Если одна нода падает, через ZooKeeper к ней перестаёт идти трафик, и другие реплики продолжают работать.
При восстановлении — новая реплика подтягивает актуальные данные, сверяясь через ZooKeeper.
В новых версия клика добавили встроенный аналог - Clickhouse Keepeer.
Итак, мы пробежались по функциям кипера, давайте теперь его установим.
Деплой в кластер Kubernetes
Да, тут всё серьезно, поэтому я создал отдельный namespace и беру следующие манифесты, тут ничего нового, конфигурация везде похожа:
service.yaml для clickhouse-client
apiVersion: v1
kind: Service
metadata:
# DNS would be like zookeeper.zoons
name: zk-cs
labels:
app: zk
spec:
ports:
- port: 2181
name: client
selector:
app: zk
what: node
service.yaml headless service для StatefulSet
apiVersion: v1
kind: Service
metadata:
# DNS would be like zookeeper-0.zookeepers.etc
name: zk-hs
labels:
app: zk
spec:
ports:
- port: 2181
name: client
- port: 2888
name: server
- port: 3888
name: leader-election
clusterIP: None
selector:
app: zk
what: node
bdb.yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: zk-pdb
spec:
selector:
matchLabels:
app: zk
maxUnavailable: 1
sts.yaml самого zookeeper`a
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: zk
spec:
selector:
matchLabels:
app: zk
serviceName: zk-hs
replicas: 2
updateStrategy:
type: RollingUpdate
podManagementPolicy: Parallel
template:
metadata:
labels:
app: zk
what: node
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: "app"
operator: In
values:
- zk
topologyKey: "kubernetes.io/hostname"
containers:
- name: kubernetes-zookeeper
imagePullPolicy: IfNotPresent
image: "docker.io/zookeeper:3.8.0-temurin"
resources:
requests:
memory: "1Gi"
cpu: "0.5"
ports:
- containerPort: 2181
name: client
- containerPort: 2888
name: server
- containerPort: 3888
name: leader-election
command:
- bash
- -x
- -c
- |
SERVERS=3 &&
HOST=`hostname -s` &&
DOMAIN=`hostname -d` &&
CLIENT_PORT=2181 &&
SERVER_PORT=2888 &&
ELECTION_PORT=3888 &&
ZOO_DATA_DIR=/var/lib/zookeeper/data &&
ZOO_DATA_LOG_DIR=/var/lib/zookeeper/datalog &&
{
echo "clientPort=${CLIENT_PORT}"
echo 'tickTime=2000'
echo 'initLimit=30000'
echo 'syncLimit=10'
echo 'maxClientCnxns=2000'
echo 'maxSessionTimeout=60000000'
echo "dataDir=${ZOO_DATA_DIR}"
echo "dataLogDir=${ZOO_DATA_LOG_DIR}"
echo 'autopurge.snapRetainCount=3'
echo 'autopurge.purgeInterval=2'
echo 'preAllocSize=131072'
echo 'snapCount=3000000'
echo 'leaderServes=yes'
echo 'standaloneEnabled=true'
echo '4lw.commands.whitelist=stat, ruok, conf, isro'
} > /conf/zoo.cfg &&
{
echo "zookeeper.root.logger=CONSOLE"
echo "zookeeper.console.threshold=INFO"
echo "log4j.rootLogger=\${zookeeper.root.logger}"
echo "log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender"
echo "log4j.appender.CONSOLE.Threshold=\${zookeeper.console.threshold}"
echo "log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout"
echo "log4j.appender.CONSOLE.layout.ConversionPattern=%d{ISO8601} [myid:%X{myid}] - %-5p [%t:%C{1}@%L] - %m%n"
} > /conf/log4j.properties &&
echo 'JVMFLAGS="-Xms128M -Xmx1G -XX:+UseG1GC"' > /conf/java.env &&
if [[ $HOST =~ (.*)-([0-9]+)$ ]]; then
NAME=${BASH_REMATCH[1]}
ORD=${BASH_REMATCH[2]}
else
echo "Failed to parse name and ordinal of Pod"
exit 1
fi &&
mkdir -p ${ZOO_DATA_DIR} &&
mkdir -p ${ZOO_DATA_LOG_DIR} &&
export MY_ID=$((ORD+1)) &&
#echo 2 > $ZOO_DATA_DIR/myid &&
echo $MY_ID > $ZOO_DATA_DIR/myid &&
if [[ $SERVERS -gt 1 ]]; then
for (( i=1; i<=$SERVERS; i++ )); do
echo "server.$i=$NAME-$((i-1)).$DOMAIN:$SERVER_PORT:$ELECTION_PORT" >> /conf/zoo.cfg;
done
fi &&
chown -Rv zookeeper "$ZOO_DATA_DIR" "$ZOO_DATA_LOG_DIR" "$ZOO_LOG_DIR" "$ZOO_CONF_DIR" &&
zkServer.sh start-foreground
readinessProbe:
exec:
command:
- bash
- -c
- "OK=$(echo ruok | nc 127.0.0.1 2181); if [[ \"$OK\" == \"imok\" ]]; then exit 0; else exit 1; fi"
initialDelaySeconds: 10
timeoutSeconds: 5
livenessProbe:
exec:
command:
- bash
- -c
- "OK=$(echo ruok | nc 127.0.0.1 2181); if [[ \"$OK\" == \"imok\" ]]; then exit 0; else exit 1; fi"
initialDelaySeconds: 10
timeoutSeconds: 5
volumeMounts:
- name: datadir
mountPath: /var/lib/zookeeper
securityContext:
runAsUser: 1000
fsGroup: 1000
Я не буду вдаваться в подробности, что именно происходит в манифестах. Скажу лишь, что это дефолтная настройка сервиса. Из интересного, у statefulset`a кипера, начиная со 101 строчки, прописана функция, которая означает, что StatefulSet сам динамически подставит правильные DNS-имена подов (zookeeper-0
, zookeeper-1
, zookeeper-2
), и не нужно вручную жёстко хардкодить IP.
После я создаю дополнительный сервис для zookeeper, в эндпоинтах которого буду присутствовать ноды clickhouse.
service.yaml
apiVersion: v1
kind: Service
metadata:
name: ch-svc
namespace: zk
spec:
clusterIP: None
type: ClusterIP
sessionAffinity: None
ports:
- name: http
port: 8123
protocol: TCP
targetPort: 8123
- name: tcp
port: 9000
protocol: TCP
targetPort: 9000
И манифест для эндпоинтов:
endpoints,yaml
apiVersion: v1
kind: Endpoints
metadata:
name: ch-svc
namespace: zk
subsets:
- addresses:
- ip: 00.00.00.00 #Ноды
- ip: 00.00.00.00 #клика
ports:
- name: tcp
port: 9000
protocol: TCP
- name: http
port: 8123
protocol: TCP
Ноды клика мы прописываем на 7-й строке
Также нам необходимо закрепить за каждым подом кипера свой сервис, каждый из которых будет смотреть на один из подов соответственно:
service.yaml
apiVersion: v1
kind: Service
metadata:
name: zk-0 #В зависимости от номера пода
namespace: zk
spec:
internalTrafficPolicy: Cluster
ports:
- port: 2181
protocol: TCP
targetPort: 2181
selector:
apps.kubernetes.io/pod-index: "0" #Указываем номер пода
sessionAffinity: None
type: ClusterIP
После того, как мы закончили с кипером, необходимо внести данные об ранее созданных соединений в configmap ingress-nginx-tcp.
Скрытый текст
apiVersion: v1
data:
"2181": zk/zk-0:2181 # +
"2182": zk/zk-1:2182 # +
"8123": zk/ch-svc:8123 # +
"9000": zk/ch-svc:9000 # +
kind: ConfigMap
metadata:
annotations:
meta.helm.sh/release-name: ingress-controller
meta.helm.sh/release-namespace: nginx-ingress
labels:
app.kubernetes.io/component: controller
app.kubernetes.io/instance: ingress-controller
app.kubernetes.io/managed-by: Helm
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
app.kubernetes.io/version: 1.2.1
helm.sh/chart: ingress-nginx-4.1.4
k8slens-edit-resource-version: v1
name: ingress-controller-ingress-nginx-tcp
namespace: nginx-ingress
В раздел data мы вносим наши открытые порты как от кипера, так и от клика
На описание установки ingress-nginx я не буду останавливаться. Познакомиться с инструментом можно здесь.
В заключении мы в эндопинты ingress-controller-ingress-nginx-controller также добавляем наши порты, после чего вносим их в деплоймент nginx, раздел портов для контейнеров и перезагружаем сервис. Внешняя настройка завершена.
Внутренняя настройка Clickhouse
Конфигурация клика проводится с помощью файла /etc/clickhouse-server/config.xml. Также можно создать в папке config.d свой файл конфигурации, который при запуске системы будет слит с основным.
Для начала нам нужно задать в разделе zookeeper его хост и порты, которые мы открывали до этого. В зависимости от кол-ва реплик клика мы заполняем поля с хостом кипера и открытыми портами. В моём случае три раза.

Перезапустим сервис
#Полный перезапуск сервиса
sudo service clickhouse-server restart
# vs
#Перезагрузка конфигурации
sudo service clickhouse-server reload
Проверка
Для проверки работоспособности зайдите в базу клика либо через СУБД по IP балансировщика, либо через ноду клика и выполните команду по созданию тестовой таблице:
CREATE TABLE test on cluster 'main' (id int) ENGINE = ReplicatedReplacingMergeTree('/clickhouse/tables/{shard}/test', '{replica}', id) order by id;
insert into test values(1);
После выполнения скрипта должны отобразится рабочие ноды, которые синхронизированно выполнили скрипт. Дополнительно можно зайти на другую ноду и выполнить селект по таблице.
Удалить тестовую таблицу следующем скриптом:
drop table test on cluster main sync;
sync в данном случае очень важно проставлять, чтобы команда отработала на всех нодах.
Создание и работа и с пользователями
Для начала нам необходимо нашему default пользователю включить User Mode. Конфигурация лежит в папке /etc/clickhouse-server/users.d
<clickhouse>
<users>
<default>
<password remove='1' />
<password_sha256_hex>daaad6e5604e8e17bd9f108d91e26afe6281dac8fda0091040a7a6d7bd9b43b5</password_sha256_hex>
<access_management>1</access_management>
<named_collection_control>1</named_collection_control>
<show_named_collections>1</show_named_collections>
<show_named_collections_secrets>1</show_named_collections_secrets>
</default>
</users>
</clickhouse>
Перезапускаем сервер
sudo service clickhouse-server restart
Создадим админа БД
Под default user мы работать не будет, а создадим полноценного администратора, который будет этим заниматься.
create user clickhouse-admin identified by 'password123'

Выдадим права на все таблицы в кластере
grant all on *.* to clickhouse_admin with grant option;

Больше про работу с правами пользователей тут.
Отключим default user
<clickhouse>
<users>
<default remove="remove">
<password remove='1' />
<password_sha256_hex>daaad6e5604e8e17bd9f108d91e26afe6281dac8fda0091040a7a6d7bd9b43b5</password_sha256_hex>
<!--
<access_management>1</access_management>
<named_collection_control>1</named_collection_control>
<show_named_collections>1</show_named_collections>
<show_named_collections_secrets>1</show_named_collections_secrets>
-->
</default>
</users>
</clickhouse>
Заключение
В этой статье я описал свою настройку кликхауса на пару с zookeeper, немного затронув ingress-nginx.
Clickhouse достаточно мощный инструмент для работы с большим объёмом данных, который при правильном использовании приносит свои плоды.
Надеюсь, вам было интересно. Это моя первая работа на этом ресурсе, скоро начну выкладывать ряд статей о работе над одним стартапом, так что ожидайте, будет очень интересно.