歴史 Go 1.19以前は、ランタイムはガーベジコレクションを制御するためにGOGCのみを提供しており、これはライブメモリに対してヒープトリガーをスケーリングします。これは、cgroupsが絶対メモリ制限を課すコンテナ化された展開には不十分であることが証明されました。開発者はランタイムが天井の概念を持っていなかったため、OOMキルに直面しました。
問題 ハードメモリ制限(例:DockerやKubernetesを介して512 MiB)があるコンテナ内でGoプロセスが実行されると、デフォルトのGOGC=100では、GCをトリガーする前にヒープが倍増することを許すことになります。コンテナ境界を意識せずに、ランタイムはカーネルがOOMキラーを呼び出すまでアロケーションを行い、プロセスをクラッシュさせ、サバイバルを優先しませんでした。
ソリューション Go 1.19では、ランタイムによって強制されるソフトメモリ制限GOMEMLIMITが導入されました。ハードキャップとは異なり、アロケーションを停止するのではなく、GCのペーシングを変更します。ヒープサイズ(スタック、グローバルデータ、およびランタイムのオーバーヘッドを含む)が制限に近づくと、ランタイムはGOGCが示唆するよりも攻撃的な新しいGCトリガーポイントを計算します。次のGCサイクルが制限を超える場合は、すぐにトリガーします。この結果、必要に応じてGCサイクルは100%のCPUを駆動することができ、スループットと安定性をトレードオフします。
import "runtime/debug" // ソフト制限を400 MiBに設定 // 値はバイト単位; 0は制限を無効にします debug.SetMemoryLimit(400 << 20) // 環境変数を介してGOMEMLIMIT=400MiBでも設定可能
危機 私たちのデータ処理パイプラインは、大きなCSVファイルを消費し、解析中にメモリを600 MiBにスパイクさせました。512 MiBの制限を持つKubernetes上にデプロイされたため、ポッドは毎時OOMKilledステータスで死にました。デフォルトのGOGCでは、制限された環境に対してヒープ比率が高すぎました。
ソリューション1: 積極的なGOGC調整 私たちは、早期のコレクションを強制するためにGOGC=20を設定することを検討しました。これによりピークメモリが約480 MiBに減少しました。しかし、CPU利用率は常に10%から40%に急増し、メモリ圧力が低いときのアイドル期間中でも同様でした。これは、リソースを無駄にし、不要にレイテンシを悪化させました。
ソリューション2: 手動GCトリガー 私たちは、高いアロケーションが報告されたときに**runtime.GC()**を呼び出すメモリウォッチドッグを実装しました。これは脆弱で、ポーリングオーバーヘッドが必要であり、突然のスパイク時にはしばしば遅すぎてトリガーし、または早すぎてスラッシングを引き起こしました。また、ランタイムが提供できる微妙なペーシングを無視しました。
ソリューション3: GOMEMLIMIT統合 デプロイメントマニフェストを介してGOMEMLIMIT=400MiB(スタックスパイクのために余裕を残す)を設定しました。ランタイムは、メモリが増えるにつれて自動的にGC頻度をスロットルしました。アイドル時にはGCは頻繁ではなく、CSV解析中にはコレクションがほぼ継続的に実行されましたが、メモリは400 MiBに保たれました。私たちは圧力のある場合にのみCPUのトレードオフを受け入れました。
決定と結果 私たちは、手動計測なしでコンテナ契約を遵守することからソリューション3を選択しました。サービスは安定し、30日間でゼロのOOMキルが記録されました。GC CPU使用率は平均8%(静的GOGCの40%と比較)で、重い解析中には25%に急増しましたが、獲得した信頼性には許容できました。
GOMEMLIMITは、計算においてゴルーチンスタックメモリをどのように考慮しますか?
多くの人がGOMEMLIMITがヒープオブジェクトのみを追跡すると考えています。実際には、この制限はGoランタイムによってマッピングされたすべてのメモリ(ヒープ、ゴルーチンスタック、ランタイムメタデータ、CGOアロケーションを含む)を含みます。ランタイムは定期的にsysメトリックを通じて使用中のメモリの推定値を更新します。何千ものゴルーチンがスタックを同時に成長させる場合、これは制限にカウントされ、小さなヒープでもGCをトリガーする可能性があります。候補者は、これは「総メモリ」制限であり、ヒープ専用ではないことをしばしば見落とします。
ライブヒープがGOMEMLIMITを永続的に超えると、アロケーションレイテンシに何が起こりますか?
候補者はしばしば、GOMEMLIMITがアロケーションをブロックするハードな天井として機能すると信じています。実際には、ソフトターゲットです。GCサイクル後のライブヒープがすでに制限を超えている場合(例:避けられない大規模なデータセットをロードしている場合)、ランタイムは次のGCトリガーを現在のヒープサイズと等しく設定し、アロケーションごとにGCが実行されます。この「GCスラッシング」はスループットよりも生存を優先します。プログラムは劇的に遅くなりますが、制限自体からパニックやクラッシュはしません。OS制限に達した場合にはまだOOMが発生する可能性がありますが、GOMEMLIMITは再利用努力を最大化することによってこれを防ごうとします。
なぜGOMEMLIMITが、メモリ使用量が制限を下回るように見える場合でもパフォーマンスの低下を引き起こす可能性があるのか?
これはスカベンジャーとペーシングのヒューリスティックスに関与しています。制限に近づくと、ランタイムはGCをより頻繁に実行するだけでなく、MADV_DONTNEEDを介して物理メモリをOSにより積極的に返します。アプリケーションが鋸歯状のアロケーションパターン(スパイクからアイドル)を持つ場合、スカベンジャーはページをリリースするかもしれませんが、次のスパイクでそれらを再度フォールトします。この「ページフォールトストーム」はレイテンシスパイクとして現れます。候補者は、GOMEMLIMITがGOGCと最低トリガー計算を介して相互作用することを見落とします:制限は実際にはGC頻度のフロアを設定し、ランタイムが成長が制限を超えると予測する場合にはGOGCをオーバーライドできます。