Groonga delta - 差分ベースでMySQL/MariaDBのデータをGroongaに取り込むツール - 2022-05-20 - ククログ

ククログ

株式会社クリアコード > ククログ > Groonga delta - 差分ベースでMySQL/MariaDBのデータをGroongaに取り込むツール

Groonga delta - 差分ベースでMySQL/MariaDBのデータをGroongaに取り込むツール

Groonga deltaというMySQL/MariaDBのデータをリアルタイムでGroongaに同期するツールを開発した須藤です。どのような使い方・設計・実装になっているかを説明します。

機能の説明

Groonga deltaは次の機能を提供します。

  • MySQL/MariaDB内にあるデータを一括でGroongaに取り込む機能
  • MySQL/MariaDBへのINSERT/UPDATE/DELETEの結果をリアルタイムでGroongaに取り込む機能
  • データ取り込み時にデータを加工する機能

これらの機能により次のようなユースケースを実現できます。

  • マスターデータはMySQL/MariaDBで管理し、全文検索はGroongaで実現する
  • アプリケーションは検索の仕方以外Groongaのことを知らずに済みたい
  • 検索性能をスケールアウトしたい

GroongaをMySQL/MariaDBに組み込むMroongaでも同様のことは実現できるのですが、MroongaはクラッシュセーフではないためInnoDBでデータを管理している場合に比べて運用コストが上がる点がネックです。Groonga deltaのアプローチではInnoDBでマスターデータを管理できます。Groongaが持っているデータが壊れたとしてもInnoDBのマスターデータを再度取り込んで復旧できるためMroongaがマスターデータを持っている場合に比べて運用コストが上がりにくいです。ただし、GroongaサーバーなどMySQL以外にも管理対象が増えるという点で運用コストは上がります。

実現方法の概要

Groonga deltaがやっていることは端的に言うとレプリケーションです。MySQL/MariaDBがソースでGroongaがレプリカのレプリケーションです。

実際、「MySQL/MariaDBへのINSERT/UPDATE/DELETEの結果をリアルタイムでGroongaに取り込む機能」はMySQL/MariaDBのレプリケーション機能で実現しています。MySQL/MariaDBにレプリケーションクライアントとして接続し「差分情報(binlog)を取得してGroongaに取り込む」ということをし続けています。ただし、取得した差分情報をそのままGroongaに取り込むわけではありません。一旦、取得した差分情報をストレージに書き出し、別途書き出された差分情報をGroongaに取り込みます。

違う:
|MySQL|ーbinlog→|Groonga delta|ー差分情報→|Groonga|

Groonga deltaのアプローチ:
|MySQL|ーbinlog→|Groonga delta|ー差分情報→|ストレージ|
|ストレージ|ー差分情報→|Groonga delta|→|Groonga|

直接Groongaに取り込むアプローチでは「Groongaが落ちるとデータを取りこぼすかもしれない」という問題があります。これは次のときに発生します。

  • Groongaが落ちている間はbinlogを処理できない
  • MySQLは古いbinlogを削除するかもしれない
  • 未処理のbinlogが削除されるとデータを取りこぼす

一方、Groonga deltaのアプローチではGroongaに直接取り込むのではなくストレージに保存するのでストレージが生きていればbinlogの取り込みは続けられます。Groongaよりストレージの方が安定しているはずなのでGroongaに直接アプローチよりもデータの取りこぼしリスクが下がります。

またGroonga deltaのアプローチだとMySQLから取得した差分情報を何度でも取り込めるというメリットがあります。これは次のときに嬉しいです。

  • GroongaのDBが壊れてデータを再度取り込まないといけなくなったとき、MySQLに接続しなくても取り込める
  • 複数のGroongaインスタンスに取り込める
    • 複数のGroongaで検索を処理する検索システムを構築できる(スケールアウトできる)

デメリットはストレージを消費することです。

Groonga deltaは次の2つのサービスからなります。

  • groonga-delta-import
  • groonga-delta-apply

groonga-delta-importがMySQLのbinlogを取得して差分情報をストレージに書き出すサービスです。データの加工もgroonga-delta-importの仕事です。

groonga-delta-applyがストレージに書き出された差分情報をGroongaに取り込むサービスです。

