@kizashi1122 こと、永田です。
@johtani さんがツイートするもんだから、あいよっと返事してしまいました。
以下、@johtani さんに話したことも話してないこともツラツラと書いていこうと思います
はじめに
弊社のサービスである「Re:lation」はメールやチャットや電話メモなどを一元的に管理できるサービスです。 「そういえば○○なメール来てたはずだけど・・・」などという時のために当然検索は必要になります。RDBMS の LIKE 検索でもいいわけですが、インデックスが効かないしデータが増えるとパフォーマンスがでないのは見えていたので、検索エンジンは専用のソフトウェアを使うべきとは思っていました。当時は世の中では Solr も現役だったのですが、同じ Lucene をエンジンとするミドルウェアでも Elasticsearch が伸びてきているのもあり、Elasticsearch を選択しました。
Elasticsearch on EC2 での運用からスタート
「Re:lation」はリリース当初(2014年)から検索機能はありました。 当時は、Amazon Elasticsearch Service はなく、自前で EC2 上に Elasticsearch を立ててました。最初はなんと1台構成です。余裕でSPOFです。使えなくなるのは検索機能だけですけど。
リリース当時の Elasticsearch のバージョンは当時の最新で v1.3.4 でした。 その後、v1.3.6, v1.7.0, v1.7.1, v2.3.2 とスキを見てはバージョンアップし、その後はだいぶとサボって2018 年の初めに v5.6.5 に上げました。
その頃にはとっくに Amazon Elasticsearch Service がローンチしており(2015年にリリース)、もう十分運用されていることもあり、去年の11月にようやく移行することができました(エンジンのバージョンは v7.1)
Amazon Elasticsearch Service への移行
これはかなり頭を悩ませました。 スナップショットをとって移行すべきか? バージョンが違うと互換性はどうなるのか?
Bit Journey 社の@michiomochi こと道川さんにも相談しましたが、やはりインデックスを作り直すのがいいだろうということになりました。一からドキュメントのインデクシングをするわけなので、互換性については考慮する必要がないのはメリットです。
つまり旧環境で運用しつつ、新環境(Amazon Elasticsearch Service)にDBからデータをインポートするバッチを流すわけです。当然、運用しながらなので旧環境にしか反映されないデータもあるわけですが、初回のインポートが完了後、データベースと新環境のインデックスの差分をみて、差分があれば吸収するバッチを何度も動かして、同期をとっていきます。ほぼ同期がとれたところで、アプリからの参照先を新環境に向け、その後再度同じバッチを流して完了です。ダウンタイムはありません。
この1からインデックスをつくってデータを流し込み、同期をとっていく作業に3週間くらいかかりました。
インデックス設計
Re:lation はマルチテナント型のウェブアプリケーションです。
テナントごとに検索できる必要があるわけです。 どうやってインデックスの設計をするかが難しいところです。
最初は、どこかで読んだ「1テナント:1インデックスにすべし」という記事を鵜呑みにして、何も考えずにそうしました。 というかこれで特に問題はありませんでした。なんなら、Github の elasticsearch-rails のリポジトリのイシューに投稿されるこの手の疑問には「1テナント:1インデックスがいいよー」と私が答えてました。
https://github.com/elastic/elasticsearch-rails/issues/321 https://github.com/elastic/elasticsearch-rails/issues/359
ただ、顧客(=テナント)が増えると当然インデックスも増えてくるわけで、インデックス多すぎ問題が出てきます。 当時で300弱くらいでした。実のところ、インデックスが多すぎによる問題は発生していませんでしたが、今後のことを考え設計を見直しました。 Elasticsearch が管理する対象のインデックスが多くなれば負荷も上がっていくことが予想されます。
次に考えられるのは「データは1インデックスにして、ドキュメントの属性としてテナント識別子を持つ」というやり方です。毎回検索条件にテナント識別子を含めればいいよねという考え方です。
ただデータ量が相当あったので、インデックスは5つにして、テナントは tenant_id で割り振っていくことにしました。シャード数は6だったかな。シャード を限定したアクセスが可能なルーティングという機能があるのを知り、使うことにしました。ルーティングを使えば、エイリアスにルーティング( _routing
)を含めることができるため、物理インデックスを意識しなくても、検索条件に明示的にテナント情報を含めなくてもアクセスできます。
actions = [{ add: { index: real_index_name, alias: alias_name, routing: tenant_id, filter: { term: { tenant_id: tenant_id } } } }] client.indices.update_aliases(body: { actions: actions })
今は基本的な構成はこの頃から変わってないですが、データ量増加にともない、インデックス数は5ですが1インデックスあたりのシャード数は8としています。(1シャードあたりのデータ量は大きくしたくない) また今はルーティング機能による高速化は実感できなかったため素直にルーティングは外しました。
actions = [{ add: { index: real_index_name, alias: alias_name, filter: { term: { tenant_id: tenant_id } } } }] client.indices.update_aliases(body: { actions: actions })
このあたり正解がなく手探りなところが辛いです。
課題
Amazon Elasticsearch Service ではインスタンスストレージが使える i3 シリーズを使っています。インスタンスストレージはEBSのようにネットワーク越しでディスクを使うわけではなく、PCIバスなどによりサーバーに直接つながっているので高速です(ただしディスクサイズは自由に決められなかったり、揮発性であるというリスクはあります)これにより大幅な検索パフォーマンスの改善ができました。お金の力すごい。
ただ、とはいえ検索遅いクエリなどはまだあったりします。ここは問題です。Elasticsearch では「一定時間で検索したところまでを返却する」ということはできますが、ウチの用途ではさすがにこれは許されません。
複数のインデックスへの割り振りは単純に tenant_id でおこなっていると上述しましたが、当然、ヘビーに使うテナントがあるインデックスに偏るということはあります。平準化が難しいです。
大谷さんにはもっと細かい実装時のお話もしましたが、それはまた別のエントリにわけたいと思います。