エンジニアの大坪です。今回は Go でサーバーサイドアプリケーションを開発しているときに使っている Tips の一つを紹介しようと思います。
計測することの重要性
サーバーサイドアプリケーションの開発において重要なことの一つに速度を正しく計測することがあります。アプリケーションの速度が十分でないときに速度の向上を図る場合、ボトルネックの特定が重要です。また、突発的にレイテンシが上がってしまったときの調査などにも耐えられるように継続的に計測していくことが求められます。
New Relic による監視
Wantedly では速度の計測に様々なサービスやツールを用いていますが、最も広く使われているものの一つに New Relic があります。New Relic では、1つのアプリケーションの中でどのような部分の処理に多くの時間を割いているのかを知ることができます。特に障害が発生したときにどこが根本の原因なのかを探るのに便利です。例えば上のグラフで Web external
だけが大きくなっていたら、「依存する microservice が遅くなったことが原因かな」と予想を立てやすくなります。
計測セグメント
Redis / PostgreSQL / Elasticsearch へのリクエストなどについては、特に特殊な設定をすることなく簡単に取得ができるように公式を含め様々な人がライブラリを作ってくれています。しかし自分で実装する部分、(上記のようないわゆる低レイヤーのミドルウェアのライブラリの外) は Ruby
や Go
というような言語名にまとめられてしまいます。つまり自分で実装したコードの中でどこがボトルネックなのかを New Relic から知ることはできないのです。自分の場合は、ある程度計算量のあるリッチな機能をもつ redis クライアントを作成しましたがこれの計算が負荷が高いときにもボトルネックなっていないかを知る必要があると考えました。
自分で作った実装の中の様子を継続的に追っていくてためには下のように計測用のコードを差し込んであげる必要があります。自分の注目したい処理の始まったタイミングと終わったタイミングにフックを挟んでいます。
// newrelic package の諸セットアップが済んでいる場合
func someTask(ctx context.Context) {
nrtx := newrelic.FromContext(ctx)
defer newrelic.StartSegment(nrtx, "someTask").End()
....
}
これで一旦は解決できますが、これを気になる場所の全てに網羅的に差し込んでいくのはなかなか大変ですし、struct
に一つ func
を増やしたときにその func
に必ず上記のようなコードを差し込んでいることを確認するのも大変になります。また簡単に計測サービスへのロックインを生んでしまうので所構わず使うことは難しいでしょう。
自動で生やす
アプリケーションが適切に設定さえされていれば上記のコードを差し込むのは非常に簡単なコピペ作業で、人間が行う必要はありません。ここで自分が思い出したのは gomock
でした。gomock
は任意の interface
を入力にとり、その interface
を満たした実装を作成します。この仕組と同様の方法で「任意の interface を受け取ってそれに一定の機能を付加した実装を返す
」という作業は原理的には難しくないことがわかります。
この欲求を満たすためのツールがないか探してみると、github.com/hexdigest/gowrap が見つかりました。これを今回の目的のために使うとしたのようになります。
New Relic 用の template を作る
下のような template を newrelic.template.txt
という名前で保存します
import (
newrelic "github.com/newrelic/go-agent"
)
{{ $decorator := (or .Vars.DecoratorName (printf "%sWithNewRelic" .Interface.Name)) }}
{{ $implName := (printf "impl%s" $decorator) }}
{{ $segmentName := .Interface.Name }}
// {{$implName}} implements {{.Interface.Type}} that is instrumented with NewRelic logging
type {{$implName}} struct {
_base {{.Interface.Type}}
}
// New{{$decorator}} instruments an implementation of the {{.Interface.Type}} with NewRelic logging
func Wrap{{$decorator}}(base {{.Interface.Type}}) {{ .Interface.Type }} {
if base == nil {
return nil
}
return {{$implName}}{
_base: base,
}
}
{{range $method := .Interface.Methods}}
// {{$method.Name}} implements {{$.Interface.Type}}
func (_d {{$implName}}) {{$method.Declaration}} {
{{- if $method.AcceptsContext}}
nrtx := newrelic.FromContext(ctx)
defer newrelic.StartSegment(nrtx, "{{ $segmentName }}/{{ $method.Name }}").End()
{{end -}}
{{ $method.Pass "_d._base." }}
}
{{end}}
Interface を用意
今回は下のようなシンプルな interface
を想定します。これを interface.go
として保存します。
package main
import "context"
type Interface interface {
Foo(ctx context.Context) int
Bar(ctx context.Context) string
}
gowrap を呼び出す
$ gowrap gen -i Interface -o ./wrapper.go -p ./ -t newrelic.template.txt
上のようにコマンドを実行すると下のようなファイルが wrappers.go
に吐かれます。WrapInterfaceWithNewRelic
という func
に Interface
を渡せばどの部分も New Relic で追えるようになります。
package main
// DO NOT EDIT!
// This code is generated with http://github.com/hexdigest/gowrap tool
// using newrelic.template.txt template
//go:generate gowrap gen -p github.com/potsbo/gowrapdemo -i Interface -t newrelic.template.txt -o wrapper.go
import (
"context"
newrelic "github.com/newrelic/go-agent"
)
// implInterfaceWithNewRelic implements Interface that is instrumented with NewRelic logging
type implInterfaceWithNewRelic struct {
_base Interface
}
// NewInterfaceWithNewRelic instruments an implementation of the Interface with NewRelic logging
func WrapInterfaceWithNewRelic(base Interface) Interface {
if base == nil {
return nil
}
return implInterfaceWithNewRelic{
_base: base,
}
}
// Bar implements Interface
func (_d implInterfaceWithNewRelic) Bar(ctx context.Context) (s1 string) {
nrtx := newrelic.FromContext(ctx)
defer newrelic.StartSegment(nrtx, "Interface/Bar").End()
return _d._base.Bar(ctx)
}
// Foo implements Interface
func (_d implInterfaceWithNewRelic) Foo(ctx context.Context) (i1 int) {
nrtx := newrelic.FromContext(ctx)
defer newrelic.StartSegment(nrtx, "Interface/Foo").End()
return _d._base.Foo(ctx)
}
今回このような検証を挟んで、自分が実装した部分は全体に与える影響は小さいものであると確認できました。まだ開発段階で interface も固まっていない段階でしたが、実装が変わってもその変更のための監視のコードを書き直す労力を使う必要がなかったため試行錯誤を繰り返すことができました。
最後に
今回は自分で template を作成しましたが、gowrap
の example を参照すると retry
や fallback
などの便利な template
が多数あるので template
をそのまま利用するだけでも十分に生産性を上げてくれるだろうと思います。
ここで紹介した方法の強い点はロジック部分のあるコードと計測などの運用のためのコードを分離できる点にあると考えています。仮に他の計測サービスに移る場合や、他のフックをはさみたい場合でも簡単に対応できます。自分たちが継続的に変更を加えたい部分のみに集中できる環境をつくるのは一つのデプロイを行うまでの速度とデプロイの安全性のどちらにも良い効果をもたらすと考えます。