これらの実現方法の詳細を説明する前に使い方を説明します。

使い方

groonga-delta-importgroonga-delta-applyはローカルにインストールして使うこともできますがGroonga deltaが提供しているDockerイメージを使うことを推奨します。具体的にはghcr.io/groonga/groonga-deltaというイメージを使ってください。最新バージョンを使う場合はghcr.io/groonga/groonga-delta:latestで特定のバージョンを使う場合(たとえば1.0.0)はghcr.io/groonga/groonga-delta:1.0.0です。

このイメージの中にgroonga-delta-importgroonga-delta-applyも入っているので次のように使います。

docker run --rm \
  ghcr.io/groonga/groonga-delta:latest \
  groonga-delta-import --server ...

docker run --rm \
  ghcr.io/groonga/groonga-delta:latest \
  groonga-delta-apply --server ...

ただし、設定ファイルや各種データを書き込むための場所を用意する必要があるので、実際はホストのパスをボリュームとしてマウントして使うことになります。詳細はこの後に説明するので、ここではghcr.io/groonga/groonga-deltaイメージを使えばすぐにgroonga-delta-importコマンドとgroonga-delta-applyコマンドを使えるということがわかれば十分です。

それではそれぞれのコマンドのより細かい使い方を説明します。

groonga-delta-importの使い方

まずgroonga-delta-import用のディレクトリーを用意します。ここではimport/ディレクトリーを用意します。

mkdir -p import

このディレクトリーの直下にconfig.yamlを用意します。設定ファイルになります。

editor import/config.yaml

次のような内容になります。delta_dirlog_dirのディレクトリーを../import/ディレクトリーと同じレベルに置いていることがポイントです。log_dirは別によいのですが、delta_dirは後で設定するgroonga-delta-applyと共有する(groonga-delta-importdelta_dirに出力した差分情報をgroonga-delta-applyがGroongaに取り込む)のでimport/以下ではなくimport/と同じレベルに置いてある方が便利なのです。

delta_dir: ../delta # 差分を保存するディレクトリー
log_dir: ../log # ログを保存するディレクトリー
local: # 静的に用意するGroongaコマンドの設定
       # スキーマや初期データやインデックス定義に使う
  dir: local # Groongaコマンドが保存されているディレクトリー
  initial_max_number: 99 # MySQLから初期データを取り込む前に
                         # どこまでのGroongaコマンドを使うか
                         # (詳細は後述)
mysql: # データを持っているMySQLサーバーの設定
       # binlog_formatはROWでないといけない!!!
  host: 192.168.0.100 # IPアドレス
  replication_slave:
    # レプリケーション用のユーザー
    # REPLIACATION SLAVE権限だけあればよい
    user: "replicator"
  replication_client:
    # レプリケーション関連の初期化用のユーザー
    # SHOW MASTER STATUSなどを実行する
    # REPLIACATION CLIENT権限だけあればよい
    user: "c-replicator"
  select:
    # 初期データ取り込み・テーブルのメタデータ取得用のユーザー
    # SELECT権限だけあればよい
    user: "selector"
mapping:
  # MySQLのデータをどのようにGroongaに取り込むかの設定(後述)
  ...:

config.yamlとは別にsecret.yamlという同じ構造の設定ファイルも使うことができます。secret.yamlはMySQL接続用のパスワードなど秘密の情報を入れておき、必要なユーザーだけが読めるようにします。(config.yamlの中にパスワードも書いてsecret.yamlを使わないという使い方もできます。)

touch import/secret.yaml
chmod go-rwx import/secret.yaml
editor import/secret.yaml

たとえば、次のようにMySQL接続用のパスワードを設定します。

mysql:
  replication_slave:
    # レプリケーション用のユーザーのパスワード
    password: "replicator-password"
  replication_client:
    # レプリケーション関連の初期化用のユーザー
    password: "c-replicator-password"
  select:
    # 初期データ取り込み・テーブルのメタデータ取得用のユーザー
    password: "selector-password"

「MySQLのデータをどのようにGroongaに取り込むかの設定」(mapping)について説明する前にlocalについて説明します。

