OpenSearchCon Europe 2024のセッション「Multi-tenancy for all workloads」を日本語でまとめます。 可能な限り正確に内容を拾えるようにリスニングに努めたつもりですが、もし誤りがあればご指摘ください。
- OpenSearchCon とは?
- Multi-tenancy for all workloads
- スピーカー
- セッションまとめ
OpenSearchCon とは?
各セッションはYouTubeで視聴可能
Multi-tenancy for all workloads
セッションリンクは以下
スピーカー
- John Handler
- AWSのシニアプリンシパルソリューションアーキテクト
- OpenSearchと検索技術について長年の経験を持つ
セッションまとめ
マルチテナント問題の深刻さ

John Handler氏は、AWSのOpenSearch Serviceを利用する多くの顧客と日々対話している。その中で、レイテンシーの問題に苦しんだり、OpenSearchドメインの安定した運用に困難を抱えている顧客の支援も頻繁に行っている。これらの問題の約80%という大多数がマルチテナントに関連する問題から生じているという。
マルチテナントは、OpenSearchクラスタを適切にデプロイし、良好なパフォーマンスを維持してトラフィックを処理するために、正しく設計しなければならない重要な要素である。マルチテナントソリューションを適切に構築することが、OpenSearchの成功の鍵となる。
すべてのOpenSearchクラスタは何らかの形でマルチテナントである

OpenSearchを使い始める際、多くの人は広大なレーストラックを疾走するレースカーのようなイメージを持っている。各リクエストがすべてのリソースを独占的に使用でき、他のトラフィックを考慮する必要がない理想的な環境だ。道路は常に開いており、跳ね橋が上がることもなく、他のドライバーのことを考える必要もない。

しかし現実のOpenSearchクラスタは、混雑した都市の道路のようなものである。常に多数のリクエストが同時に処理されており、それぞれのサイズと複雑さは様々だ。これらのリクエストは基盤となるリソースを巡って競合し、たった1つの遅いリクエストが全体のトラフィックを何マイルにもわたって停滞させることもある。

すべてのOpenSearchクラスタは何らかの形でマルチテナントである。テナンシーはコンテナのイメージで捉えることができ、それらのコンテナをどのようにパッキングするかが重要となる。クラスタを設計する際は、トラフィックの状況を考慮し、適切にデプロイすることが不可欠である。
単一ワークロード
まずは物事をシンプルに考えるため、単一ワークロードにおけるリソースの考え方を整理する。

リソース配分の基本メカニズム

インデックスはOpenSearchにおける主要なテナントコンテナである。テナンシーは様々なレベルで存在し、多言語ドキュメントの場合はドキュメントレベルでも存在するが、最も中心的なのはインデックスレベルのテナンシーである。
インデックスはリクエストの送信先となり、インデクシングや検索の際に指定される。検索ワークロードでは通常1つのインデックスを使用し、ログワークロードでは1つのアクティブなインデックスと多数の古い受動的なインデックスを持つことが多い。インデックスこそがクラスタ内の最も自然なテナント単位である。

インデックスはシャードで構成されており、プライマリシャード数を設定することでデータの分割数を決定する。OpenSearchはこれらのシャードをハードウェアに分散配置し、処理能力を割り当てる。シャードこそが実際の作業を行うワーカーであり、インデクシングやクエリ処理のためのリソースにアクセスする重要なコンポーネントである。

リクエストが送信されると、まず単一のノードに到達し、そのノードがコーディネーターとして機能する。コーディネーターノードは、リクエストを処理に必要なすべてのシャードにネットワーク経由で分散し、各シャードが作業を実行して結果を返す。このプロセス全体を通じて、リクエストはシステムの各段階でキューイングされる。

