pprofについて
TL;DR
pprofについて改めてまとめてみました。
関連パッケージが複数あったりして、初学者が導入しようとすると割とハマるかと思いまして記載しました。
この記事ではGoのWebアプリケーションをpprofでプロファイリングする場合について説明します。
pprofとは
Goのプロファイリングツールで添付のようにCPU負荷や処理時間、メモリ使用量などをブラウザで表示してくれます。
Graph表示
Flame Graph表示
Flame Graph表示も存在します。
TOP表示
負荷がかかったfunction, method情報をリスト表示も可能です。
pprofの仕組み
- client側でpprofの開始・取得コマンドを実行
- server側のpprof用のhandlerが実行されてserver側にてプロファイリングを開始
- server側にて一定時間経過後(profileの場合はデフォルト30s)プロファイル結果を.pd.gzファイルに書き込みレスポンスにて返す
- ダウンロードした.pd.gzファイルをブラウザにて表示
pprofのGraphの見方
-
ノードの色:
- 大きな正の累積値は赤
- 大きな負のcum値は緑色
- ゼロに近いcum値は灰色
-
ノードのフォントサイズ:
- フォントサイズが大きいほど、絶対フラット値が大きい
- フォントサイズが小さいほど、絶対フラット値が小さい
-
エッジの厚み:
- エッジが厚いほど、そのパスに沿って使用されたリソースが多い
- エッジが薄いほど、そのパスに沿って使用されたリソースが少ない
-
エッジカラー:
- 大きな正の値は赤
- 大きな負の値は緑色
- ゼロに近い値は灰色
-
エッジの種類
- ソリッドエッジ:一方の場所がもう一方の場所を直接呼び出す
- 破線のエッジ:接続された2つの場所の間のいくつかの場所が削除された
pprofのrepositoryとpackage
repository
package
-
cmd/pprof
- pprofコマンドを担っている(内部でbaseのpprofを呼んでいる)
-
runtime/pprof
- Goプロセスのプロファイリングを担う
-
http/pprof
- ServerSide側のプロファイリングを担う(内部でruntime/pprofを呼んでいる)
pprofの使い方
厳密には他のやり方もありますが
1. net/http/pprofをimport(もしくは初期化のみのimport)
import(
_ "net/http/pprof"
)
※次項で説明しますが、この設定だけではpporfが使用できない場合があります。
2. pprofコマンド実行
go tool pprof -http=":22222" http://localhost:8080/debug/pprof/profile
ServerSideアプリケーションにpprofを仕込むときの注意点
アプリケーションに下記を仕込めば動くと書いてある記載を見かけます。
import(
_ "net/http/pprof"
)
ですが、使用しているrouterによってはそのままでは使用できない場合があります。
使用可能
1行追加しただけでpprofが使用可能
import (
"net"
"net/http"
_ "net/http/pprof" // 追加
)
func main() {
http.HandleFunc("/", hello())
err = http.ListenAndServe(":8080", nil)
使用不可能
1行追加しただけでpprofが使用不可能
import (
"github.com/gorilla/mux"
"net"
"net/http"
_ "net/http/pprof" // 追加
)
func main() {
r := mux.NewRouter() // gorilla/muxを使用
r.HandleFunc("/", hello())
lin, err := net.Listen("tcp", ":8080")
if err != nil {
panic(err)
}
defer lin.Close()
s := new(http.Server)
s.Handler = r
s.Serve(lin)
net/http/pprofパッケージの初期化時に何をしているか見ます。
下記のroutingを行っています。
先程のpackageの初期化にて下記が実行されています。
func init() {
http.HandleFunc("/debug/pprof/", Index)
http.HandleFunc("/debug/pprof/cmdline", Cmdline)
http.HandleFunc("/debug/pprof/profile", Profile)
http.HandleFunc("/debug/pprof/symbol", Symbol)
http.HandleFunc("/debug/pprof/trace", Trace)
}
pprofを使用するには上記の/debug/pprof以下のurlがhandlingされている必要があります。
結果的にhttp.DefaultServeMuxにHandleFuncされいます。
つまりhttp.DefaultServeMuxを使用してhttpをListenしていれば初期化動作だけでpprofを使用できます。ただし、gorilla/muxはhttp.DefaultServeMuxではなく独自のhttp Handlerを使用している場合はProfilingはできないのであります。
そのような場合は下記のいずれかの対応が必要です。
- http.DefaultServeMuxでHandling済の情報をわたしてあげる
- 自前で HandleFuncする
1. http.DefaultServeMuxでHandling済の情報をわたしてあげる
import (
"github.com/gorilla/mux"
"net"
"net/http"
_ "net/http/pprof" // 追加
)
func main() {
r := mux.NewRouter()
r.PathPrefix("/debug/pprof/").Handler(http.DefaultServeMux) // 追加
r.HandleFunc("/", hello())
lin, err := net.Listen("tcp", ":8080")
if err != nil {
panic(err)
}
defer lin.Close()
s := new(http.Server)
s.Handler = r
s.Serve(lin)
2. 自前で HandleFuncする
import (
"github.com/gorilla/mux"
"net"
"net/http"
"net/http/pprof" // 追加(importしているだけ)
)
func main() {
r := mux.NewRouter()
r.HandleFunc("/debug/pprof/", pprof.Index) // 追加
r.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) // 追加
r.HandleFunc("/debug/pprof/profile", pprof.Profile) // 追加
r.HandleFunc("/debug/pprof/symbol", pprof.Symbol) // 追加
r.HandleFunc("/debug/pprof/heap", pprof.Handler("heap").ServeHTTP) // 追加
r.HandleFunc("/", hello())
lin, err := net.Listen("tcp", ":8080")
if err != nil {
panic(err)
}
defer lin.Close()
s := new(http.Server)
s.Handler = r
s.Serve(lin)
その他のケースは下記を参照してください。
最後に
モニタリングツールと比較して
NewRelic、DatadogのAPMやその他のOpent Trace系のモニタリングツールなどと比較してみると下記のようなことが言えるかと思います。プロファイリングとモニタリングではそもそもできることが違うのですが。。
-
長所
- 手軽な設定で全実行functionを調査してくれる
-
上記のようにハマることがあるが、それでも設定が手軽な方
- これがOpenTraceでfuncレベルの性能を計測しようとすると
親Span、子Span、さらにその子孫のSpanを各functionに設定が必要(全funcitonに仕込むのは事実上不可能)
- これがOpenTraceでfuncレベルの性能を計測しようとすると
-
上記のようにハマることがあるが、それでも設定が手軽な方
- 変更点の差分比較がしやすい
- pprofコマンドのbaseオプションを使用してプロファイルの比較表示もしてくれる
- 手軽な設定で全実行functionを調査してくれる
-
短所
- DBなどの外部リソースの性能調査はできない
- 外部APIとの連携した性能調査はできない
Discussion