💽

monstacheでmongodbとelasticsearchを同期する

2023/06/19に公開

mongodbでjsonデータを管理し、検索・分析はelasticsearchにオフロードしたい、というニーズは多いかと思います。そのときどのようにmongodbとelasticsearchの同期をとるかという問題が発生します。

この時、同期の仕方には主に2パターンのやり方があると思っています。

  1. ツールを挟んでmongodbからelasticsearchに対する片方向同期を行う
  2. アプリケーションから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-namespacedirect-read-dynamic-include-regex は初回全同期するコレクションを指定しています。 direct-read-dyanmis-include-regex に同期対象のコレクション名を記載します。このオプションを記載する場合、 direct-read-namespace の値は [""] で固定です。

[[script]] セクションにはelasticsearchへデータを渡す前にdocumentを加工するためのスクリプトを書くことができます。言語はJavascriptです。今回は真偽値field deletedtrue だったらそもそも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