各ノードには、リクエスト処理に必要な複数のリソースが存在する。コーディネーターノードにもキューがあり、シャードを持つノードにもキューがある。最終的にCPUがキューからリクエストを取り出して処理する。
リクエストを処理する際にはCPU、ディスクアクセス、メモリ(Javaヒープ、オフヒープRAM、ベクトルRAM)が必要となる。垂直スケーリングではCPUやオフヒープRAMを増やすことができるが、Javaヒープサイズより多くのヒープRAMを追加することはできない。より多くのヒープRAMやキュー容量が必要な場合は、水平スケーリングを選択する必要がある。

インデクシングでは、コーディネーター用に1つのCPU、各プライマリシャードとレプリカ用にそれぞれ1つのCPUが必要となる。
検索では、コーディネーター用に1つのCPU、プライマリシャード1につき1つのCPUが必要となる。(注:この計算は単純化のため、Concurrent Segement Searchを計算に入れていなさそう)
デフォルトの5プライマリ・1レプリカのインデックスでは、10個の総シャードがあるため、インデクシングには11個のCPU、検索には6個のCPUが必要となる。
スループットの理論的限界

CPUキャパシティの観点から見ると、インデクシング用のスレッドプールはvCPU数まで拡張される。スレッドがタスクを取得すると、完了するまでそのタスクに専念し、その後CPUを解放する。そのため、スループットはリクエスト処理時間によって制限される。
検索クエリの処理でも同様に、検索スレッドプールがCPU数まで拡張され、処理時間がスループットを決定する。読み書きが混合する場合、インデクシングプールと検索プールがCPUを巡って競合するため、より複雑になるが、基本的な原理は同じである。

理論的には、各CPUは1秒あたり1秒分の処理能力を提供するため、クラスタのスループットは総CPU数を平均リクエスト処理時間で割った値で制限される。例えば、200ノードで各v64CPUのクラスタ(計12,800CPU)で、シャードが適切に分散しており、平均クエリ時間が10ミリ秒の場合、理論的には128万クエリ/秒が実現可能な最大値となる。実際の運用では、81データノードで28,000クエリ/秒を達成した例を見たことがある。それは単一ノード、単一プライマリインデックスに80レプリカという構成だった。
インデクシングは比較的高速であり、小さなドキュメントであれば1ms程度で格納できる。200ノードで各v64CPUのクラスタ(計12,800CPU)の前提では、128万〜1280万リクエストを処理できる計算になる。実際の運用では、80ノードクラスタで毎分2億メトリクス(毎秒130万メトリクス)を処理した例を見たことがある。

単一ワークロードの場合、これらの指標を基にCPU数、ノード数、シャーディング戦略を計画できる。シャードとノードを簡単に整合させることができ、理想的な分散を達成できる。読み書きが重い場合を除いて競合は発生せず、サイジングの決定も比較的簡単である。
マルチテナント

ここからは、より複雑なマルチテナントの議論に入っていく。
マルチテナント戦略の3つのアプローチ

テナンシーはコンテナへのメンバーシップの概念で捉えられる。複数のログストリームワークロードでは各ログストリームがテナントとなり、各インデックスがクラスタコンテナ内のテナントとなる。検索側では、単一インデックス内の複数ユーザーがテナントとなることが多く、これらはインデックスコンテナ内のテナントである。

これらのテナントを管理する主な戦略は3つある。
サイロ戦略では、各テナントが独自のコンテナを持つ。検索ワークロードでは各ユーザーが独自のインデックスを持ち、ログワークロードでは各ログストリームや日付ごとに独自のインデックスを作成する。
プール戦略では、複数のテナントを同じコンテナに配置する。検索では1つのインデックスにすべてのテナントを含め、ログでは小規模なストリームを1つのインデックスに統合し、フィールドとクエリフィルターで切り替える。
現実的には、ほとんどの場合3つめのハイブリッド戦略に行き着く。必ず起こることだが、100のテナントのうち99は規模Xだが、100番目のテナントは規模100Xという状況になる。20,000のテナントのうち、1つ、10、または15のテナントが桁違いに大きくなることは避けられない。
この場合、大規模テナントを独自のコンテナに分離する。ログワークロードなら、そのテナントを独自のクラスタに分割して個別に管理する。単一テナントをスケーリングする方がはるかに簡単で効率的であり、専用のハードウェアを正確に割り当てることができる。他のテナントと組み合わせていると、最大サイズに合わせて全体を引き上げる必要があり、非効率が生じる。
サイロモデル

