クラスタリング#

LXD はクラスタリングモードで実行できます。クラスタリングモードでは複数台の LXD サーバが同じ分散データベースを共有し、REST API や lxc クライアントで統合管理できます。

この機能は API 拡張の "clustering" の一部として導入しました。

クラスタの形成#

まず、ブートストラップノードを選択する必要があります。既存の LXD サーバでも新しいインスタンスでもブートストラップノードになれます。ブートストラップノードとなるサーバを決めた後は、ブートストラップノードを初期化し、それからクラスタへ追加ノードを参加させます。この処理はインタラクティブに行えますし、前もって定義ファイルを作成しても行えます。

クラスタに追加するノードはすべて、ストーレージプールとネットワークについて、ブートストラップノードと同じ構成を持たなければなりません。ノード特有の設定として持てる唯一の設定は、ストレージプールに対する sourcesize、ネットワークに対する bridge.external_interface です。

クラスタ内のノード数としては 3 以上を強く推奨します。これは少なくとも 1 ノードが落ちても分散状態のクオラムを確立できるからです(分散状態は Raft アルゴリズムを使ってレプリケーションされている SQLite データベースに保管されています)。ノード数が 3 より小さくなるとクラスタ内のただ 1 つのノードだけが SQLite データベースを保管します。第 3 のノードがクラスタに参加したときに、第 2 と第 3 のノードがデータベースの複製を受け取ります。

インタラクティブに行う方法#

lxd init を実行し、最初の質問("Would you like to use LXD clustering?")に yes と答えます。そして、そのノードを特定する名前、他のノードが接続するための IP もしくは DNS アドレスを選択します。そして、既存のクラスタに加わるかどうかの質問には no と答えます。最後に、オプショナルでストレージプールとネットワークブリッジを作成できます。これで、最初のクラスタノードが起動し、ネットワークが利用できるようになります。

更に追加のノードをクラスタに追加できます。しかし、追加ノードの既存データはすべて失われるため、追加のノードは完全に新しい LXD サーバであるか、追加前にすべての情報をクリアしたノードである必要があります。

既存のクラスタにメンバーを追加するには 2 つの方法があります。トラスト・パスワードを使うか参加トークンを使うかです。 新規メンバーの参加トークンは既存のクラスタで次のコマンドを使って事前に生成します。

lxc cluster add <new member name>

これで 1 度だけ使える参加トークンが生成されます。これは lxd init の参加トークンを質問するプロンプトで使えます。参加トークンは既存のオンラインメンバーのアドレスと 1 度だけ使えるシークレットとクラスタの証明書のフィンガープリントを含んでいます。参加トークンは lxd init 実行時に聞かれる複数の質問に自動的に回答するのに使われるので、回答が必要な質問の数を減らすことができます。

あるいは参加トークンの代わりにトラスト・パスワードを使うこともできます。

ノードを追加するために、lxd init を実行し、クラスタリングを使うかどうかの質問に yes と答えます。ブートストラップノード、それまでに参加したノードとは異なる名前を指定します。IP もしくは DNS アドレスを指定し、既存のクラスタに加わるかどうかの質問には yes と答えます。

参加トークンがある場合は参加トークンを持っているかの質問に yes と回答し、参加トークンを求めるプロンプトに対してトークンをコピーします。

参加トークンは持っていないがトラスト・パスワードを持っている場合は参加トークンを持っているかの質問に no と答えます。その後クラスタ内の既存のノードのアドレスを 1 つ選び表示されたフィンガープリントが既存メンバーのクラスタ証明書にマッチするかをチェックします。

サーバごとの設定#

上で述べたように LXD のクラスタメンバーはたいていは同一のシステムであると想定されます。

しかしディスクの多少の順序の違いやネットワークインターフェースの名前が違いに適応するため、 LXD は一部の設定をサーバごとに記録します。 クラスタにそのような設定が存在するときは、新しく追加されるサーバにはその設定に対する値を指定する必要があります。

