その理由を探るべく、我々はアマゾンの奥地へと向かった。
環境
- 少なくともElasticsearch 2以上はこの記事該当
概要
古事記にも書かれていたんじゃないかってレベルで、「Elasticsearchには32GB以上のメモリを割り当てるべきではない」とよく言われる。ESのオプション設定記事とか見てるとよく書かれている。
が、なぜ32GBなのか、Elasticsearchのヒープサイズに32GB以上を設定するとどうなってしまうのか。
そもそも本当なのか
2020年11月15日現在の最新は7系だが、公式ドキュメントを確認してみる。
Important Elasticsearch configuration | Elasticsearch Reference [7.x] | Elastic
Set Xmx and Xms to no more than the threshold that the JVM uses for compressed object pointers (compressed oops).
The exact threshold varies but is near 32 GB.
You can verify that you are under the threshold by looking for a line in the logs like the following:
確かに32GBより下にしろ的なことが書かれている
Javaのメモリについて
そもそもElasticsearchはJavaアプリケーションであり、JVM上で動作する。 よって、メモリの設定も jvm.options とかで設定できる
いったんElasticsearchから離れて、ピュアなjavaで検証する。
すごい人がめっちゃいいサンプルコードを用意してくれたのでありがたく使う。なんと内容はLinkedListをひたすら増やし続けて割り当てられたメモリすべてを使う潰すコード。
// https://gist.github.com/CodingFabian/8708393 public class Memory { // Dummy Entity representing usual data objects private static class Entity { public String name; public String detail; public Double amount; public Integer age; } // Linked list offers table inserts and helps illustrating the issue by using multiple // references per entry public static java.util.LinkedList<Entity> entities = new java.util.LinkedList<>(); // This threshold should be 2 times Xmn. It ensures the loop stops before a full GC happens. private static final int MB = 1024 * 1024; private static final int THRESHOLD = 100 * MB; public static void main(String[] args) { System.out.println("Total Memory (in bytes): " + Runtime.getRuntime().totalMemory()); System.out.println("Free Memory (in bytes): " + Runtime.getRuntime().freeMemory()); System.out.println("Max Memory (in bytes): " + Runtime.getRuntime().maxMemory()); while (true) { appendEntitiesToDataStructure(); terminateBeforeFullGCorOOMEcanHappen(); } } private static void appendEntitiesToDataStructure() { entities.add(new Entity()); } private static void terminateBeforeFullGCorOOMEcanHappen() { if (Runtime.getRuntime().freeMemory() < THRESHOLD) { System.out.println("Elements created and added to LinkedList: " + entities.size()); System.exit(0); } } }
これをヒープサイズを31GBに指定して実行してみる。
root@memory01:~# java -Xms31g -Xmx31g -Xmn50m Memory.java Total Memory (in bytes): 33285996544 Free Memory (in bytes): 33251882600 Max Memory (in bytes): 33285996544 Elements created and added to LinkedList: 538353486
これを見ると 538,353,486個のLinkedListを作成できた、と言える。
ではもう1GB増やして、32GBで再度実行してみる。
root@memory01:~# java -Xms32g -Xmx32g -Xmn50m Memory.java Total Memory (in bytes): 34359738368 Free Memory (in bytes): 34323161688 Max Memory (in bytes): 34359738368 Elements created and added to LinkedList: 353838793
結果353,838,793個。明らかにおかしい。Memory欄を見るとさっきより使用できるメモリ量は増えているのに、作成できたリストの数はさっきより全然少ない。本来なら1GB分増えているはず。。。
これはJavaの仕様で、Javaには 圧縮オブジェクトポインター機能がある。これはJavaオブジェクトを圧縮することで実メモリ使用量を削減できるありがたい機能なのだが、ヒープサイズが32GB未満が条件となる。
だからさっきの例ではヒープサイズ32GBの割当てより、31GBのほうがより多くのLinkedListを作成できたというわけ
当然この制約はElasticsearchも例外ではなく、メモリ設定が8GBとか32GB未満なら圧縮機能が有効化されていい感じになるが、32GBにしてしまうと一気にメモリ効率が悪くなってしまう。だから32GBを指定するぐらいなら
-Xms32766m -Xmx32766m
で32GB未満(32*1024= 32768MBより少ない値)を設定しろってこと。
では64GBとかは
やろうと思えばできる。効率のことは一旦忘れ、31GBの 538353486 を超える以上のヒープサイズを割り当ててしまえば結果的にはESがより多くのメモリを使えることになる。
例えば以下は64GBで実行した場合 702338196 > 538353486なので、効率は悪いが64GBのほうがより多くのLinkedListを生成できている。
root@memory01:~# java -Xms64g -Xmx64g -Xmn50m Memory.java Total Memory (in bytes): 68719476736 Free Memory (in bytes): 68708200928 Max Memory (in bytes): 68719476736 Elements created and added to LinkedList: 702338196
蛇足
今回は実際にサンプルコードで試したので実感できたが、冒頭の公式ドキュメントをもう一回読み直してみる
Set Xmx and Xms to no more than the threshold that the JVM uses for compressed object pointers (compressed oops).
The exact threshold varies but is near 32 GB.
You can verify that you are under the threshold by looking for a line in the logs like the following:
公式ドキュメントはちゃんと読もうね(ニッコリ