プロジェクト

全般

プロフィール

Cassandra DBをDocker Composeで動かす

Cassandra DB のクラスタを、1台のPC上で構成します。主に開発・検証用です。

同じマシン上で複数のCassandraノードをDockerコンテナで実行するには、複数のDockerコンテナを連動して実行する Docker compose を使用するのが定番です。

まず、作業ディレクトリを作成し、そこに compose.yaml ファイルを定義します。
通常 各ノードのcassandra.yamlに設定を記述する方法は、同じDockerイメージから複数コンテナを起動する場合に適用が難しいので、環境変数から設定を各ノードに入れます。

方針

Cassandra DBのクラスタを新規に構成するには、シードノードと呼ぶノードをまず最初に起動します。シードノードが動作状態に達してから、2番目のノードを起動しクラスタに参加させます。2番目のノードが動作しクラスタに参加した後に3番目のノードを起動していきます。4つ以上ノードがある場合は同様に順番にノードを起動していきます。
クラスタの新規構成時に、もしノードを一斉に起動してしまうとトークンの分担がノード間で正しく行われないのでトークンのコンフリクトが発生します。

一度クラスタ構成ができてしまうと、以後はどの順序でノードを起動してもクラスタが維持されます。
ただし、同時に起動するとうまくクラスタに参加できずエラー終了するノードが発生してしまいます。

なお、コンテナのデータ(Cassandra DBのデータ)を削除してしまうと、次の起動は新規クラスタの構成となってしまいます。

クラスタを新規に構成する手順と、クラスタ構成後の各ノードの起動・終了は手順がことなるので、1つのcomposeで両方に対応することが難しいです。次の2案があります。案1が実装容易ですが、案2は難易度が上がります。

  • 案1)クラスタを構成したのちに compose upですべてのノードを起動する。クラスタを新規構成(データを削除してからの起動)するときは、手動もしくは別のスクリプトで1ノードずつ時間をおいて、またはステータスを確認しクラスタに参加状態となってから次のノードを起動する
  • 案2)compseの定義でhealthcheckにノードがクラスタに参加状態となっていることを確認する定義を記述

ステップ by ステップ

クラスタを組まないで3ノード実行

compose.yamlの記述

3ノードをDocker Composeで実行するので、サービスを3つ記述します。サービスの内容は、起動するだけの最低限の記述としてDockerイメージ名とコンテナ名を指定します。

services:
  cassandra1:
    image: cassandra:5.0.9
    container_name: cassandra1

  cassandra2:
    image: cassandra:5.0.9
    container_name: cassandra2

  cassandra3:
    image: cassandra:5.0.9
    container_name: cassandra3
  • 最低限 image の指定が必要
  • container_nameを省略すると、デフォルトで <compose.yamlのあるディレクトリ名>-<サービス名> がコンテナ名となります。

単一コンテナの起動

まずはこの設定ファイルで単一のコンテナ(cassandra1)が起動できるか確認します。

~cassandra_docker$ docker compose up -d cassandra1
[+] Running 1/1
 ✔ Container cassandra1  Started                                                                                                                                            0.2s
~cassandra_docker$ docker container ls
CONTAINER ID   IMAGE             COMMAND                  CREATED         STATUS         PORTS                                         NAMES
f1a96b1273ef   cassandra:5.0.9   "docker-entrypoint.s…"   4 seconds ago   Up 4 seconds   7000-7001/tcp, 7199/tcp, 9042/tcp, 9160/tcp   cassandra1

3ノードの起動

Docker composeで3つのコンテナが起動できることを確認します。

~cassandra_docker$ docker compose up -d
[+] Running 4/4
✔ Network docker2_default  Created                                                                                                                                          ✔ Container cassandra2     Started                                                                                                                                          ✔ Container cassandra3     Started                                                                                                                                        
✔ Container cassandra1     Started 

この状態で、ノードの情報を参照するとクラスタにはなっておらず各コンテナが単独ノードとして動いています。

~cassandra_docker$  docker exec cassandra1 nodetool status
Datacenter: datacenter1
=======================
Status=Up/Down
|/ State=Normal/Leaving/Joining/Moving
--  Address     Load        Tokens  Owns (effective)  Host ID                               Rack
UN  172.19.0.4  172.12 KiB  16      100.0%            cb9bc4d4-6ebb-4353-bbf3-1d6605abf508  rack1