これはたいていの場合インタラクティブな lxd init の実行時に、ストレージやネットワークに関連するいくつかの設定キーの値をユーザーに尋ねることで実現されます。

典型的には以下のような項目が対象になります。

  • ストレージプールのソースデバイス(空にするとループデバイスを作成)

  • ZFS zpool の名前(デフォルトは LXD プールの名前)

  • ブリッジネットワークの外部インターフェース(空にすると追加しない)

  • マネージドされた物理あるいは macvlan ネットワークの親のネットワークデバイスの名前(設定必須)

どういう質問があるかは /1.0/cluster API エンドポイントに問い合わせることで事前に確認できます(スクリプトを書く際に有効です)。 これは lxc query /1.0/cluster や他の API クライアントを使って実行できます。

事前に定義して行う方法#

事前にブートストラップノードの設定内容を書いた定義ファイルを作成できます。例えば:

config:
  core.trust_password: sekret
  core.https_address: 10.55.60.171:8443
  images.auto_update_interval: 15
storage_pools:
- name: default
  driver: dir
networks:
- name: lxdbr0
  type: bridge
  config:
    ipv4.address: 192.168.100.14/24
    ipv6.address: none
profiles:
- name: default
  devices:
    root:
      path: /
      pool: default
      type: disk
    eth0:
      name: eth0
      nictype: bridged
      parent: lxdbr0
      type: nic
cluster:
  server_name: node1
  enabled: true

定義ファイルを作成したあと、cat <preseed-file> | lxd init --preseed を実行し、最初のノードを作成します。

次に、他のノードのブートストラップファイルを作成します。cluster セクションに、追加するノード固有のデータと設定値を指定するだけです。

ターゲットとなるブートストラップノードのアドレスと証明書を必ず含めてください。cluster_certificate に対する YAML 互換のエントリーを作成するには、sed ':a;N;$!ba;s/\n/\n\n/g' /var/lib/lxd/cluster.crt (あるいは snap ユーザーは sed ':a;N;$!ba;s/\n/\n\n/g' /var/snap/lxd/common/lxd/cluster.crt)のようにコマンドを実行します。このコマンドはブートストラップノードで実行する必要があります。 cluster_certificate_path キー(これにはクラスタ証明書の有効なパスを設定します)を cluster_certificate キーの代わりに使うこともできます。

例えば:

cluster:
  enabled: true
  server_name: node2
  server_address: 10.55.60.155:8443
  cluster_address: 10.55.60.171:8443
  cluster_certificate: "-----BEGIN CERTIFICATE-----

opyQ1VRpAg2sV2C4W8irbNqeUsTeZZxhLqp4vNOXXBBrSqUCdPu1JXADV0kavg1l

2sXYoMobyV3K+RaJgsr1OiHjacGiGCQT3YyNGGY/n5zgT/8xI0Dquvja0bNkaf6f

...

-----END CERTIFICATE-----
"
  cluster_password: sekret
  member_config:
  - entity: storage-pool
    name: default
    key: source
    value: ""

クラスタ参加トークンを使用してクラスタに参加する際は、下記のフィールドは省略できます。

  • server_name

  • cluster_address

  • cluster_certificate

  • cluster_password

そして代わりにフルのトークンを cluster_token フィールドを使って渡せます。

クラスタの管理#

クラスタが形成されると、lxc cluster list を実行して、ノードのリストと状態を見ることができます。ノードそれぞれのもっと詳細な情報は lxc cluster show <node name> を実行して取得できます。

クラスタメンバーの設定#

各クラスタメンバーは以下のサポートされるネームスペース内で独自のキー・バリュー設定を持てます。

  • user (ユーザーのメタデータ用に自由形式のキー・バリュー)

  • scheduler (メンバーが自クラスタによりどのように動的にターゲットされるかに関連するオプション)

現状サポートされるキーは以下の通りです。

キー

デフォルト値

説明

scheduler.instance

string

all

all の場合、インスタンスの最も少ないメンバーが、インスタンス作成の対象として自動的に選択されます。 manual の場合、インスタンスは --target が指定されたときのみメンバーにターゲットされます。group の場合、インスタンスは --target=@<group> が指定されたときのみメンバーにターゲットされます。

