私は 100 近くの OpenSearch クラスターを管理しています。先日、Heap のアラートが鳴りました。ただし全クラスターではなく、いくつかのクラスターだけです。このアラートの条件は Heap 使用率が 75% を超えることです。鳴ったクラスターは数分後に自然に回復し、また鳴る、を繰り返していました。

最初はノードに問題が起きたのではと心配になり、まずクラスターに異常がないか確認しました。

  • クラスターの状態:Green で、Unassigned Shard はなし
  • 書き込みは正常:Fluentd のバッファは溜まっておらず、リトライも続いていない
  • Pod は OOM になっていない:再起動回数は増えておらず、Last State も OOMKilled ではない

3 つとも正常だったので、Heap のメトリクスを開きました。すると Heap 使用率は「上がり続けて、落ちて、また上がる」というノコギリ型の波形でした。アラートはちょうど山の頂点で鳴り、数分後に落ちたところで回復していたのです。

最初は 75% のしきい値を上げようと考えました。しかし鳴るのは一部のクラスターだけなので、しきい値だけの問題ではなさそうです。さらに不思議だったのは、同じノコギリ型でも、ほかのクラスターは 60% で落ちるのに、このクラスターは 80% まで上がってから落ちることでした。設定がほかと違うのか? 特に忙しくてメモリが足りないのか?

これに答えるには、まず JVM がどうやってメモリを回収するのか、特に Young GC を理解する必要があります。

JVM のメモリ回収のしくみ

Java プログラムは新しいオブジェクトを次々と作ります。使い終わったオブジェクトはすぐには消えず、GC(ガベージコレクション)が片付けるまで残ります。そのため Heap 使用量は GC が回収するまで上がり続け、回収されると下がります。これがノコギリ型の正体です。

JVM の GC は「ほとんどのオブジェクトはすぐに死ぬ」という観察に基づいています。そのため Heap は 2 つの領域に分かれています。

  • Young 領域:新しいオブジェクトが生まれる場所。中のオブジェクトの多くはすぐに死ぬ
  • Old 領域:何度かの回収を生き延びたオブジェクトが移される場所。通常は長く生き続ける

JDK 11 以降の OpenSearch はデフォルトで G1 という GC を使います。回収には主に 2 種類あります。

  • Young GC:Young 領域だけを回収します。プログラムは一瞬止まりますが、通常は数十ミリ秒です。生きているオブジェクトが少ないため、ほぼ常に発生しており、頻繁に起きても問題ありません
  • Full GC:Heap 全体を回収します。プログラムは数秒、ひどい場合はそれ以上止まります。その間ノードはクラスターマネージャーのヘルスチェックに応答できません。止まっている時間が長ければ(デフォルトでは 1 回 10 秒待ち、3 回連続で応答なし)、ノードはクラスターから外され、Shard の再配置が必要になります。本当に避けたいのはこれです

G1 は普段から Old 領域の一部も少しずつ回収しますが、一度にすべてを片付けるわけではありません。

クラスターごとにノコギリの高さが違う理由

Young GC は Young 領域がいっぱいになったときに起きるので、ノコギリの頂点はおおよそ次のようになります。

頂点 ≈ Old 領域の使用量 + Young 領域のサイズ

Young 領域のサイズは固定ではなく、G1 が動的に決めます。G1 には停止時間の目標(デフォルト 200ms)があり、そのクラスターで 1 回の回収にかかる時間を予測して、目標を超えない Young 領域のサイズを選びます。デフォルトの範囲は Heap 全体のおよそ 5% から 60% です。クラスターごとに負荷が違うので、選ばれるサイズも違います。

仮の例を挙げます。

Old 領域Young 領域ノコギリの頂点
クラスター A20%40%60%
クラスター B20%60%80%

2 つのクラスターの Old 領域は同じですが、G1 が B に大きめの Young 領域を選んだため、B は 80% まで上がってから落ちます。B の状態が悪いわけではなく、ノコギリが高いだけです。

最初の疑問に答えると:

  • 設定がほかと違うのか? 必ずしもそうではありません。Young 領域のサイズは G1 が自分で調整するので、もともと違って当然です
  • 特に忙しいのか? 忙しいと Young 領域が早く埋まり、GC が頻繁になってノコギリは細かくなりますが、高くなるとは限りません
  • メモリが足りないのか? 見るべきはノコギリの高さではなく、GC の後にどれだけ残っているかです

つまり固定の Heap% しきい値のアラートは、そもそも複数のクラスターに共通して使えません。100 近いクラスターのうち数個しか鳴らなかったのは、正常なノコギリの頂点がたまたま 75% を超えるクラスターだけが引っかかったからです。

見るべきは GC 後の Old 領域の底

Young 領域のサイズがノコギリを上下させるなら、その影響を受けない部分を見ればよいのです。それが Old 領域 です。ここには Shard の固定的な構造、Cluster State、Fielddata、キャッシュなど長寿のオブジェクトが入っており、使用量は Young 領域のサイズで変わりません。

GC で回収された後の Old 領域の底の値は、「実際に使われているメモリの量」に最も近い値です。正確な値ではありません(G1 は一度に Old 領域の一部しか回収しないため、底は実際の使用量より少し高くなります)が、クラスター間で比較する指標としては十分です。Old 領域の底ですら長期間高いままなら、本当に使っているにせよ G1 の回収が追いつかないにせよ、そのノードは限界が近いということです。

min_over_time で一定期間の Old 領域使用量の最小値を取り、Heap の上限で割ります。

min_over_time(opensearch_jvm_mem_pool_used_bytes{pool="old"}[15m])
  / on(cluster, node) opensearch_jvm_mem_heap_max_bytes

時間窓は G1 が Old 領域を一巡回収する周期より長くする必要があります。15 分はあくまで出発点で、クラスターに合わせて調整してください。私はアラートのしきい値を 0.80 から始め、for: 15m を組み合わせました。

まとめ

Heap 全体のしきい値でアラートを設定するのではなく、Old 領域の量を使いましょう。

  • Heap 全体:ノコギリの頂点は Young 領域がいっぱいになりかけていることを示すだけで、高さはクラスターごとに違います。固定の Heap% しきい値は複数のクラスターに共通して使えません
  • GC 後の Old 領域の底:Young 領域のサイズの影響を受けないので、異なるクラスターでも同じしきい値を使えます