クラスタを構成する

この3つのCassandraコンテナで同一のクラスタを組むには、シードノードの設定が必要です。

シード・ノードの定義

ノードが起動したときに、クラスタ構成を問い合わせるノードをシードとして指定します。同一データセンター内で最低1つのノードを指定しますが、運用では障害を考慮して2つないし3つをシードに指定するのが定番です。シードが正常に稼働していないと、新規参入するノードがクラスタに加わることができません。

新規にクラスタを構築するときは、まずシードノードが正常に起動したあとに、追加のノードを起動していきます。

   cassandra2:
     image: cassandra:5.0.9
     container_name: cassandra2
+    environment:
+      - CASSANDRA_SEEDS: cassandra1

   cassandra3:
     image: cassandra:5.0.9
     container_name: cassandra3
+    environment:
+      - CASSANDRA_SEEDS: cassandra1

シードノードとなるcassandra1にはシードの定義は不要です。

まず、cassandra1から手動で順番にコンテナを起動し、都度 nodetool でステータスを確認します。

  • cassandra1の起動
    docker compose up -d cassandra1
  • cassandra1のnodetool status確認
    $ docker exec cassandra1 nodetool status
    Datacenter: datacenter1
    =======================
    Status=Up/Down
    |/ State=Normal/Leaving/Joining/Moving
    --  Address     Load        Tokens  Owns (effective)  Host ID                               Rack
    UN  172.19.0.4  172.12 KiB  16      100.0%            cb9bc4d4-6ebb-4353-bbf3-1d6605abf508  rack1
    
  • cassandra1のステータスがUNになったことを確認し、cassandra2の起動
    docker compose up -d cassandra2
  • しばらく待ってから、nodetoolのsutatus確認
    $ docker exec cassandra1 nodetool status
    Datacenter: datacenter1
    =======================
    Status=Up/Down
    |/ State=Normal/Leaving/Joining/Moving
    --  Address     Load        Tokens  Owns (effective)  Host ID                               Rack
    UN  172.19.0.2  119.83 KiB  16      100.0%            f2a68fdc-c15d-445c-9a67-e6f0c81aefcc  rack1
    UN  172.19.0.3  119.67 KiB  16      100.0%            e92cbc68-bd5f-4755-8f00-2d082c856a97  rack1
    
  • cassandra2のステータスがUNになったことを確認し、 cassandra3の起動
    docker compose up -d cassandra3
  • しばらく待ってから、nodetoolのsutatus確認
    $ docker exec cassandra1 nodetool status
    Datacenter: datacenter1
    =======================
    Status=Up/Down
    |/ State=Normal/Leaving/Joining/Moving
    --  Address     Load        Tokens  Owns (effective)  Host ID                               Rack
    UN  172.19.0.4  30.9 KiB    16      76.0%             03efb4bf-0722-4188-925d-d3fcc18a79ee  rack1
    UN  172.19.0.2  119.83 KiB  16      64.7%             f2a68fdc-c15d-445c-9a67-e6f0c81aefcc  rack1
    UN  172.19.0.3  85.11 KiB   16      59.3%             e92cbc68-bd5f-4755-8f00-2d082c856a97  rack1
    
  • cluster_nameはデフォルトでTest Clusterとなるので、cluster_name未設定でも同じクラスタを組むことができます
  • docker compose down -v でデータを削除したあとに docker compose up -d で起動すると、クラスタ構成の情報も削除されているのでクラスタの新規構成となります。すると、起動タイミングがほぼ同時となるため、シードノードが起動完了するまえにノードが起動してしまい、クラスタが構成できません

healthcheckの設定

Cassandraのノードが起動完了し、動作可能となっているかをチェックする設定を追加します。
手段は、nodetoolを実行して応答が返ってくるかの確認をします。
以下のhealthcheck設定を各ノードに記述します。