user.*

string

-

自由形式のユーザーのキー・バリュー・ストレージ (検索で使用可能)

クラスタメンバーの役割#

以下の役割が LXD クラスタメンバーに設定できます。 自動的な役割は LXD 自身によって設定され、ユーザーは変更できません。

役割

自動的

説明

database

yes

分散データベースの投票メンバー

database-leader

yes

分散データベースの現在のリーダー

database-standby

yes

分散データベースのスタンバイ (投票しない) メンバー

event-hub

no

LXD 内部イベントの交換点 (ハブ) (最低2つ必要)

ovn-chassis

no

OVN ネットワークのアップリンクゲートウェイ候補

投票 (voting) メンバーとスタンバイメンバー#

クラスタは状態を保管するために分散 データベース を使用します。 クラスタ内の全てのノードはユーザーのリクエストに応えるためにそのような分散データベースにアクセスする必要があります。

クラスタ内に多くのノードがある場合、それらのうちいくつかだけがデータベースのデータを複製するために選ばれます。 選ばれた各オンードは投票者 (voter) としてあるいはスタンバイとしてデータを複製できます。 データベース(とそれに由来するクラスタ)は投票者の過半数がオンラインである限り利用可能です。 別の投票者が正常にシャットダウンした時やオフラインであると検出された時はスタンバイノードが自動的に投票者に昇格されます。

投票ノードのデフォルト数は 3 で、スタンバイノードのデフォルト数は 2 です。 これは 1 度に最大で 1 つの投票ノードの電源を切る限りあなたのクラスタは稼働し続けることを意味します。

投票ノードとスタンバイノードの望ましい数は以下のように変更できます。

lxc config set cluster.max_voters <n>

そして

lxc config set cluster.max_standby <n>

投票者の最大数は奇数で最低でも 3 であるという制約があります。 一方、スタンバイノードは 0 から 5 の間でなければなりません。

ノードの削除#

クラスタからノードをクリーンに削除するには、lxc cluster remove <node name> を使います。

オフラインノードとフォールトトレランス#

都度、選出されたクラスタリーダーが存在し、そのリーダーが他のノードの健全性をモニタリングします。20 秒以上ノードがダウンした場合は、ステータスは OFFLINE とマークされ、そのノード上での操作はできなくなります。また、すべてのノードで状態の変更が必要な操作が可能です。

リーダーがオフラインに移行した場合、他のノードが新しいリーダーに選出されます。

オフラインノードがオンラインに戻るとすぐに、ふたたび操作できるようになります。

ノードをオンラインに戻せないとき、ノードをオンラインに戻したくないときは、lxc cluster remove --force <node name> を使ってクラスタからノードを削除できます。

反応しないノードがオフラインと認識されるまでの秒数は以下のようにして変更できます。

lxc config set cluster.offline_threshold <n seconds>

最小値は 10 秒です。

ノードのアップグレード#

クラスタをアップグレードするためには、すべてのノードをアップグレードし、すべてが確実に同じバージョンの LXD にする必要があります。

単一のノードをアップグレードするには、単にホスト上で(snap や他のパッケージ管理システムを使って) lxd/lxc バイナリをアップグレードし、LXD デーモンを再起動します。

デーモンの新バージョンでデータベーススキーマや API が変更になった場合は、再起動したノードは Blocked 状態に移行する可能性があります。これは、クラスタ内にまだアップグレードされていないノードが存在し、その上で古いバージョンが動作している場合に起こります。ノードが Blocked 状態にあるとき、このノードは LXD API リクエストを一切受け付けません(詳しく言うと、実行中のインスタンスは動き続けますが、ノード上の lxc コマンドは動きません)。

ブロックされていないノード上で lxc cluster list を実行すると、ノードがブロックされているかどうかを確認できます。

