Go言語入門:ガベージコレクター -Vol.26-

スポンサーリンク
Go言語入門:ガベージコレクター -Vol.26- ノウハウ
Go言語入門:ガベージコレクター -Vol.26-
この記事は約14分で読めます。
よっしー
よっしー

こんにちは。よっしーです(^^)

本日は、Go言語のガベージコレクターついて解説しています。

スポンサーリンク

背景

Goのガベージコレクター(GC)は、多くの開発者にとって「ブラックボックス」のような存在です。メモリ管理を自動で行ってくれる便利な仕組みである一方、アプリケーションのパフォーマンスに大きな影響を与える要因でもあります。「なぜ突然レスポンスが遅くなるのか?」「メモリ使用量が想定より多いのはなぜか?」「GCの停止時間をもっと短くできないか?」—— こうした疑問は、Goで高性能なアプリケーションを開発する上で避けて通れない課題です。

本記事では、Go公式ドキュメントの「ガベージコレクションガイド」を日本語で紹介します。このガイドは、GCの動作原理を理解し、その知見を活用してアプリケーションのリソース使用効率を改善することを目的としています。特筆すべきは、このドキュメントがガベージコレクションの前提知識を一切要求しない点です。Go言語の基本的な知識さえあれば、誰でもGCの仕組みを深く理解できるよう設計されています。

なぜ今、GCの理解が重要なのでしょうか。クラウドネイティブ時代において、リソースの効率的な活用はコスト削減に直結します。また、マイクロサービスアーキテクチャでは、各サービスのレイテンシが全体のユーザー体験に影響するため、GCによる一時停止を最小限に抑えることが求められます。このガイドを通じて、「なんとなく動いている」から「理解して最適化できる」レベルへとステップアップし、より高品質なGoアプリケーションの開発を目指しましょう。

Linux透過的ヒュージページ (THP)

プログラムがメモリにアクセスする際、CPUは使用する仮想メモリアドレスを、アクセスしようとしているデータを参照する物理メモリアドレスに変換する必要があります。これを行うために、CPUは「ページテーブル」を参照します。ページテーブルは、オペレーティングシステムが管理する仮想メモリから物理メモリへのマッピングを表すデータ構造です。ページテーブルの各エントリは、ページと呼ばれる物理メモリの不可分なブロックを表します。

透過的ヒュージページ(THP)は、連続した仮想メモリ領域を支える物理メモリのページを、ヒュージページと呼ばれるより大きなメモリブロックで透過的に置き換えるLinuxの機能です。より大きなブロックを使用することで、同じメモリ領域を表すために必要なページテーブルエントリが少なくなり、ページテーブルの検索時間が改善されます。ただし、ヒュージページの小さな部分のみがシステムによって使用される場合、より大きなブロックはより多くの無駄を意味します。

本番環境でGoプログラムを実行する場合、Linux上で透過的ヒュージページを有効にすると、追加のメモリ使用を犠牲にして、スループットとレイテンシを改善できます。小さなヒープを持つアプリケーションはTHPから恩恵を受けない傾向があり、かなりの量の追加メモリを使用する可能性があります(最大50%)。ただし、大きなヒープ(1 GiB以上)を持つアプリケーションは、非常に多くの追加メモリオーバーヘッド(1-2%以下)なしで、かなりの恩恵を受ける傾向があります(最大10%のスループット)。いずれの場合でも、THP設定を認識しておくことは有用であり、実験を行うことを常にお勧めします。