MySQLからGroongaにデータを取り込むにはGroonga側にデータを取り込む場所がないといけません。つまり、データを取り込む先のテーブルやカラムが必要です。また、高速に検索するためにはインデックス定義も必要です。そのようなMySQL上の各レコードと直接対応しないGroonga上の操作のためにlocalは存在します。ちなみ、localはローカルにあるファイルからデータ(Groongaコマンド)を取り込むという意味です。

今回はimport/local/以下にGroongaコマンドを配置するという設定にした(local.dirlocalにした)ので、import/local/以下にどのようにファイルを配置するかを説明します。

import/local/以下のファイルは数字から始める必要があります。たとえば、000_logs.grnとか001_users.grnといった感じです。groogna-delta-importはその数字を10進数の数値として解釈して昇順にソートします。ソートしたら順番に実行します。

数値は連続していなくても構いません。たとえば、000_logs.grn002_groups.grnだけしかなくても構いません。そのため、0から99までは初期スキーマ定義、100から199までは初期インデックス定義、200以降は導入後のスキーマ変更というように範囲で使い方を決めて運用にすることになります。このとき重要なのがMySQLから初期データを取り込む(binlogからの差分取り込みではなくすでにMySQLに入っているデータを取り込む)前に実行したいか後に実行したいかの境目です。境目を判断するときの基準は「テーブル定義・プラグイン読み込み」は前で「インデックス定義」は後です。なぜかというとインデックスを作ってからデータを入れるよりデータを入れてからインデックスを作る方が高速だからです。データを入れてからインデックスを作ると静的なインデックス構築を使えます。

たとえば、次のようなスキーマを使いたいとします。

# ログ
table_create logs TABLE_HASH_KEY ShortText
column_create logs message COLUMN_SCALAR ShortText

# ユーザー
table_create users TABLE_HASH_KEY ShortText
column_create users age COLUMN_SCALAR UInt8

# logs.messageを全文検索するためのインデックス
table_create terms lexicon TABLE_PAT_KEY ShortText \
  --default_tokenizer TokenBigram \
  --normalizer NormalizerNFKC130
column_create terms logs_message \
  COLUMN_INDEX|WITH_POSITION \
  logs message

この場合、logsテーブルとusersテーブルの定義はMySQLから初期データを取り込む前に実行して、termsテーブルは後に実行するとよいということです。

これを制御するための設定が後で説明するとしていたlocal.initial_max_numberです。↑の設定では99にしていました。この数値の設定まではMySQLから初期データを取り込む前に実行します。ということで、次のようなファイル構成にします。

# import/local/000_logs.grn
# ログ
table_create logs TABLE_HASH_KEY ShortText
column_create logs message COLUMN_SCALAR ShortText
# import/local/001_users.grn
# ユーザー
table_create users TABLE_HASH_KEY ShortText
column_create users name COLUMN_SCALAR ShortText
column_create users birthday COLUMN_SCALAR Time
# import/local/100_logs_index.grn # ←99より大きい!
# logs.messageを全文検索するためのインデックス
table_create terms lexicon TABLE_PAT_KEY ShortText \
  --default_tokenizer TokenBigram \
  --normalizer NormalizerNFKC130
column_create terms logs_message \
  COLUMN_INDEX|WITH_POSITION \
  logs message

これでテーブル定義はMySQLから初期データを取り込む前に実行し、インデックス定義は後に実行します。

運用後にスキーマを変更する場合はimport/local/以下にファイルを追加していくことになりますが既存のファイルより大きい数値にすることを忘れないでください。そうしないと新しく追加したファイルが処理されません。たとえば、↑の例だと099_groups.grnを追加しても処理ません。101_groups.grnなど100_logs_index.grn100よりも大きな数値にします。

それでは後で説明するとしていた「MySQLのデータをどのようにGroongaに取り込むかの設定」(mapping)について説明します。

これは非常に重要な設定でいろいろな機能があります。少し長くなりますが一通り説明します。

mappingは次のようにGroonga側のテーブル名をキー、そのGroonga側のテーブルにどのようにデータを入れるかを指定するのが値のマッピング(このマッピングはYAML用語のマッピング)です。

mapping:
  Groongaのテーブル名1:
    ... # どうやって↑にデータを入れるか
  Groongaのテーブル名2:
    ... # どうやって↑にデータを入れるか
  ...

