monstacheでmongodbとelasticsearchを同期する
mongodbでjsonデータを管理し、検索・分析はelasticsearchにオフロードしたい、というニーズは多いかと思います。そのときどのようにmongodbとelasticsearchの同期をとるかという問題が発生します。
この時、同期の仕方には主に2パターンのやり方があると思っています。
- ツールを挟んでmongodbからelasticsearchに対する片方向同期を行う
- アプリケーションからmongodbとelasticsearch両方にデータを書き込む
2 のアプローチだと mongooseのmiddlewareに処理を挟むなどのアプローチがあるようなのですが、トランザクションを組むのが大変そうなので、1 のアプローチをなるべく取りたいと考えていました。調べてみるとmonstacheというツールが、カスタマイズ性を担保しつつ、同期をとるのを任せられそうなので使ってみることにしました。
monstache?
monstacheはmongodbからelasticsearchへ片方向同期を掛けるためのgoで書かれたサーバです。単純に同期するだけならmongodbとelasticsearchの接続エンドポイントを記載するだけで簡単に使い始めることができます。また、ある程度カスタマイズした運用も可能で、論理削除されたデータは同期しないようにできたり、特定のfieldを変更・追加・削除できたりします。本番運用を見据えてHA構成を組むこともでき、至れり尽くせり感がありますね。
ドキュメントはこちら。
実際に同期してみる
では実際にmonstacheサーバを起動してみましょう。monstacheはプロジェクトの公式dockerコンテナイメージが公開されているので、ありがたくコンテナで実行してみましょう。今回は3台のサーバでHA構成を取ってみるため、各サーバで同じ作業を行います。
まず、コンフィグファイル config.toml
を書きます。単純に同期し始めるだけなら以下のように書けばokです。
mongo-url = "<mongodbのendpoint>"
elasticsearch-urls = ["<elasticsearchのendpoint>"]
今回はHA構成を試しつつ、初回全同期、fieldの変更・追加・削除も試してみたいので、以下のようにコンフィグを書いてみました。
mongo-url = "<mongodbのendpoint>"
elasticsearch-urls = ["<elasticsearchのendpoint>"]
cluster-name = "MonstacheCluster"
direct-read-namespaces = [""]
direct-read-dynamic-include-regex = "monstache-test\..*"
[[script]]
script = """
module.exports = function(doc) {
if (doc.deleted) {
return false;
}
delete doc.deleted;
doc.added_field = doc.description + " added_info";
return doc;
}
cluster-name
にmonstacheクラスタの名前を記載します。この名前を書いておくとmongodbにmonstache
コレクションが作成されて、今どのmonstacheサーバがelasticsearchとの同期を担当しているか、いつまでその同期が有効かという時間が書かれたドキュメントが挿入されます。monstacheサーバはこのデータをお互いに確認しており、データ同期を担当しているサーバは同期有効期限を更新し続けます。この有効期限の更新が途切れた時、monstacheクラスタは同期担当サーバが停止したと判断し、別のサーバが同期を肩代わりすることでHAを実現しています。
direct-read-namespace
と direct-read-dynamic-include-regex
は初回全同期するコレクションを指定しています。 direct-read-dyanmis-include-regex
に同期対象のコレクション名を記載します。このオプションを記載する場合、 direct-read-namespace
の値は [""]
で固定です。
[[script]]
セクションにはelasticsearchへデータを渡す前にdocumentを加工するためのスクリプトを書くことができます。言語はJavascriptです。今回は真偽値field deleted
が true
だったらそもそもdocumentを同期せず、同期対象のdocumentから deleted
fieldを外し、 added_field
というfieldにデータを追加して同期を行っています。
コンフィグファイルが書けたら、docker compose用の設定ファイルを以下のように準備します。monstacheの設定ファイルは /etc/monstache/config.toml
に配置されている前提となります。
version: '3.9'
services:
monstache:
image: rwynn/monstache:rel6
working_dir: /monstache
command: -f ./config.toml
volumes:
- /etc/monstache:/monstache/
restart: always
あとは、docker compose up -d
するだけ。
sudo docker compose up -d
すると、実際に同期をとっているサーバには、ログが以下のように出力されます。
INFO 2023/06/15 07:21:43 Started monstache version 6.7.11
INFO 2023/06/15 07:21:43 Go version go1.17.4
INFO 2023/06/15 07:21:43 MongoDB go driver v1.10.3
INFO 2023/06/15 07:21:43 Elasticsearch go driver 7.0.31
INFO 2023/06/15 07:21:43 Successfully connected to MongoDB version 6.0.6
INFO 2023/06/15 07:21:43 Successfully connected to Elasticsearch version 8.8.1
INFO 2023/06/15 07:21:43 Joined cluster MonstacheCluster
INFO 2023/06/15 07:21:43 Starting work for cluster MonstacheCluster
一方で同期していないサーバでは以下のようにログが出力されました。Pausing work
とあるので、待機しているみたいですね。
INFO 2023/06/15 07:21:48 Started monstache version 6.7.11
INFO 2023/06/15 07:21:48 Go version go1.17.4
INFO 2023/06/15 07:21:48 MongoDB go driver v1.10.3
INFO 2023/06/15 07:21:48 Elasticsearch go driver 7.0.31
INFO 2023/06/15 07:21:48 Successfully connected to MongoDB version 6.0.6
INFO 2023/06/15 07:21:48 Successfully connected to Elasticsearch version 8.8.1
INFO 2023/06/15 07:21:48 Joined cluster MonstacheCluster
INFO 2023/06/15 07:21:48 Pausing work for cluster MonstacheCluster
今回はサンプルアプリとして Articles
アプリをRailsで作ってテストしてみます。単純にArticle
モデルをscaffoldしただけですが。。。
このアプリはmongodbにだけ書き込みを行うようにセットアップしてあります。
実際に記事が一つ登録されていますが、elasticsearchの検索apiを実際に叩いてみると、こんな感じでelasticsearchにも登録されています。また登録されている内容も deleted
fieldは存在せず、 added_field
fieldが新たに追加されています。
root@code-server:~# curl -H 'Content-type: application/json' -d '{"query":{"match_all":{}}}' 'http://endpoint.es.dynamis.bbrfkr.net/monstache-test.articles/_search' | jq .
{
<中略>
"hits": {
"total": {
"value": 1,
"relation": "eq"
},
"max_score": 1,
"hits": [
{
"_index": "monstache-test.articles",
"_id": "648f9f3b02f9ea0001159b4d",
"_score": 1,
"_source": {
"added_field": "monstache特集 added_info",
"created_at": "2023-06-19T00:20:11.434Z",
"description": "monstache特集",
"title": "いま、monstacheがあつい!",
"updated_at": "2023-06-19T00:20:11.434Z"
}
}
]
}
}
また、deleted fieldを true
にしてみると、、、
root@code-server:~# curl -H 'Content-type: application/json' -d '{"query":{"match_all":{}}}' 'http://endpoint.es.dynamis.bbrfkr.net/monstache-test.articles/_search' | jq .
{
<中略>
"hits": {
"total": {
"value": 0,
"relation": "eq"
},
"max_score": null,
"hits": []
}
}
このようにelasticsearch側からも削除されていることがわかります。
HAの検証
最後にHAのテストをしてみましょう。稼働しているmonstacheコンテナを落としてみます。
sudo docker compose down
すると、別のmonstacheコンテナのログには以下のようなログが出力され、同期が肩代わりされたことがわかります。
INFO 2023/06/15 07:21:48 Joined cluster MonstacheCluster
INFO 2023/06/15 07:21:48 Pausing work for cluster MonstacheCluster
INFO 2023/06/19 00:44:28 Resuming work for cluster MonstacheCluster
INFO 2023/06/19 00:44:28 Dynamic direct read candidates: [monstache-test.articles]
INFO 2023/06/19 00:44:28 Listening for events
INFO 2023/06/19 00:44:28 Watching changes on the deployment
INFO 2023/06/19 00:44:28 Resuming from timestamp {T:1687135275 I:2}
実際にArticleをを追加してみても。。。
root@code-server:~# curl -H 'Content-type: application/json' -d '{"query":{"match_all":{}}}' 'http://endpoint.es.dynamis.bbrfkr.net/monstache-test.articles/_search' | jq .
{
<中略>
"hits": {
"total": {
"value": 1,
"relation": "eq"
},
"max_score": 1,
"hits": [
{
"_index": "monstache-test.articles",
"_id": "648fa65002f9ea0001159b4e",
"_score": 1,
"_source": {
"added_field": "monstache特集第二号 added_info",
"created_at": "2023-06-19T00:50:24.832Z",
"description": "monstache特集第二号",
"title": "あなたのmonstacheは大丈夫?",
"updated_at": "2023-06-19T00:50:24.832Z"
}
}
]
}
}
このようにちゃんと同期が続いていることがわかりますね!
Discussion