検索用DBをElasticsearchにリプレイスした話
はじめに
当方かれこれ10年ほどシステム開発に携わっているバックエンドエンジニアです。
2023年10月〜2024年8月にかけて、タイミーにおける求人検索エンジンのリプレイスに携わっておりまして、この度無事リリースを完了したのでそのあたりの知見をまとめます。
入社前
入社時からのこのプロジェクトを行っていますので、まずは入社経緯から。
前職で大規模なリプレイスプロジェクトに携わってまして、PJ終了に伴い手が空いたこと&諸々の事情で転職を考えていました。
その時に以前一緒に仕事をしたことのあるGさんに近況伺いしたところ、ちょうどタイミーでバックエンドエンジニアを募集しているようなので詳しく話を伺いました。
(Gさんとは某社で大規模アクセスデータの集計バッチ改修プロジェクトを一緒にした仲間です。あれはあれで辛いPJだった・・・)
会社の経費で寿司食いながら聞いたところによると(リファラルっていいね!)、求人情報が全部RDBに入っていてそれをSQLで直接検索かけているとのこと。しかもそのRDBには求人以外の大事なデータが色々のっているらしい。
・・・ん?なにそれ?
なんでそんなイルカ拷問かけてるの?
過去一緒にやったバッチ改修PJではHadoopが限界までブン回されてぴえんを超えてぱおんとなっていたのを思い出して懐かしい気持ちになりました。
急成長しているプロダクトってスケールアウトでゴリ押ししがちだよねとか話しながら、流石にそれはDB分けなあかんなという話になってジョインする運びになりました。
ちなみにバックエンドはRailsなんですが、この時点で書いたことないです。
立ち上げと分析
前職の引き継ぎをしゃしゃっと終わらせて入社。
プロジェクト遂行のための新規チームが立ち上がると聞いていたのですが・・ん?自分一人?
翌月にEM、4ヶ月後にデータサイエンティストの方が新規に入ってきましたが基本的には開発は一人体制でした。
データ分析
モニタデータは蓄積されていたっぽいのでまずは分析してレポート上げるところから。
ざっと見たところ次のことがわかりました。
実行計画とか見た感じ、現行のテーブル構成ではほぼ最適なクエリっぽい。
スケールアップ/アウトされてCPU/メモリが余裕ぶっこいているのにレイテンシがジワジワ遅いので支配的なのはリクエスト規模ではなくデータ規模っぽい。
全求人を日付でパーティション分けをしているわけですが、求人増加でパーティション内の求人数が増加して、そのへんのスキャン/フィルター/ソートコストが重くなっている感じでした。
ざっくり概算でもフィルターだけでも O(N)、ソートに限って言えば O(NlogN) だからそんなとこだろうという所感です。
本番環境にお邪魔して実際に流れているクエリにLimit足して流して傾向見てもそんな気配が読み取れたので仮説としては上々です。
リクエストを振り分けてもイルカ一匹の仕事の絶対量が大きいのでしんどくなってきた感じですね。
こりゃDBアーキテクチャ変えんとあかんわ、ということで色々案出しをしました。
DBアーキテクチャ検討
少々面倒なんですが検討の俎上にはメジャーどころの技術をズラッと一旦並べてから消し込む手段を使いました。(この辺の水平思考にはChatGPTくんが便利です)
マイナーどころは保守の観点からはじめから落とすにしてもメジャーどころは一応比較検討したというペライチがあると便利です。
アーキテクチャ変更はコストが高いので、意思決定者に本当に妥当な選択をしたのかを納得してもらいやすいように記録に残す必要があります。
ここから不適切な物を消し込んでいけば自ずと絞られていくわけですが、
集計せずフィルタがメインなので行志向
時系列やグラフ志向などの特殊用途に該当せず
シーク/フィルタ/ソード処理の負荷分散ができること
保守性の観点からそれなりに枯れてる技術
などを考慮した結果Elasticsearchの採用が決定しました。
パーティションの求人データを一匹でごそっと舐めるのがしんどくて遅くなってるんだから、シャード分散で舐めさせてマージソートするようにしたら台数Mで計算量割れるでしょという見立てです。
インフラ準備と性能検証
アーキテクチャ採択案に判子が押されたら、超特急でやるのがインフラ調達です。
車は急に止まれないし、インフラは急には動かせない。
予算は?担当部署は?セキュリティは?手を動かすというよりもそのへんの調整ごとが主に面倒なところです。
(IaaSなんでまだスピーディなのが救いですね)
なんですが、とりあえずそのへんはEMにまるっとお願いして自分は概算性能検証をしていました。
というのもこのPJの最大のリスクは性能です。
アーキテクチャを見て早くなるはずという見立てはあるものも、仮説は仮説。
いざElasticsearchでリプレイスして動かしてみて性能悪かったら「なんの成果も挙げられませんでしたぁ!」と叫ぶしかない。
当然モノができるまでは正確な性能なんてわかりっこないわけですが、概算検証でざっくりあたりをつけることはできるはずです。
概算性能検証
性能のざっくり計算のために目をつけたのがAWSのOpenSearchです。
Elasticsearchとは別物なんですが、
DBアーキテクチャは同じなのでそう大きく数字としては外さないはず
AWS使ってるのでサクッと立つ
ということで安い、早い、そこそこ美味いプランです。
そこに本番相当データぶち込んで、完成時に飛んでくるであろうリクエストを捏ねてスクリプトで連打させればざっとこれくらいという数字が出ます。
結果、超早かったです。
従前の7-8倍ぐらい。
実際の運用では、
求人更新の反映負荷がかかる
検索結果の求人IDをもとにRDBから表示用の実データ引っ張る遅延がある
などなどの理由で素直に8倍とはならないのですが、かなり悪く見積もっても性能目標は達成できそうです。一旦安心。
システム設計書とか実装計画とか書いてるうちにインフラが揃ったので開発着手となりました。(インフラ担当の仕事早いと助かりますね)
この2月時点ではだいぶ楽観して6月に終わる予定を引いていました。
性能実測しながら開発
実装計画は4段階ぐらいに分けて開発していました。
トラブルなど起きつつ、概ね計画通りに進行しました。
1. index構築手順の実装
APIサービスコンテナからElasticsearchに道を通したり認可情報を引き回したりする実装です。
なにかあったときにIndex再構築できるように運用コマンド整備したりしました。
作業規模として軽いんですけど、基盤箇所触るのでシステムをふっ飛ばしたりセキュリティに穴を開けたりしかねないので神経を使うところです。
ここでは何も問題は起きませんでした。
2. 性能測定機能の実装
次にやったのが、現行の検索リクエストをElasticsearchに飛ばす機構の実装です。
OpenSearchで概算性能予想を立てていますが予想は予想、どのタイミングで潜在リスクを掘り当てるか分かりません。
この時点では検索リクエストからElasticsearchクエリを組み立てるコードはないので、一旦空クエリ飛ばすように実装します。
開発を進めてロジックを追加していく中でジワジワ遅くなっていくので、性能値を常にモニターしながら開発をする形になります。
性能のTDDです。
見せてもらおうかElasticsearchの性能とやらを。
3. ゴリゴリ実装
コードレビュー体制の確立で少しごたついたものの、ここは特に何も問題なく進行しました。
機能としてはRDBのレスポンスと同じ結果を返すことが要求されるので、先に合致テストを組み込んでTDDで開発する手法を取りました。
データサイエンティストとの方がチームに入ってくれたのもあり、レビューとかをお任せできるようになってからは爆速で進みました。
余談:Rails初心者だが大丈夫か?
大丈夫だ、問題ない。
業務ロジックを書くうえでは他の言語とそんな大差がないのと、Rails固有の事情はChatGPTくんに聞いたらだいたい教えてくれます。
ちょっと書き方が古かったり、Railsっぽくない書き方が混ざってるとは思いますが、ソフトウェアアーキテクチャを壊してたりクラス設計がトチ狂ったりしてたりしない限り後で修正は効きます。
一旦のところは、動けばよかろうなのだァァァァッ!!
受け入れテストと本番の魔物
TDDで大方機能バグは取れているのと、性能も裏でずっと測っているので性能値も大方わかっています。
あとは裏表をスイッチするだけなのですが、とはいえ本番環境には魔物が潜んでいるので警戒は怠れません。
開発者のユーザーIDを含む検索リクエストのみElasticsearch側に倒れるように制御していざ本番環境テスト!
いました、本番の魔物。
極めて稀なケースなんですが、求人の更新が正しくElasticsearch側に反映されない不具合がいくつか見つかりました。
RDBのリードレプリカ反映時間原因
テストパターン漏れとロジックミス
ライブラリ側の挙動が「妙だな・・・」
など理由は様々な要因が複合していたのですが、一つ一つのバグは小さいのでプチプチ潰していきました。
最終的にはRDB側とElasticsearch側のデータ整合性検証スクリプトを回して検知した差分を自動復旧かけさせることで乖離を抑え込みました。
結構この作業が大変で、当初リリースを予定していた6月をオーバーして開発完了は7月上旬となりました。
うー悔しい。
カナリアリリース
当初からカナリアリリースはするつもりでした。
というわけで計画通りに7月いっぱいかけて順次検索リクエストをElasticsearchに倒していきました。
この瞬間がドキドキします。
1%リクエスト開放
ログに張り付いてエラーが発生しないか、負荷モニタが異常な挙動をしないかを確認します。
一通り本番環境テストで不具合を潰していますが、想定外のデータパターンによる不具合が起きないとも限りません。
1%とはいえ莫大なリクエストが流れ込みますので、開放して数時間でほとんどのデータパターンは試されるはずです。
何も踏まないことを祈るしかない。
問題なし!
20%リクエスト開放
負荷状況に注意しながら更に開放して、数日のあいだ様子を見ます。
モニタに写り込まない不具合が発生していないかの確認のためです。
こちらでは見つけられないのでユーザーから不具合報告を待つしかありません。
ここも祈るしかないです。
報告なし!
100%リクエスト開放
最後に完全開放して一週間程度のあいだ安定稼働を確認します。
問題なし!
特に大きな問題なくElasticsearchへのリプレイスが完了しました!
まとめ
プロジェクトの完了までのポイントをまとめておこうと思います。
腹をくくる
いきなり根性論かよと思うかもしれませんが、一定期間予算をぶっこんでリスクを孕んだプロジェクトを走りきるには、やり切るぞという覚悟がいります。
リアーキテクチャリングはリスクを伴う作業なので障害もしばしば起きます。(実際に本プロジェクトでも関連箇所で数件の障害が起きています)
未知の既存不具合を踏んだり、他の機能開発のストップを掛けることもあるかと思います。
とはいえリアーキテクチャリングはプロダクトが成長していくうえでどこかでやらなければいけません。
経営判断で突如中止となったプロジェクトとかも見てきたので、この辺は開発者の技量はもちろんのことですが、経営層との信頼関係維持は大切だなと思います。
「見積もりも報告もする。できる範囲でリスクは抑える。だから日和らず完遂まで投資しろ」って言えるかは大事です。
大きなリスクから潰す
受け入れテストでは様々な問題が噴出し、結果として開発完了が2週間ほど伸びています。
しかしプロジェクト存続に影響を与えたり、大規模スケジュール遅延を引き起こすようなものはありませんでした。
これはプロジェクト序盤に大きなリスクを潰せたからこそだと思っています。
プロジェクトの最大のリスクはなにか?
早く潰すにはどうすればいいか?
この辺をプロジェクトの節目節目で確認することで、開発における重要なポイントは必然的に絞られます。
重要なポイントさえ押さえて早期対処していれば、プロジェクト後半で多少バグやエラーが見つかったところで、全体としてはさして脅威ではありません。
チームワーク
リアーキテクチャリングはどう頑張っても一人では走りきれません。
チームワークは通常の機能開発でも欠かせないですが、リアーキテクチャリングの場合はより広範囲の部署を巻き込んでいきます。
インフラの整備に始まり広範囲のコード修正、組織自体が抱える問題に当たることもあるでしょう。
インフラ選定によっては経理などにも影響がります。
プロジェクト完走までには様々な人の協力が必要です。
協力していただくすべての方々に感謝の気持ちを忘れず、それでいて必要なな協力は積極的にお願いしていくというのは必要になってきます。
さいごに(広告欄)
書籍:Clean Architecture
アーキテクチャを触る人すべてにおすすめの本。
例の同心円状の図をClean Architectureだと誤解してないですか?
境界の引き方こそがキモです。
企業:タイミー
スキマバイトアプリでシェアNo1のタイミーを開発しています。
バックエンドエンジニアを募集しているそうですよ。