1 of 49

ISUCONの為のpprof

Go Conference ‘19 Summer in Fukuoka

🍉🌴🏄‍♂️ GoConference’19 🏄‍♂️🌴🍉

2 of 49

Fukuoka Gopher @seike460�

所属:株式会社Fusic �名前:清家史郎 (せいけしろう) �技術:Go、PHP、AWS

カンファレンス運営

  • PHPカンファレンス福岡2019
  • Go Conference‘19 Summer in Fukuoka
  • ServerlessDays 2019 Tokyo/Fukuoka

2

🍉🌴🏄‍♂️ GoConference’19 🏄‍♂️🌴🍉

3 of 49

  • ServerlessDays 2019 Tokyo
    • 2019年10月21日 ワークショップ@DMMラボ
    • 2019年10月22日 カンファレンスデイ@Tabloid
      • papercall 是非ご応募ください!
  • ServerlessDays 2019 Fukuoka
    • 2019年12月13日 ワークショップ@会場調整中
    • 2019年12月14日 カンファレンスデイ@会場稟議中

ServerlessなGopherのプロポーザルお待ちしております!

(福岡は鋭意PaperCall準備中…!)

3

🍉🌴🏄‍♂️ GoConference’19 🏄‍♂️🌴🍉

4 of 49

Agenda

4

🍉🌴🏄‍♂️ GoConference’19 🏄‍♂️🌴🍉

5 of 49

Agenda

  • ISUCONとpprof
  • pprofの概要
  • pprofの利用方法
  • pprofを使ったISUCONへのアプローチ
  • pprofのソースを読んでみる
  • まとめ

5

🍉🌴🏄‍♂️ GoConference’19 🏄‍♂️🌴🍉

6 of 49

ISUCONとpprof

6

🍉🌴🏄‍♂️ GoConference’19 🏄‍♂️🌴🍉

7 of 49

ISUCONとは

  • Iikanji Spead Up CONtest、通称ISUCON
  • お題となるWebサービスを決められたレギュレーションの中で�限界まで高速化を図るチューニングバトル(基本は3人構成)
  • 優勝賞金 100万円
  • インフラ、アプリ、オペレーション、チームワーク等�ありとあらゆるものが求められる

7

🍉🌴🏄‍♂️ GoConference’19 🏄‍♂️🌴🍉

8 of 49

8

🍉🌴🏄‍♂️ GoConference’19 🏄‍♂️🌴🍉

9 of 49

ISUCON9 予選 採点

  • 採点は採点条件をクリアした参加者の間で、�性能値の高さを競うものとする。
  • 予選参加者に提供される性能計測ツールに�参加者が実行リクエストを送り、�その後ツール側から計測用の処理が実行される。
  • 採点条件・性能値・性能計測ツールについての詳細は、予選当日のマニュアルに記載する。

9

🍉🌴🏄‍♂️ GoConference’19 🏄‍♂️🌴🍉

10 of 49

ISUCON9 予選レギュレーション (ソフトウェア事項抜粋)

  • アクセス先のURI、ただしサーバー側で生成する部分(IDなど)は�文字種([0-9] や [0-9a-zA-Z_] など)を変えない範囲で自由に生成しても良い
  • レスポンス(HTML)のDOM構造(表示に影響しない範囲での空白文字の増減は許可される)
  • JavaScript/CSSファイルの内容
  • 画像および動画等のメディアファイルの内容�以下の様な事は許可される
  • 複数台あるサーバーの役割の変更
  • DBスキーマの変更やインデックスの作成・削除
  • データベースに利用するミドルウェアの変更
  • キャッシュ機構の追加、ジョブキュー機構の追加による遅延書き込み
  • 他の言語による再実装

10

🍉🌴🏄‍♂️ GoConference’19 🏄‍♂️🌴🍉

11 of 49

レギュレーションの内であれば自由

  • ボトルネックが意図的に設置される
  • こちらの特定が非常に重要
  • 出来る事は非常に多く、推測で動く事が出来ます

11

🍉🌴🏄‍♂️ GoConference’19 🏄‍♂️🌴🍉

12 of 49