Linux環境で透過的ヒュージページを有効または無効にするには、/sys/kernel/mm/transparent_hugepage/enabledを変更します。詳細については、公式のLinux管理ガイドを参照してください。Linux本番環境で透過的ヒュージページを有効にする場合、Goプログラムには以下の追加設定をお勧めします。

  • **/sys/kernel/mm/transparent_hugepage/defragdeferまたはdefer+madviseに設定します。**この設定は、Linuxカーネルが通常のページをヒュージページに結合する積極性を制御します。deferは、カーネルにヒュージページを遅延的にバックグラウンドで結合するように指示します。より積極的な設定は、メモリが制約されたシステムで停止を引き起こし、アプリケーションのレイテンシを損なうことがよくあります。defer+madvisedeferに似ていますが、明示的にヒュージページを要求し、パフォーマンスのためにそれらを必要とするシステム上の他のアプリケーションに対してより優しいです。
  • **/sys/kernel/mm/transparent_hugepage/khugepaged/max_ptes_none0に設定します。**この設定は、Linuxカーネルデーモンがヒュージページを割り当てようとするときに、追加でいくつのページを割り当てられるかを制御します。デフォルト設定は最大限に積極的で、GoランタイムがメモリをOSに返すために行う作業を元に戻すことがよくあります。Go 1.21以前は、Goランタイムはデフォルト設定の悪影響を軽減しようとしましたが、CPUコストがかかりました。Go 1.21+およびLinux 6.2+では、Goランタイムはヒュージページの状態を変更しなくなりました。Go 1.21.1以降にアップグレードする際にメモリ使用量の増加が発生した場合は、この設定を適用してみてください。問題が解決する可能性が高いです。追加の回避策として、プロセスレベルでヒュージページを無効にするためにPR_SET_THP_DISABLEPrctl関数を呼び出すか、GODEBUG=disablethp=1(Go 1.21.6およびGo 1.22で追加予定)を設定してヒープメモリのヒュージページを無効にできます。なお、GODEBUG設定は将来のリリースで削除される可能性があります。

Linux透過的ヒュージページ (THP) – 本番環境の設定

THPは、Linuxのメモリ管理を改善する機能ですが、Goアプリには注意が必要です。

仮想メモリとページテーブル

基本概念

アプリケーション
    ↓ (仮想アドレス)
ページテーブル (翻訳)
    ↓ (物理アドレス)
物理メモリ(RAM)

例え話:

住所システム:

仮想アドレス = 「東京都渋谷区1-2-3」
  ↓ (ページテーブル = 地図)
物理アドレス = 「実際の建物の場所」

通常のページ

通常のページサイズ: 4 KB

1 GB のメモリ = 262,144 ページ
                 ↓
ページテーブルが巨大!

視覚化:

物理メモリ:
┌────┬────┬────┬────┬────┬────┐
│4KB │4KB │4KB │4KB │4KB │4KB │ ...
└────┴────┴────┴────┴────┴────┘

ページテーブル:
Entry 1 → ページ1
Entry 2 → ページ2
Entry 3 → ページ3
... (26万エントリ!)

ヒュージページとは?

大きなページサイズ

ヒュージページサイズ: 2 MB (通常の512倍)

1 GB のメモリ = 512 ヒュージページ
                 ↓
ページテーブルが小さい!

視覚化:

物理メモリ:
┌──────────┬──────────┬──────────┐
│   2MB    │   2MB    │   2MB    │ ...
└──────────┴──────────┴──────────┘

ページテーブル:
Entry 1 → ヒュージページ1
Entry 2 → ヒュージページ2
... (512エントリのみ!)

メリット・デメリット

メリット:
✅ ページテーブルが小さい
✅ アドレス変換が速い
✅ CPUキャッシュ効率が良い

デメリット:
❌ メモリの無駄が出やすい
❌ 小さなヒープでは逆効果
❌ 設定次第でGCと干渉

例え話:

本の収納:

通常のページ = 小さな箱
- 1冊ずつ収納
- 無駄がない
- 箱の管理が大変

ヒュージページ = 大きな箱
- まとめて収納
- 管理が楽
- でも、1冊だけなら無駄

Goアプリケーションへの影響

小さなヒープ (< 1 GB)

効果:
❌ スループット: +0% (改善なし)
❌ メモリ使用量: +50% (増加!)

理由:
- ヒープが小さい
- ヒュージページが部分的にしか使われない
- メモリの無駄が大きい

具体例:

アプリのヒープ: 100 MB

通常のページ:
使用メモリ: 100 MB (実際の使用量)

ヒュージページ:
使用メモリ: 150 MB (50%増!)
  ↑ 50 MBは使われていない

大きなヒープ (≥ 1 GB)

