日本語版の「øMQガイドブック」を読んだまとめ。
ZeroMQのコンセプト
コンテキストとソケット
- ZeroMQでは、プログラミング単位ごとに「コンテキスト」が必要になる(基本的にはプロセスごとだけれど、スレッドごとに用意しても問題はない)。これがI/Oスレッドの動作などの具体的な処理を担ってくれる。
- ユーザプログラムの求めに応じて、コンテキストから「ソケット」が提供される。これはTCPソケットのようなものというよりは、むしろ「メッセージ送受信のための汎用窓口」と考えた方がよい。
メッセージの構造
基本は「ZeroMQ String」という構造(バイト数 + 実際のバイト列)。これをつなげていくことで通信単位(メッセージパケット)を作る。
パケットの最小構造は、「アドレス+メッセージフレーム」という構造。実際には双方を区別するために、両者の間にスペーサー(空のフレーム)が入る。つまり最小パケットは「アドレス+空フレーム+ボディフレーム」という構造。
後述するように、メッセージフレームを複数つけた「multipartメッセージ」という形のパケットを送信することもできる。この場合、やはり個々のボディ間にスペーサーをつけて「アドレス+空フレーム+ボディ1+空フレーム+ボディ2…」という形にする。
ちなみにZeroMQは使い勝手良く作られているので、実際のソケットプログラミングではROUTERやDEALER(どちらも後述)を使わない限り「アドレス」部分を処理する必要はない。メッセージ本体をsend
するだけでパケットがきちんと送信されるし、受信したパケットの最初のrecv
では(最初の)メッセージボディを取得することができる。
メッセージのやり取り
ひとつひとつのメッセージパケットのsend
は、非同期的に行われる:
- 送信が終わるまでブロックされる、ということがない。
- 接続の状態や送受信はコンテキスト内のI/Oスレッドによって管理される。
- たとえ
send
を呼んだ段階で接続先が存在しなくても、処理はブロックされないしエラーも起きない。接続が成立した段階でI/Oスレッドがメッセージを送信する。(ただしrecv
は相手からメッセージが返ってくるまでブロックされる)
以上のことから、ユーザプログラム側はsend
以降の処理について気にすることなく、別の処理に入ることができる。分散プログラムにおいてあと残る問題は、「相手側のエラーをどのように検出し、それに応じてこちら側の処理をどう回復させるか」(=プログラムの信頼性)というポイントだけになる。
ちなみに単一のパケットは分割されることなくまとめて送信される。受信側でも、ひとつのパケットのまとまりが完全に受信されるまでrecv
によって読み出せるようにはならない。その意味で「パケット」がアトミックな処理単位であるといえる。
ネットワークの設計
Binderとconnector
2種類の役割:エンドポイント(静的アドレス)にbind
する側(サーバー)と、そこにconnect
する側(クライアント)がある。2種類のコンポーネントは、ソケット(ZeroMQソケット)を通じてメッセージをやり取りする。
- 普通のサーバ・クライアントプログラムの場合は明らかだけれど、ネットワークトポロジーによってはあまり明瞭でない場合がある。
- 基本的には「より静的に存在すると仮定される側」がエンドポイントにbindする、と考えて設計するのがよい。
- サーバーは、同じZeroMQソケットで異なるエンドポイントに
bind
できる- 異なるプロトコルを横断して同じサービスを提供することができる
- プロトコルの変化に応じてアプリケーションコードを変える必要がない
TCPプログラムにあるような「サーバーがbind
してからクライアントがconnect
する」という制限は、ZeroMQソケットには存在しない(例外として、スレッド間通信では存在する)。そのあたりのhandlingは、I/Oスレッドが自動でやってくれる。
役割依存的なソケット処理戦略のパターン
- ソケット処理の戦略は、ソケット生成時に設定する。
- 役割に応じたデフォルトの処理のパターン(メッセージをどう処理するかとか、接続が切れた場合にどうするかとか)が知られているらしい。ZeroMQ上の処理戦略はそのパターンの役割名で設定できる:
名前 | bind可否 | 対1/対N | Write-first | Read-first | ルーティング | 代表的な接続相手 |
---|---|---|---|---|---|---|
REP | ○ | 対N | × | ○ | × | REQ |
REQ | × | 対1 | ○ | × | × | REP |
PUB | ○ | 対N | ○ | (Write-only) | × | SUB |
XPUB | ○ | 対N | ○ | (Write-only) | × | SUB |
SUB | × | 対1 | (Read-only) | ○ | × | PUB |
XSUB | × | 対N | (Read-only) | ○ | × | PUB |
PUSH | ? | 対1? | ○ | (Write-only) | × | PULL |
PULL | ? | 対1? | (Read-only) | ○ | × | PUSH |
ROUTER | ○ | 対N | × | ○ | ○ | REQ |
DEALER | ○? | 対N | ○ | × | ○ | REP |
- 機能が制限されるほど使い方がシンプルなので、バグが混入しにくくなる。
- 「対N」型のソケットをサービス群とユーザ群の仲立ちとすることで、フレキシビリティの高いネットワークプログラムを構築することができる。
代表的な接続パターン:
- REP–REQパターン:サーバとクライアントが1対1でやり取りする。
- PUB–SUBパターン:PUBが勝手に発行するメッセージを、SUBが勝手に受け取る。
- PUSH-PULLパターン:パイプライン型とも呼ばれる。PUSH側でリクエストを生成し、何段階かのコンポーネントによる処理の結果をPULL側で回収する。
仲介者が入るパターンは(できたら)別記。
対1 vs 対N
- 個々のコンポーネントは原則として対1のソケット(REQやSUBなど)を持つ。
- ネットワーク/プログラムの集約点(サービスの窓口だとか)では、アルゴリズムを分散させないために対Nのソケット(代表的にはREPやPUB)を持つのが効果的だと考えられる。
Write-first vs Read-first
- ZeroMQの場合、ソケットの種別によって「どちらから通信を始めるか」が決まっている。ここでは便宜上、最初に通信を送る側をWrite-first、最初の通信を待つ側をRead-firstとしている。
- Read-first, Write-firstを意識してソケット種別を考えることで、通信の混乱を防ぐことができると考えられる。
- サービスのどちら側をWrite-firstにするかはケースバイケース。PUB–SUBの場合はPUBから送信するのが妥当だし、REQ–REPの場合はREQから送信した方がよいだろう。
ルーティング
多分ルーティングについては稿を改めて書く、と思う…。
- 基本的な対Nソケット(PUBやREP)では、メッセージを送信する対象が暗黙的に決まっており、プログラムからコントロールすることはできない;
send
やrecv
ではメッセージボディのみを取得することができ、アドレス部分を参照・変更できないようになっている(逆に言うと、返信先を指定しなくてもZeroMQが勝手によろしくやってくれる)。 - ROUTERとDEALERは例外で、アドレスを含めたメッセージパケットのコントロールをすることができる。受信したパケットの種類に応じて送信パケットの宛先を変えたりしたい場合、これらの種類のソケットを使うことになる。
SUBソケットでも、multipartメッセージを使用してPUBから送られてくるメッセージをフィルタリングすることができる。
メッセージAPI
C言語の場合。
基本の関数
zmq_send()
とzmq_recv()
を用いた方法が一番簡単。
- 任意のバイト列を、対象のソケットから/へ送受信するための関数
- メモリやロックの問題が発生しづらいので安全
- multipartとか任意のヘッダーとかつけたい場合はこの方法ではできない。
- より高度(high-level)なヘルパー関数もあるらしい。
メッセージオブジェクトを扱う
メッセージをもっとコントロールして生成したい場合は、zmq_msg_t
型のメッセージオブジェクト(構造体)を自前で調達・処理する。
受信側の方法
-
zmq_msg_init()
で空のメッセージオブジェクトを生成 -
zmq_msg_recv()
でメッセージオブジェクトに受信メッセージを入れる - (適当な処理)
- multipartの場合、この後に
zmq_msg_more(&msg)
で続きのフレームがあるかどうかを確認する(続きがあるなら、別のメッセージオブジェクトで受信する) - 必要なくなったら
zmq_msg_close()
でオブジェクトを解放する
送信側の方法
-
zmq_msg_init()
で空のメッセージオブジェクトを生成 - (適当にメッセージを埋める)
-
zmq_msg_send()
でメッセージを送信。この場合、zmq_msg_close()
を呼ぶ必要はないらしい。 - multipartメッセージで続きがある場合、前のメッセージの送信の際に
zmq_msg_send()
でZMQ_SNDMORE
フラグを設定してやる(3番目の引数)。こうすることで、I/Oスレッドが「まだメッセージフレームがある」と認識する。
Multipartのメッセージ
複数の「メッセージフレーム(本体)」を持ったメッセージのまとまりは「multipart」と呼ばれている。
- 送信時に
zmq_msg_send()
でZMQ_SNDMORE
フラグを設定することで、後続のメッセージフレームと繋げることができる。 - 送信側I/Oスレッドは、後続のフレームが全部揃うまで送信処理を開始しない(状況によっては「揃えばすぐに開始する」とも限らない)。
- 受信側I/Oスレッドでは、全てのフレームを受信するまで
zmq_msg_recv()
で受信可能にならない。 - 受信側プログラムは、
zmq_msg_more()
を用いて後続があるかをチェックできる。
ここから
まずはREP–REQパターンか何かでちょっと動かしてみるのがよいだろう…。ルーティングについてはもう少ししてからまとめてみる。