サイロモデルでログ分析の基盤を構築するとたら、ログソースが独自のクラスタを持つことになる。様々なデバイスからのsyslogがData Prepperなどのインジェストパスウェイを通じてOpenSearch Serviceに送信される。プライマリシャードを5に設定した場合、実際には10個のシャード(レプリカを含む)が配置されるが、これは理想的な配置にはならない。
よく見られるスキューの例として、2つのノードに2つずつシャードが配置され、1つのノードには1つのシャードしか配置されないケースがある。シャードが1つのノードは2つのノードより処理が速く終わるため、ワークロードの不均衡が生じる。この種のワークロード不均衡は常に発生する問題であり、シャード数とノード数を整合させることが推奨される理由の1つである。
サイロモデルの利点は、すべてを独立してスケーリングできることだ。各クラスタは単一テナントであり、テナントごとに適切なサイズに調整することが容易である。テナントが分離されることで、1つが落ちても他は影響を受けない。この影響の最小化は重要な設計ポイントである。
一方で、7,000、10,000、20,000ものクラスタを運用することは極めて困難である。監視と運用の体制を整え、障害時に修復できる体制が必要となる。テナント間でクエリを実行したい場合はクロスクラスタ検索が必要になり、運用が煩雑なるデメリットもある。
プールモデル
プールモデルが必要とされる背景

プールモデルが必要とされる背景として一般的に見られるのは、複数のインデックスパターンを持つケースである。中央集約型ログ環境では、複数のエンドユーザーグループがOpenSearchの利用者となり、それぞれのワークロードをOpenSearchに送信する。これらのグループは異なる速度とサイズを持ち、多くの場合、彼らが何を送信しているか、どんなクエリを実行しているかを直接制御することはできない。これらの環境は運用が非常に困難であり、何らかのガバナンスが必要となる。
複数インデックスでのプールモデル

このシナリオに対処する方法として、多くの顧客が採用するのがプールモデルのアプローチである。単一のドメインまたはクラスタを立ち上げ、すべてのトラフィックをそこに送信し、インデックスを単一クラスタに集約する。インデックスレベルのアクセス制御やきめ細やかなアクセスコントロールを使用して、誰が何を見られるかを制御する。
この方法はより実装が簡単である。多くの人は、ドメインを立ち上げてダウンストリームユーザーに提供し、彼らが必要に応じてデータを送信すればすべてうまくいくという考えでこのアプローチに至る。
プール化の利点として、異なるインデックスライフサイクルをサポートできる点がある。グループAは30日保持、グループBは90日保持といった要件に対応可能で、各グループが独自のインデックスセットを持つため、大きなクラスタ内で好きなように管理できる。テナントがインデックスであるため、きめ細やかなアクセスコントロールで特定のインデックスへのアクセスを許可または拒否でき、アクセス制御の実装も簡単になる。テナントの移動も容易になる。
しかし、プール化アプローチにも欠点がある。シャード数と非アクティブシャード数が大幅に増加する問題である。前述のシャードとCPUの関係を考えると、常にインデクシングを行う異なるインデックスをサポートするために必要なCPU数で圧倒されることになる。
スキューも大きな問題となる。あるグループは1日1GBのログを送信し、別のグループは1日20TBを送信する。これらの異なるサイズのワークロードでシャードとノードを均等に配置することは非常に困難である。手動でAllocation APIを使用してシャードを移動することもできるが、60,000シャードを手動で移動するのは現実的ではない。
また、異なるスケールがコストの問題を引き起こす。実際のワークロードだけでなく、ワークロード間の干渉にも対処し、全員がある程度満足できるようにスケーリングする必要がある。
もう1つの問題は、Disk I/Oの消費である。インデクシングワークロードのI/Oは、インデックスのリフレッシュに基づいて消費される。リフレッシュ間隔を設定すると、インデックスが更新される頻度とデータがディスクにフラッシュされる頻度が制御される。1つのインデックスではOpenSearchのRAMバッファリングが書き込み量を軽減するが、1000個のインデックスがあると、リフレッシュ間隔が互いに干渉し、ディスクへのI/Oチャネルを消費してしまう。特にネットワーク接続ストレージでは、これが大きな問題となる。
単一インデックスでのプールモデル