MySQL側の複数のテーブルのレコードを1つのGroongaのテーブルに集約することもできます。これはどうしてかというと、Groongaのスキーマを設計するときは同時に検索したいものを1つのテーブルに集約することがすごく重要だからです。詳細は階層構造データ用のGroongaのスキーマ設計方法を参照してください。ということで、次のように複数の取り込み元のMySQLのテーブルを書けます。データを取り込むときはGroongaの_keyが必須です。_keyがないとMySQL側のレコードとGroonga側のレコードを同定できず、DELETE/UPDATEを同期できません。

mapping:
  users: # 取り込み先のGroongaのテーブル名
    sources:
      - database: app1 # 取り込み元のMySQLのデータベース名
        table: admins  # 取り込み元のMySQLのテーブル名
        columns:
          # キーは取り込み先のGroongaのカラム名
          # 値は取り込まれる値

          # _keyは必須!
          # 値の中の%{id}は取り込み元のMySQLのidカラムの値に展開される
          _key: "app1-admins-%{id}"
          # 値の中の%{name}は取り込み元のMySQLのnameカラムの値に展開される
          name: "%{name}"
      - database: app2   # 取り込み元のMySQLのデータベース名
        table: operators # 取り込み元のMySQLのテーブル名
        columns:
          # キーは取り込み先のGroongaのカラム名
          # 値は取り込まれる値

          # _keyは必須!
          # 値の中の%{id}は取り込み元のMySQLのidカラムの値に展開される
          _key: "app2-operators-%{id}"
          # 値の中の%{full_name}は取り込み元のMySQLのfull_nameカラムの値に展開される
          name: "%{full_name}"

↑の例ではcolumnsの中には単純にGroongaのカラムとMySQLのカラムの値からGroongaの値を作るためのテンプレートを指定していました。Groongaのカラムの型が文字列(ShortTextとか)の場合はこれでよいのですが、数値(UInt8とか)や時刻(Time)の場合は明示的に型を指定しないといけません。指定しなくてもGroongaのキャスト機能で動くこともあるのですが、指定した方が安全です。適切な型を指定するには次のようにtypeを指定します。

mapping:
  users: # 取り込み先のGroongaのテーブル名
    sources:
      - database: app1 # 取り込み元のMySQLのデータベース名
        table: admins  # 取り込み元のMySQLのテーブル名
        columns:
          _key: "app1-admins-%{id}"
          birthday:
            # 値はtemplateに書く
            template: "%{birth_year}-%{birth_month}-%{birth_day}"
            # 型はtypeに書く
            type: Time

単純な文字列の連結でGroongaのカラムの値を作れない場合は次のようにexpressionを使ってRubyの式を書くことができます。expressionを使う場合、MySQLのどのカラムを使うかをsource_column_namesで指定しないといけないことに注意してください。groonga-delta-importは必要なカラムのみMySQLから取得することで無駄なデータの転送による性能低下を抑えているからです。source_column_namesから漏れているとMySQLから該当カラムを取得していないのでエラーが発生します。

mapping:
  logs: # 取り込み先のGroongaのテーブル名
    sources:
      - database: app1 # 取り込み元のMySQLのデータベース名
        table: logs    # 取り込み元のMySQLのテーブル名
        columns:
          _key: "app1-logs-%{id}"
          message:
            # 式はexpressionに書く
            # %{message}ではなくmessageだけでMySQLのカラム名を参照できる
            # stripはRubyが提供している先頭と最後の空白すべてを削除するメソッド
            expression: |
              category + ": " + detail.strip
            # expressionで使っているMySQLのカラム名をすべて列挙する
            source_column_names:
              - category
              - detail

restrictionを使うことで取り込むレコードを制限することもできます。今のところ時刻の値が特定の範囲内のデータにあるかだけで制限できます。これはGroonga deltaを開発した案件のユースケースではこの機能だけで十分だったからです。どういうときに使うかというと、MySQLにGroongaでは扱えない範囲の時刻(たとえば0000-01-01とか)が入っていた場合にそのレコードを無視するために使います。

