Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Go1.19から始めるGCのチューニング方法

 Go1.19から始めるGCのチューニング方法

芳賀壮

June 02, 2023
Tweet

Other Decks in Programming

Transcript

  1. Go1.19から始めるGCのチューニング方法
    芳賀壮
    1
    Go Conference 2023/06/02

    View Slide

  2. | 芳賀壮(はがたけ)
    所属: 株式会社サイバーエージェント
    職種: バックエンドエンジニア
    #Go #Python #AI
    #アウトドア #サカナクション
    @take_twit_2
    2

    View Slide

  3. |Go1.19から始めるGCのチューニング方法|
    | アジェンダ
    1. GC周りの前提知識
    2. GOGC/GOMEMLIMITの役割
    3. 内部実装
    4. GCチューニング事例
    3

    View Slide

  4. GC周りの前提知識
    4

    View Slide

  5. |Go1.19から始めるGCのチューニング方法|
    | メモリ領域
    スタック
    Go)基本的に関数のローカル変数などが割り当てられる
    ローカル変数の生存期間は関数のスコープによって管理
    ヒープ
    Go)グローバル変数など関数を横断する変数などが割り当てられる
    生存期間は静的に決定されず、GCによって動的に管理
    GC(Garbage Collection)
    ヒープ領域の使わなくなったメモリを再利用するための処理
    GCがうまくいかないとメモリリークが生じてしまう
    メモリリーク:確保したメモリ領域を解放せず放置してしまうこと
    5

    View Slide

  6. |Go1.19から始めるGCのチューニング方法|
    | GCはどうやって引き起こされるのか?
    ヒープ(gcTriggerHeap)
    ヒープ使用量が閾値を超えた場合
    時間(gcTriggerTime)
    前回GCから一定時間経過した場合
    明示的な実行(gcTriggerCycle)
    runtime.GC()関数を呼びだした場合
    6

    View Slide

  7. GOGC/GOMEMLIMITの役割
    7

    View Slide

  8. |Go1.19から始めるGCのチューニング方法|
    | GOGCの役割
    GOGC: GC頻度を操作する環境変数
    使用ヒープメモリ量に対して、設定した割合分(GOGC)
    新たに割り当てられた際にGCが実行される
    GC目標ヒープ量 = 使用ヒープ量 + (使用ヒープ量 + GC始点) * GOGC / 100
    ↓デフォルト(GOGC=100)の場合↓
    GC目標ヒープ量 = 使用ヒープ量 *2 + α
    8
    GC root
    Target heap memory Live heap Live heap

    View Slide

  9. |Go1.19から始めるGCのチューニング方法|
    | GOMEMLIMITの役割
    GOMEMLIMIT: メモリ使用量を制限する環境変数
    メリット
    「メモリ使用量が急激に増加した場合、OOMを防ぐことができる」
    以前:前回割り当てられた量との差分でしかGCを起動できなかった
    例)仮に上限の90%のメモリが確保されたとする。通常、次のヒープ量によるGCはその倍、上限の180%のメモリ
    を確保した時となる。つまりGCよりも先にOOMが発生してしまう。
    → 上限を起因としたGCが設定でき、OOMを防ぐ一因に!
    9

    View Slide

  10. |Go1.19から始めるGCのチューニング方法|
    | GOGC・GOMEMLIMITが登録される箇所
    mgc.go/gcinit() → mgcpacer.go/readGOGC(), readGOMEMLIMIT()
    gcControllerState
    GC動作を制御するための構造体。
    GCの周期、次のGCトリガーなどを管理
    10

    View Slide

  11. |Go1.19から始めるGCのチューニング方法|
    | どう使われているのか
    次のGCで確保するヒープ量(HeapGoal)を計算する時に用いられる
    →gcControllerState.heapGoalInternal()
    11

    View Slide

  12. 内部実装
    12

    View Slide

  13. |Go1.19から始めるGCのチューニング方法|
    | GoのGCの仕組み(コンカレントマーク&スイープ方式)
    スパン(mspan)
    メモリ管理の基本単位
    オブジェクトが保存されている
    オブジェクト
    ヒープ上に割り当てられたデータ
    (変数、スライス、など)
    ルート(Root)
    マーク処理の始点となるオブジェクト
    ヒープ内外に存在する(スタック)
    13
    Root
    Heap

    View Slide

  14. |Go1.19から始めるGCのチューニング方法|
    | GoのGCの仕組み(コンカレントマーク&スイープ方式)
    マーク処理
    使用しているヒープ領域を「色付け」
    →追跡可能なオブジェクトをチェックする
    スイープ処理
    色付けしたヒープ領域以外を解放
    →使われていないオブジェクトを解放
    14
    Heap
    Root

    View Slide

  15. |Go1.19から始めるGCのチューニング方法|
    | GoのGCの仕組み(コンカレントマーク&スイープ方式)
    マーク処理
    使用しているヒープ領域を「色付け」
    →追跡可能なオブジェクトをチェックする
    スイープ処理
    色付けしたヒープ領域以外を解放
    →使われていないオブジェクトを解放
    15
    Heap
    Root

    View Slide

  16. |Go1.19から始めるGCのチューニング方法|
    | GoのGCの仕組み(コンカレントマーク&スイープ方式)
    マーク処理
    使用しているヒープ領域を「色付け」
    →追跡可能なオブジェクトをチェックする
    スイープ処理
    色付けしたヒープ領域以外を解放
    →使われていないオブジェクトを解放
    16
    Heap
    Root

    View Slide

  17. |Go1.19から始めるGCのチューニング方法|
    | GoのGCの仕組み(コンカレントマーク&スイープ方式)
    17
    Heap
    Root
    コンカレント
    GCがアプリケーションと並列に処理
    → 処理が高速化(STWが短縮)
      → マーク漏れが生じる危険性
    マーク漏れを防ぐ仕組み
    色付ける(tri-color)
    未探索: 白、探索中:グレー、探索終了:黒
     
    ライトバリア
    詳しくは、こちら

    View Slide

  18. |Go1.19から始めるGCのチューニング方法|
    mgc.go/gcStart()
    | Goのメモリ管理
    18
    sweep
    termination
    前回GCのスイープ
    処理の完了確認
    _GCoff
    STW
    mark
    termination
    mark sweep
    _GCmark _GCmarktermination
    STW
    _GCoff
    STW
    マーク処理 マーク処理の完了
    確認
    スイープ処理のため
    各種情報更新
    スイープ処理
    STW GCによりアプリケーションスレッドが止まる現象
    _GChoge GCフェーズ

    View Slide

  19. |Go1.19から始めるGCのチューニング方法|
    mgc.go/gcStart()
    | Goのメモリ管理
    19
    sweep
    termination
    前回GCのスイープ
    処理の完了確認
    _GCoff
    STW
    mark
    termination
    mark sweep
    _GCmark _GCmarktermination
    STW
    _GCoff
    STW
    マーク処理 マーク処理の完了
    確認
    スイープ処理のため
    各種情報更新
    スイープ処理
    STW GCによりアプリケーションスレッドが止まる現象
    _GChoge GCフェーズ

    View Slide

  20. |Go1.19から始めるGCのチューニング方法|
    | mark
    20
    _GCmark
    gcBgMark
    StartWorkers
    gcMark
    RootPrepare
    gcBgMark
    Prepare
    gcMark
    TinyAllocs
    マーク処理の本体
    gcBgMarkWorkerの
    起動関数
    →ゴルーチンにより、
    マルチスレッド処理さ
    れる
    gcBgMarkWorkerの
    ゴルーチン数と
    待機数をカウント
    する値の初期化
    マーク処理の始点
    となるルート(root)
    の準備
    スレッドごと保持し
    ている微小なメモリ
    割り当て(tiny
    allocs)をマーク
    する処理

    View Slide

  21. |Go1.19から始めるGCのチューニング方法|
    | gcBgMarkWorker
    21
    gopark sleep
    gcMark
    Done
    gcDrain
    gcBgMarkWorker
    をキューに入れる
    機構
    マーク処理が必要
    な時にすぐできる
    よう準備
    マーク処理を行う
    部分
    参照できる
    オブジェクトを
    マークする
    mark phaseの
    終了処理

    sweep処理
    _GCmark

    View Slide

  22. |Go1.19から始めるGCのチューニング方法|
    | gcDrain
    22
    _GCmark
    mgcmark.go/markroot() mgcmark.go/scanobject()
    Heap
    Root
    Heap
    Root
    Heap
    Root

    View Slide

  23. |Go1.19から始めるGCのチューニング方法|
    | gcDrain
    マーク対象:マークビット(markBits)
    オブジェクトと1対1の関係を持つデータ構造
    maskが0以外であればマーク済と判断
    マーク処理:markBits.maskを変更する処理
    mgcmask.go/greyobject()
    23
    _GCmark

    View Slide

  24. |Go1.19から始めるGCのチューニング方法|
    | gcBgMarkWorker
    24
    gopark sleep
    gcMark
    Done
    gcDrain
    gcBgMarkWorker
    をキューに入れる
    機構
    マーク処理が必要
    な時にすぐできる
    よう準備
    マーク処理を行う
    部分。
    参照できるオブ
    ジェクトをマーク
    する。
    mark phaseの
    終了処理

    sweep処理
    _GCmark

    View Slide

  25. |Go1.19から始めるGCのチューニング方法|
    | gcMarkDone
    マークフェーズの終了処理とスイープ処理を担う関数
    1. マーク処理で使っていたバッファ(gcWork)のタスク完了を確認
    2. バッファの解放など
    3. gcMarkTermination
    - マーク処理の終了確認
    - スイープ処理(gcSweep)
    - GC時間やCPU使用率などのメトリクス計測
    25
    _GCmarktermination STW
    _GCmark

    View Slide

  26. |Go1.19から始めるGCのチューニング方法|
    | gcSweep
    ヒープ上のスパン(mspan)内のオブジェクトに対して解放処理を行う
    sweepone() → mgcsweep.go/sweep()
    対象オブジェクトのmarkBitsを見てマークがなければ解放
    全オブジェクトをスイープし、スパン自体が
    使用されていない場合はスパンも解放
    26
    STW
    _GCoff
    Heap
    Root

    View Slide

  27. |Go1.19から始めるGCのチューニング方法|
    | Goのメモリ管理
    27
    sweep
    termination
    _GCoff
    STW
    mark
    termination
    mark sweep
    _GCmark _GCmarktermination
    STW
    _GCoff
    STW
    gcStart gcBgMarkWorker
    gcDrain()
    gcMark
    Termination()
    gcSweep()
    gcBgMarkStartWorkers()

    View Slide

  28. GCチューニング事例
    28

    View Slide

  29. |Go1.19から始めるGCのチューニング方法|
    | GCチューニング事例
    開発中のプロダクションコードを使ってGOMEMLIMITの検証試験を
    実施しました。
    参考事例としてみていただければと思います。
    29

    View Slide

  30. |Go1.19から始めるGCのチューニング方法|
    | GCチューニング事例
    検証試験のシナリオ特性
    - ゲーム事業
    - ユーザーの基本動作を確認
    (ユーザー登録、インゲーム動作(バトルなど)、アウトゲーム動作(ガチャなど))
    - Write操作が多い
    - 大量のデータをメモリ上にキャッシュする
    30

    View Slide

  31. |Go1.19から始めるGCのチューニング方法|
    | GCチューニング事例
    検証環境
    - アプリケーションサーバー
    - ECS on EC2 (c6g.xlarge)
    - メモリ制限(ハードリミット):3GiB
    - RDS (common:t3.small, user:r5.xlarge)
    - Elasticache (cache.r6g.large × 2)
    31

    View Slide

  32. |Go1.19から始めるGCのチューニング方法|
    | チューニング試験
    3パターンで実施
    32
    パターン1 パターン2 パターン3
    GOGC 100 500 off
    GOMEMLIMIT - - 2700(3GiB)
    備考 デフォルト 既存設定 GOMEMLIMIT導入
    …, a good rule of thumb is to leave an additional 5-10% of headroom to account for
    memory sources the Go runtime is unaware of.
    (Goランタイムが認識できないメモリソースを考慮し、さらに5~10%のヘッドルームを残すのが良い経験則です。)
    引用:A Guide to the Go Garbage Collector

    View Slide

  33. |Go1.19から始めるGCのチューニング方法|
    | チューニング結果
    - パターン1 → パターン2 → パターン3の順で改善が見られる
    - GOMEMLIMITを取り入れたパターンが一番改善
    33
    パターン1
    GOGC=100
    パターン2
    GOGC=500
    パターン3
    GOGC=off,
    MEMLIMIT=2700
    RPS 最大値 1162 1347 1371
    平均 1117 1290 1310
    APIサーバー CPU使用率(最大) 385.84 379.57 377.14
    NW IN(最大) 44.5 MiB/s 55.27 MiB/s 58.61MiB/s
    NW OUT(最大) 54.3 MiB/s 54.3 MiB/s 68.91 MiB/

    View Slide

  34. |Go1.19から始めるGCのチューニング方法|
    | チューニング結果
    34
    パターン1
    GOGC=100
    パターン2
    GOGC=500
    パターン3
    GOGC=off,
    MEMLIMIT=2700
    プロファイル GC総回数 8210 2263 510
    GCで使用されるCPU割合
    (gcBgMarkWorker)
    10.22% 2.62% 1.0%
    ヒープ使用量 274MiB 769MiB 2604MiB
    - GOGCが大きいほどGC頻度が低い
    - GOGCが大きい→GCトリガー値が大きい→GC頻度が低い
    - GCで使用されるCPU割合も低い
    - GOMEMLIMITを取り入れたパターンはGC頻度が最も低い
    - 設定したヒープ上限(2700MiB)まで使用している

    View Slide

  35. |Go1.19から始めるGCのチューニング方法|
    While the memory limit is a powerful tool, ..., it's still important to use it thoughtfully.
    (メモリ制限は強力なツールであり、Goランタイムは誤用による最悪の挙動を軽減するための措
    置を講じていますが、それでも思慮深く使用することが重要です。)
    Do take advantage of the memory limit when the execution environment of your Go program
    is entirely within your control, and the Go program is the only program with access to some
    set of resources.
    (メモリ制限を利用するのは、Goプログラムの実行環境が完全に自分のコントロール下にあり、
    Goプログラムだけが何らかのリソース(コンテナのメモリ制限のような何らかのメモリ予約)に
    アクセスできる場合です。)
    今回
    - マシン上で実行するアプリケーションはGo製APIサーバーのみ
    - ECSのタスク定義より、コンテナ数・メモリ使用量を制限
    | 考察
    35
    「A Guide to the Go Garbage Collector」 Suggested usesより
    本環境がGOMEMLIMITの導入に適しており、検証結果からも有効性が見られる

    View Slide

  36. |Go1.19から始めるGCのチューニング方法|
    | まとめ
    GOGC/GOMEMLIMIT
    Go1.19よりGOMEMLIMITが導入
    → メモリ上限をGCトリガーに設定可能
    内部実装
    マーク・スイープ処理を説明
    チューニング事例
    Goプログラムの実行環境が制御可能であり、Goプログラムだけが
    リソースにアクセスできる場合であれば、GOMEMLIMITの導入の余地がある
    36

    View Slide

  37. |Go1.19から始めるGCのチューニング方法|
    | 参考資料
    A Guide to the Go Garbage Collector
    Go 1.19のメモリ周りの更新
    GoのGC (garbage collector)について理解する
    两万字长文带你深入Go语言GC源码
    Goにおけるメモリ管理の可視化
    37

    View Slide

  38. おわり
    38

    View Slide