効果:
✅ スループット: +5〜10% (改善!)
✅ メモリ使用量: +1〜2% (許容範囲)

理由:
- ヒープが大きい
- ヒュージページが効率的に使われる
- 無駄が相対的に小さい

具体例:

アプリのヒープ: 10 GB

通常のページ:
スループット: 1000 req/s
使用メモリ: 10 GB

ヒュージページ:
スループット: 1100 req/s (+10%)
使用メモリ: 10.2 GB (+2%)

THP設定の確認と変更

現在の設定を確認

# THPが有効か確認
cat /sys/kernel/mm/transparent_hugepage/enabled

# 出力例:
# always [madvise] never
#        ↑
#      現在の設定

設定値の意味:

always:
- 常にヒュージページを使用
- 最も積極的
- Goには推奨しない

madvise:
- アプリが明示的に要求した時のみ
- 推奨設定

never:
- ヒュージページを使用しない
- 小さなヒープには適切

設定の変更 (一時的)

# madviseに設定(推奨)
echo madvise | sudo tee /sys/kernel/mm/transparent_hugepage/enabled

# 無効化
echo never | sudo tee /sys/kernel/mm/transparent_hugepage/enabled

設定の変更 (永続的)

# /etc/rc.local に追加
echo 'echo madvise > /sys/kernel/mm/transparent_hugepage/enabled' | sudo tee -a /etc/rc.local

# または systemd サービスを作成
sudo systemctl edit --force --full thp.service
[Unit]
Description=Disable Transparent Huge Pages

[Service]
Type=oneshot
ExecStart=/bin/sh -c 'echo madvise > /sys/kernel/mm/transparent_hugepage/enabled'

[Install]
WantedBy=multi-user.target
sudo systemctl enable thp.service

推奨設定

設定1: defragの設定

# 現在の設定を確認
cat /sys/kernel/mm/transparent_hugepage/defrag

# 推奨設定に変更
echo defer | sudo tee /sys/kernel/mm/transparent_hugepage/defrag

# または
echo 'defer+madvise' | sudo tee /sys/kernel/mm/transparent_hugepage/defrag

設定値の意味:

always:
- 積極的にヒュージページ化
- アプリが停止する可能性
- ❌ 推奨しない

defer:
- 遅延的にヒュージページ化
- バックグラウンドで実行
- ✅ 推奨

defer+madvise:
- deferと同じ
- 他のアプリに配慮
- ✅ 最も推奨

never:
- ヒュージページ化しない

例え話:

部屋の片付け:

always = 今すぐ片付け
- 作業が止まる
- ストレス

defer = 後で片付け
- 作業を続けられる
- 快適

never = 片付けない
- 効率悪い

設定2: max_ptes_noneの設定

# 現在の設定を確認
cat /sys/kernel/mm/transparent_hugepage/khugepaged/max_ptes_none

# 推奨設定に変更
echo 0 | sudo tee /sys/kernel/mm/transparent_hugepage/khugepaged/max_ptes_none

なぜ0に設定するのか:

デフォルト(511):
- カーネルが積極的にメモリを割り当て
- GoランタイムがOSに返したメモリを取り戻す
- メモリ使用量が増加

推奨(0):
- カーネルが控えめ
- Goランタイムのメモリ管理を尊重
- メモリ使用量が安定

Go 1.21以降の変更

Go 1.21での変更点

Go 1.20以前:
- Goランタイムが積極的にTHPを制御
- CPUオーバーヘッドあり
- メモリ使用量は安定

Go 1.21以降:
- Goランタイムは制御しない
- CPUオーバーヘッドなし
- max_ptes_none=511だとメモリ増加

対処法

方法1: カーネル設定を変更 (推奨)

echo 0 | sudo tee /sys/kernel/mm/transparent_hugepage/khugepaged/max_ptes_none

方法2: プロセスレベルで無効化

package main

import (
    "syscall"
)

func init() {
    // THPをプロセスレベルで無効化
    syscall.Prctl(syscall.PR_SET_THP_DISABLE, 1, 0, 0, 0)
}

方法3: 環境変数で無効化 (Go 1.21.6+)