推測するな計測せよ

  • Rob Pike氏のNotes on Programming in C (1989/2/21)が由来と言われてる(らしい)
    • UNIX哲学
  • ボトルネックを計測する為のツールが必要
    • Goのruntimeに対する計測ツール
      • pprof

12

🍉🌴🏄‍♂️ GoConference’19 🏄‍♂️🌴🍉

13 of 49

pprofの概要

13

🍉🌴🏄‍♂️ GoConference’19 🏄‍♂️🌴🍉

14 of 49

pprofとは

  • プロファイリングデータを生成してビジュアライズしてくれるツール
  • 「runtime/pprof」と「net/http/pprof」がある
  • runtime
    • runtimeに対するプロファイリングデータ取得、本体
  • net/http
    • http server 等に対するプロファイリングデータ取得を助けるパッケージ
      • 内部的には「runtime/pprof」を呼んでる

14

🍉🌴🏄‍♂️ GoConference’19 🏄‍♂️🌴🍉

15 of 49

利用イメージ

15

🍉🌴🏄‍♂️ GoConference’19 🏄‍♂️🌴🍉

16 of 49

pprofの利用方法

16

🍉🌴🏄‍♂️ GoConference’19 🏄‍♂️🌴🍉

17 of 49

runtime/pprofの単体での利用方法

  • CPU
    • CPUプロファイリング開始関数(pprof.StartCPUProfile)
    • CPUプロファイリング終了関数 (pprof.StopCPUProfile)※基本はdefer
  • メモリ
    • ヒーププロファイル関数(pprof.WriteHeapProfile)
  • 興味がある人は以下ソースを読んで見ると良さそう
    • runtime/pprof/pprof.go
    • runtime/cpuprof.go
    • runtime/mprof.go

17

🍉🌴🏄‍♂️ GoConference’19 🏄‍♂️🌴🍉

18 of 49

runtime/pprofにて取得出来る項目

  • 取得出来るプロファイル
    • goroutineProfile
      • goroutineのプロファイル
    • threadcreateProfile
      • Process内で作成されるthreadに関するプロファイル
    • heapProfile
      • ヒープメモリに関するプロファイル
    • allocsProfile
      • メモリの割当量に関するプロファイル
    • blockProfile
      • 同期処理で待ちが発生している関数のプロファイル
    • mutexProfile
      • プロセス間の排他制御を行っている関数のプロファイル

18

🍉🌴🏄‍♂️ GoConference’19 🏄‍♂️🌴🍉

19 of 49

example (golang.org)

19

var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to `file`")

var memprofile = flag.String("memprofile", "", "write memory profile to `file`")

func main() {

flag.Parse()

if *cpuprofile != "" {

f, err := os.Create(*cpuprofile)

if err != nil {

log.Fatal("could not create CPU profile: ", err)

}

defer f.Close()

if err := pprof.StartCPUProfile(f); err != nil {

log.Fatal("could not start CPU profile: ", err)

}

defer pprof.StopCPUProfile()

}

// ... rest of the program ...

if *memprofile != "" {

f, err := os.Create(*memprofile)

if err != nil {

log.Fatal("could not create memory profile: ", err)

}

defer f.Close()

runtime.GC() // get up-to-date statistics

if err := pprof.WriteHeapProfile(f); err != nil {

log.Fatal("could not write memory profile: ", err)

}

}

}

🍉🌴🏄‍♂️ GoConference’19 🏄‍♂️🌴🍉

20 of 49

net/http/pprofの利用方法

  • プログラムにnet/http/pprofを仕込む(WEBサーバが立ち上がる)
  • 立ち上がったWEBサーバーのPort、URLアクセスするとURIに応じた動作
  • 例)
    • go tool pprof -seconds 60 http://localhost:6060/debug/pprof/profile
      • 関数プロファイルの取得を開始、60秒間のプロファイリング結果を取得�一定時間後、結果を出力する
        • この関数の出力をそのままビジュアライズの入力にする事が出来る

20

🍉🌴🏄‍♂️ GoConference’19 🏄‍♂️🌴🍉

21 of 49

