キーワード検索をリプレイス&独立した検索サービスを立ち上げた
はじめに
ようやく、ユーザ向けにリリースが終わった様子なのでここで一つまとめておこうと思います。
どこの部分をリプレイスしたのか?
この部分です
PC
SP
おことわり
フロントのHTMLの組み込みのところは別のチームのエンジニアにお願いしていて、自分はバックエンド側のみになります
今までの課題感や要件的なもの
- 今まではmysqlのLike検索だった。サービスが成長して、Like検索だとSlow Queryが多く出はじめたでこれを対応したい
- mysqlのチューニングやプラグインで対応していく方向性でも出来ないことはないと思うけど、手数がいるし、スケール難易度が上がるのでその方向性はやめたい
- feed検索サービスとして、既存のシステムからなるべく切り離して独立性を保って運用していきたい
- マイクロサービス化の流れですね〜
- せっかく、新しくするのなら、サジェストとかは入れたいな〜
対応チーム
自分一人です
どんな構成で検索システムを開発したのか? 其々説明する
まずは検索システム自体の選定
- まぁ一人なので、可能な限りmanagedされたサービスを使うべきだなと思っていた
- 間違っても自作なんてしちゃ...エンドレスだと思います
- 検索エンジンとなればまぁSolrかElasticsearchだなぁ
- まぁElasticsearchだろうなぁー
- AWSのElasticsearch ServiceとElasticCloudを比較し始める
- まぁElasticsearchだろうなぁー
で、圧倒的にElasticCloudのが運用者からすると楽だと思った。 あとは今後要件によってプラグイン開発の必要性が出てきた場合はAWSのElasticsearch Serviceだと不可能(現時点)なので困るなぁと思ってElasticCloudにした
将来的な要望みたいなのを聞いているとおそらく、synonym辞書関連とかその辺りでカスタムプラグインを作る必要性が出てくる気がしたのも理由のひとつ。
今後の利用想定も含めてインデックスされるサイズや、想定リクエストを計算。当初カスタムプラグインを使わないとしたのでスタンダートプランにした。
Elasticsearchの把握
- とにかくドキュメントがかなり充実していたのでひたすら読んだ。途中ただの英語の勉強の時間みたくなってたけど、ひたすら全てに目を通した
- よくわからない所はlocalに構築したElasticsearchで試しながら理解に努めた
- ちょうどタイミングよくElastic{ON}ツアー@東京が開催されたので、参加してキャッチアップに努めた
- Tokyo Elasticsearch user conference | Elastic
- その場でメールで対応してくれている elasticの担当者と直接お話する事ができて、後日構築する構成/要件に関して直接相談・レビューしてもらえたのはありがたかった
- BKDツリーによる検索スピードの向上の情報を知れたり
- Magic WANDの素晴らしさを知れて楽しかった
全体アーキテクチャ
今回はElasticsearch部分はElasticCloudでまるっとマネージされるのでインデックスの更新の運用だったり、既存システムとどう独立して稼働させるかという部分が考えるポイントでした。
今回の構成を考えるにあたり、日時のindexの更新が多くて約20件程度(今後数年で大きく増えるものではない)、今後の発展として、機械学習関連の要件を見越したいという事を念頭に設計しました
要件と現状の分析はざっくり下記
- 更新頻度からしてインデックス更新に関わるワークフローは待機している時間が圧倒的に多い
- 検索対象になる連携データは、今後の要件によって何かしらの処理・整形が必要になる事が考えられたのでfeed側の内部DBに格納した方が良さそう
- これも現時点では常にselectやupdate,deleteが行われるものではない
という事から下記のように基本設計をしました
point
Functional
- 待機している時間が圧倒的に多いのでLambdaを活用
- 内部DBも活用時間が少ないのでAuroraのserverlessを活用
- Lambdaから内部DBに接続するので、connection数を食いつぶさないように一工夫を入れた
- Data APIの利用は諦めました
- コールドスタート + VPC内lambdaでも5~10sec程度で動き出すので全然okだった
- ビジネス的には数分内に検索可能な状態が作り出せれば良いとの事だったので
- Lambdaの中は全てGoでクリーンアーキテクチャにのっとり開発した
- この辺(Goのdatabase周りのテストではinterfaceを最大限に活用しようという話 - nakamura244 blog)の話の延長上にクリーンアーキテクチャを適用して構築した
SQS
- 検索対象となるデータ(=プロジェクト情報)が更新されたら、SQSにqueueが登録されます
- SQSのキューを取ってくるとLambdaが稼働します。そのLambdaは適切な粒度のjobに分割していて、Step Functionsで管理しています
- SQSを挟んでいる理由は、feedサービスを独立したものにする為とfeedサービス側のメンテナンス等で他のサービスに影響を与えない為です
- メンテナンスを実行する場合は、dequeueをやめて、メンテをする流れを想定
Step Functions
- 今後データの整形や機械学習関連のワークフローが具体的に出てきた時の為に柔軟に組み換えがしやすいように各ジョブを適切な粒度に分けて、Step Functionsでつなげる事にした
- 途中で何かエラーを検知したら、Slackへ通知されます
- その後、最終的にElasticsearchのドキュメント更新を行います
- Step Functionsの中身はこんな感じです
API Gateway
実際の検索をさせる時に直接Elasticsearchに接続して検索でも良かったが、API Gatewayを挟む事にした。理由は下記
- 利用する他のチームの人がElasticsearchのクエリー記法を覚える必要が出てきてしまう
- 独立したサービス(マイクロサービス文脈)で考えると、クエリーは簡単な形式が望ましい
- クエリーによってはリニアな検索になってしまう事もあり得るのでそのあたりの制御が難しくなってしまうだろうと思った
- 検索されたキーワードを今後の為に集計しときたいな〜と思った
- 検索キーワードをCloudWatchに一定期間残すようにして、それをLambdaでキャッチして再びElasticsearchに入れている
- そうする事でこの機能によって検索クエリーのレスポンスに影響が出ないようにした
- 検索されたキーワード自体はいつまでも必要なデータではないので、Elasticsearch側のindex-lifecycle機能を使って古いものは削除するようにした
- なので
search_keyword.yyyy.mm.dd
のようにindex管理するようにしている - Managing the index lifecycle | Elasticsearch Reference [7.4] | Elastic
- なので
- 検索キーワードをCloudWatchに一定期間残すようにして、それをLambdaでキャッチして再びElasticsearchに入れている
- もし、想定外の検索リクエストがきて、やばいとなった時に一時的にthrottle機能でしのぎたいと思った
- throttle機能発動中にElasticsearchのクラスターのupdateしたいと考えていた
- Elasticsearchへの接続元をセキュアに保ちたかった
- IAMだったり、headerのAPIkeyだったりを活用してセキュアな接続を実現しようとした
- Elasticsearch側ではBasic認証での接続制限しか提供されてなかった事も背景にある
- API Gatewayの所でのキャッシュ機能もorigin=Elasticsearchを守る意味でもアリだなと思った
- プロキシーがいる事のメリットはあるだろう
致し方なく妥協した所
- 検索対象となるデータを取ってくる所ですが、makuake側の内部のエンドポイントが存在しない為、直接DB参照に致し方なくしました(妥協)
- これによってmakuake側のDBスキーマの変更を気にしなければいけなくなったが、やむなし。
- 今後、疎結合のエンドポイントが出来上がったら参照を切り替え予定
ちょっとハマった点
Lambdaから内部DBに接続する所でLambdaがスケールしていっぱい稼働するとmysqlのconnectionを食いつぶしてしまう所でした。
Data APIがまだ出たばかりでちょっと不安だったので、代わりにStep Functionsの実行状況を見ながら次のjobの実行を制御するようにした
DescribeExecution - AWS Step Functions
または、Aurora Serverlessの部分をDynamoDBにしたり、Elasticsearch内に同様の役割のindex(=ストレージ)を作ってしまうというような代替もありだと思う
この辺りはAWS LOFTにちょっと通って相談させてもらいました。担当頂いたソリューションアーキテクトの方にはお世話になりました🙇🏻
パフォーマンス検証
検索クエリー
- 秒間NアクセスをN秒間続けてという一般的な負荷試験を実施した
- どこにクリアすべき基準を持っていくるかによって、Elasticsearchの組むクラスターのスペックは決まりますが、割と最小構成にした。
- API-Gatewayでのキャッシュ、Elasticsearch側でのキャッシュをそれぞれ試したが、前者のAPI-Gatewayでのキャッシュさせた方が結果は良かった
- そりゃそうなんだけど
- 当然だけどindexの設計やクエリーによっても結果は変わるのでその辺りは注意でその情報も一緒にまとめた
結果はまとめてesaへ
↑イメージ
今の構成でどこまでのリクエストは耐えれるかを把握できた
ワークフロー
- SQSのqueueを大量に貯めてから、一気にワークフローを動かす試験を実施しました
- 何かしらの障害が発生して、SQSのqueueが溜まってしまった状態や、更新頻度が想定よりかなり増えた時の事を想定したテストを実施した
- SQSのqueueがN件でN分ほどかかるというだいたいの処理時間が把握できました
- 試験中エラーがCloudWatch上に出力されていないかを確認しました
- 最終的なSQSの更新件数がElasticsearch側の更新件数と一致したかどうかを確認しました
セキュリティ関連
- 基本的にAWS上に構築されている部分はIAMベースで且つVPC内で利用が制限されている
- ElasticsearchもKibanaを用いたユーザ管理をしている(ElasticCloud内で提供されている機能)
- このユーザはselectしかできないユーザ、このユーザは更新も可能なユーザといった感じ
モニタリング関連
- 会社の方針に基本合わせる事にした
- mackerel+data dog,slack
- あとはKibanaのMonitoringを見てる
- KibanaのWatcherは今のところ使っていない
データライフサイクル
- CloudWatch内に出るログ関連は基本的にN日で削除設定をしている
- キーワード検索ログのインデックスを日付ごとに作成し、index life sycle 設定でindexを自動削除で運用
- ElasticsearchのSnapShotは1日1回で3日分だけを保存するようにしている
- 作成しているインデックスがゼロから10min以内で作れてしまうぐらい小さい点や、ビジネス的な要件によって変わると思う
Dev / CI / CD
このあたりも自身での運用サーバはなくて、外部サービスで完結するようにしている
しかし、最近apexのメンテが終了したらしく、デプロイツールの変更をしなきゃいけない...
GitHub - apex/apex: Build, deploy, and manage AWS Lambda functions with ease.
最近であればGitHub Actionsを使ったフローもできると思われる
infrastructure as code
他のチームはterraform使っていて、正論だけで言えばterraformでcode化すべきなんだけど、正直一人チームではToo Matchだし、先に進めづらい...😰
設定jsonをesaかgithubにcommitしておく事で対応している
人が増えるような事があれば対応をまた考える
今のところ得られた効果
1
まぁmysql Like検索から比べたら可用性や負荷耐性などはもちろんアップ
2
サーバ負荷が目に見えて改善した
- mysqlのLike検索してた分がごそっとなくなって、レスポンスが早くなっている事が確認できる
3
コスト面。多分費用対効果は良いはず
STG環境1つと本番環境を1つを運用していて下記のコストが毎月発生している
- ElasticCloudは計算上、過去2年間で最大時の負荷に耐えれるスペックのインスタンスで構成して約月190ドル
- AWS Lambdaで約月3.5ドル
- AWS StepFunctionsで約月0.5ドル
- AWS Aurora Serverless 約月72ドル
- AWS API Gateway 約月55ドル(その月のアクセス数による)
トータルで321ドルで監視のmackerelを入れたとしても+10ドル程度で331ドルになります。
1ドル107円計算で変換すると約35,542円になります。約4万円弱で単なる検索システムだけでなく、独立した1つのマイクロサービスのシステムを構築できたということはそれなりに満足している
可視化してる
1
検索にかかっている時間(ms)をグラフにしたら平均で見てたりします
よく検索されるクエリーTOP20の検索時間(ms)はちょっと気にしてたりします
この辺の数値はSLAの一つに入れて日々運用していく形になるかと思います
2
どんなキーワードで検索されている事が多いのか等を眺めてたりします。
ちょっとしたホットワードがわかったりします
ElasticCloudへの要望点もあったりする
- ElasticCloudのログインアカウントが一つしか設定できない。チームでのログイン管理が無理。なのでセキュアなログイン設定ができない
- 本当はGoogle Authenticatorとか使いたいけど、ログインアカウントが一つしかないので... 他の人がログインできなくなる
- 問い合わせサポートだけの上位サブスクリプションプランが欲しい
- 現状の仕組みだとサポート対応だけを迅速に行いたい場合でもゴールド以上のサブスクリプションプランに移行いないといけない。
- トライアルで試していたが、カスタムプラグインをアップロードできるが、消すことができない
- なので不要となったカスタムプラグインがいつも画面に存在してて見にくくなる時がある
- アラートのメールはどうやらstatusがgreenじゃない時に発泡されるが、diskのサイズやメモリの使用具合で閾値決めれて、自分でアラートが出せるようになるとありがたい
- Advanced watchとか使ったらやりたい事できるのかなぁ...
今まで運用してきての踏んだ不具合
1: Lambdaでのエラー
下記のようなエラーを出力した
"Error": "Lambda.SdkClientException", "Cause": "Unable to execute HTTP request: The target server failed to respond"
内容は下記のものらしい。retryを設定
Lambda サービス例外の処理 - AWS Step Functions
2: Apexの終了
メンテが終わると途中で知り、デプロイツールを今後変えて行かなきゃいけない...
今後の予定
検索に評価指数の導入
キーワード検索の結果からリンクをクリックした場合、下記のようなURLになっている
https://www.makuake.com/project/perfect_beer04/?from=keywordsearch&keyword=%E3%83%93%E3%83%BC%E3%83%AB&disp_order=1
GETパラメータでfrom=keywordsearch&keyword=ビール&disp_order=1
となっている。
これはキーワード検索ページからビール
というワードで検索して1番目に表示された結果をクリックして遷移してきたという意味になります
これをアクセスログベースで集計してDiscounted cumulative gainという指標に照らし合わせて向上させていこうと考えています
Discounted cumulative gain - Wikipedia
Google Analytics側にイベントを送って集計でも良いんだが、一番確実な方法を取ろうとしている
これはこれでAWS Glueあたりをうまく活用していきたいと思っている
最後
- いかに手数をかけずにコストも抑え、独立したマイクロサービスの1つの検索システムを作れるかという点においては満足している
- 2ヶ月ほどで出来たこともちょっと満足している
- ElasticCloudの中の製品はまだもっと面白いの機能があって、十分にプロダクトに活かしきれていないという点はこれから改善していきたい
- 評価指数を元にこれから精度の改善に努めたい
- ただし、UIの部分も範囲になってくるので、その辺りをどう進めたら良いのだろうかとちょっと考えている。私はデザインできない。。。
- 他社さんでもキーワード検索をリソースの関係上ひとりで担当する場合がまぁまぁあるんじゃないかと思います
- その際に少しでもこの情報が役に立てば幸いです
おまけ
- バックエンドが完了した後、最終的なフロントエンドの組み込みは他のチームが担当してくれた。しかし、そのチームの事情により、3ヶ月の保留になってしまった
- マイクロサービス化の流れで他のチームに自分が影響を与えることが出来ず、致し方なく3ヶ月もの間待つことになった。こんな歪みも発生するんだんぁーと実感
- 実は自分が構築した構成と似たパッケージ=functionbeatというものがあった
- Functionbeat | クラウドデータのための軽量シッパー | Elastic
- 今後の拡張性等を考えると自分で作ってよかったとは思うが、要件や状況次第では全然選択しても良いものだと思った