読者です 読者をやめる 読者になる 読者になる

Go でいい感じのコマンドを作れるツールキットの紹介

SRE@ymmt2005 です。最近は systemd が好物です。

今回は GitHub でサイボウズが公開している Go 言語のプロジェクト群、特にいい感じのコマンドを作れる github.com/cybozu-go/cmd について紹介します。

SRE チームでは最近 Go でツールを開発する機会が多くなっています。最初のうちは決まった作り方をしていなかったため、コマンドごとに仕様がばらばらで、以下のような問題がでてきました。

  • REST API サーバーのアクセスログを記録しないコマンドがある
  • 外部コマンド実行時のログを記録しないコマンドがある
  • SIGTERM 等シグナル処理の方法がばらばら
  • ログファイルのリオープンができずローテートしにくい
  • ログメッセージの形式がばらばら

大雑把にいうと、ログとシグナル処理がきちんとしてないと扱いにくいわけです。 具体的にどうなっていると「いい感じ」か書き連ねてみると:

  • SIGINT/SIGTERM で実行中の処理を終えてから止まる(graceful stop)
  • SIGHUP で設定再読み込みないし実行中の処理を終えてから再起動(graceful restart)
  • SIGUSR1 でログファイルをリオープン
  • 統一された形式の機械処理可能なログ出力
  • HTTP アクセスの結果はログに出力
  • 外部コマンド実行結果はログに出力
  • ログレベルを調節可能
  • 一連の処理を追える識別子がログに含まれる

な感じと思うのですがいかがでしょう。

最後の識別子については分かりにくいので補足すると、Go は goroutine というプロセスでもスレッドでもない仕組みで並列に処理が走るので、goroutine ごとに識別子がないと一連の処理が追えないのです。が、実は goroutine には ID というものがないので、どうするかという問題です。

結論から言うと、github.com/cybozu-go/cmd (以後 cmd) を使うと上に挙げた仕様を満たすコマンドがさくっと作れます。サイボウズが公開している Go 言語製プロダクトは全て cmd に対応済みです。

cmd の仕組み

cmd は Go 1.7 で標準パッケージとなった context を中心に組み立てられたツールキットです。context を使うと、以下のような問題にうまく対処できます。

  • SIGTERM とタイムアウト処理のどちらか先に来たほうで処理を中断
  • リクエストごとに異なる値を関数コール間で引き継ぎ

うまく使えば graceful stop や goroutine の識別子問題を解消できるわけです。

cmdcontext を利用して任意のタイミングで中断を指示できるバリア同期の仕組みを提供します。cmd.Environment がそれで、Cancel(err error) メソッドを呼び出すことで任意のタイミングでコンテキストをキャンセルし、実行中の goroutine が戻ってくるのを待機するようになっています。

import (
    "context"
    "github.com/cybozu-go/cmd"
    "github.com/cybozu-go/log"
)

func main() {
    env := cmd.NewEnvironment(context.Background())

    // goroutine で関数を呼び出し
    // non-nil な error を返すとすぐ Cancel が呼び出される
    env.Go(func(ctx context.Context) error {
        for {
            select {
            case <-ctx.Done():
                // キャンセルされたら中断
                return nil
            default:
            }
            doSomething()
        }
    })

    //env.Go()...
    env.Stop()

    // バリア同期
    err := env.Wait()
    if err != nil {
        log.ErrorExit(err)
    }
}

cmd フレームワークは大域的な cmd.Environment を用意して、SIGINT/SIGTERM が飛んでくると大域 Environment をキャンセルするシグナルハンドラを設定します。大域 Environment が提供する context にぶら下がることで、プログラム全体の goroutine が graceful に制御できるというわけです。

さらに、cmd は graceful stop が可能なネットワークサーバーを作る機能を提供しています。例えば HTTPServer は標準パッケージの http.Server をラップして、アクセスログの出力と graceful stop する機能を追加します。

func main() {
    s := &cmd.HTTPServer{
        // 標準の http.Server をくるむだけ
        Server: &http.Server{
            Handler: http.FileServer(http.Dir("/path/to/files")),
        },
    }

    // 標準の ListenAndServe と違い内部で Go() を使いすぐ戻ってくる
    err := s.ListenAndServe()
    if err != nil {
        log.ErrorExit(err)
    }

    // SIGINT/SIGTERM がきたら graceful stop する
    err = cmd.Wait()

    // cmd.IsSignaled でシグナル受信したか判別できる
    if err != nil && !cmd.IsSignaled(err) {
        log.ErrorExit(err)
    }
}

cmd.HTTPServer はそれ以外に、ハンドラに渡す http.Request に一意な識別子(リクエストID)を持たせた context を付けています。

ほかにも一杯あるのですが、詳しい話はチュートリアルを用意したのでそちらでご確認ください。あと cmd.HTTPServer の実装テクニックは個人ブログ(英語)で解説しているので、興味があればご覧ください。

log の仕組み

上記の例でログについては説明を省いていましたが、ログは github.com/cybozu-go/log (以後 log) という別パッケージで実装した機能を使っています。

log は俗にいう属性ログという機能を提供しています。属性ログは、key-value 形式で任意の属性を構造化して記録できる仕組みのものです。こんな感じ:

log.Error("message", map[string]interface{}{
    "field1": []int{1, 2, 3},
    "field2": time.Now(),
})

出力形式として、人間にも読み易い syslog に似た形式(plain)と、logfmt という形式に加え、JSON Lines をサポートしています。cmd フレームワークを使うと、出力形式やログレベルは共通のコマンドラインオプションで変更可能になっています。

その他のパッケージ

  • github.com/cybozu-go/netutil

    ネットワーク関係のツール群です。 IsNoRouteToHost のようなネットワークエラーの判別関数などがあります。

  • github.com/cybozu-go/goma

    シンプルなモニタリングツールです。 なにかあれば、なにかするというルールを設定ファイルや REST API やコマンドラインでさくさくと定義できるようになっています。

  • github.com/cybozu-go/transocks

    redsocks のような透過 SOCKS プロキシを実現するツールです。 IPv4/v6 両対応で、SOCKS 以外に HTTP プロキシ(CONNECT) にも対応しています。

  • github.com/cybozu-go/usocksd

    SOCKS4/4a/5 対応の SOCKS サーバーです。 外向け通信に使う IP アドレスを動的に制御する機能を備えているのが特長です。 アクセスログもばっちり出るので、セキュリティ監査にはもってこいです。

  • github.com/cybozu-go/aptutil

    apt-mirror や apt-cacher-ng といったツールを置き換えるものです。 以前の紹介記事をご覧ください。

まとめ

いい感じの Go プログラムを作れる github.com/cybozu-go/cmd と仲間たちを紹介しました。使ってみて良かったらぜひブログなどでご感想をお寄せください!