残りのノードのアップグレードを進めると、最後のノードをアップグレードするまでは、ノードはすべて Blocked 状態に移行します。その時点で、Blocked ノードは古いノードが残っていないかを確認し、再度操作できるようになります。

クラスタメンバーの待避と復元#

再起動が必要なシステムアップデートを適用する定例メンテナンスやハードウェアの構成変更などで、指定したサーバ上の全てのインスタンスを空にしたいことが時々あります。

これは lxc cluster evacuate <NAME> で実行できます。 このコマンドはそのサーバ上の全てのインスタンスをマイグレートし、他のクラスタメンバーに移動します。 待避が行われたクラスタメンバーは "evacuated" 状態に遷移し、そこではインスタンスの生成は禁止されます。

メンテナンスが完了したら lxc cluster restore <NAME> を実行するとサーバを通常の実行状態に戻し、このサーバ上に元々あって一時的に他のサーバに移動していたインスタンスをこのサーバ上に戻します。

指定のインスタンスの挙動は cluster.evacuate インスタンス設定キーで指定できます。 boot.host_shutdown_timeout 設定キーを尊重してインスタンスはクリーンにシャットダウンされます。

Failure domains#

Failure domain はシャットダウンしたかクラッシュしたクラスタメンバーに role を割り当てる際にどのノードが優先されるかを指示するのに使います。 例えば、現在 database role を持つクラスタメンバーがシャットダウンした場合、 LXD は同じ failure domain 内の別のクラスタメンバーが存在すればそれに database role を割り当てようと試みます。

クラスタメンバーの failure domain を変更するには lxc cluster edit <member> コマンドラインツールか、 PUT /1.0/cluster/<member> REST API が使用できます。

クォーラム消失からの復旧#

各 LXD クラスタはデータベースノードとして機能するメンバーを最大 3 つまで持つことができます。 恒久的にデータベースノードとして機能するクラスタメンバーの過半数を失った場合 (例えば 3 メンバーのクラスタで 2 メンバーを失った場合)、 クラスタは利用不可能になります。しかし、 1 つでもデータベースノードが生き残っていれば、クラスタをリカバーすることができます。

クラスタメンバーがデータベースノードとして設定されているかどうかをチェックするには、クラスタのいずれかの生き残っているメンバーにログオンして以下のコマンドを実行します。

lxd cluster list-database

これは LXD デーモンが実行中でなくても実行できます。

一覧表示されたメンバーの中で、生き残っていてログインしたものを選びます (コマンドを実行したメンバーと異なる場合)。

LXD デーモンが実行していないことを確認したうえで次のコマンドを実行します。

lxd cluster recover-from-quorum-loss

この時点で LXD デーモンを再起動できるようになり、データベースはオンラインに復帰するはずです。

データベースからは何の情報も削除されていないことに注意してください。特に失われたクラスタメンバーに関する情報は、それらのインスタンスについてのメタデータも含めて、まだそこに残っています。 この情報は失われたインスタンスを再度作成する必要がある場合に、さらなるリカバーのステップで利用することができます。

失われたクラスタメンバーを恒久的に削除するためには、次のコマンドが利用できます。

lxc cluster remove <name> --force

ここでは lxd ではなく通常の lxc コマンドを使う必要があることに注意してください。

アドレスを変更してクラスタメンバーをリカバーする#

クラスタの一部のメンバーが到達不可能になった場合や、IPアドレスやリッスンするポート番号の変更によりクラスタ自体が到達不可能になった場合、クラスタは再設定できます。

クラスタの各メンバー上で、 LXD が実行していないときに、以下のコマンドを実行します。

lxd cluster edit

このセクションの全てのコマンドは lxc ではなく lxd を使うことに注意してください。

このコマンドはこのノード上で最後に記録されたクラスタの他のノードに関する情報を YAML 形式で表示します。

# Latest dqlite segment ID: 1234 # 最新の dqlite のセグメント ID