mapping:
  logs: # 取り込み先のGroongaのテーブル名
    restriction:
      # 時刻型の値が1970-01-01から2100-01-01の範囲外にあるカラムが
      # 1つでもあるレコードは処理対象外とする
      time:
        min: "1970-01-01T00:00:00Z"
        max: "2100-01-01T00:00:00Z"

mappingの書き方は以上です。

これでgroonga-delta-import用のディレクトリーimport/を使う準備ができました。import/ではなくimport/の親ディレクトリーをボリュームとしてマウントします。これは、import/config.yamlの中で../delta/など親ディレクトリーの直下にあるディレクトリーを参照しているからです。マウントしたらgroonga-delta-import --dirでマウントしたパスの下にあるimport/を指定して使います。

docker run --rm \
  --volume $PWD:/var/lib/groonga-delta \
  ghcr.io/groonga/groonga-delta:latest \
  groonga-delta-import \
    --server \
    --dir /var/lib/groonga-delta/import

groonga-delta-applyの使い方

続いてgroonga-delta-applyの使い方を説明します。groonga-delta-applyも専用のディレクトリーを用意します。ここではapply/ディレクトリーを用意します。

mkdir -p apply

このディレクトリーの直下にconfig.yamlを用意します。設定ファイルになります。

editor apply/config.yaml

次のような内容になります。

log_dir: ../log # ログを保存するディレクトリー
local: # ローカルにある差分情報を取り込む
  # groonga-delta-importのdelta_dirで指定したディレクトリーと同じ場所
  delta_dir: ../delta
groonga: # 取り込み先のGroongaの設定
  # GroongaサーバーのURL
  url: "http://192.168.0.200:10041"
  # Groongaサーバーからのレスポンスがあるまでのタイムアウト。
  # 初期データを取り込むときに1つのリクエストで
  # 100万件のレコードを取り込むことがあるので長めに設定。
  read_timeout: 1800

groonga-delta-applyにはgroonga-delta-importにあったようなGroongaデータとMySQLデータのマッピングとかが必要ないのでそれほど設定することはありません。

これでgroonga-delta-apply用のディレクトリーapply/を使う準備ができました。apply/ではなくapply/の親ディレクトリーをボリュームとしてマウントします。これは、apply/config.yamlの中で../delta/など親ディレクトリーの直下にあるディレクトリーを参照しているからです。マウントしたらgroonga-delta-apply --dirでマウントしたパスの下にあるapply/を指定して使います。

docker run --rm \
  --volume $PWD:/var/lib/groonga-delta \
  ghcr.io/groonga/groonga-delta:latest \
  groonga-delta-import \
    --server \
    --dir /var/lib/groonga-delta/apply

運用時の設定

動作検証する分には手動でdocker runしてもよいのですが実運用で手動起動はありえません。ということで実運用時はサービス化します。サービス化の実現例としてDocker deltaを使った案件でのやり方を紹介します。

AlmaLinux 8を使ったのでDockerではなくPodmanを使いました。Podmanにはsystemdの設定を生成する機能があるのでそれを使ってサービス化しました。

まず、Groonga delta用の専用ユーザーgroonga-deltaを用意します。設定ファイルは/home/groonga-delta/以下に置きます。実行ユーザはgroonga-deltaにします。

systemdの設定は次のようなスクリプトをgroonga-deltaユーザーで実行すると用意できます。sudoの部分は別のスクリプトにして別のユーザーで実行した方がよいかもしれません。実際はAnsibleで設定したのでこのスクリプトは使っていません。

for service in import apply; do
  container_name=groonga-delta-${service}

  # コンテナーを作る
  podman create \
    --env TZ=Asia/Tokyo \
    --name ${container_name} \
    --replace \
    --user $(id -u groonga-delta):$(id -g groonga-delta) \
    --volume /home/groonga-delta:/home/groonga-delta:z \
    ghcr.io/groonga/groonga-delta:latest \
    groonga-delta-${service} \
      --server \
      --dir /home/groonga-delta/${service}

  # systemdの設定を生成する
  podman generate systemd \
    --files \
    --name \
    --new \
    --no-header \
    ${container_name}
  # podmanのログをjounalctlで見やすいのでpodmanのデーモン化をやめる
  sed -i -e 's/ -d / /g' container-${container_name}.service

  # インストール
  sudo -H cp container-${container_name}.service /etc/systemd/system/
  sudo -H systemctl --daemon-reload
  sudo -H systemctl enable --now container-${container_name}
