🔍

pprofについて

2021/02/05に公開

TL;DR

pprofについて改めてまとめてみました。
関連パッケージが複数あったりして、初学者が導入しようとすると割とハマるかと思いまして記載しました。
この記事ではGoのWebアプリケーションをpprofでプロファイリングする場合について説明します。

pprofとは

Goのプロファイリングツールで添付のようにCPU負荷や処理時間、メモリ使用量などをブラウザで表示してくれます。

Graph表示

graph

Flame Graph表示

Flame Graph表示も存在します。

flame_graph

TOP表示

負荷がかかったfunction, method情報をリスト表示も可能です。
top_viewing

pprofの仕組み

  1. client側でpprofの開始・取得コマンドを実行
  2. server側のpprof用のhandlerが実行されてserver側にてプロファイリングを開始
  3. server側にて一定時間経過後(profileの場合はデフォルト30s)プロファイル結果を.pd.gzファイルに書き込みレスポンスにて返す
  4. ダウンロードした.pd.gzファイルをブラウザにて表示

overview

pprofのGraphの見方

graph_explanation

  • ノードの色:

    • 大きな正の累積値は赤
    • 大きな負の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されいます。
https://github.com/golang/go/blob/53094ac844c48b0574d5374348cc09d7649104c3/src/net/http/server.go#L2506
つまりhttp.DefaultServeMuxを使用してhttpをListenしていれば初期化動作だけでpprofを使用できます。

ただし、gorilla/muxはhttp.DefaultServeMuxではなく独自のhttp Handlerを使用している場合はProfilingはできないのであります。

そのような場合は下記のいずれかの対応が必要です。

  1. http.DefaultServeMuxでHandling済の情報をわたしてあげる
  2. 自前で 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)

その他のケースは下記を参照してください。
https://github.com/muroon/pprof_sample

最後に

モニタリングツールと比較して

NewRelic、DatadogのAPMやその他のOpent Trace系のモニタリングツールなどと比較してみると下記のようなことが言えるかと思います。プロファイリングとモニタリングではそもそもできることが違うのですが。。

  • 長所

    • 手軽な設定で全実行functionを調査してくれる
      • 上記のようにハマることがあるが、それでも設定が手軽な方
        • これがOpenTraceでfuncレベルの性能を計測しようとすると
          親Span、子Span、さらにその子孫のSpanを各functionに設定が必要(全funcitonに仕込むのは事実上不可能)
    • 変更点の差分比較がしやすい
  • 短所

    • DBなどの外部リソースの性能調査はできない
    • 外部APIとの連携した性能調査はできない

Discussion