net/http/pprofの提供URL一覧

  • 提供されるURL一覧
    • /debug/pprof
      • 各プロファイリング結果に対するリンクを提供
    • /debug/pprof/cmdline
      • 実行中プログラムをコマンドラインで応答する
    • /debug/pprof/profile
      • プロファイリングの開始
    • /debug/pprof/symbol
      • program counterの数を提供
    • /debug/pprof/trace
      • 1秒間の実行トレース結果を提供

21

🍉🌴🏄‍♂️ GoConference’19 🏄‍♂️🌴🍉

22 of 49

結果入力利用方法(CUI)

  • go tool pprof <format> [options] [binary] <source>
  • CUIが立ち上がるのでそこで更にコマンドを実行
    • (例)
      • top 10
        • 時間がかかった関数順にテキストベースで10件表示
      • png、git、pdf
        • それぞれのフォーマットにて、結果を出力する
      • web
        • 結果をSVG化してブラウザで表示
    • graphviz が必要

22

🍉🌴🏄‍♂️ GoConference’19 🏄‍♂️🌴🍉

23 of 49

結果入力利用方法(GUI)

  • 結果入力を読み込ませて、様々な方式でビジュアライズ
    • 最初のデモ
  • go tool pprof -http [host]:[port] [options] [binary] <source>
    • (例)
      • go tool pprof -http="10.0.0.1:8081" [binary] <source>
        • IP 10.0.0.1 port 8081 で待受

23

🍉🌴🏄‍♂️ GoConference’19 🏄‍♂️🌴🍉

24 of 49

ISUCON におけるオペレーションイメージ

24

🍉🌴🏄‍♂️ GoConference’19 🏄‍♂️🌴🍉

25 of 49

pprofを使ったISUCONへのアプローチ

25

🍉🌴🏄‍♂️ GoConference’19 🏄‍♂️🌴🍉

26 of 49

yisuconを利用して初期アプローチ

  • https://github.com/yahoojapan/yisucon
    • MIT「特に利用の制限はありません、ご自由にご利用ください。」との事
      • 本当にありがとうございます...!
  • 現状そのままでは動きません
    • 弊社岡崎(@Y_uuu)が調整した際のブログをご参照ください
  • 解き方は、これからやりたい方がいらっしゃると思うので詳細は触れません
    • あくまで初期アプローチまでのプロセスをお伝えします

26

🍉🌴🏄‍♂️ GoConference’19 🏄‍♂️🌴🍉

27 of 49

pprofの設置

  • 以下の要領でpprofを設置します
    • main関数の最初にプロファイリング開始の関数を設置
  • pprofでブラウザ出力
    • go tool pprof -seconds 60 -http="10.0.1.83:9090" isuwitter http://localhost:6060/debug/pprof/profile
      • 60秒間「isuwitter」に対して「/debug/pprof/profile」にて�プロファイリングを開始した結果を「IP:10.0.1.83かつPort:9090" 」にて待ち受ける
      • (おさらい)go tool pprof -http [host]:[port] [options] [binary] <source>

27

runtime.SetBlockProfileRate(1)  ← blocking profilerの有効化

runtime.SetMutexProfileFraction(1)  ← mutex profilerの有効化

go func() {

log.Println(http.ListenAndServe("0.0.0.0:6060", nil))

}()

🍉🌴🏄‍♂️ GoConference’19 🏄‍♂️🌴🍉

28 of 49

ビジュアライズの各種の見方の説明

28

🍉🌴🏄‍♂️ GoConference’19 🏄‍♂️🌴🍉

29 of 49

結果 (Top)

29

🍉🌴🏄‍♂️ GoConference’19 🏄‍♂️🌴🍉

30 of 49

結果の見方 (Top)

  • Top
      • Flat:関数の処理時間
      • Flat%:各Flatの全体に対する割合
      • Sum%:スタック履歴からの累計Flat%
      • Cum:待ち時間も含めた処理時間
      • Cum%:各Cumの全体に対する割合
      • Name:関数名

30

🍉🌴🏄‍♂️ GoConference’19 🏄‍♂️🌴🍉

31 of 49

結果の見方 (Graph)

31