done

実現方法の詳細

使い方を説明したので実現方法の詳細を説明します。使うだけの人はここは飛ばしてもよいです。

binlog取得機能の実装

MySQLからbinlogを取得する機能はmariadb-connector-cを使っています。これはMariaDBが提供するMySQL/MariaDBのクライアントライブラリーです。MySQLが提供するクライアントライブラリーはMySQLにレプリケーションクライアントとして接続する機能が入っていないのですがmariadb-connector-cには入っているのでmariadb-connector-cを使っています。

なお、MySQL/MariaDBが提供するmysqlbinlogコマンドを使ってbinlogを取得する実装も入っているのですがあまり安定した実装にできなかったので非推奨です。

ただ、開発時にはmariadb-connector-cのレプリケーションクライアント機能に問題があったので改良してパッチを送ってあります。次のように必要な変更は取り込まれているので最新のmariadb-connector-cでは期待通り動きます。

ただ、↑の改良が入ったmariadb-connector-cのパッケージがない環境もまだ多いので前述のDockerイメージを使うことを推奨しています。あのDockerイメージにはまだ取り込まれていないパッチも含んだmariadb-connector-cが入っています。

Groonga deltaはRubyで実装されているのでmariadb-connector-cにレプリケーションクライアント機能があってもRubyバインディングがなければ使えません。Rubyにはmysql2というMySQL提供のクライアントライブラリーにもmariadb-connector-cにも対応したバインディングがあるのですが、mariadb-connector-cにだけあるレプリケーションクライアント機能のバインディングは含まれていません。

ということでmysql2にレプリケーションクライアント機能のバインディングを追加するバインディングを開発しました。それがmysql2-replicationです。

mariadb-connector-cのレプリケーションクライアント機能は受信したbinlogをパースする機能を提供していません。Rubyでbinlogをパースする機能を実装したmysql_binlogがあるのでそれを使ってパースしてもよいのですが、今回はbinlogの流量がかなり多い環境でも使いたく、速度が遅いとイヤだったのでCで実装しているmysql2-replication内でbinlogをパースしています。速度は比較していないのですがmysql_binlogより速いはずです。すごく速いといいな。

なお、mysqlbinlogコマンドを使った実装のときはmysql_binlogでbinlogをパースしています。

差分情報の保存方法

差分情報は次のようにストレージに保存します。各ファイルにタイムスタンプがついていることがポイントです。

. -- schema -- 2021-04-05 -- 2021-04-05-15-26-06-436686418.grn
  |         |             +- 2021-04-05-15-26-07-436686418.grn
  |         |             +- 2021-04-05-15-26-08-436686418.grn
  |         +- packed -- 2021-04-05-15-26-06-436686418 -- 2021-04-05-15-26-06-436686418.grn
  |                   +- 2021-04-05-15-26-08-436686418 -- 2021-04-05-15-26-07-436686418.grn
  |                                                    +- 2021-04-05-15-26-08-436686418.grn
  +- data -- ${TABLE1} -- YYYY-MM-DD -- YYYY-MM-DD-hh-mm-ss-NNNNNNNNN-ACTION.parquet
          |            |             +- ...
          |            +- ...
          |            +- packed -- ...
          +- ${TABLE2} -- ...
          .
          .
          .

groonga-delta-applyは自分がどのタイムスタンプまで差分を適用したかという情報を持っていて、適用済みのタイムスタンプより後のタイムスタンプの差分情報があったらタイムスタンプが古い順に適用します。こうすることでMySQLのデータと同期できます。なお、↑の設計を考えた後で知ったのですがDelta Lakeも同じようなアプローチでデータを同期していました。

packedというディレクトリーは最適化のためのディレクトリーです。小さな差分情報がたまり過ぎると1から差分を取り込み直すとき(システムの検索性能を上げるために新しいGroongaインスタンスを作るときとか)にオーバーヘッドが大きいです。そのため、ある程度差分情報がたまったら差分情報を大きな塊にしてしまいます。これがpackedというディレクトリーに置く情報です。groonga-delta-applyは最初の取り込み時にだけまずpackedがあるかを探し、あればそれを適用し対応する未packedの差分情報は使いません。これで1から差分を取り込むときの速度を上げます。なお、packedを取り込む機能は実装されていますが作る機能は現時点で実装されていないので実質使えません。

