Go言語向けFailure Injectionツール: gofail

こんにちは、Necoチームの池添(@zoetro)です。

gofailは、etcdの開発チームがつくったFailure Injectionのためのツールです。 Go言語で書かれたプログラム中に故意にエラーを発生させるポイント(failpoint)を埋め込み、任意のタイミングでプログラムの挙動を変えることができます。 公式ドキュメントに記載されていない項目が多々あるので、詳しい使い方を紹介したいと思います。

なお、本記事は2018年8月時点のソースコードを対象に解説しています。

gofailのインストール

gofail をインストールします。

$ go get -u github.com/etcd-io/gofail/...

failpointの埋め込み

プログラム中にfailpointを埋め込むためには、Go言語のプログラム中に gofail というキーワードから始まるコメントを記述します。 なお、ブロックコメントの場合はfailpointとして認識されないため、必ず行コメントで記述する必要があります。

コメントの1行目にはトリガーとなる変数定義を記述し、2行目以降にはfailpointを有効化した時に実行されるコードを記述することができます。 複数行の処理を埋め込みたい場合は、空行をあけずにコメントを記述します。

例えば任意の値を返すようなfailpointを埋め込みたい場合は、下記のように1行目に返す値となる変数宣言、2行目にreturnを記述します。

func someFunc() string {
    // gofail: var SomeFuncString string
    // return SomeFuncString
    return "default"
}

なお、failpointとなる変数の型は bool, int, string, struct{} のみになります。

fail発生後に任意の処理を実行する必要がない場合(panicを発生させる場合など)は、以下のように struct {} 型の変数宣言のみを記述します。

func ExampleOneLineFunc() string {
    // gofail: var ExampleOneLine struct{}
    return "abc"
}

failpointを有効化した時にgotoやラベル付きcontinue文を実行したい場合は、下記のように gofail の後ろにラベル名を記述することもできます。

func ExampleRetry() {
    // gofail: RETRY:
    for i := 0; ; i++ {
        fmt.Println(i)
        time.Sleep(time.Second)
        // gofail: var RetryLabel struct{}
        // goto RETRY
    }
}

failpointを埋め込んでビルド

failpointを埋め込んでビルドするには、まず下記のコマンドを実行します。

$ gofail enable

するとgofailから始まるコメントを記述した箇所が書き換えられ、*.fail.go というファイルが生成されます。 あとは生成されたコードを含めて通常通りにビルドします。

書き換えられたコードをもとに戻すには、以下のコマンドを実行します。

$ gofail disable

failpointの有効化

埋め込まれたfailpointは初期状態ではoffになっているので、実際にfailさせるためにはfailpointの有効化をおこなう必要があります。 failpointを有効化させる手段としては、プログラムの起動時に環境変数で指定するかHTTP APIで有効化するか2通りの方法がありますが、ここではHTTP APIを利用した方法を紹介します。

まずgofailのエンドポイントを環境変数で指定してプログラムを起動します。

GOFAIL_HTTP="127.0.0.1:1234" ./cmd

下記のAPIを叩くと、failpointの一覧を取得することができます。

$ curl http://127.0.0.1:1234/
my/package/path/SomeFuncString=
my/package/path/ExampleOneLine=
my/package/path/ExampleLabels=

failpointを有効化するためには下記のAPIを実行します。 ここでは、SomeFuncStringhelloという文字列を返すように設定しています。

$ curl http://127.0.0.1:1234/my/package/path/SomeFuncString -XPUT -d'return("hello")'

再度下記のAPIを実行すると、failpointが設定されていることが確認できます。

$ curl http://127.0.0.1:1234/
my/package/path/SomeFuncString=return("hello")
my/package/path/ExampleOneLine=
my/package/path/ExampleLabels=

この状態でsomeFunc()関数が実行されると、helloという文字列が返ってくるようになります。

failpointを無効化するには下記のAPIを実行します。

$ curl http://127.0.0.1:1234/my/package/path/SomeFuncString -XDELETE

failpointで実行可能なアクション

failpointには、下記のアクションを指定することができます。

  • off: failpointの無効化
  • return: 任意の値を返す(返すことのできる型は bool, int, string, struct{} のみ)
  • sleep: 一定時間待つ(待ち時間はDurationフォーマットで指定可能。intで指定した場合はミリ秒になる)
  • panic: panicを発生させる
  • break: デバッガによるbreakを発生させる
  • print: 標準出力に文字列を出力する

また、failを発生させる回数や確率を指定することもできます。

下記の例では、failpointが呼び出された最初の3回はhelloという文字列を返し、その後は通常の状態に戻ります。

$ curl http://127.0.0.1:1234/my/package/path/SomeFuncString -XPUT -d'3*return("hello")'

下記の例では、50%の確率でhelloを返すようになります。

$ curl http://127.0.0.1:1234/my/package/path/SomeFuncString -XPUT -d'50.0%return("hello")'

おわりに

gofailを利用すると任意のタイミングでソフトウェアの挙動を変えることができ、Failure Injection Testをおこなう際にはとても便利なので、ぜひ使ってみてください。

我々はgofailを利用した耐障害性試験を実施し、信頼性の高いインフラ基盤の構築を進めています。次回の記事ではそのあたりを詳しく紹介したいと思います。