ZookeeperからClickHouse Keeperへの移行

この記事はスタンバイ Advent Calendar 2022の10日目の記事です。
昨日は@jsoizoさんの[AWS JDBC Driver for MySQLのfailover機能に関する動作検証](https://jsoizo.hatenablog.com/entry/2022/12/09/070000)でした。

## ClickHouse Keeper?

ClickHouseではレプリケーションや分散DDLの実行などの同期システムとして [Zookeeper](https://zookeeper.apache.org/) を使用しています。

https://clickhouse.com/docs/en/manage/replication-and-sharding

ZookeeperはKafkaのバックグラウンドということもあり高速で安定しており信頼度も高いシステムです。しかし、ClickHouseの連携システムという点では構築や不具合時の運用に手間がかかっていました。そこで、去年リリースの[21.4](https://clickhouse.com/docs/en/whats-new/changelog/2021#buildtestingpackaging-improvement-7) から大替独自のClickHouse Keeperが組み込まれました。Zookeeperと採用するアルゴリズムや言語は異なりますが、同じインターフェースで作成されています。

https://clickhouse.com/docs/en/operations/clickhouse-keeper

ClickHouse Keeperが作られた経緯としては、Zookeeperがスナップショットの増加や障害時のリカバリなど運用が難しいため、という理由だったはず🤔 ただ、それ以上にclickhouse-serverに組み込まれていることから、別途同期用のサーバを立てることなくレプリケーションを行うことができるようになったことが強いと感じます。

この記事ではdockerを用いてローカル環境でZookeeper構成からClickHouse Keeperへの移行を試していきます。

## Zookeeper構成で立ち上げる

`docker-compose.yml` と必要な設定ファイル群を用意します。
clickhouse-serverを2コンテナ(`clickhouse-server-1`、`clickhouse-server-2`)、zookeeperを1コンテナの構成で構築します。

```bash
> tree .
.
├── docker-compose.yml
└── etc
    └── clickhouse-server
        └── config.d
            ├── clickhouse-server-1-macros.xml
            ├── clickhouse-server-2-macros.xml
            └── zookeeper-config.xml
```

```xml:etc/clickhouse-server/config.d/zookeeper-config.xml
<?xml version="1.0"?>
<clickhouse>
    <listen_host>0.0.0.0</listen_host>
    <remote_servers>
        <sample_cluster>
            <shard>
                <replica>
                    <host>clickhouse-server-1</host>
                    <port>9000</port>
                </replica>
                <replica>
                    <host>clickhouse-server-2</host>
                    <port>9000</port>
                </replica>
            </shard>
        </sample_cluster>
    </remote_servers>
    <zookeeper>
        <node>
            <host>zookeeper</host>
            <port>2181</port>
        </node>
    </zookeeper>
</clickhouse>
```

```xml:etc/clickhouse-server/config.d/clickhouse-server-1-macros.xml
<?xml version="1.0"?>
<clickhouse>
    <macros>
        <cluster>sample_cluster</cluster>
        <shard>0</shard>
        <replica>0</replica>
    </macros>
</clickhouse>
```

```xml:etc/clickhouse-server/config.d/clickhouse-server-2-macros.xml
<?xml version="1.0"?>
<clickhouse>
    <macros>
        <cluster>sample_cluster</cluster>
        <shard>0</shard>
        <replica>1</replica>
    </macros>
</clickhouse>
```

```yaml:docker-compose.yml
version: '3.7'
services:
  clickhouse-server-1:
    container_name: clickhouse-server-1
    image: clickhouse/clickhouse-server:22.11
    ports:
      - "9000:9000"
      - "8123:8123"
    volumes:
      - ./clickhouse-server-1-data:/var/lib/clickhouse
      - ./etc/clickhouse-server/config.d/zookeeper-config.xml:/etc/clickhouse-server/config.d/zookeeper-config.xml
      - ./etc/clickhouse-server/config.d/clickhouse-server-1-macros.xml:/etc/clickhouse-server/config.d/clickhouse-server-1-macros.xml
  clickhouse-server-2:
    container_name: clickhouse-server-2
    image: clickhouse/clickhouse-server:22.11
    ports:
      - "9001:9000"
      - "8124:8123"
    volumes:
      - ./clickhouse-server-2-data:/var/lib/clickhouse
      - ./etc/clickhouse-server/config.d/zookeeper-config.xml:/etc/clickhouse-server/config.d/zookeeper-config.xml
      - ./etc/clickhouse-server/config.d/clickhouse-server-2-macros.xml:/etc/clickhouse-server/config.d/clickhouse-server-2-macros.xml
  zookeeper:
    container_name: zookeeper
    image: zookeeper
    ports:
      - "2181:2181"
    volumes:
      - ./volumes/tmp/zookeeper/data:/data
      - ./volumes/tmp/zookeeper/datalog:/datalog
```

ファイルの作成が終わったら `docker compose` で起動します。

```bash
> docker compose up -d
[+] Running 4/4
 ⠿ Network ac2022_default         Created    0.1s
 ⠿ Container clickhouse-server-1  Started    0.6s
 ⠿ Container clickhouse-server-2  Started    0.6s
 ⠿ Container zookeeper            Started    0.5s
```

立ち上がったらクライアントを用いてアクセスします。

```bash
> docker exec -it clickhouse-server-1 clickhouse-client
ClickHouse client version 22.11.1.1360 (official build).
Connecting to localhost:9000 as user default.
Connected to ClickHouse server version 22.11.1 revision 54460.

ce8ce5ce80a5 :) 
```

 `system.zookeeper` への検索が行えることでZookeeperとの連携ができていることが確認できます。

```sql
ce8ce5ce80a5 :) SELECT * FROM system.zookeeper WHERE path IN ('/', '/clickhouse')

┌─name───────┬─value─┬─path────────┐
│ zookeeper  │       │ /           │
│ clickhouse │       │ /           │
│ task_queue │       │ /clickhouse │
└────────────┴───────┴─────────────┘

3 rows in set. Elapsed: 0.006 sec.
```

連携が確認できたら、動作確認用のレプリケーションテーブルの作成を行っていきます。

```sql
ce8ce5ce80a5 :) CREATE TABLE sample ON CLUSTER '{cluster}' (id Int, val String) ENGINE = ReplicatedMergeTree('/clickhouse/tables/{shard}/sample', '{replica}') ORDER BY id

┌─host────────────────┬─port─┬─status─┬─error─┬─num_hosts_remaining─┬─num_hosts_active─┐
│ clickhouse-server-2 │ 9000 │      0 │       │                   1 │                0 │
│ clickhouse-server-1 │ 9000 │      0 │       │                   0 │                0 │
└─────────────────────┴──────┴────────┴───────┴─────────────────────┴──────────────────┘

2 rows in set. Elapsed: 0.120 sec. 

ce8ce5ce80a5 :) INSERT INTO sample VALUES (1, 'abc'), (2, 'bcd')

Ok.

2 rows in set. Elapsed: 0.031 sec.

ce8ce5ce80a5 :) SELECT * FROM sample

┌─id─┬─val─┐
│  1 │ abc │
│  2 │ bcd │
└────┴─────┘

2 rows in set. Elapsed: 0.005 sec. 
```

`clickhouse-server-2` でもテーブルが作成されデータが同期されていることを確認します。

```bash
> docker exec -it clickhouse-server-2 clickhouse-client
ClickHouse client version 22.11.1.1360 (official build).
Connecting to localhost:9000 as user default.
Connected to ClickHouse server version 22.11.1 revision 54460.
```

```sql
9333fd4d6715 :) SELECT * FROM sample

SELECT *
FROM sample

Query id: 57808e49-35c3-436e-8b57-ce15a4f3df68

┌─id─┬─val─┐
│  1 │ abc │
│  2 │ bcd │
└────┴─────┘

2 rows in set. Elapsed: 0.006 sec. 
```

Zookeeperにパスが作成されていることも確認します。

```sql
9333fd4d6715 :) SELECT * FROM system.zookeeper WHERE path = '/clickhouse/tables/0'

┌─name───┬─value─┬─path─────────────────┐
│ sample │       │ /clickhouse/tables/0 │
└────────┴───────┴──────────────────────┘

1 row in set. Elapsed: 0.002 sec. 
```

ここまででZookeeperの構成でレプリケーションテーブルを作成、データの登録まで行いました。これからZookeeperのデータをclickhouse-keeperへ移行、コンテナの構成もclickhouse-keeperへ切り替えます。

## Zookeeper -> Clickhouse Keeperへのデータ移行

マイグレーションについてはドキュメントがあります。専用のツール(`clickhouse-keeper-converter`)も用意されており、ZooKeeperのバージョンが `3.4` 以上の場合これが使用できます。ツールはclickhouse-serverのイメージに入っています。

https://clickhouse.com/docs/en/operations/clickhouse-keeper#migration-from-zookeeper

今回はドキュメントに従った移行を見ていきます。
まずはZookeeperを停止し、スナップショットとデータログをclickhouse-serverに移動します。`docker compose`で行うには `volumes` を駆使していきます。

```bash
> docker compose down 
[+] Running 2/2
 ⠿ Container zookeeper            Removed    10.2s
 ⠿ Container clickhouse-server-2  Removed    10.2s
 ⠿ Container clickhouse-server-1  Removed    10.3s
 ⠿ Network ac2022_default         Removed    0.1s
```

今回、必要なZookeeperのデータは`volumes/tmp`に入るようにしています。なぜか`clickhouse-keeper`のイメージには　`clickhouse-keeper-converter`が入っていないので、ここでは`clickhouse-server-1`にデータを移して変換を行います。  
`docker-compose.yml`を次のように変更し、dockerを立ち上げます。

```yaml:docker-compose.yml
version: '3.7'
services:
  clickhouse-server-1:
    container_name: clickhouse-server-1
    image: clickhouse/clickhouse-server:22.11
    ports:
      - "9000:9000"
      - "8123:8123"
    volumes:
      - ./volumes/tmp:/tmp  # Zookeeperのデータをマウントする
      - ./clickhouse-server-1-data:/var/lib/clickhouse
      - ./etc/clickhouse-server/config.d/zookeeper-config.xml:/etc/clickhouse-server/config.d/zookeeper-config.xml
      - ./etc/clickhouse-server/config.d/clickhouse-server-1-macros.xml:/etc/clickhouse-server/config.d/clickhouse-server-1-macros.xml
```

```bash
> docker compose up -d
[+] Running 3/3
 ⠿ Network ac2022_default         Created    0.0s
 ⠿ Container clickhouse-server-1  Started    0.5s
````

立ち上がったら`clickhouse-server-1`に`bash`で入り、`clickhouse-keeper-converter`を実行します。

```bash
> docker exec -it clickhouse-server-1 bash

bash-5.1# clickhouse-keeper-converter --zookeeper-logs-dir /tmp/zookeeper/datalog/version-2 --zookeeper-snapshots-dir /tmp/zookeeper/data/version-2 --output-dir /tmp/keeper/snapshots

Totally have 1 snapshots, will use latest
Deserializing storage snapshot /tmp/zookeeper/data/version-2/snapshot.0
Magic deserialized, looks OK
Sessions and timeouts deserialized
ACLs deserialized
Deserializing data from snapshot
Finished, snapshot ZXID 0
Totally have 1 logs
Deserializing log /tmp/zookeeper/datalog/version-2/log.1
Header looks OK
Finished /tmp/zookeeper/datalog/version-2/log.1 deserialization, totally read 112 records
Snapshot serialized to path:/tmp/keeper/snapshots/snapshot_13.bin.zstd
```

成功すると変更後のファイルが出力されます。今回は最終行にある(`/tmp/keeper/snapshots/snapshot_13.bin.zstd`)が成果物になります。
変換が終わったらサーバを落としてください。

```bash
> docker compose down 
[+] Running 2/2
 ⠿ Container clickhouse-server-1  Removed    10.3s
 ⠿ Network ac2022_default         Removed    0.1s
```

変換したスナップショットファイルはclickhouse-keeperのスナップショット用のディレクトリに移す必要があります。ここでは以下の通りローカルにマウント用のディレクトリを作成し、変換後のファイルを移動します。

```bash
> mkdir -p volumes/var/lib/clickhouse/coordination/snapshots
> cp volumes/tmp/keeper/snapshots/* volumes/var/lib/clickhouse/coordination/snapshots/.
```

## Clickhouse Keeperの立ち上げ

Zookeeperのデータの変換も終わり、clickhouse-keeperを用いた構成を構築していきます。まずは必要なファイルを作成します。

```bash
> tree .
.
├── docker-compose.yml
├── etc
│   ├── clickhouse-keeper
│   │   └── keeper_config.xml # <-- このファイルを追加
│   └── clickhouse-server
│       └── config.d
│           ├── clickhouse-keeper-config.xml # <-- このファイルを追加
│           ├── clickhouse-server-1-macros.xml
│           └── clickhouse-server-2-macros.xml
└── volumes
```

`keeper_config.xml` はデフォルトのファイルに `<listen_host>0.0.0.0</listen_host>` を追加しています。clickhouse-serverみたいにconfigの項目で上書きができれば良かったのですが、見た感じできなそうなのでサーバから持ってきたファイルを変更し、ファイルごと上書きます。
設定ファイルを見るとスナップショットのパスなどの設定があるので、先ほど変換したファイルはこのパスにマウントします。

```xml:etc/clickhouse-keeper/keeper_config.xml
<?xml version="1.0"?>
<clickhouse>
    <listen_host>0.0.0.0</listen_host> <!-- この行を追加 -->
    <logger>
        <!-- Possible levels [1]:

          - none (turns off logging)
          - fatal
          - critical
          - error
          - warning
          - notice
          - information
          - debug
          - trace

            [1]: https://github.com/pocoproject/poco/blob/poco-1.9.4-release/Foundation/include/Poco/Logger.h#L105-L114
        -->
        <level>trace</level>
        <log>/var/log/clickhouse-keeper/clickhouse-keeper.log</log>
        <errorlog>/var/log/clickhouse-keeper/clickhouse-keeper.err.log</errorlog>
        <!-- Rotation policy
             See https://github.com/pocoproject/poco/blob/poco-1.9.4-release/Foundation/include/Poco/FileChannel.h#L54-L85
          -->
        <size>1000M</size>
        <count>10</count>
        <!-- <console>1</console> --> <!-- Default behavior is autodetection (log to console if not daemon mode and is tty) -->
    </logger>

    <max_connections>4096</max_connections>

    <keeper_server>
            <tcp_port>9181</tcp_port>

            <!-- Must be unique among all keeper serves -->
            <server_id>1</server_id>

            <log_storage_path>/var/lib/clickhouse/coordination/logs</log_storage_path>
            <snapshot_storage_path>/var/lib/clickhouse/coordination/snapshots</snapshot_storage_path>

            <coordination_settings>
                <operation_timeout_ms>10000</operation_timeout_ms>
                <min_session_timeout_ms>10000</min_session_timeout_ms>
                <session_timeout_ms>100000</session_timeout_ms>
                <raft_logs_level>information</raft_logs_level>
                <!-- All settings listed in https://github.com/ClickHouse/ClickHouse/blob/master/src/Coordination/CoordinationSettings.h -->
            </coordination_settings>

            <!-- enable sanity hostname checks for cluster configuration (e.g. if localhost is used with remote endpoints) -->
            <hostname_checks_enabled>true</hostname_checks_enabled>
            <raft_configuration>
                <server>
                    <id>1</id>

                    <!-- Internal port and hostname -->
                    <hostname>localhost</hostname>
                    <port>9234</port>
                </server>

                <!-- Add more servers here -->

            </raft_configuration>
    </keeper_server>


    <openSSL>
      <server>
            <!-- Used for secure tcp port -->
            <!-- openssl req -subj "/CN=localhost" -new -newkey rsa:2048 -days 365 -nodes -x509 -keyout /etc/clickhouse-server/server.key -out /etc/clickhouse-server/server.crt -->
            <certificateFile>/etc/clickhouse-keeper/server.crt</certificateFile>
            <privateKeyFile>/etc/clickhouse-keeper/server.key</privateKeyFile>
            <!-- dhparams are optional. You can delete the <dhParamsFile> element.
                 To generate dhparams, use the following command:
                  openssl dhparam -out /etc/clickhouse-keeper/dhparam.pem 4096
                 Only file format with BEGIN DH PARAMETERS is supported.
              -->
            <dhParamsFile>/etc/clickhouse-keeper/dhparam.pem</dhParamsFile>
            <verificationMode>none</verificationMode>
            <loadDefaultCAFile>true</loadDefaultCAFile>
            <cacheSessions>true</cacheSessions>
            <disableProtocols>sslv2,sslv3</disableProtocols>
            <preferServerCiphers>true</preferServerCiphers>
        </server>
    </openSSL>

</clickhouse>
```

clickhouse-server側の設定はzookeeperの設定をclickhouse-keeperに置き換えるだけです。

```xml:etc/clickhouse-server/config.d/clickhouse-keeper-config.xml
<?xml version="1.0"?>
<clickhouse>
    <listen_host>0.0.0.0</listen_host>
    <remote_servers>
        <sample_cluster>
            <shard>
                <replica>
                    <host>clickhouse-server-1</host>
                    <port>9000</port>
                </replica>
                <replica>
                    <host>clickhouse-server-2</host>
                    <port>9000</port>
                </replica>
            </shard>
        </sample_cluster>
    </remote_servers>
    <zookeeper>
        <node>
            <host>clickhouse-keeper</host>
            <port>9181</port>
        </node>
    </zookeeper>
</clickhouse>
```

最後にdocker-compose.ymlです。zookeeperの定義を削除し、clickhouse-keeperの定義を追加します。clickhouse-serverにマウントする設定ファイルもClickHouse Keeperのものに変更します。変換に使用したマウントは削除します。

```yaml:docker-compose.yml
version: '3.7'
services:
  clickhouse-server-1:
    container_name: clickhouse-server-1
    image: clickhouse/clickhouse-server:22.11
    ports:
      - "9000:9000"
      - "8123:8123"
    volumes:
      - ./volumes/clickhouse-server-1-data:/var/lib/clickhouse
      - ./etc/clickhouse-server/config.d/clickhouse-keeper-config.xml:/etc/clickhouse-server/config.d/clickhouse-keeper-config.xml
      - ./etc/clickhouse-server/config.d/clickhouse-server-1-macros.xml:/etc/clickhouse-server/config.d/clickhouse-server-1-macros.xml
  clickhouse-server-2:
    container_name: clickhouse-server-2
    image: clickhouse/clickhouse-server:22.11
    ports:
      - "9001:9000"
      - "8124:8123"
    volumes:
      - ./volumes/clickhouse-server-2-data:/var/lib/clickhouse
      - ./etc/clickhouse-server/config.d/clickhouse-keeper-config.xml:/etc/clickhouse-server/config.d/clickhouse-keeper-config.xml
      - ./etc/clickhouse-server/config.d/clickhouse-server-2-macros.xml:/etc/clickhouse-server/config.d/clickhouse-server-2-macros.xml
  clickhouse-keeper:
    container_name: clickhouse-keeper
    image: clickhouse/clickhouse-keeper:22-alpine
    ports:
      - "9181:9181"
    volumes:
      - ./volumes/var/lib/clickhouse/coordination/snapshots:/var/lib/clickhouse/coordination/snapshots
      - ./etc/clickhouse-keeper/keeper_config.xml:/etc/clickhouse-keeper/keeper_config.xml
```

ファイルを作成したらdockerを起動して確認します。

```bash
> docker compose up -d
[+] Running 4/4
 ⠿ Container clickhouse-keeper    Started    0.4s
 ⠿ Container clickhouse-server-2  Running    0.0s
 ⠿ Container clickhouse-server-1  Running    0.0s
```

`clickhouse-server-1`　から確認します。

```bash
> docker exec -it clickhouse-server-1 clickhouse-client
ClickHouse client version 22.11.1.1360 (official build).
Connecting to localhost:9000 as user default.
Connected to ClickHouse server version 22.11.1 revision 54460.
```

Zookeeperのスナップショットの移行が行われていることを確認します。

```sql
84483aff7bbe :) SELECT * FROM system.zookeeper WHERE path = '/clickhouse/tables/0'

┌─name───┬─value─┬─path─────────────────┐
│ sample │       │ /clickhouse/tables/0 │
└────────┴───────┴──────────────────────┘

1 row in set. Elapsed: 0.003 sec. 
```

パスが存在することを確認できました。  
続いてテーブルの検索と、データを追加してレプリケーションが働くことを確認します。

```sql
84483aff7bbe :) SELECT * FROM sample

┌─id─┬─val─┐
│  1 │ abc │
│  2 │ bcd │
└────┴─────┘

2 rows in set. Elapsed: 0.003 sec. 

84483aff7bbe :) INSERT INTO sample VALUES (3, 'cdf')

Ok.

1 row in set. Elapsed: 0.031 sec.

84483aff7bbe :) SELECT * FROM sample

┌─id─┬─val─┐
│  1 │ abc │
│  2 │ bcd │
└────┴─────┘
┌─id─┬─val─┐
│  3 │ cdf │
└────┴─────┘

3 rows in set. Elapsed: 0.006 sec.
```

`clickhouse-server-2`　からレプリケーションを確認します。

```bash
$ docker exec -it clickhouse-server-2 clickhouse-client
ClickHouse client version 22.11.1.1360 (official build).
Connecting to localhost:9000 as user default.
Connected to ClickHouse server version 22.11.1 revision 54460.
```

```sql
c41d6e631408 :) SELECT * FROM sample

┌─id─┬─val─┐
│  1 │ abc │
│  2 │ bcd │
└────┴─────┘
┌─id─┬─val─┐
│  3 │ cdf │
└────┴─────┘

3 rows in set. Elapsed: 0.006 sec. 
```

## さいごに

ZookeeperからClickHouse Keeperへの移行を行いました。サーバの停止が必要とはなりますが専用のツールも用意されていて（データ量が多い場合などいくつか考慮することはありそうですが、）それなりに簡単に移行できました。
ClickHouseのホスティングサービス[ClickHouse Cloud](https://clickhouse.com/cloud)や[Altinity.Cloud](https://altinity.com/cloud-database/)もできたことから今後も運用が容易になるような機能やツールが追加されていくような気がしています。

今回作成したファイルは以下にあります

https://github.com/matsuiyuki/toybox/tree/main/clickhouse-migration-keeper

Dockerを利用して試すのは気軽にできていいのですが、データや設定ファイルなど考慮することが多く面倒でした。あと、記事にするとごちゃっとなりますね😅