はじめに
こんにちは。IT基盤部第二グループの若松です。普段は、ゲームプラットフォームにおけるインフラの安定稼働とコスト最適化に日々取り組んでいます。
私たちのチームでは、GCE (Google Compute Engine) を活用したインフラ運用を行っています。GCE のインスタンスを使用する上で、コスト最適化の観点でインスタンススペックの適正化が重要となります。その中で、E2 マシンタイプを使用している一部のインスタンスで top コマンドのプロセス別表示では説明できないメモリ使用量の増加が起き、コスト削減の障壁となっていました。
本記事では、このメモリ使用量の増加現象の根本原因を調査し、その結果Linuxカーネル・GCE ハイパーバイザの仕組みからくる見掛け上のメモリ使用量増加であることがわかった事例をご紹介します。そして、その調査結果を元に見掛け上のメモリ使用と実際のメモリ使用をメトリクスで区別できるようにし、安全なコスト削減へと繋げた方法をお伝えします。
この記事をお読みいただくことで、GCE の E2 マシンタイプで「virtio メモリバルーニング」と呼ばれる仕組みがどのように機能し、メモリが見掛け上使用される事象が発生するのかを深く理解していただけるかと思います。また、大規模インフラ特有の日々の運用で直面する一見小さな現象の解明がスケールすることで大きなコスト削減につながる面白さを感じていただけることを願っています。
見掛け上のメモリ使用量増加という課題
私たちのチームでは、コスト最適化の一環としてGCEインスタンスの適切なサイジングを常に見直しています。とくに、e2-highmem-8(CPU 8コア、メモリ 64GB) のような高性能なマシンタイプを使用しているインスタンスが十数台存在しており、CPU 使用率から判断するとより少ないCPUリソースでも十分に稼働できることが分かっていました。
しかし、これらのインスタンスでは、数分間という短時間のメモリ使用量が急増するタイミングが日に数度程度の頻度で観測されており、これがインスタンスサイズ縮小の大きな障壁となっていました。単純にインスタンスタイプを下げるとこの急増時にメモリが不足し OOM を起こす可能性があったため、原因が特定できないままのスペックダウンはできない状況でした。また、すでに E2 の highmem で vCPU あたりのメモリが上限の 8GB に達しているため、これ以上 CPU に対するメモリの比率を上げるには、N2 や N4 などのシリーズで拡張メモリ付きのカスタムマシンタイプを使用する必要があり、リソースに対するコスト効率が悪くなります。このため、CPU のみのスペックダウンではコスト削減効果が減少してしまうことからも、原因を究明しておきたい状況でした。
このため、原因を特定するために、発生時のプロセスごとのメモリ使用量を確認しました。以下がメモリ使用量増加が起きている最中の top の出力となります。メモリ使用率ベースでソートするように設定しており、メモリ使用率の高いプロセスが上位に表示されるようになっています。
top - 02:12:56 up 491 days, 0 min, 2 users, load average: 1.57, 0.94, 0.68
Tasks: 615 total, 2 running, 538 sleeping, 0 stopped, 0 zombie
%Cpu(s): 1.5 us, 1.0 sy, 0.1 ni, 97.0 id, 0.0 wa, 0.0 hi, 0.3 si, 0.0 st
KiB Mem : 65863984 total, 13351540 free, 51490760 used, 1021684 buff/cache
KiB Swap: 0 total, 0 free, 0 used. 13683132 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
4443 user1 20 0 1197304 504044 19236 S 0.0 0.8 5:56.32 process_a
17292 user1 20 0 1224004 404144 13196 S 0.0 0.6 3:35.72 process_b
17122 user1 20 0 1154200 354816 13248 S 0.0 0.5 1:10.05 process_c
24780 user1 20 0 1124268 307008 11684 S 0.0 0.5 30:52.35 process_d
...
この出力を見ると、KiB Mem の行には「51490760 used(約52GB)」と表示されています。このインスタンスでは平常時のメモリ使用量が約 18GB であり、これと比べて約 34GB 増加している状態です。
しかし、下のプロセスリストを確認しても、process_a が約500MB、process_b が約400MBといったように、個々のプロセスが大量のメモリを消費しているわけではありませんでした。 これらのプロセスの RES(Resident Set Size、常駐メモリ)を合計しても、52GBという数値には遠くおよびません。ここから、通常のアプリによるメモリ使用量増加ではなく、何らかの OS などに起因する特殊なメモリ使用量の増加が起きていることが推察されました。
vmstat を用いた原因調査
top コマンドや Node Exporter による常に動いている計測結果では原因が不明だったため、さらに詳細な OS 領域まで踏み込んだメモリ使用状況を把握する必要がありました。そこで、メモリ使用量急増の発生頻度が高いインスタンスで、/proc/vmstat の内容を10秒ごとに継続的に記録することにしました。/proc/vmstat からはシステム全体のメモリ管理に関する詳細な統計情報を取得できます。記録された /proc/vmstat の内容を分析したところ、balloon_inflate・balloon_deflate という値で特徴的な差分が見つかりました。平常時にはこの2つの値が一致していますが、メモリ使用量が急増している異常時には、両者の間に大きな差分が生じていることが判明しました。実際に確認された balloon_inflate と balloon_deflate の値が以下です。
# 平常時
balloon_inflate 12819840788
balloon_deflate 12819840788
# メモリ使用量増加時
balloon_inflate 12805570836
balloon_deflate 12797230612
この 2つの値の意味と読み取り方は
Linux カーネルの導入コミット
で説明されています。この値はメモリバルーンと呼ばれる仮想マシン上で動く Linux マシンのメモリ増減機構により確保・解放されたメモリページ量を示すものになります。balloon_inflate はバルーンに追加されたメモリページ数の累計、balloon_deflate はバルーンから戻されたメモリページ数の累計となります。このため、(balloon_inflate - balloon_deflate) で現在バルーンに含まれるメモリページ数を計算できます。今回の場合、1ページ 4KiBであるため、バルーンに含まれるメモリは (12805570836 - 12797230612)ページ x 4KiB/ページ = 33,360,896KiB ≒ 34.2GB(31.8GiB) となり、top により確認されたメモリ増加量に極めて近い値となります。
ここから、今回のメモリ使用量増加はメモリバルーンによるものであることがわかりました。これをもとに、GCE でメモリバルーンが膨らむ条件を調べたところ、E2 マシンタイプでは virtio メモリバルーンと呼ばれる動的リソース管理メカニズムがあるという公式ドキュメントが見つかりました。
virtio メモリバルーニング
ここで、virtio メモリバルーニングについてもう少し詳しく説明します。この機能は、GCE E2 マシンタイプ特有の動的リソース管理メカニズムです。ハイパーバイザがゲスト OS 内での未使用メモリを確保するインターフェースを提供します。これにより、GCE 基盤内で物理メモリがより効率的に活用され、E2 マシンタイプの高いコストパフォーマンスが実現されています。
具体的には、ハイパーバイザがゲスト OS に対して、一定量の空きメモリページを提供するように要求するプロセスを「メモリバルーンの膨張 (inflate)」と呼びます。これにより、ホストはゲストが使用していないメモリを一時的に回収し、それを他のVMに割り当てて効率的にリソースを利用できるようになります。逆に、ハイパーバイザがゲストOSにメモリを返すことを「メモリバルーンの収縮 (deflate)」と呼びます。 ハイパーバイザは inflate・deflate を行うことでメモリを仮想的に確保します。 そして、確保したメモリをハイパーバイザや別のゲスト OS が活用することで、全体としてのリソース効率が向上します。 これにより得られる、GCE 側で物理メモリの利用効率向上が E2 マシンタイプの高いコストパフォーマンスを支える要素の1つとなっています。
このメモリバルーニングによるメモリ確保は、以下にある通りゲスト OS の空き領域の情報をもとに動作に影響のない範囲で行われるものになります。
ゲスト オペレーティング システムは、使用可能なメモリをホストシステムに知らせます。ホストは、未使用のメモリをオンデマンドでその他のプロセスに再割り当てし、メモリを効率的に使用します。
このため、今回観測したバルーン分は、ゲストの実メモリ需要を評価する際に除外できます。ただし、バルーンが発生したことだけでは継続的なスペックダウンの安全性は判断できないため、代表的な期間のピーク需要を別途確認する必要があります。
なお、動的リソース管理が行われるマシンタイプは E2 マシンタイプの他に N4・N4A・N4D マシンタイプがあります。しかし、これらのマシンタイプでは E2 マシンタイプとは異なる仕組みとなっており、virtio メモリバルーニングは使用されません。このため、今回確認された見掛け上のメモリ使用量増加が発生するのは E2 マシンタイプのみとなります。
Node Exporter 設定変更による可視化
ここまでの調査で実際に観測できたメモリ使用量増加については、原因がvirtio メモリバルーニングであると特定できました。しかし、安全にスペックダウンを行うためには、他のインスタンス・時間でもメモリバルーン以外でのメモリ使用量増加が発生していないと断言できるだけの十分な回数の観測を行いたいです。
私たちの環境では Grafana + Prometheus + Node Exporter でのメモリ使用量およびその内訳の監視を行っています。Node Exporter のデフォルト設定でも vmstat の監視は行うのですが、ストレージなども考慮して一部のメトリクス以外は export しないようになっています。そして、balloon_inflate・balloon_deflate・balloon_migrate といったメモリバルーニングに関連する詳細なメトリクスはこのデフォルトでの収集対象とはなっておらず、計測できていない状態となっていました。
そこで Node Exporter に --collector.vmstat.fields オプションを設定し、これらのメトリクスを収集できるように設定を変更しました。--collector.vmstat.fields オプションは vmstat 中から取り出すメトリクスを正規表現で指定できるオプションです。 Node Exporter の vmstat メトリクスでは /proc/vmstat から指定された正規表現に合致するフィールドのみを取り出す実装がされています(
実装箇所
)。そこで、Node Exporter に以下のオプションを設定することで、メモリバルーンに関するメトリクスを収集できるようにしました。
--collector.vmstat.fields=^(oom_kill|pgpg|pswp|pg.*fault|balloon_).*
この設定により、vmstat から提供される balloon_inflate・balloon_deflate のメトリクスが Prometheus 経由で収集され、Grafana で可視化できるようになりました。これにより、継続的なメモリバルーンの観測が可能になり、正確な状況把握ができるようになりました。
以下が実際にメモリバルーンが発生した際のメモリ使用率分布のグラフです。青の Unused の領域を削る形で黄色で示された Balloon の領域が大きく増え、ハイパーバイザが5分程度メモリを確保していることが見て取れます。このメモリ確保は空き領域を検知して行われるものなため、インスタンスのスペックダウンにおいては考慮する必要のないものとなります。
その後、一定期間観測を続け、メモリバルーンを除いたメモリ使用量が十分少ないことが確認できました。
コスト削減効果
ここまでの原因調査および計測対象メトリクスの追加により、インスタンスのスペックダウンを安全に行えることが確認できました。これによって、対象となるインスタンス十数台でマシンタイプを e2-highmem-8(CPU 8コア、メモリ 64GB)から e2-highmem-4(CPU 4コア、メモリ 32GB) へのスペックダウンを行い、対象インスタンスのコストを約50%削減することができました。
また、今回元々対象としたインスタンス以外でも E2 マシンタイプは多数使用されていましたが、これらについても調査した結果、追加のスペックダウンの余地が見つかりました。このように、メモリ使用量上昇という局所的な事象に関する詳細な調査がスケールし、より大きなコスト削減につながりました。
まとめ
本記事では、GCE E2 マシンタイプで発生していた「見掛け上のメモリ使用量増加」という課題に対し、その根本原因を突き止め解決に至るまでの流れをご紹介しました。
私たちは、top コマンドでは捉えきれないLinuxカーネル内部のメモリ使用状況を vmstat を用いて詳細に調査し、その原因がGCE E2 VM特有のvirtio メモリバルーニングであることを特定しました。さらに、Node Exporterのオプションを活用してメモリバルーニング関連メトリクスを監視に組み込むことで、アプリケーションによるメモリ消費と区別し、真のメモリ使用状況を正確に可視化できるようになりました。この一連の取り組みにより、見掛け上のメモリ使用量増加が運用上の実メモリ不足ではないことを確証し、大規模なインスタンスのスペックダウンとコスト削減を実現しました。
このような問題を掘り下げて根本原因を特定することでスケールし、当初の想定よりも大きなコスト削減につながるのは規模の大きいインフラ運用の醍醐味の1つと思います。
本記事が、読者の皆様の今後の業務の一助となれば幸いです。
最後まで読んでいただき、ありがとうございます!
この記事をシェアしていただける方はこちらからお願いします。