January 6, 2019

Hashicorp Raftを使ってNGTサーバをクラスタ化してみる

Yahoo! JAPANが公開している高次元ベクトルの近傍探索ライブラリNGTをWebアプリケーション内で利用しようとする場合、基本的にはこのライブラリを直接使うか、もしくはNGTのサーバ実装であるngtdをHTTP経由で利用することになるのではないかと思います。

例えば、下記のBASEのエントリでは「類似画像API」の内部でNGTライブラリを利用し、日次バッチで生成したインデックスを読み込んで検索機能を提供している様子が紹介されています(おそらくこの部分の実装をngtdに置き換えても同等の機能を提供できるのではないかと思います)。

上記の事例では追加された商品に対してインデックスを日次バッチで生成していましたが、サービスの特性によっては即時にインデクシングしたいというケースもあるかと思います(例えば、同じEコマースでもフリマアプリのような形態では日次バッチを待たずに売り切れてしまうかもしれません)。ngtdは挿入や削除といった更新操作をサポートしていますが、単一ノードで動作するためこのノードがダウンするとそれまでの更新操作が失われてしまいます。ngtdを利用するアプリケーション側で対処することもできそうですが、ngtd自体がクラスタ化に対応すれば(もしくはクラスタ化に対応した別のNGTサーバ実装が登場すれば)より便利にNGTを利用できそうです。

Hashicorp Raftを利用した分散NGTサーバ

RaftをGoで実装したライブラリにgithub.com/hashicorp/raft(以下Hashicorp Raft)というものがあり、その名の通りConsul等のHashicorp製品に利用されています。リーダーの選出からログの管理まで、Raftベースの分散アプリケーションに必要な機能が一通り揃っているので(試しに動かしてみるだけならログを適用するステートマシンだけ自分で実装すればいい)、今回はこれを利用してクラスタ化に対応したNGTサーバを実験的に実装してみました。

自分で実装したのは以下の部分。NGTのGoバインディングであるgongtを利用しました。

  • 外部から挿入・検索・削除のリクエストを受けるためのHTTP API
    • 各ノードがNGTインデックスのレプリカを持っているので検索はその場で結果を返す
    • リーダーノード上のHTTP APIが挿入・削除のリクエストを受けた場合は各ノードにそのログを転送する
  • 各ノードで挿入・削除のリクエストのログを受け取った場合のNGTインデックスの更新処理
  • 各ノード上でのスナップショット処理・リストア処理

たったこれだけの実装でも更新リクエストを投げた時に各ノードで同じインデックスが保持されている状態を実現することができ、ノードをダウンさせた時も再起動時に他のノードのログやスナップショットが自動的に転送されて復旧される様子を観察できます。

今回はお試し的な実装だったのでクラスタへのjoin処理が雑であったり、更新系のリクエストがフォロワーノードに投げられた時にリーダーに転送しない(クライアントが自力でリーダーを見つけ出してリクエストしないといけない)等いろいろ問題がありとても実サービスで使えるようなものではありませんが、これをベースにしていけばクラスタ対応版ngtdという当初の目的は達成できそうです。

これまでまったく存在を知らなかったのですが、実際にHashicorp Raftを使ったSQLiteベースの分散データベースとしてrqliteというものもあるようなので、Hashicorp Raftを使ったアプリケーションを開発する際には参考になるかもしれないです(まだ見てない)。

© Shun Takebayashi