members:
  - id: 1             # このノードの内部 ID (読み取り専用)
    name: node1       # クラスタメンバーの名前 (読み取り専用)
    address: 10.0.0.10:8443 # このノードの最新のアドレス (書き込み可)
    role: voter             # このノードの最新のロール (書き込み可)
  - id: 2
   name: node2
    address: 10.0.0.11:8443
    role: stand-by
  - id: 3
   name: node3
    address: 10.0.0.12:8443
    role: spare

この設定からメンバーを削除したり、スタンバイのノードを投票者 (voter) に変えたりは出来ません。 それらの変更にはグローバルデータベースが必要になるかもしれないからです。 重要なこととして、最低 2 つのノードが投票者 (voter) であること (メンバー数が 2 のクラスタのケースを除いて。メンバー数が 2 のクラスタは 1 つの投票者 (voter) で十分です) を覚えておいてください。 そうでないとクォーラムが成立しません。

必要な変更を終えたら、クラスタ内の各メンバー上で同様に変更を行います。 各メンバー上で LXD をリロードすると、設定に書かれた全てのノードを含んだ状態でクラスタ全体がオンラインに戻るはずです。

データベースからは何の情報も削除されていないので、クラスタメンバーとインスタンスの全ての情報はデータベースに残っていることに注意してください。

インスタンス#

クラスタ上の任意のノード上でインスタンスを起動できます。例えば、node1 から:

lxc launch --target node2 ubuntu:22.04 c1

のように実行すれば、node2 上で Ubuntu 22.04 コンテナが起動します。

ターゲットを指定せずにインスタンスを起動したときは、インスタンスの数が一番少ないサーバ上でインスタンスが起動されます。全てのサーバが同じ数のインスタンスを持っている場合はランダムに選ばれます。

以下のように実行すると、インスタンス上のすべてのコンテナをリストできます:

lxc list

NODE 列がコンテナが実行中のノードを示します。

インスタンスが起動後、任意のノードからそのコンテナを操作できます。例えば、node1 から:

lxc exec c1 ls /
lxc stop c1
lxc delete c1
lxc pull file c1/etc/hosts .

のように操作できます。

Raft メンバーシップの手動での変更#

何か予期せぬ出来事が起こった場合など、クラスタの Raft メンバーシップの設定を手動で変更する必要がある状況があるかもしれません。

例えばクリーンに削除できなかったクラスタメンバーがある場合、 lxc cluster list に表示されないですが、引き続き Raft 設定の一部になってしまう場合があるかもしれません (この状況は lxd sql local "SELECT * FROM raft_nodes" で確認できます)。

この場合は以下のように実行すると

lxd cluster remove-raft-node <address>

残ってしまったノードを削除できます。

イメージ#

デフォルトではデータベースメンバを持っているのと同じ数のクラスタに LXD はイメージを複製します。これは通常はクラスタ内で最大3つのコピーを 持つことを意味します。

耐障害性とイメージがローカルにある可能性を上げるためにこの数を増やす ことができます。

特別な値である "-1" は全てのノードにイメージをコピーするために使用できます。

この数を 1 に設定することでイメージの複製を無効にできます。

lxc config set cluster.images_minimal_replica 1

ストレージプール#

先に述べたように、すべてのノードは同一のストレージプールを持たなければなりません。異なるノード上のプール間の違いは、設定項目、sourcesizezfs.pool_name のみです。

新たにストレージプールを作成するためには、すべてのノードでストレージプールをを定義する必要があります。例えば:

lxc storage create --target node1 data zfs source=/dev/vdb1
lxc storage create --target node2 data zfs source=/dev/vdc1

のようにします。

新しいストレージプールをノード上に定義する際、ノード固有で与えることのできる設定項目は上記設定のみです。

この時点ではプールはまだ実際には作られていませんが、定義はされています(lxc storage list を実行すると、状態が Pending とマークされています)。

次のように実行しましょう:

lxc storage create data zfs

するとストレージがすべてのノードでインスタンス化されます。特定のノードで定義を行っていない場合、もしくはノードがダウンしている場合は、エラーが返ります。

この最後の storage create コマンドには、ノード固有ではない(上記参照)任意の設定項目を与えることができます。

