OpenSearch Serverlessを検証した所感とCloudWatchダッシュボードの作成をしてみた
こんにちは!
any株式会社でエンジニアをしている @huuya です!
弊社が運営しているナレッジ経営クラウドのQastでは検索基盤にOpenSearch Serviceを利用しております。
先日OpenSearch Serverlessが一般公開されたので導入検討のため実際に触って検証を行ってみたので所感をまとめます。
特に気になるところ
検証を行ったコレクションタイプは「検索」になります。
「時系列」のコレクションタイプは検証しておりません。
OpenSearch Serverlessを検証する前の調査を以前行ったので是非こちらも御覧ください🤗
indexingの負荷が下がった
従来のOpenSearch Serviceではマスターノードとデータノードという構成でクラスターを組めていましたが、
OpenSearch Serverlessではsearchとindexingのインスタンスが内部的に完全に分離されたことで、
従来のOpenSearch Serviceよりは遥かに耐久性の高い検索基盤を作れそうです。
特に以下2点についてはOpenSearch Serverlessが持っているポテンシャルだと思います!
- indexingに負荷がかかっている状況でも検索への影響が無くなる
- 検索に負荷がかかっている状況でもindexingへの影響が無くなる
また、これまでは少々の負荷でも429エラーやタイムアウトエラーを吐いていたOpenSearchが、
indexingの検証をした限りでは429エラーやタイムアウトエラーを殆ど吐かなくなっていました😳
※ 検証データや方法によって環境差はあると思います。
実際にopensearch-benchmarkを使ってベンチマークを取って定量面で比較をしようとしたのですが、
ローカルにインストールされたOpenSearchにしか対応していなかったので断念しました😢
スパイク負荷でのOCUのオートスケールは遅れが出る
突発的且つ大きな負荷がかかる場合は若干(数十秒〜数分)オートスケールが遅れている気がしましたが、
同程度の負荷を掛け続けている最中にはそのような遅れが発生していないように思えました。
メトリクスを参照してもどれくらいのスピード感でオートスケールがされたのか分からないため、
あくまで体感としてそれくらいと捉えて頂ければと思います🙏
Serverlessでは未対応の機能がある
「サポートされていない機能がまだまだある」に記載しておりますが、
ある程度システム側で改修を行ったり運用方法の見直しが必要そうだったので要検討という所感です。
まずはCloudWatchのダッシュボードの作成をしてみた
コレクションの詳細ページのモニタリングタブに相当するCloudWatchのダッシュボードも作成してみました。
カスタムで以下のメトリクスも追加しています。
- 検索とindexingのOCU数
- ホットストレージ内の合計データ数 (GiB)
- 削除されたドキュメント数(カウント)
OpenSearch Serverlessでモニタリング可能な指標の詳細は公式ドキュメントを参照ください。
上記で作成したダッシュボードのサンプルは以下のコマンドで作成できます。
aws cloudwatch put-dashboard --dashboard-name test-opensearch-serverless --dashboard-body file://test_opensearch_serverless_dashboard.json
CLIオプションの --dashboard-body
については下記になります。
--dashboard-body
以下のパラメータは環境に応じて適宜修正を行ってください。
{{region}}
{{ClientId}}
{{CollectionId}}
{{CollectionName}}
{
"widgets": [
{
"height": 1,
"width": 24,
"y": 0,
"x": 0,
"type": "text",
"properties": {
"markdown": "# OCU"
}
},
{
"type": "metric",
"x": 0,
"y": 1,
"width": 8,
"height": 8,
"properties": {
"metrics": [
[
"AWS/AOSS",
"IndexingOCU",
"ClientId",
"{{ClientId}}"
]
],
"view": "timeSeries",
"stacked": false,
"region": "{{region}}",
"stat": "Sum",
"period": 60,
"title": "インデックス作成容量 (OCU)"
}
},
{
"type": "metric",
"x": 8,
"y": 1,
"width": 8,
"height": 8,
"properties": {
"metrics": [
[
"AWS/AOSS",
"SearchOCU",
"ClientId",
"{{ClientId}}"
]
],
"view": "timeSeries",
"stacked": false,
"region": "{{region}}",
"stat": "Sum",
"period": 60,
"title": "検索容量 (OCU)"
}
},
{
"height": 1,
"width": 24,
"y": 9,
"x": 0,
"type": "text",
"properties": {
"markdown": "# インデックス作成のパフォーマンス"
}
},
{
"type": "metric",
"x": 0,
"y": 10,
"width": 8,
"height": 8,
"properties": {
"metrics": [
[
"AWS/AOSS",
"IngestionDataRate",
"CollectionName",
"{{CollectionName}}",
"CollectionId",
"{{CollectionId}}",
"ClientId",
"{{ClientId}}"
]
],
"view": "timeSeries",
"stacked": false,
"region": "{{region}}",
"stat": "Sum",
"period": 60,
"title": "インデックス作成データレート (ギガバイト/秒)"
}
},
{
"type": "metric",
"x": 8,
"y": 10,
"width": 8,
"height": 8,
"properties": {
"metrics": [
[
"AWS/AOSS",
"IngestionRequestSuccess",
"CollectionName",
"{{CollectionName}}",
"CollectionId",
"{{CollectionId}}",
"ClientId",
"{{ClientId}}"
]
],
"view": "timeSeries",
"stacked": false,
"region": "{{region}}",
"stat": "Sum",
"period": 60,
"title": "インデックス作成成功率 (カウント)"
}
},
{
"type": "metric",
"x": 16,
"y": 10,
"width": 8,
"height": 8,
"properties": {
"metrics": [
[
"AWS/AOSS",
"IngestionRequestRate",
"CollectionName",
"{{CollectionName}}",
"CollectionId",
"{{CollectionId}}",
"ClientId",
"{{ClientId}}"
]
],
"view": "timeSeries",
"stacked": false,
"region": "{{region}}",
"stat": "Average",
"period": 60,
"title": "インデックス作成リクエスト率 (カウント)"
}
},
{
"type": "metric",
"x": 0,
"y": 18,
"width": 8,
"height": 8,
"properties": {
"metrics": [
[
"AWS/AOSS",
"IngestionRequestLatency",
"CollectionName",
"{{CollectionName}}",
"CollectionId",
"{{CollectionId}}",
"ClientId",
"{{ClientId}}"
]
],
"view": "timeSeries",
"stacked": false,
"region": "{{region}}",
"stat": "Average",
"period": 60,
"title": "インデックス作成のレイテンシー (ミリ秒)"
}
},
{
"height": 1,
"width": 24,
"y": 26,
"x": 0,
"type": "text",
"properties": {
"markdown": "# ストレージ"
}
},
{
"type": "metric",
"x": 0,
"y": 27,
"width": 8,
"height": 8,
"properties": {
"metrics": [
[
"AWS/AOSS",
"StorageUsedInS3",
"CollectionName",
"{{CollectionName}}",
"CollectionId",
"{{CollectionId}}",
"ClientId",
"{{ClientId}}"
]
],
"view": "timeSeries",
"stacked": false,
"region": "{{region}}",
"stat": "Sum",
"period": 60,
"title": "S3 で使用されているストレージ (GiB)"
}
},
{
"type": "metric",
"x": 8,
"y": 27,
"width": 8,
"height": 8,
"properties": {
"metrics": [
[
"AWS/AOSS",
"HotStorageUsed",
"CollectionName",
"{{CollectionName}}",
"CollectionId",
"{{CollectionId}}",
"ClientId",
"{{ClientId}}"
]
],
"view": "timeSeries",
"stacked": false,
"region": "{{region}}",
"stat": "Sum",
"period": 60,
"title": "ホットストレージ内の合計データ数 (GiB)"
}
},
{
"height": 1,
"width": 20,
"y": 35,
"x": 0,
"type": "text",
"properties": {
"markdown": "# 検索のパフォーマンス"
}
},
{
"type": "metric",
"x": 0,
"y": 36,
"width": 8,
"height": 8,
"properties": {
"metrics": [
[
"AWS/AOSS",
"SearchRequestRate",
"CollectionName",
"{{CollectionName}}",
"CollectionId",
"{{CollectionId}}",
"ClientId",
"{{ClientId}}"
]
],
"view": "timeSeries",
"stacked": false,
"region": "{{region}}",
"stat": "Sum",
"period": 60,
"title": "クエリの数 (オペレーション/分)"
}
},
{
"type": "metric",
"x": 8,
"y": 36,
"width": 8,
"height": 8,
"properties": {
"metrics": [
[
"AWS/AOSS",
"SearchRequestLatency",
"CollectionName",
"{{CollectionName}}",
"CollectionId",
"{{CollectionId}}",
"ClientId",
"{{ClientId}}"
]
],
"view": "timeSeries",
"stacked": false,
"region": "{{region}}",
"stat": "Average",
"period": 60,
"title": "検索レイテンシー (ミリ秒)"
}
},
{
"type": "metric",
"x": 16,
"y": 36,
"width": 8,
"height": 8,
"properties": {
"metrics": [
[
"AWS/AOSS",
"SearchableDocuments",
"CollectionName",
"{{CollectionName}}",
"CollectionId",
"{{CollectionId}}",
"ClientId",
"{{ClientId}}"
]
],
"view": "timeSeries",
"stacked": false,
"region": "{{region}}",
"stat": "Sum",
"period": 60,
"title": "検索可能なドキュメント (カウント)"
}
},
{
"height": 1,
"width": 20,
"y": 44,
"x": 0,
"type": "text",
"properties": {
"markdown": "# エラー"
}
},
{
"type": "metric",
"x": 0,
"y": 52,
"width": 8,
"height": 8,
"properties": {
"metrics": [
[
"AWS/AOSS",
"SearchRequestErrors",
"CollectionName",
"{{CollectionName}}",
"CollectionId",
"{{CollectionId}}",
"ClientId",
"{{ClientId}}"
]
],
"view": "timeSeries",
"stacked": false,
"region": "{{region}}",
"stat": "Sum",
"period": 60,
"title": "検索エラーの数"
}
},
{
"type": "metric",
"x": 8,
"y": 52,
"width": 8,
"height": 8,
"properties": {
"metrics": [
[
"AWS/AOSS",
"IngestionRequestErrors",
"CollectionName",
"{{CollectionName}}",
"CollectionId",
"{{CollectionId}}",
"ClientId",
"{{ClientId}}"
]
],
"view": "timeSeries",
"stacked": false,
"region": "{{region}}",
"stat": "Sum",
"period": 60,
"title": "取り込みエラーの数"
}
},
{
"type": "metric",
"x": 16,
"y": 52,
"width": 8,
"height": 8,
"properties": {
"metrics": [
[ "AWS/AOSS", "2xx", "CollectionName", "{{CollectionName}}", "CollectionId", "{{CollectionId}}", "ClientId", "{{ClientId}}" ],
[ ".", "3xx", ".", ".", ".", ".", ".", "." ],
[ ".", "4xx", ".", ".", ".", ".", ".", "." ],
[ ".", "5xx", ".", ".", ".", ".", ".", "." ]
],
"view": "timeSeries",
"stacked": false,
"region": "{{region}}",
"stat": "Sum",
"period": 60,
"title": "レスポンスコードによる HTTP リクエスト (カウント)"
}
},
{
"type": "metric",
"x": 0,
"y": 60,
"width": 8,
"height": 8,
"properties": {
"metrics": [
[
"AWS/AOSS",
"IngestionDocumentErrors",
"CollectionName",
"{{CollectionName}}",
"CollectionId",
"{{CollectionId}}",
"ClientId",
"{{ClientId}}"
]
],
"view": "timeSeries",
"stacked": false,
"region": "{{region}}",
"stat": "Sum",
"period": 60,
"title": "取り込みリクエストのエラー"
}
},
{
"height": 1,
"width": 20,
"y": 68,
"x": 0,
"type": "text",
"properties": {
"markdown": "# その他"
}
},
{
"type": "metric",
"x": 0,
"y": 69,
"width": 8,
"height": 8,
"properties": {
"metrics": [
[
"AWS/AOSS",
"DeletedDocuments",
"CollectionName",
"{{CollectionName}}",
"CollectionId",
"{{CollectionId}}",
"ClientId",
"{{ClientId}}"
]
],
"view": "timeSeries",
"stacked": false,
"region": "{{region}}",
"stat": "Sum",
"period": 60,
"title": "削除されたドキュメント数(カウント)"
}
}
]
}
調査して分かったこと
基本的にはAWS Black Beltや公式ドキュメントを隅々まで読んでキャッチアップを行いつつ、
実際に触って検証をしてみました。
未対応のメトリクスがある
従来のOpenSearchServiceで監視出来ていた以下のメトリクスが未対応でした。
- CPU使用率
- メモリ使用率
- JVMメモリプレッシャー
- IOレベルの読み書きに関連するメトリクス
- スレッドプールのサイズやキューの数、リジェクト数等
また、対応しているメトリクスについてOCU単位で監視出来るものはありませんでした。
データへのアクセスポリシーを細かく設定出来る
以下の操作に対してIAMユーザ、IAMロール単位で権限の設定が出来ます。
また任意のコレクションや任意のindexのみに権限を絞り込むことが可能です。
- インデックス
- 読み取り
- 作成
- 更新
- 削除
- ドキュメント
- 読み取り
- 書き込み、更新
以下の操作についてもIAMユーザやIAMロール単位で権限の設定が出来て、
任意のコレクションのみに権限を絞り込むことが可能となっておりました。
- aliasまたはtemplate
- 読み取り
- 作成
- 更新
- 削除
認証方法は従来から変更された
引用元 2023/03/02 AWS Black Belt Online Seminar Amazon OpenSearch Serverless P47
ダッシュボードへのログイン時
SAML連携をしていない場合、IAMユーザ又はIAMロールでの認証が必須になります。
※ 3月28日以降からはログイン時にAWSコンソールでのIAM認証情報を使用して自動ログイン出来るようになっています。
Starting March 28, OpenSearch Serverless will no longer support OpenSearch Dashboards login page with IAM access key id and secret key as inputs. Instead, we are introducing a new feature, fast Dashboard login in the AWS Console that will automatically log you into OpenSearch Dashboards using the IAM credentials of your AWS Console once you click the Dashboards endpoint link.
Note: To login with SAML, you will need to copy the URL and paste in the browser to access IDP select page.
引用元 AWSコンソールのヘッダー通知
APIリクエスト時
IAMユーザ又はIAMロールでの認証が必須になっていました。
各言語ごとのClientライブラリで署名したHTTPリクエストをする方法は公式ドキュメントを参考にしました。
サポートされていない機能がまだまだある
引用元 2023/03/02 AWS Black Belt Online Seminar Amazon OpenSearch Serverless P64
公式のBlackBeltに掲載されている上記の機能以外にも未対応の機能がありました
- カスタムパッケージ(analyzerでsynonym graphの使用が出来ない)
- スロークエリをCloudWatch Logsへ出力する(検索、indexing共に未対応: 公式フォーラム
- エラーログや監査ログも未対応でした
HTTPリクエスト時のペイロードの最大サイズは従来と同じっぽい
従来のOpenSearch Serviceではインスタンスタイプ毎に10MB、若しくは100MBのようになっておりましたが、
OpenSearchServerlessの場合は公式ドキュメントに記載はありませんでした。
詳しい検証を行っていないので確信は持てないですが、
10MB以上のリクエストは受け付けてくれて100MB以上の場合は413 Payload Too Largeエラーが返却されたので、恐らく100MBになっているのだと思います。
(検証時のOCUは検索、indexing共に2つずつ。
コレクション単位で使用しているOCU数が分からない
現時点ではコレクション単位で何個のOCUを使用しているか可視化出来ないようです。
公式ドキュメントにも記載がありますが、
IndexingOCUやSearchOCUのディメンションにClientIdの指定しか出来ないためです。
コレクション単位でOCU数の最大値を設定出来ない
現時点ではコレクション単位でのOCU数の最大値を設定出来ないため、実質リージョン単位で検索とindexingのOCU数の最大値を設定することになります。
opensearch-jsをTypeScriptで利用する場合はv2.2.1以上を使用する必要がある
const { defaultProvider } = require('@aws-sdk/credential-provider-node');
const { Client } = require('@opensearch-project/opensearch');
const { AwsSigv4Signer } = require('@opensearch-project/opensearch/aws');
const client = new Client({
...AwsSigv4Signer({
region: 'us-west-2',
service: 'aoss',
getCredentials: () => {
const credentialsProvider = defaultProvider();
return credentialsProvider();
},
}),
node: ''
});
AWSの公式ドキュメントでは上記のようにクライアントを定義して利用できると記載がありますが、
AwsSigv4Signer
に service
に対応する型がそもそも定義されていなかったのでv2.2.0では使用出来ない状態にありました。
このバグはv2.2.1のリリースで修正されたのでTypeScriptで使用する場合は注意が必要です。
opensearch-jsでのClientを作成する時にAWSの認証情報を引数で指定出来なかったので対応した
本来は環境変数や専用の設定ファイル経由で無く引数でも指定したいですよね。
@aws-sdk/credential-provider-nodeのdefaultProviderはデフォルトで環境変数からの取得を試みて、
それが無ければ ~/.aws/credentials
や ~/.aws/config
からの取得を行うような実装になっていました。
ライブラリの認証に利用する情報が環境変数や設定ファイルからの取得となると実質暗黙的な依存関係となってしまうので、出来ればそのような運用を避けたいところです。
そのため、defaultProviderへ引数から認証情報を渡せるように作ってPRを提出してみました。
必要最低限の手段で認証情報を取得したい場合は以下のようにカスタムのProvoiderを実装するのが良いかと思います。
import { AwsCredentialIdentity, MemoizedProvider } from "@aws-sdk/types";
import { chain, CredentialsProviderError, memoize } from "@aws-sdk/property-provider";
import { fromArgs, FromArgsInit } from "./credentialProviderArgs";
export const customProvider = (init: FromArgsInit = {}): MemoizedProvider<AwsCredentialIdentity> =>
memoize(
chain(
fromArgs(init),
async () => {
throw new CredentialsProviderError("Could not load credentials from any providers", false);
}
),
(credentials) => credentials.expiration !== undefined && credentials.expiration.getTime() - Date.now() < 300000,
(credentials) => credentials.expiration !== undefined
);
上記に対応するcredential-providerは以下のような実装になるかと思います。
import { CredentialsProviderError } from "@aws-sdk/property-provider";
import { AwsCredentialIdentityProvider } from "@aws-sdk/types";
export type FromArgsInit = {
accessKeyId: string;
secretAccessKey: string;
sessionToken?: string;
expiry?: string;
}
export const fromArgs = (init: FromArgsInit): AwsCredentialIdentityProvider => async () => {
const {accessKeyId, secretAccessKey, sessionToken, expiry} = init;
if (!accessKeyId || !secretAccessKey) {
throw new CredentialsProviderError("Unable to find arguments credentials.");
}
return {
accessKeyId,
secretAccessKey,
...(sessionToken && { sessionToken }),
...(expiry && { expiration: new Date(expiry) }),
};
};
このようなカスタムのProviderを実装することで以下のように引数で認証情報を設定出来るようになります。
import { Client } from '@opensearch-project/opensearch';
import { AwsSigv4Signer } from '@opensearch-project/opensearch/aws';
import { customProvider } from './customProvider';
const client = new Client({
...AwsSigv4Signer({
region: 'us-west-2',
service: 'aoss',
getCredentials: async () => {
const credentialsProvider = customProvider({
accessKeyId,
secretAccessKey,
sessionToken,
expiry: new Date().toISOString(),
);
return await credentialsProvider();
},
}),
node: ''
});
まとめ
OpenSearch Serverlessはまだ一般公開されたばかりで、
従来のOpenSearch Serviceでサポートされていた機能が対応していない部分が色々ありますが、
シャード戦略や最大負荷に合わせたインスタンスタイプの設定などの運用が不要となることでかなりのポテンシャルを秘めているかと思います!
また、searchとindexingのノードが内部アーキテクチャ上分離されていることで、
検索に負荷が掛かっている状況でもindexingに影響しないことや
その逆のindexingに負荷がかかっている状況でも検索に影響しない点でも検討の余地が十分にあると感じました👏
引き続きOpenSearch Serverlessを触ってみようと思います!
ありがとうございました!
Discussion