分散システム向けのフォールトインジェクションフレームワーク Jepsen
January 26, 2025概要
分散システムのテストをClojureで実装するためのフレームワークにJepsenがある。 Jepsenのテストケースは、システムへの操作、操作後のシステムに期待する状態、障害と操作のスケジューリングの3要素からなる。 テストケースは、スケジューリング通りに操作と障害を起こし、最後にシステムの応答が期待通りか確認する。 Jepsenの公式サイトにはJepsenによる主要な分散システムのテスト結果が公開されている。 また、Kubernetes内部で使わているKVSのetcdの公式ウェブサイトにもJepsenのテスト結果が公開されている。 分散システムの開発側からも参照されているので、分散システムにおけるJepsenの認知度は高いように思える。
その一方で、公式ドキュメントは整備されていないようだった。 たとえば、チュートリアルはetcdを例題にJepsenの使い方を解説しているが、etcdのクライアントライブラリが古く、実際に実装してもチュートリアル通りに実行できない。 かわりに、2相コミットするアプリケーションとテストケースを実装したが、実装のためにJepsenのソースコードを調べる必要があった。 実装したアプリケーションとテストケースはGitHubのリポジトリに公開してある。
それでも、依然として、Jepsenを使ってテストケース実装するほうが、スクラッチからテストケースを実装するよりも効率的ではあると思う。 そこで、今後の備忘録をかねて、Jepsenを試すにあたって、チュートリアルに不足を感じた部分を書き留めておく。
テストケース
Jepsenのテストケースは、jepsen.core/run!
関数が受けとれるマップであり、マップの定義はjepsen.core/run!
関数のドキュメントにある。
名前の通りjepsen.core/run!
関数はテストケースを実行する。
jepsen.core/run!
は、チュートリアルにあるjepsen.cli/run!
と違い、呼び出し後にテストケースのプロセスを終了しない。
REPLでテストケースを試すならjepsen.core/run!
を使うといいだろう。
:nodes A sequence of string node names involved in the test
:concurrency (optional) How many processes to run concurrently
:ssh SSH credential information: a map containing...
:username The username to connect with (root)
:password The password to use
:sudo-password The password to use for sudo, if needed
:port SSH listening port (22)
:private-key-path A path to an SSH identity file (~/.ssh/id_rsa)
:strict-host-key-checking Whether or not to verify host keys
:logging Logging options; see jepsen.store/start-logging!
:os The operating system; given by the OS protocol
:db The database to configure: given by the DB protocol
:remote The remote to use for control actions. Try, for example,
(jepsen.control.sshj/remote).
:client A client for the database
:nemesis A client for failures
:generator A generator of operations to apply to the DB
:checker Verifies that the history is valid
:log-files A list of paths to logfiles/dirs which should be captured at
the end of the test.
:nonserializable-keys A collection of top-level keys in the test which
shouldn't be serialized to disk.
:leave-db-running? Whether to leave the DB running at the end of the test.
Jepsen automatically adds some additional keys during the run
:start-time When the test began
:history The operations the clients and nemesis performed
:results The results from the checker, once the test is completed
In addition, tests have some fields added by Jepsen which are present during
their execution, but not persisted.
:barrier A CyclicBarrier, mainly used for synchronizing DB setup
:store State used for reading and writing data to and from disk
:sessions Connected sessions used by jepsen.control to talk to nodes
分散システムとの通信
クライアントは、:remote
で指定した方法で分散システムに接続する。
:remote
の値はプロトコルRemote
の実装であり、この実装で:nodes
で指定された分散システムのノードに接続する。
プロトコルはClojureの抽象データ構造であり、JavaやGoのインターフェースに相当する。
マップの:ssh
から推測できるように、デフォルトではSSHでノードに接続する。
:strict-host-key-checking
が有効であれば、IPにあるノードの秘密鍵が前回のテストケースから変わった場合に、接続を中断する。
コンテナ技術を使う場合などのテスト用のノードが永続的でない環境では、無効にしておく必要があるだろう。
SSH接続のための実装clj_ssh
に加え、Dockerコンテナのノードに接続するための接続するための実装DockerRemote
もある。
ところが、分散システムをDockerで、テストケースをホストで実行したときには、DockerRemote
を使えず、Remote
を実装することになった。
DockerRemote
を使うときは<ipv4>:<port>
形式でノードを指定する必要がある。
その場合、ネットワークを分断するためにコンテナ内部で実行されるiptables
の引数に<ipv4>:<port>
が使われ、ホストを解決できずにエラーになった。
ssh
のときはポートを指定しなくてよい。
ノードの初期設定
:os
の値は、OS
プロトコルの実装であり、テストに必要なリソースをノードに配置する。
たとえば、Jepsenから提供されているDebianの実装は、iptables
などのプログラムをノードにインストールする。
:os
の役割がテストケースに必要なリソースの準備にあるのに対し、:db
の値はテスト対象のシステム自体の初期化を担う。
:db
の値も、これまでに紹介したエントリと同様に、プロトコルの実装であり、DB
にプロトコルの宣言がある。
システムへの操作
テスト対象に対する操作のスケジューリングは:generator
、システムへの操作は:client
, システムへの障害は:nemesis
の値で定義される。
それぞれ、プロトコルGenerator, Client
, Nemesis
の実装であり、:generator
の定めるスケジュール通りに:client
, :nemesis
の関数が呼び出される。
実行結果の確認
最後に:checker
の値になるChecker
の実装で実行結果を確認する。
実装は実行履歴を受けとり、:valid?
キーの真偽値でテストの妥当性を示すマップを返す。
プロトコルだけでなく、実装もJepsenから提供されている。
とくにNemesis
, Generator
, Checker
については、関数合成可能な実装が公開されている。
うまく組み合わせると実装の手間を省けるだろう。