ストレージボリューム#

各ボリュームは特定のノード上に存在しています。lxc storage volume list は、特定のボリュームがどのノードにあるかを示す NODE 列を表示します。

異なるボリュームは、異なるノード(例えば image volumes)上に存在する限りは同じ名前を持てます。複数のノードが与えた名前のボリュームを持つ場合には、ボリュームコマンドに --target <node name> を与える必要がある点を除いて、ストレージボリュームはクラスタ化されていない場合と同じ方法で管理できます。

例えば:

# Create a volume on the node this client is pointing at
lxc storage volume create default web

# Create a volume with the same node on another node
lxc storage volume create default web --target node2

# Show the two volumes defined
lxc storage volume show default web --target node1
lxc storage volume show default web --target node2

ネットワーク#

先に述べたように、すべてのノードは同じネットワークを定義しなければなりません。

異なるノード間のネットワークで異なっても良い設定は、それらのオプショナルの設定項目だけです。 特定のクラスタノード上に新しいネットワークを定義する際、設定可能な有効なオプショナルな設定項目は bridge.external_interfacesparent だけです。 これらは各ノード上で異なる値が設定可能です(それぞれの定義については ネットワーク設定 の文書を参照してください)。

新しいネットワークを作成するには、最初にすべてのノードで以下のように定義を行う必要があります:

lxc network create --target node1 my-network
lxc network create --target node2 my-network

この時点では、ネットワークはまだ実際には作成されていません。しかし定義はされています(lxc network list を実行すると、状態が Pending とマークされています)。

次のように実行しましょう:

lxc network create my-network

するとネットワークがすべてのノード上でインスタンス化されます。特定のノードで定義していない場合、もしくはノードがダウンしている場合は、エラーが返ります。

この最後の network create コマンドには、ノード固有ではない(上記参照)任意の設定項目を与えることができます。

分離した REST API とクラスタネットワーク#

クライアントの REST API エンドポイントとクラスタ内のノード間の内部的なトラフィック (例えば REST API に DNS ラウンドロビンとともに仮想 IP アドレスを使うために) で別のネットワークを設定できます。

このためには、クラスタの最初のノードを cluster.https_address 設定キーを 使ってブートストラップする必要があります。例えば以下の定義ファイルを使うと

config:
  core.trust_password: sekret
  core.https_address: my.lxd.cluster:8443
  cluster.https_address: 10.55.60.171:8443
...

(YAML 定義ファイルの残りは上記と同じ)。

新しいノードを参加させるには、まず REST API のアドレスを設定します。 例えば lxc クライアントを使って以下のように実行し

lxc config set core.https_address my.lxd.cluster:8443

そして通常通り PUT /1.0/cluster API エンドポイントを使って、 server_address フィールドで参加するノードのアドレスを設定します。 定義ファイルを使うなら YAML のペイロードは完全に上記のものと同じに なるでしょう。

クラスタ証明書の更新#

LXD のクラスタ内の全てのサーバは同じ共有された証明書で応答します。 これは通常は有効期限が10年の標準的な自己署名証明書です。

何か他のもの、例えば Let's Encrypt で取得した有効な証明書、に置き換えたい場合は lxc cluster update-certificate を使ってクラスタ内の全てのサーバの証明書を置き換えることが出来ます。

クラスタグループ#

LXD のクラスタでは、メンバーはクラスタグループに追加できます。デフォルトでは全てのメンバーは default グループに属します。

クラスタメンバーは lxc cluster group assign コマンドを使ってグループに割り当てられます。

lxc cluster group create gpu
lxc cluster group assign cluster:node1 gpu

クラスタグループがあると、個別のメンバーの代わりに特定のグループをターゲットにできます。 これは --target を使うときに @ の接頭辞を使うことで実現できます。

lxc launch ubuntu/22.04 cluster:ubuntu --target=@gpu

もし scheduler.instanceall (デフォルト) か group に設定されていれば、上のコマンドにより gpu グループに属するクラスタメンバー上にインスタンスが作成されます。