こんにちは、サーバーエンジニアの牧野です。 今回はGoで開発しているアプリケーションでContinuous Profilingを実践するために導入した profefe を紹介したいと思います。
Continuous Profilingとは
Continuous Profilingとは、ざっくり言うと本番環境で継続的にプロファイリングすることを指します。Continuous Profilingができると、本番環境でのみ発生するパフォーマンスの問題を捉えることができたり、継続的にプロファイリングすることで問題が発生する前後の状態を比較することができます。
Goには pprof
というプロファイリングのための標準パッケージがあり、プロファイリング自体は容易に行うことができますが、Continuous Profilingを実現するとなると、以下のような課題と向き合う必要があります。
- 本番環境でオーバーヘッドが少なく安全にプロファイリングを実行できるか
- どこにプロファイリング結果を保存するか
- 保存したプロファイリング結果をどのようにして検索・抽出するか
今回はこれらの課題を解決するために、profefe というOSSを導入しました。
Continuous Profilingを支援するサービスとして、Cloud Profilerや Datadog Continuous Profilerといったサービスがありますが、データの保持期間に上限があったりするので、より柔軟な運用をしたいとなるとprofefeのようなOSSが選択肢に入ってくるかと思います。
profefeについて
profefeは、CollectorとAgentという2つのコンポーネントから構成されています。
Collector
Collectorはプロファイルを受け取るサーバーです。Docker Imageが提供されているので、以下のコマンドで起動することができます。
$ docker run -d -p 10100:10100 profefe/profefe
以下のようにPOSTメソッドでプロファイルを送ると、profefeがプロファイルを保存します。
$ curl -X POST \ "http://localhost:10100/api/0/profiles?service=<service>&type=cpu" \ --data-binary @pprof.profefe.samples.cpu.001.pb.gz
プロファイルはpprofのフォーマットに従ってさえさえいれば良く、Go以外の言語でも使用することができます。
プロファイルを検索・抽出するためのAPIが提供されており、たとえば特定の期間のプロファイルをマージした結果を参照することができます。
$ go tool pprof \ 'http://localhost:10100/api/0/profiles/merge?service=<service>&type=<type>&from=<created_from>&to=<created_to>'
また、プロファイル保存時にlabelを指定することができ、プロファイルを検索するときの条件に指定することができます。例えば、labelにアプリケーションのversionを加えて、go tool pprof
のbase
オプションを利用してversion間のプロファイル差分を見ることができます。
$ go tool pprof \ -base 'http://localhost:10100/api/0/profiles/merge?service=<service>&type=<type>&from=<created_from>&to=<created_to>&version=0.0.1' \ 'http://localhost:10100/api/0/profiles/merge?service=<service>&type=<type>&from=<created_from>&to=<created_to>&version=0.0.2'
プロファイルを保存するストレージは差し替えが可能になっており、Badger DB・AWS S3・Clickhouse DBを保存先として指定することができます。
ただ、今回はプロファイルをGoogle Cloud Storage(GCS)に保存したかったので、GCSをprofefeのストレージとして扱う実装を行いました。
profefeのストレージは以下のinterfaceを満たせばよく、integration testも用意されているので、さくっと実装することができました。手持ちの技術スタックに合わせて、ストレージを差し替えることができるのもprofefeの良い点だと思います。
type Storage interface { Writer Reader } type Writer interface { WriteProfile(ctx context.Context, params *WriteProfileParams, r io.Reader) (profile.Meta, error) } type Reader interface { FindProfiles(ctx context.Context, params *FindProfilesParams) ([]profile.Meta, error) FindProfileIDs(ctx context.Context, params *FindProfilesParams) ([]profile.ID, error) ListProfiles(ctx context.Context, pid []profile.ID) (ProfileList, error) ListServices(ctx context.Context) ([]string, error) }
今回の実装はPRとして送っています。現時点ではまだマージされていませんが、"the change looks good to me."というコメントをいただいているので、そのうちマージされるかと思います。 マージされました 🎉
github.com
Agent
Agentは以下のようにアプリケーションに組み込んで使用します。
import "github.com/profefe/profefe/agent" func main() { _, err := agent.Start("<profefe-url>", "<service-name>") ... }
Agentはgoroutineを起動し、定期的にプロファイリングを実行し、Collectorに送信します。プロファイリングにはruntime/pprof
パッケージが使われています。
デフォルトでは1分おきに10秒間プロファイリングを実行します。Diagnostics - The Go Programming Language にもある通り、pprofは本番環境でも安全に実行できるとのことですが、オーバーヘッドはゼロではないので、プロファイリングの実行時間・間隔を調整して許容できる範囲を探ると良いと思います。ちなみにミラティブではデフォルトの設定のまま使用していますが、profefeの導入前後でCPU使用率に大きな変化はありませんでした。
また、異なるインスタンスで同時にプロファイリングが実行されてシステム全体の性能が劣化することがないように、ランダムにsleepを入れることで、インスタンス間でプロファイリングの実行タイミングを分散するような工夫がされていたりします。
このAgentは必ずしもアプリケーションに組み込む必要はありません。アプリケーションがnet/http/pprof
を組み込んでいれば、cronなどで定期的にプロファイリングを実行してCollecterに送信する、といった使い方をすることも可能です。
おわりに
ミラティブでは本番環境にprofefeを導入して数週間経過しましたが、特に問題なく使うことができています。 profefeを導入してContinuous Profilingの基盤は整備できましたが、実運用としてどのように実践していくかはまだ固まっておらず、これから模索していくところです。今後知見が溜まってきましたら、またテックブログにて共有できればと思っております。
Continuous Profilingに関しては、GoogleのデータセンターのContinuous Profiling基盤に関する論文があるので、興味を持った方は読んでみると楽しめるかと思います。
Google-Wide Profiling: A Continuous Profiling Infrastructure for Data Centers – Google Research
また、profefeの作者の方がprofefeを作った背景をブログ記事に書いているので、こちらもオススメです。
Continuous Profiling and Go. There are lots of hidden details we… | by Vladimir Varankin | Medium
We are hiring!
ミラティブではサーバーエンジニアを募集中です!
- Goで大規模サービスの開発をしたい
- サーバーシステムの基盤の整備をしたい
- ゲーム×ライブ配信サービスの開発をしたい
といった方のご応募をお待ちしております!