スキーマの差分情報はGroongaコマンド形式で保存しますが、データの差分情報は可能な限りApache Parquet形式で保存します。これはデータをより速く処理するためです。GroongaにデータをロードするにはJSON形式かApache Arrow形式を使う必要がありますが、高速なデータ交換のために設計されたApache Arrowの方が高速です。Apache Parquet形式で保存されたデータは高速にApache Arrow形式に変換できるのでApache Parquet形式で保存しておくとGroongaへの取り込みが速くなるのです。

参考:Apache Arrowのご紹介

schema/data/を分ける必要あるの?とかファイル名のACTIONってなに?とか気になる人がいたとして、気にしているだけじゃなく実際に私に聞いてきたら説明します。)

今後の改良案

実装の詳細を一通り説明したので今後の改良案について説明します。現時点では今の機能で十分なのですが、今後の案件次第で次のような改良ができたらいいなぁと思っています。

  • PostgreSQL対応
  • Groongaのレプリケーションに応用
  • Amazon S3やGoogle Cloud Storageなどオンラインストレージ対応
  • Groongaクラスターに対する検索機能の追加

PostgreSQLのロジカルレプリケーション機能を使うとMySQLのbinlogを使ったレプリケーションのようなことができます。そのため、PostgreSQLからリアルタイムでデータを取得する仕組みも同じように実現できるはずです。

Groongaにはレプリケーション機能がなく、Fluentdとfluent-plugin-groongaを使ったレプリケーションなどで実現する必要があります。groonga-delta-importがGroongaのように振る舞うことでGroongaクライントから差分情報を取得して保存することができるはずです。つまり、groonga-delta-importベースでGroongaのレプリケーションを実現できるはずです。fluent-plugin-groongaを使ったアプローチはpush型のレプリケーション(ソースがレプリカに差分情報をpush)なので、レプリカの追加が大変(レプリカはどうやって初期データを用意する?)だったりソースに負荷が集まりやすかったりします。一方、Groonga deltaのアプローチはpull型のレプリケーション(レプリカが差分情報をpull)なので、レプリカの追加が容易(ストレージから差分情報を取得するだけ)だったりソースに負荷が集中したりしません。ということで、Groongaのレプリケーションの実現に使えるとよさそうな気がしませんか?

ローカルのストレージだと複数のマシン上にあるGroongaサーバーで差分情報を共有することが難しいです。オンラインストレージに差分情報を保存したりオンラインストレージから差分情報を取り込めたりできるとGroongaクラスターの構築が簡単になります。差分情報の読み書き処理はすでにモジュール化してあるのでキレイに実現できるはずです。

今のGroonga deltaの仕組みでも(NFS上に差分情報を保存すれば)Groongaクラスターを作ることはできますが、大きなデータを分割してクラスター内の各Groongaサーバーが処理できるわけではありません。各Groongaサーバーはすべて同じデータを持っていて各サーバーで処理を完結させるだけです。そのため、クラスターを組んでも組まなくても扱えるデータ量は同じです。Groonga deltaを使って各サーバーで持つデータを分散し、検索時に分散したデータをそれぞれのサーバーで検索しその結果をマージすれば、クラスターを組むことでより大きなデータを扱うことができます。

という感じでいろいろ広がりがありそうだと思っています。

まとめ

Groonga deltaというMySQL/MariaDBのデータをGroongaに同期するツールの使い方・設計・実装を紹介しました。Mroongaとは違ったアプローチでMySQL/MariaDB内のデータを全文検索したい人は試してみてください。

PostgreSQLのデータでも同じようなことがしたい!という人はGroongaのサポートサービスを検討してください。

2022年4月から毎週火曜日の12:15-12:45にこのような技術的な話をGroonga開発者に直接聞ける「Groonga開発者に聞け!(グルカイ!)」というYouTube Liveを始めています!connpassのGroongaグループまたはYouTubeのGroongaチャンネルに登録しておけば通知が届くので活用してください。