ETCD を探る
この記事を書くきっかけは、Kubernetes 関連の知識を整理していたときに、etcd という名前に何度も出くわしたことでした。クラスタの状態はその中に保存され、サービスディスカバリもそれに依存し、少なくない設定センターの基盤も etcd です。ブラックボックスとして扱うより、単独で取り出して一度構築し、整理してみたほうがよいと考えました。分散システムでは、次のような問題によく遭遇します。
- サービスノード間で設定を共有する必要がある
- システムにサービスディスカバリが必要
- 分散ロックには協調のためのセンターが必要
- クラスタには整合性のある状態ストレージが必要
これらの問題は、本質的にはどれも 信頼できる分散協調システム を必要としています。
単一マシンの時代なら、これらのニーズはデータベースのテーブル一つ、あるいはファイル一つで解決できました。しかしノードが増えると、問題はこう変わります。複数のレプリカの間で「現在の状態」についてどう合意するのか?誰の言い分が正なのか?あるノードが落ちた後、データはまだ信頼できるのか?これこそが分散協調システムが答えるべき問いです。
そして現代のクラウドネイティブ体系で最もよく使われるコンポーネントが etcd です。
例えば:
- Kubernetes
- CoreDNS
- service mesh
- 分散設定センター
これらのシステムの基盤はいずれも etcd に依存しています。
etcd とは何か
簡単に言えば:
etcd は高信頼の分散 Key-Value データベースです。
しかし、普通のデータベースとの最大の違いは:
「分散協調」のために設計されている点です。
この位置づけがトレードオフを決めています。大量データや極限のスループットを追求するのではなく、「保存したデータの一つ一つが信頼できること」を追求します。したがって、メタデータ、設定、状態といった小さくてもクリティカルなデータの保存に適しており、業務用データベースとして使うのには向いていません。
主な特徴は次の通りです。
- 強整合性(Strong Consistency)
- 分散クラスタのサポート
- Watch による監視機構
- トランザクションのサポート
- リース(Lease)機構
これらの特性の組み合わせが実に面白いのです。
-
Watch により、クライアントは特定の key やプレフィックスの変化を購読できます。設定を一つ変えれば全ノードがリアルタイムに感知でき、ポーリングは不要です。
-
Lease は TTL 付きの「ハートビート証憑」に相当します。key はリースに紐付けることができ、クライアントが更新を止めると key は自動的に削除されます。サービス登録と障害ノードの除外はこれによって実現されています。
-
トランザクション(Txn)は compare-and-swap のセマンティクスを提供します。「この key が存在しなければ書き込む」をアトミックに完了でき、これが分散ロックの基礎です。
多くの分散システムが etcd を次の用途に使っています。
- サービスレジストリ
- 設定センター
- 分散ロック
- Leader 選出
例えば Kubernetes は クラスタ状態 全体を etcd に保存しています。kube-apiserver が etcd を直接読み書きする唯一のコンポーネントで、他のコントローラーはすべて apiserver の Watch 機構を通じて間接的に状態変化を感知します。
強整合性(Raft 合意アルゴリズム)
etcd の内部では Raft 合意アルゴリズム が使われています。
Raft の核心となる考え方は:
クラスタ全体から一つの Leader ノード が選出されます。
すべての書き込みリクエストは Leader を経由しなければなりません。
流れはおおよそ次の通りです。
クライアント → Leader → 過半数のノードへ同期 → コミット成功
そして:
半数を超えるノードが書き込みを確認したとき
にのみ、データは本当にコミットされたことになります。
ここをもう少し細かく見てみましょう。Raft では各ノードは三つの役割のいずれかにあります。Leader、Follower、Candidate です。正常運転時は Leader が一つだけ存在し、すべての Follower に周期的にハートビートを送信します。ある Follower がタイムアウト時間内にハートビートを受け取れないと、自身を Candidate に変えて選挙を開始し、過半数の票を得たノードが新しい Leader になります。
書き込みはログレプリケーション(Log Replication)で行われます。Leader はまず書き込み操作を自身のログに追記し、それからすべての Follower に並列に送信します。多数派(majority、つまり半数超)のノードがこのログを永続化した時点で、Leader はそれをコミット済みとマークし、クライアントに応答します。
「多数派」という設計こそが強整合性の鍵です。任意の二つの多数派集合は必ず交わりを持つため、たとえ Leader がクラッシュしても、新しく選出された Leader は必ずコミット済みのデータをすべて含んでおり、書き込みが失われることはありません。
これは、etcd クラスタのノード数として奇数(3、5、7)が推奨される理由の説明にもなります。
- 3 ノードなら 1 台の故障を、5 ノードなら 2 台の故障を許容できる
- 4 ノードでも許容できる故障は 1 台のみ(多数派に 3 台必要)。フォールトトレランスは 3 ノードと同じなのに、同期のオーバーヘッドが一つ増えるだけ
etcd クラスタの構築
デプロイ環境:
オペレーティングシステム:
Debian 11.5.0
1 etcd のダウンロード
GitHub から etcd のバイナリファイルをダウンロードします。
https://github.com/etcd-io/etcd/releases
release ページにはプラットフォームごとの圧縮パッケージが用意されているので、対応するアーキテクチャ(linux-amd64 など)を選んでダウンロードすれば OK です。etcd は単一の静的バイナリで、追加のランタイム依存がありません。これがデプロイの簡単さの理由の一つです。
2 etcd のインストール
解凍後、以下のファイルを次の場所に配置します。
/usr/local/bin
主なファイルは:
etcd
etcdctl
二つのファイルの役割分担は明確です。etcd はサーバープロセスで、etcdctl はコマンドラインクライアントです。以降のすべての照会、書き込み、メンテナンス操作は後者を通じて行います。/usr/local/bin に置くのは PATH に入れるためで、どのディレクトリからでも直接呼び出せるようになります。
3 実行権限の設定
# 解凍したファイルには実行ビットが付いていないことがあるので、手動で付与する
sudo chmod +x /usr/local/bin/etcd
sudo chmod +x /usr/local/bin/etcdctl
ここまでで、単一ノードのバイナリの準備が整いました。クラスタモードでは、各ノードで上記のインストール手順を繰り返した上で、起動パラメータでそれぞれの名前、リッスンアドレス、初期クラスタメンバーリストを宣言します。ノード間は 2380 ポートで相互通信し、クライアントは 2379 ポートでアクセスします。私の環境では TLS を有効にしており、証明書は /opt/etcd/ssl にまとめて置いてあるため、以下の検証コマンドにはすべて証明書のパラメータを付ける必要があります。
4 クラスタの検証
etcdctl を使ってデータを確認できます。
# クラスタ内のすべての key を列挙する(key のみ表示、value は表示しない)
# --cacert/--cert/--key:TLS 相互認証に必要な CA 証明書とクライアント証明書
# --endpoints:3 つのノードをすべて書いておくと、クライアントが自動的に負荷分散とフェイルオーバーを行う
ETCDCTL_API=3 etcdctl \
--cacert=/opt/etcd/ssl/ca.pem \
--cert=/opt/etcd/ssl/server.pem \
--key=/opt/etcd/ssl/server-key.pem \
--endpoints="https://172.24.93.151:2379,https://172.24.93.149:2379,https://172.24.93.150:2379" \
get / --prefix --keys-only
コマンドが結果を返せば、クライアントからクラスタまでの経路、証明書、多数派の状態がすべて疎通していることを意味します。--prefix はプレフィックスマッチを表し、/ と組み合わせると全 key の走査に相当します。
key の削除:
# プレフィックスで削除する。空のプレフィックス "" はすべての key にマッチする —— 本番環境では要注意
ETCDCTL_API=3 etcdctl \
--cacert=/opt/etcd/ssl/ca.pem \
--cert=/opt/etcd/ssl/server.pem \
--key=/opt/etcd/ssl/server-key.pem \
--endpoints="https://172.24.93.151:2379,https://172.24.93.149:2379,https://172.24.93.150:2379" \
del --prefix ""
del --prefix "" は全件削除です。この etcd が Kubernetes のバックエンドとして動いている場合、このコマンドはクラスタ状態全体を消去するのと同じことになります。実行前に必ず対象環境を確認してください。
なぜ分散協調には Redis より etcd が適しているのか
多くの人がこう尋ねます。
Redis でも分散ロックはできるのでは?
確かにできます。
しかし Redis は AP システム であり、etcd は CP システム です。
簡単に理解するなら:
Redis は 可用性と性能 を重視します。
etcd は 整合性と信頼性 を重視します。
ロックの場面に具体化すると、違いは障害時の挙動に現れます。Redis のマスター・スレーブレプリケーションは非同期です。マスターノードがロックを書き込んだ後、同期される前にダウンすると、スレーブがマスターに昇格した時点でロックは「消えて」しまい、二つのクライアントが同時にロックを保持する可能性があります。Redlock 方式はこの問題の緩和を試みていますが、その安全性についてはコミュニティで議論が続いています。一方 etcd の書き込みは多数派の確認を経なければ成功とならず、Leader の切り替えでコミット済みデータが失われることはないため、障害シナリオでもロックのセマンティクスは成立し続けます。
代償はもちろん性能です。一回の書き込みに多数派同期一巡とディスクへの永続化が必要で、レイテンシもスループットも Redis には遠く及びません。したがって結論は「どちらが優れているか」ではなく、場面による使い分けです。キャッシュ、カウンター、まれな失効を許容できる軽量ロックには Redis を、設定、Leader 選出、厳密な相互排他が必須のロックには etcd を使いましょう。
ハマりどころと注意点
構築と利用の過程で、記録しておく価値のあるポイントがいくつかあります。
-
環境変数
ETCDCTL_API=3を忘れないこと。古いバージョンの etcdctl はデフォルトで v2 API を使いますが、v2 と v3 のデータは完全に分離されています。API バージョンを間違えると「確かに書き込んだのに見つからない」という見かけ上の不具合が起きます。比較的新しいバージョンではデフォルトが v3 になっていますが、スクリプト内では明示的に宣言しておいて損はありません。 -
--endpointsには全ノードを書いておくことを推奨します。一つのノードだけ書いた場合、そのノードが故障するとクライアントは即座に接続を失います。全部書いておけば、クライアントは健全なノードに自動的に切り替えられます。 -
証明書のパスと SAN を一致させること。TLS を有効化した後は、証明書内の IP・ドメイン名が endpoints で実際に使うアドレスをカバーしていなければならず、そうでないとハンドシェイク段階で拒否されます。
-
ノード数は奇数を保つこと。理由は前述の Raft の部分で説明した通りです。偶数ノードはフォールトトレランスを高めず、同期コストを増やすだけです。
-
etcd はディスクレイテンシに敏感です。Raft のログはコミットのたびに fsync が必要で、ディスクが遅いと書き込みレイテンシが直接跳ね上がり、Leader 選挙の揺らぎを引き起こすことさえあります。条件が許すならできるだけ SSD を使いましょう。
まとめ
etcd は本質的に「性能を整合性と引き換えにする」典型的な設計です。Raft の多数派コミットにより、いかなる時点でも読み取ったコミット済みデータは信頼でき、Watch、Lease、トランザクションといった機構が「信頼できる状態ストレージ」をさらにサービスディスカバリ、Leader 選出、分散ロックの汎用的な土台へと押し上げています。構築自体は複雑ではありません——バイナリ二つと一組の起動パラメータだけです。本当に頭を使うべきなのは、TLS 証明書、ノード設計、そして適用範囲の見極めです。Redis との役割分担をしっかり考え、それぞれが得意とする場面で使うほうが、どちらが強いかを論争するよりずっと意義があります。
COMMENTS