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 の識別子問題を解消できるわけです。
cmd
は context
を利用して任意のタイミングで中断を指示できるバリア同期の仕組みを提供します。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
フレームワークを使うと、出力形式やログレベルは共通のコマンドラインオプションで変更可能になっています。
その他のパッケージ
-
ネットワーク関係のツール群です。
IsNoRouteToHost
のようなネットワークエラーの判別関数などがあります。 -
シンプルなモニタリングツールです。 なにかあれば、なにかするというルールを設定ファイルや REST API やコマンドラインでさくさくと定義できるようになっています。
github.com/cybozu-go/transocks
redsocks のような透過 SOCKS プロキシを実現するツールです。 IPv4/v6 両対応で、SOCKS 以外に HTTP プロキシ(CONNECT) にも対応しています。
-
SOCKS4/4a/5 対応の SOCKS サーバーです。 外向け通信に使う IP アドレスを動的に制御する機能を備えているのが特長です。 アクセスログもばっちり出るので、セキュリティ監査にはもってこいです。
-
apt-mirror や apt-cacher-ng といったツールを置き換えるものです。 以前の紹介記事をご覧ください。
まとめ
いい感じの Go プログラムを作れる github.com/cybozu-go/cmd
と仲間たちを紹介しました。使ってみて良かったらぜひブログなどでご感想をお寄せください!