分散システム向けのフォールトインジェクションフレームワーク 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については、関数合成可能な実装が公開されている。
うまく組み合わせると実装の手間を省けるだろう。