max clause countの動的生成ロジック

背景

Elasticsearch8.0から indices.query.bool.max_clause_countがdeprercatedになりました。

これまで自分で設定できていましたが、動的に設定されるようになったということでどういうロジックで値が決まるのかコードを参照しながら確認します。

max clause countが何かについてはこちらの記事を確認ください。

ryook.hatenablog.jp

結論

先に結論を書くと、以下の式で決定します。

max clause count = (Nodeのメモリサイズ(MB) * 64) / ((NodeのCPU数 * 1.5) + 1)

目次

変更点

これまで自分で設定できていたものが、動的に設定されるようになりました。

ドキュメントには、

Larger heaps lead to higher values, and larger thread pools result in lower values. This limit has a minimum value of 1024 and will in most cases be larger (for example, a node with 30Gb RAM and 48 CPUs will have a maximum clause count of around 27,000)

ヒープが大きいと値が大きくなり、スレッドプールが大きいと値が小さくなります。最小値は 1024 で、ほとんどの場合はこれより大きくなります(たとえば、30Gb RAM と 48 CPU のノードでは、最大句数は約 27,000 になります)(by DeepL)

とかかれています。

Search settings | Elasticsearch Guide [8.6] | Elastic

どうやって計算されるのか

30Gb RAM と 48 CPU のノードでは、最大句数は約 27,000 になります

といわれても、どういうロジックで決まるのか気になります。

Elasticsearchのコードを探してみると、SearchUtilsクラスにそれらしきロジックを見つけました。

github.com

該当箇所を抜粋するとこういうロジックらしいです。

long maxClauseCount = (heapInMb * 64 / threadPoolSize);

return Math.max(DEFAULT_MAX_CLAUSE_COUNT, (int) maxClauseCount);

コメントには

BooleanClauseはポスト、位置、オフセット、影響などをロードするために、最大で16kのメモリを使用することがあるためヒープサイズ(kb)を16kで割るか、ヒープサイズ(mb)を64倍することで1 つのスレッドプールでサポートできる句の最大数を計算して、それを同時検索可能数で割ることで最大数を算出しています。(by DeepL)

ということらしいです。

ロジックがわかったところで、それぞれの値はどうやって決まるのか見ていきます。

各値について

threadPoolSize

ドキュメントみるとsearchのthread poolは int((# of allocated processors * 3) / 2) + 1 になるようです。

Thread pools | Elasticsearch Guide [8.6] | Elastic

どういうことかコードを見てみます。

まず先ほどのSearchUtilsクラスでthreadPoolからsearchの設定を取得しているようです。

public static int calculateMaxClauseValue(ThreadPool threadPool) {
        int searchThreadPoolSize = threadPool.info(ThreadPool.Names.SEARCH).getMax();
  ....
    }

calculateMaxClauseValueメソッドはNodeクラスのコンストラクタで呼ばれます。

github.com

このときに引数で渡されるthreadPoolのインスタンスも作成されます。

github.com

ThreadPoolクラスの実装を見ていくと、コンストラクタで ((allocatedProcessors * 3) / 2) + 1 と定義されていました。(該当コードのみ抜粋)

final int allocatedProcessors = EsExecutors.allocatedProcessors(settings);

builders.put(
            Names.SEARCH,
            new FixedExecutorBuilder(settings, Names.SEARCH, searchOrGetThreadPoolSize(allocatedProcessors), 1000, true)
        );


public static int searchOrGetThreadPoolSize(final int allocatedProcessors) {
        return ((allocatedProcessors * 3) / 2) + 1;
    }

allocated processors

割当られるプロセスの数は自動で割り振られるか自分で設定できます。

Thread pools | Elasticsearch Guide [8.6] | Elastic

自動で割り振られる場合は EsExecutors クラスの allocatedProcessorsメソッドで割当数が決まります。

public static int allocatedProcessors(final Settings settings) {  
    return NODE_PROCESSORS_SETTING.get(settings).roundUp();  
}

github.com

Returns the number of allocated processors. Defaults to Runtime.availableProcessors() but can be overridden by passing a Settings instance with the key node.processors set to the desired value.

EsExecutors - elasticsearch 8.6.0 javadoc

とのことなので、なにも設定していなければ 仮想マシンのプロセッサ数がそのまま採用されるようです。

計算方法

改めて計算方法をまとめると

max clause count = (Nodeのメモリサイズ(MB) * 64) / ((NodeのCPU数 * 1.5) + 1)

となります。

ドキュメントの例(30Gb RAM と 48 CPU のノードでは、最大句数は約 27,000 )を検証すると、プロセッサ数はCPU数、heapはメモリの最大30GB = 30720MBとおけば

(30720 * 64) / ((48 * 3 / 2) + 1) = 26932.602739726 で約27000になります。

とはいえ、heapにメモリを全部使うことはまあ無いと思うので現実的にはその半分くらいとすると、max_clause_countの目安はだいたい次の式になりそうです。

(Nodeのメモリサイズ(MB) * 32) / ((NodeのCPU数 * 1.5) + 1)