🍉🌴🏄‍♂️ GoConference’19 🏄‍♂️🌴🍉

32 of 49

結果の見方 (Graph)

  • Graph
    • 関数の呼び出し元から呼び出し先を示す
    • 関数に時間がかかっているものをビジュアル的に大きく、赤くしてくれる
      • 基本的には赤くなってるものを対処
        • sleepやwaitも対象なので本当にその処理を改善すべきなのかは見極める�赤くなっている関数の呼び出し元も含めて考えるのが良い

32

🍉🌴🏄‍♂️ GoConference’19 🏄‍♂️🌴🍉

33 of 49

結果の見方 (Flame Graph)

33

🍉🌴🏄‍♂️ GoConference’19 🏄‍♂️🌴🍉

34 of 49

結果の見方 (Flame Graph)

  • Flame Graph
    • 元々は uber/go-torch だったものが公式に取り込まれた様子
    • 処理の流れをクリックすることでわかる
      • 時間がかかっているものを長さで表してくれる
        • 順にクリックしていくことで原因の特定に役立つ

34

🍉🌴🏄‍♂️ GoConference’19 🏄‍♂️🌴🍉

35 of 49

結果の見方 (Peek)

35

🍉🌴🏄‍♂️ GoConference’19 🏄‍♂️🌴🍉

36 of 49

結果の見方 (Peek)

  • Peek
    • TOPで見れているものを実行行数も含めて確認可能
      • どの関数のどの行にて時間がかかっているかがわかる

36

🍉🌴🏄‍♂️ GoConference’19 🏄‍♂️🌴🍉

37 of 49

結果の見方 (source)

37

🍉🌴🏄‍♂️ GoConference’19 🏄‍♂️🌴🍉

38 of 49

結果の見方 (source)

  • source
    • Peekを更にソースコードよりに表示

38

🍉🌴🏄‍♂️ GoConference’19 🏄‍♂️🌴🍉

39 of 49

特定したボトルネックを対処後

39

🍉🌴🏄‍♂️ GoConference’19 🏄‍♂️🌴🍉

40 of 49

アプローチ方法のおさらい

  • pprofを仕掛ける
  • プロファイル開始、ベンチマークを実行
  • 結果から問題の関数を特定
  • 対処
  • プロファイル開始に戻り、以下ループ
  • 優勝、100万円です

40

🍉🌴🏄‍♂️ GoConference’19 🏄‍♂️🌴🍉

41 of 49

pprofのソースを読んでみる

41

🍉🌴🏄‍♂️ GoConference’19 🏄‍♂️🌴🍉

42 of 49

net/http/pprof (func init)

  • net/http/pprof
    • わりと内容はライト(runtime/pprofが本体)
      • init() にて各URLにハンドラを仕掛ける

42

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)

}

🍉🌴🏄‍♂️ GoConference’19 🏄‍♂️🌴🍉

43 of 49

net/http/pprof (func Profile)

  • Header設定
  • profileする時間
  • profile実行中sleep
  • ファイル出力

43

func Profile(w http.ResponseWriter, r *http.Request) {

w.Header().Set("X-Content-Type-Options", "nosniff")

sec, err := strconv.ParseInt(r.FormValue("seconds"), 10, 64)

if sec <= 0 || err != nil {

sec = 30

}

if durationExceedsWriteTimeout(r, float64(sec)) {

serveError(w, http.StatusBadRequest, "profile duration exceeds server's WriteTimeout")

return

}

// Set Content Type assuming StartCPUProfile will work,

// because if it does it starts writing.

w.Header().Set("Content-Type", "application/octet-stream")

w.Header().Set("Content-Disposition", `attachment; filename="profile"`)

if err := pprof.StartCPUProfile(w); err != nil {

// StartCPUProfile failed, so no writes yet.

serveError(w, http.StatusInternalServerError,

fmt.Sprintf("Could not enable CPU profiling: %s", err))

return

}

sleep(w, time.Duration(sec)*time.Second)

pprof.StopCPUProfile()

}

🍉🌴🏄‍♂️ GoConference’19 🏄‍♂️🌴🍉

44 of 49

