Всем привет! В этой статье я расскажу свой опыт установки БД 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 его хост и порты, которые мы открывали до этого. В зависимости от кол-ва реплик клика мы заполняем поля с хостом кипера и открытыми портами. В моём случае три раза.

 Блок с zookeeper
Блок с 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 достаточно мощный инструмент для работы с большим объёмом данных, который при правильном использовании приносит свои плоды.

Надеюсь, вам было интересно. Это моя первая работа на этом ресурсе, скоро начну выкладывать ряд статей о работе над одним стартапом, так что ожидайте, будет очень интересно.

Комментарии (0)