service:
  cassandra1:
    :
    healthcheck:
      test: ["CMD-SHELL", "cqlsh -e 'describe cluster' || exit 1"]
      interval: 10s
      timeout: 5s
      retries: 20
  • interval(デフォルト30秒)
    コンテナ起動時点を0として、interval指定時間が経過する都度ヘルスチェックを開始します。
  • timeout(デフォルト30秒)
    チェックを実行してから timeout指定時間以内にチェックが終わらない場合チェックが失敗したものとみなします。
  • retries(デフォルト3回)
    チェックが retries指定回数失敗したらunhealthy状態に遷移します。
  • start-period(デフォルト0)
    起動後、start-periodで指定した時間が経過するまではstart-intervalで指定した周期でチェックを実行します。start-period期間内のチェック失敗回数には加算されません。
  • start-interval(デフォルト5秒)
    上述

起動順番の設定

クラスタ構成後においても、ノードの起動を複数同時にするとノード間通信がしばらく確立できずエラーになることもあります。そこで、クラスタ構成後の起動を順番付けします。

services:
  cassandra1:
    :
  cassandra2:
    :
    depends_on:
      cassandra1:
        condition: service_healthy
    :
  cassandra3:
    depends_on:
      cassandra2:
        condition: service_healthy

  • cassandra2は、cassandra1のヘルスチェックがOKとなったら起動します。
  • cassandra3は、cassandra2のヘルスチェックがOKとなったら起動します。

これでラグを付けて起動することができます。

  • 注)新規クラスタ構成時は、このラグでは不十分です

使用メモリサイズの指定

同一PC上に複数Cassandra DBノードを実行するときは、各ノードで使用するメモリの合計をホストPCのメモリに応じて制限する必要があります。Cassandra DBは、JavaVM上で実行しますが、JavaVMはデフォルトでホストPCのメモリの1/4の容量をJavaヒープに確保します。3ノードを実行すると大半のメモリがとられてしまうので、設定で最大ヒープサイズを制限します。

services:
  cassandra1:
    :
    environment:
      :
      MAX_HEAP_SIZE=1G
  • JavaVM (JDK) 17 では、デフォルトのGC(ガベージコレクタ)がG1GCであり、G1GCではHEAP_NEWSIZEの指定は不要

データの永続化

データベースのデータを、Docker コンテナを破棄したときにも残して再利用できるように Volume としてコンテナ外部に保存します。

使用する volume の定義

トップレベルのvolumes項目に、使用する volume を定義します。

volumes:
  cassandra-data-1:
  cassandra-data-2:
  cassandra-data-3:

ここに定義したボリュームが未作成のときは、composeを起動するときに、docker compose up -d で作成されます。事前に手動で docker volume create をする必要がありません。

ボリュームをコンテナにマウントする設定を、各servicesのサービス毎に記述します。

services:
  cassandra1:
    :
    volumes:
      cassandra-data-1:/var/lib/cassandra

ここまでのまとめ

これまでの compose.yaml の全体を示します。

services:
  cassandra1:
    image: cassandra:5.0.9
    container_name: cassandra1
    environment:
      - MAX_HEAP_SIZE=1G
    volumes:
      - cassandra-data-1:/var/lib/cassandra
    healthcheck:
      test: ["CMD-SHELL", "cqlsh -e 'describe cluster' || exit 1"]
      interval: 10s
      timeout: 5s
      retries: 20

  cassandra2:
    image: cassandra:5.0.9
    container_name: cassandra2
    environment:
      - CASSANDRA_SEEDS=cassandra1
      - MAX_HEAP_SIZE=1G
    volumes:
      - cassandra-data-2:/var/lib/cassandra
    depends_on:
      cassandra1:
        condition: service_healthy
    healthcheck:
      test: ["CMD-SHELL", "cqlsh -e 'describe cluster' || exit 1"]
      interval: 10s
      timeout: 5s
      retries: 20

  cassandra3:
    image: cassandra:5.0.9
    container_name: cassandra3
    environment:
      - CASSANDRA_SEEDS=cassandra1
      - MAX_HEAP_SIZE=1G
    volumes:
      - cassandra-data-3:/var/lib/cassandra
    depends_on:
      cassandra2:
        condition: service_healthy
    healthcheck:
      test: ["CMD-SHELL", "cqlsh -e 'describe cluster' || exit 1"]
      interval: 10s
      timeout: 5s
      retries: 20