runtime/pprof ( func StartCPUProfile)

  • runtime/pprof
    • CPUプロファイルの場合、StartCPUProfile
  • CPUのLockを取得しながらUnlockを仕掛ける
  • チャンネルを設ける
  • 二重profilingにならないようにチェック
  • プロファイリングで状態にする
  • runtime.SetCPUProfileRateへ
  • profileWriterで書き込み

44

func StartCPUProfile(w io.Writer) error {

// The runtime routines allow a variable profiling rate,

// but in practice operating systems cannot trigger signals

// at more than about 500 Hz, and our processing of the

// signal is not cheap (mostly getting the stack trace).

// 100 Hz is a reasonable choice: it is frequent enough to

// produce useful data, rare enough not to bog down the

// system, and a nice round number to make it easy to

// convert sample counts to seconds. Instead of requiring

// each client to specify the frequency, we hard code it.

const hz = 100

cpu.Lock()

defer cpu.Unlock()

if cpu.done == nil {

cpu.done = make(chan bool)

}

// Double-check.

if cpu.profiling {

return fmt.Errorf("cpu profiling already in use")

}

cpu.profiling = true

runtime.SetCPUProfileRate(hz)

go profileWriter(w)

return nil

}

🍉🌴🏄‍♂️ GoConference’19 🏄‍♂️🌴🍉

45 of 49

runtime ( func SetCPUProfileRate)

  • 大雑把に以下の流れ
    • 各Threadに対するプロファイラセットしにいく
    • OS毎のCPUに対する処理に分岐
    • libcallにてsetitimerが呼ばれ、�ユーザー空間で実行されている時間を測定
  • 最終的にシステムコールを行うことで�動作しているプログラムの実行時間を取得

45

func SetCPUProfileRate(hz int) {

// Clamp hz to something reasonable.

if hz < 0 {

hz = 0

}

if hz > 1000000 {

hz = 1000000

}

lock(&cpuprof.lock)

if hz > 0 {

if cpuprof.on || cpuprof.log != nil {

print("runtime: cannot set cpu profile rate until previous profile has finished.\n")

unlock(&cpuprof.lock)

return

}

cpuprof.on = true

cpuprof.log = newProfBuf(1, 1<<17, 1<<14)

hdr := [1]uint64{uint64(hz)}

cpuprof.log.write(nil, nanotime(), hdr[:], nil)

setcpuprofilerate(int32(hz))

} else if cpuprof.on {

setcpuprofilerate(0)

cpuprof.on = false

cpuprof.addExtra()

cpuprof.log.close()

}

unlock(&cpuprof.lock)

}

🍉🌴🏄‍♂️ GoConference’19 🏄‍♂️🌴🍉

46 of 49

runtime/pprof ( func StopCPUProfile)

  • CPUのLockを取得しながらUnlockを仕掛ける
  • プロファイリングでは無いなら何もしない
  • runtime.SetCPUProfileRateへ
  • cpu.doneに値が送る

46

func StopCPUProfile() {

cpu.Lock()

defer cpu.Unlock()

if !cpu.profiling {

return

}

cpu.profiling = false

runtime.SetCPUProfileRate(0)

<-cpu.done

}

🍉🌴🏄‍♂️ GoConference’19 🏄‍♂️🌴🍉

47 of 49

まとめ

47

🍉🌴🏄‍♂️ GoConference’19 🏄‍♂️🌴🍉

48 of 49

まとめ

  • ISUCONではボトルネックを特定する事が重要
    • pprofで解決しよう
  • ビジュアライズが非常に便利
    • 利用できる様になると実案件にてシステムが健全になります
  • 推測するな計測せよ
    • 計測、対処のループを回せば理論上優勝
  • Goのソースコードリーディングは学びがある(Goらしいコード、OS)
    • pprofに関わらず、ソースコードを読むと学びがある

48

🍉🌴🏄‍♂️ GoConference’19 🏄‍♂️🌴🍉

49 of 49

Thank you!

Fusicは技術が大好きなエンジニアを募集しています

https://fusic.github.io/

49

🍉🌴🏄‍♂️ GoConference’19 🏄‍♂️🌴🍉