多くの場合、最初はすべてに個別のインデックスを割り当てることが多いが、1日1GBの50個のワークロードがあれば、それらを1つのインデックスにプールする方が一般的に効率的である。50セットのインデックスを運用する代わりに、すべてのテナントを組み合わせた1インデックスを運用する。
ワークロード間の検索にはフィルターを使用する。各ワークロードにテナント識別子を含むメタデータエンベロープを書き込み、ダッシュボードやクエリではそのテナント識別子でフィルターを含める。
これによりシャード数が大幅に削減される。各シャードはJavaヒープ内でスペースを消費するため、最大ヒープサイズでもノードあたり1000シャードという制限がある。リクエストを処理していなくても、シャードを保持するだけでヒープを使い果たさないようにする必要がある。インデックスにプールすることでシャード数を削減し、ドメインとクラスタのパフォーマンスが大幅に向上する。
単一インデックスの場合、バランスを取ることも容易になる。ただし、ノイジーネイバー問題は依然として存在する。1日1GBの95%と1日10TBの5%がいる場合、10TBのワークロードが他のすべてに干渉する。また、異なるワークロードサイズでのコスト非効率も残る。
ドキュメントルーティング

プールされたケースでは、ドキュメントルーティングを使用して非常に高いテナンシーを処理できる。これは通常、単一インデックスに1,000、10,000、または15,000のテナントを持つ検索ワークロードの文脈で使用される。
デフォルトでは、インデクシングリクエストのデータはプライマリシャード全体でランダムにハッシュ分散される。これは実際ほとんどの場合で良好なパフォーマンスを提供する。しかし、データの分散に使用するハッシュキーを指定することもできる。URLパラメータとして送信し、通常はテナントIDをハッシュキーとして使用する。
そのテナントのすべてのデータが単一ノード(またはそれより広い範囲)にハッシュされ、すべての検索リクエストがその1つのノードだけに送信される。
通常、検索リクエストは複数のシャードに分散(スキャッター)され、各シャードが並列に検索を実行し、その結果をコーディネーターノードに集約(ギャザー)する。この並列処理は大量のデータを高速に検索するには効果的だが、ネットワーク通信と結果集約のオーバーヘッドが発生する。
テナント数が数万規模になると、各テナントのデータ量は相対的に小さくなる。この場合、スキャッター・ギャザーのオーバーヘッドが並列処理のメリットを上回ることがある。ドキュメントルーティングを使用して特定テナントのデータを1つのノードに集約すれば、検索リクエストはそのノードだけで完結し、ネットワーク通信や結果集約のコストを削減できる。
ただし、この手法が効果を発揮するのは数万のテナントがある場合に限られる。テナント数が少ない場合は、各テナントのデータ量が大きいため、スキャッター・ギャザーによる並列処理の方が有利となる。データ量、テナント数、クエリパターンに応じて、このトレードオフを慎重に評価する必要がある。
ハイブリッドモデル

実運用では、最終的に何らかのハイブリッドモデルに行き着くことが多い。小規模データセットは単一インデックスまたは単一ドメインのインデックスセットにプールし、非常に大規模なテナントは別のコンテナ(この場合はクラスタ)に分割して独立してスケーリングする。
これが最もコスト効率の高い方法である。小規模テナントのプーリングの利点と、大規模テナントの独立スケーリングの利点を両方得られる。ワークロードに合わせて柔軟に成長でき、最も柔軟な方法である。ただし、運用は少し複雑になり、テナントをどこに配置するかの管理と、それらのテナントを実行する複数のクラスタの管理が必要となる。