GODEBUG=disablethp=1 ./myapp

実践的な設定例

小さなヒープのアプリ (< 1 GB)

# THPを無効化
echo never | sudo tee /sys/kernel/mm/transparent_hugepage/enabled

# または、GODEBUGで無効化
GODEBUG=disablethp=1 ./myapp

大きなヒープのアプリ (≥ 1 GB)

# THPを有効化(madvise)
echo madvise | sudo tee /sys/kernel/mm/transparent_hugepage/enabled

# defragを設定
echo defer | sudo tee /sys/kernel/mm/transparent_hugepage/defrag

# max_ptes_noneを設定
echo 0 | sudo tee /sys/kernel/mm/transparent_hugepage/khugepaged/max_ptes_none

Dockerコンテナでの設定

# Dockerfile
FROM golang:1.21

# THPを無効化するスクリプト
RUN echo '#!/bin/sh' > /usr/local/bin/disable-thp.sh && \
    echo 'echo never > /sys/kernel/mm/transparent_hugepage/enabled' >> /usr/local/bin/disable-thp.sh && \
    chmod +x /usr/local/bin/disable-thp.sh

# アプリケーション
COPY . /app
WORKDIR /app
RUN go build -o myapp

CMD ["/app/myapp"]
# docker run with privileged
docker run --privileged myimage

注意: コンテナ内からは通常変更できません。ホストOSで設定が必要です。

モニタリング

THPの使用状況を確認

# THPの統計を表示
cat /proc/meminfo | grep -i huge

# 出力例:
AnonHugePages:    204800 kB  ← 使用中のヒュージページ
ShmemHugePages:        0 kB
HugePages_Total:       0
HugePages_Free:        0
HugePages_Rsvd:        0
HugePages_Surp:        0
Hugepagesize:       2048 kB

アプリケーションの効果測定

# Before: THP有効
./myapp &
APP_PID=$!

# メモリ使用量を記録
ps aux | grep myapp

# スループットを測定
wrk -t 12 -c 400 -d 30s http://localhost:8080

# After: THP無効
GODEBUG=disablethp=1 ./myapp &

# 再度測定して比較

トラブルシューティング

問題1: メモリ使用量が増加

症状:
Go 1.21にアップグレード後、メモリが増加

原因:
max_ptes_none がデフォルト(511)

解決:
echo 0 > /sys/kernel/mm/transparent_hugepage/khugepaged/max_ptes_none

問題2: レイテンシが悪化

症状:
THPを有効にしたらレイテンシが悪化

原因:
defragが積極的すぎる(always)

解決:
echo defer > /sys/kernel/mm/transparent_hugepage/defrag

問題3: OOMキラーが発動

症状:
小さなヒープなのにOOM

原因:
THPによるメモリ浪費

解決:
echo never > /sys/kernel/mm/transparent_hugepage/enabled

チェックリスト

THP設定の確認:

□ 現在のTHP設定を確認した
□ ヒープサイズを確認した
□ 適切な設定を選択した
□ defragを設定した
□ max_ptes_noneを設定した
□ 効果を測定した
□ モニタリングを設定した

まとめ表

ヒープサイズTHP設定期待効果推奨設定
< 1 GBneverメモリ節約enabled=never
≥ 1 GBmadviseスループット向上enabled=madvise<br>defrag=defer<br>max_ptes_none=0

最重要ポイント:

ヒープサイズで設定を変える! 小さなヒープ → THP無効 大きなヒープ → THP有効(適切な設定で)

推奨アプローチ:

  1. 測定: 現在のパフォーマンスを記録
  2. 設定: ヒープサイズに応じて設定
  3. 検証: 効果を測定
  4. 調整: 必要に応じて微調整

Linuxの設定も最適化の一部です! 🐧

おわりに 

本日は、Go言語のガベージコレクターについて解説しました。

よっしー
よっしー

何か質問や相談があれば、コメントをお願いします。また、エンジニア案件の相談にも随時対応していますので、お気軽にお問い合わせください。

それでは、また明日お会いしましょう(^^)

コメント

タイトルとURLをコピーしました