こんにちは。Droonga開発チームの結城(Piro)です。
Groonga Advent Calendar 16日目は、13日目に引き続きDroongaの解説です。
Droongaを題材にした過去4回の記事(6日目、7日目、9日目、13日目)では、Droongaの動作の紹介を交えつつ、分散処理の初歩の初歩を解説しました。
今回は、Droongaにおいてそれらの分散処理をつつがなく実行するための基盤となっている、分散データ処理エンジンとしての部分に焦点を当てて、設計や実装の概要を紹介したいと思います。
Droongaクラスタ内での通信の基本
これまでの説明の中で何度か、「検索リクエストをランダムに転送する」や「更新リクエストを適切なパーティションに転送する」といったフレーズが出てきたと思います。
これは、実際にはどのような事が起こっているのでしょうか?
Droongaの通信プロトコルとメッセージ形式
大量のメッセージを効率よく送れるという理由と、当初はfluentdのプラグインとして実装されていたという歴史的経緯から、Droongaクラスタ内のノード同士はfluentdプロトコルでメッセージを送り合って通信します。
fluentdプロトコルはHTTPのようなリクエスト・レスポンス型の通信ではなく、送信側がメッセージを送りきったらそれでおしまいという、一方通行のプロトコルです。
DroongaではここにJSONオブジェクト相当の形式で独自のメッセージをMessagePackして載せて、リッチな情報をやりとりしています。
以下は、ホスト名がclient0
であるコンピュータ上で動作しているクライアントが、いずこかのDroongaノードに対して送信するselect
コマンドのリクエストにあたるメッセージの例です。
なんとなく、意味が見て取れるでしょうか?
{
"id": 01234,
"type": "select",
"date": "2014-12-16T00:00:00Z",
"from": "client0:10031/droonga",
"replyTo": "client0:10031/droonga",
"dataset": "Default",
"body": { "table": "Stores",
"output_columns": "name",
"limit": 10 }
}
Droongaノードは、クラスタ外からこのようなメッセージが流入してくると、受信したノード自身でそれを処理したり、あるいは他のノードに転送したりします。
しかし転送とはいっても、メールのリダイレクトのようにそのまま同じメッセージを転送するわけではありません。
Droongaクラスタ内の通信は、通信の仕方そのものは同じですが、分散と集約を行えるようにするために、クラスタ内を流通するメッセージは内容が若干変更されています。
この記事では説明を簡単にするために、クラスタ外との通信で見られるメッセージをサンプルとして示すことにします。
クラスタ内で実際にやり取りされるメッセージの内容が気になる方は、droonga-engine
のソースを見てみて下さい。
リクエストとレスポンス
fluentdプロトコルは一方通行の通信ですが、select
コマンドのようなメッセージは、一方的にリクエストを送りつけるだけでは意味がありません。
何らかの形で結果を送り返してもらい、それを受け取る必要があります。
そこで出てくるのが、先のリクエストのreplyTo
というフィールドです。
ここには処理結果のメッセージを最終的にどこに送ればよいのかが書かれており、通常はクライアントが動作しているコンピュータ自身が指定されます。
(droonga-engine-join
などのいくつかの管理コマンドにおいて--receiver-host
というオプションで指定した作業マシン自体のホスト名は、ここに使われています。)
replyTo
があるメッセージを受け取ったノードは、最終的な処理の結果をreplyTo
で示された宛先に送ります。
また、その時には元のメッセージのid
フィールドの値を処理結果のメッセージのinReplyTo
フィールドに付与します。
以下は、先のselect
のレスポンスにあたる処理結果のメッセージの例です。
{
"id": 56789,
"type": "select.result",
"date": "2014-12-16T00:00:01Z",
"from": "node1:10031/droonga",
"inReplyTo": 01234,
"dataset": "Default",
"body": [
[0, 1401358896.360356, 0.0035653114318847656],
[
[
[40],
[["name", "ShortText" ]],
["1st Avenue & 75th St. - New York NY (W)"],
["76th & Second - New York NY (W)"],
["Herald Square- Macy's - New York NY"],
["Macy's 5th Floor - Herald Square - New York NY (W)"],
["80th & York - New York NY (W)"],
["Columbus @ 67th - New York NY (W)"],
["45th & Broadway - New York NY (W)"],
["Marriott Marquis - Lobby - New York NY"],
["Second @ 81st - New York NY (W)"],
["52nd & Seventh - New York NY (W)"]
]
]
]
}
お気付きの方もいるかもしれませんが、この辺りの名前付けはSMTPに倣っています。
メールを受け取る時と同じように、クライアントはreplyTo
を指定してメッセージを送り、受け取ったメッセージのinReplyTo
を見てどのメッセージに対する返事かを対応付ける、という具合ですね。
レスポンスを受け取るサーバーソケット
レスポンスとなるメッセージもfluentdプロトコルで送られてくるため、これを受け取るにはサーバーソケットが必要です。
Droongaノード同士の通信では常にサーバーが動作していますので、レスポンスはそこを通じて受け取れます。
しかしDroongaノードでないクライアントではそうもいきませんので、そのためのサーバーソケットを開いておかなくてはなりません。
こういう場面で有用なのがクライアントライブラリです。
droonga-client-ruby
は名前の通りのRuby製ライブラリで、droonga-client
というGemパッケージとしてインストールできます。
これを使うと、サーバーソケットの準備やリクエストとレスポンスの対応付けなどの面倒な部分はライブラリに任せて、以下のように比較的簡単にDroongaクラスタと通信できます。
require "droonga-client"
client = Droonga::Client.new(
:host => "node0",
:port => 10031,
:tag => "droogna",
:protocol => :droonga,
:timeout => 1,
:receiver_host => "client0",
:receiver_port => 0,
)
request_message = {
"dataset" => "Default",
"type" => "select",
...
}
response_message = client.request(request_message)
実際に、droonga-engine
付属の各種管理コマンドや、drndump
、drntest
、drnbench
といったツール群も、このdroonga-client
を使って開発されています。
ここまででで述べた通信の流れを図にすると、以下のようになります。
図中で、リクエストのメッセージを送った先のノードと、レスポンスを返してきたノードとが異なっている事に気がつきましたか?
これは、リクエストを受け取ったnode0が、自動的にそれをnode1に転送し、node1が検索を実行してレスポンスを返した、という風な状況を示しています。
Droongaクラスタではこのように、リクエストの受け付けと実際の処理(+レスポンス送信)すらも分割されるため、特定のノードだけが過負荷になるという事態が発生しにくいようになっています。
これが、Droongaにおける通信の概要です。
プロトコルアダプター
ところで、これまでのDroongaでの分散処理の解説をご覧になった方は、先の図を見て「あれ? リクエストを受け取ったnode0からレスポンスが返ってくるんじゃないの?」と思ったのではないでしょうか?
確かに、これまでの記事では以下のような、リクエストを受け取ったノードがレスポンスを返しているような図を示していました。
実は、ここまででは省略していましたが、これらのDroongaノードではdroonga-engine
とdroonga-http-server
の両方のサービスが動作している想定でした。
省略しないでこの図を描き直すと、以下のようになります。
Droongaクラスタの通信の仕組みは、Droongaクラスタの外にあるクライアントから利用するには面倒が多いです。
そこで、DroongaプロジェクトではDroongaクラスタとクライアントの仲介役も併せて開発・提供しています。
先程紹介したdroonga-client-ruby
もその1つです。
droonga-http-server
は、HTTPとDroongaネイティブの通信プロトコルとを仲介する変換器として働きます。
そのため、Droongaプロジェクトではこのような存在をプロトコルアダプターと呼んでいます。
droonga-http-server
があるおかげで、ユーザはGroonga互換のHTTPサーバとしてDroongaクラスタを簡単に利用できるようになっています。
Droongaプロジェクトとしては開発する予定はありませんが、必要であれば、GQTPとDroongaプロトコルのプロトコルアダプターや、SMTPとDroongaプロトコルのプロトコルアダプターなども開発することができます。
まとめ
長くなってきたので、一旦まとめます。
- Droongaでは、ノード間の通信にはfluentdプロトコルを使っています。
- fluentdプロトコルは、リクエスト・レスポンス型ではない一方通行のプロトコルです。
fluentdプロトコルの上でリクエストとレスポンスを扱うために、DroongaではSMTPを模した方法でリクエストとレスポンスを対応付けています。 - クラスタ外のクライアントからDroongaクラスタにアクセスできるように、Droongaプロジェクトではクライアントライブラリやプロトコルアダプターを提供しています。
次回予告
Droongaでのノード間通信にどのような方法が使われているのかは分かりました。
しかし、「流入してきたリクエストをどのノードに転送すればよいか」についてはまだ分からないままです。
Groonga Advent Calendar 18日目の記事では、リクエストの転送先ノードを特定するために必要なクラスタ構成の情報を、ノードがどのように把握しているのかを解説します。
乞う御期待!