テナント管理の方法論として、特定のテナントIDから特定のクラスタと特定のインデックスへのポインタを持つデータベースを用意する。最も重要な読み取り専用フラグなど、一連のフラグも追加する。
クエリ側では、リクエストが来たらデータベースでそのテナントの場所を検索し、その場所にリクエストをルーティングする。
インデクシング側では、何らかのキューまたはデータ取り込み方法があり、インデクシングリクエストが来たらデータベースをチェックする。読み取り専用フラグが設定されている場合は、そのリクエストを再キューイングし、読み取りフラグがtrueになるまでキューで回転させ続ける。この方法により、テナントを再インデックスして別の場所に移動している間も、テナントの読み取り可能性を維持できる。これにより、読み取りのダウンタイムなし(データの若干の古さはあるが)でテナントを別のコンテナに移動できる。
セルベースアーキテクチャ
マルチテナント環境でのリソース消費の問題

マルチテナントシナリオでは、リフレッシュ間隔とCPU需要が複合的に増大する。複数のインデックスがアクティブな場合、それぞれが同時にリソースを必要とするため、必要なCPU数は単純な合計以上に増える。
CPUの使用は不均等になり、ルーティングやシャードのサイジングでバランスを取ろうとしても、スキューした使用パターンが発生する。手動でシャードを移動することで解決できるが、10,000や20,000のシャードがある場合、Allocate APIを手動で入力してシャードを移動するのは現実的ではない。
セルベースアーキテクチャによるスケーラビリティ

セルベースアーキテクチャでは、コンシューマーをほぼ同じサイズのチャンクに分割し、1つのチャンクのサイジングを決定してから、そのチャンクに単一クラスタを割り当てる。これは成長と縮小を繰り返す。
テナントをより簡単にスケーリングできる小さな部分に分割し、ワークロードが成長するにつれて新しいテナント用のセルを追加できる。これは使用率だけでなく、ワークロードの成長も管理する方法である。
通常、これらは何らかのルーティングの仕組みが必要となる。DNSベースのルーティングや、構築したプロキシを間に配置し、データベースを参照してこのテナントをどこに配置するか、このテナントのトラフィックをどこに送信するかを判断する。
ハブアンドスポークアーキテクチャ

ハブアンドスポークアーキテクチャは、個別のサイロまたはセルに対して、OpenSearchとクロスクラスタ接続を使用してすべてのワークロードのヘッドとして機能する。クロスクラスタ検索を通じて、実際のデータを持つ正しいセルまたはクラスタにアクセスできる。
この場合、プロキシの役割をOpenSearch自体が担う。中央のハブクラスタは、複数のクラスタ間でリクエストを調整するため、計算とネットワーキングのためにスケーリングする必要がある。多くの計算と通信が必要となる。
大規模組織で複数グループにまたがるセキュリティログすべてにセキュリティ部門がアクセスする必要がある監視ユースケースでは、このクロスクラスタハブアンドスポーク方法論が実現方法となる。
大規模検索ワークロードの最適化

20〜30GBの比較的小さな検索セットに対して高いクエリスループットが必要な場合、通常の方法はレプリカでスケーリングすることである。1つのOpenSearchクラスタでレプリカを追加し、ノードを追加できる。

しかし、これは実際には最良の方法ではない。複雑なクエリに対する高いクエリボリュームをサポートするために数百ノードまで成長したクラスタを見たことがあるが、そのようなクラスタは非常に不安定だった。数百ノードになると、ノード障害の影響範囲が大きくなり、トラフィックの干渉も多くなる。
経験上、これらの超巨大クラスタはうまく行かないことが多い。40〜60ノードが適切な量のように思われ、そこからセルベースアーキテクチャに移行し、40ノード、60ノードのチャンクに分割すると、より良く動作する。