volumes:
  cassandra-data-1:
  cassandra-data-2:
  cassandra-data-3:

新規クラスタ構築対応

手動でクラスタ新規構成する手順をcomposeで実装します。healthcheckで、自身のノードがクラスタに参加状態(UN)となったことをもってhealth状態とします。

    healthcheck:
      test: ["CMD-SHELL", 'nodetool status | grep -qE "^UN[[:space:]]+$$(hostname -i)[[:space:]]"']
      interval: 2m
      start_period: 2m
      timeout: 10s
      retries: 3
  • hostname -i でノード自身のIPアドレスを実行時に取得します。通常のLinux OS上ではhostname -iコマンドは複数のIPアドレスを返却し、最初の値はループバック(127.0.0.1)となることがありますが、Dockerコンテナ上ではループバックは返らず、接続しているネットワークの数に相当するIPアドレスが返ります。

これで、手動と同じ手順で複数ノードを順番に起動しクラスタを構成することができました。

compose定義の共通化

上述のhealthcheckは、どのコンテナでも共通です。同じ定義を複数個所に記述するのはメンテナンス上好ましくないので、設定を共通化します。
compse定義では、yamlのアンカー(&)、エイリアス(*)を使って共通化を図ることができます。

  • cassandra1のhealthcheckに定義を記述、このhealthcheckにアンカーを設定
  • cassandra2、3のhealthcheckは、エイリアスでcassandra1のhealthcheckを参照する定義を記述
  cassandra1:
    (中略)
    healthcheck: &healthcheck
      test: ["CMD-SHELL", 'nodetool status | grep -qE "^UN[[:space:]]+$$(hostname -i)[[:space:]]"']
      interval: 2m
      start_period: 2m
      timeout: 10s
      retries: 3

  cassandra2:
    (中略)
    healthcheck: *healthcheck

  cassandra3:
    (中略)
    healthcheck: *healthcheck

cassandra1のhealthcheckのキーに、&<名前> でアンカーを設定します。
cassandra2と3のhealthcheckのキーに、*<名前> でアンカー設定した項目のエイリアスを設定します。

environmentとhealthcheckの共通化

上述のhealthcheckに加え、environmentを共通化したcompose定義を次に示します。

services:
  cassandra1:
    image: cassandra:5.0.9
    container_name: cassandra1
    environment: &environment
      - CASSANDRA_SEEDS=cassandra1
      - MAX_HEAP_SIZE=1G
    volumes:
      - cassandra-data-1:/var/lib/cassandra
    healthcheck: &healthcheck
      test: ["CMD-SHELL", 'nodetool status | grep -qE "^UN[[:space:]]+$$(hostname -i)[[:space:]]"']
      interval: 2m
      start_period: 2m
      timeout: 10s
      retries: 3

  cassandra2:
    image: cassandra:5.0.9
    container_name: cassandra2
    environment: *environment
    volumes:
      - cassandra-data-2:/var/lib/cassandra
    depends_on:
      cassandra1:
        condition: service_healthy
    healthcheck: *healthcheck

  cassandra3:
    image: cassandra:5.0.9
    container_name: cassandra3
    environment: *environment
    volumes:
      - cassandra-data-3:/var/lib/cassandra
    depends_on:
      cassandra2:
        condition: service_healthy
    healthcheck: *healthcheck

volumes:
  cassandra-data-1:
  cassandra-data-2:
  cassandra-data-3:
  • cassandra1のenvironmentに、CASSANDRA_SEEDS=cassandra1を追記、これで3ノード共通定義となります。シードノードの設定に自身を参照する記述は問題ありません。

未整理メモ

  • Snitchの設定
    Rack、Data centerを意識しない構成なら SimpleSnitchでもよい
    Rack、Data centerを考慮する構成なら GossipingPropertyFileSnitch
  • GossipingPropertyFileSnitchを設定したら、CASSANDRA_DCを明示的に指定
    • cassandra.yaml の cluster_name設定、cassandra-rackdc.properties の dc設定と同等

資料

Running Apache Cassandra Single and Multi-Node Clusters on Docker with Docker Compose

Quick Setup of a Multi-node Cluster Database with Cassandra


7日前に更新