検索ワークロードで非常に大規模にスケーリングする場合、単一クラスタ内でのレプリケーションをやめ、データを複数のクラスタに複製する方が良い。DNSやプロキシベースで、またはその他の方法でトラフィックをすべてのクラスタに均等にルーティングする。
これにより優れた分離が得られる。クラスタの1つを失っても、3つあれば容量の30%しか失わず、10個あれば10%しか失わない。その障害を乗り越えることができ、影響範囲を削減できる。
プロキシレイヤーによるガバナンス

中央集約型クラスタを運用していて、ダウンストリームユーザーがOpenSearchにとって都合の良い方法で行動しない場合(これが通常である。なぜ彼らがそれを考慮する必要があるだろうか)、トラフィックの送信元、クエリの送信元を簡単に把握する手段がない。異なるテナントを互いに分離する方法、正しくルーティングする方法も不明である。
そこで採用されるのが、トラフィックの転送を実装するスマートプロキシレイヤーである。これは通常クエリ側で使用される。自動スケーリングされるプロキシアプリケーションは、第一にOpenSearchへのトラフィックをプロキシできる必要がある。
第二に、トラフィックをキャプチャできるようにする。これにより、誰が何を送信したかを理解でき、サイジング演習を行いたい場合やトラフィックを別の場所に送信したい場合に、そのトラフィックをリプレイできる。
また、ワークロードに優先順位を付けることもできる。ワークロードAにはトラフィックの50%、ワークロードBには20%を割り当て、スロットリングやクラスタへのトラフィックシェーピングを行える。

インデクシング側でも、リプレイ用にデータをドロップオフしたり、トラフィックをシェーピングしたりできる。さらに、このプロキシからリクエストとレスポンスをキャプチャできる。誰がこのリクエストを送信したか、リクエストの処理にどれだけの壁時計時間がかかったかをキャプチャし、そのロギングをOpenSearchに書き込むことで、クラスタへのトラフィックのテナントレベルビューを持つことができる。このプロキシレイヤーは、これらの扱いにくい複数エンドユーザーのマルチテナントユースケースにとって非常に有用で重要である。
ベストプラクティスのまとめ

マルチテナント環境でOpenSearchを成功させるためのベストプラクティスをまとめると、まず小規模テナントには単一のプールインデックス、大規模テナントには単一のインデックスまたはクラスタを使用する。これがハイブリッドデプロイメント戦略であり、おそらく最終的にここに行き着くだろう。
インデックスのロールオーバーは、時間ではなくサイズベースで行う。一般的には日単位でインデックスのライフサイクルを管理するが、1GBのワークロードと10TBのワークロードがある場合、1GBのワークロードがインデックス、シャードなどをはるかに多く消費してしまう。代わりに50GBまで待ってからロールオーバーすることで、日単位でのライフサイクル管理能力は失うが、すべてが同じサイズになる。OpenSearchは、すべてが同じサイズの時に非常にうまく動作する。
テナントを移動する能力を維持することも重要である。テナンシー戦略について説明したように、あるテナントへのアクセスを維持しながら別の場所に再インデックスできるデータベースを保持する方法がある。それが必要なソリューションでない場合でも、サイズが変化するにつれてテナントを再配布する方法のソリューションが必要である。
シャードサイズは、検索で10〜30GB、ログで50GBを推奨する。シャード対CPUの比率は約1:1.5を目指す。これにより理想的には約66%の使用率となり、クラスタレベルのアクティビティのためのオーバーヘッドが残る。
空きストレージは15%以上を維持する。15%未満になると、OpenSearchは空きストレージを維持しようとして奇妙な動作を始める。
ノードあたりの最大シャード数は、ヒープ内のGB数の25倍とする。
単一クラスタのシャード数は60,000未満に保ち、30,000未満ならはるかに良い。クラスタマネージャーの改善により、この高い制限での動作が改善されているが、それでも制限は存在する。
そして最も重要なのは、ハイブリッドを計画することである。最初からハイブリッドモデルを想定して設計することで、将来の移行コストを大幅に削減できる。