同期プロトコルと非同期プロトコル

今のところチラシの裏的な話。


ネットワークプロトコルは、

  1. リクエストした順番通りにレスポンスを返す(リクエストの順番とレスポンスの順番が同期している)
  2. リクエストした順番通りにレスポンスが返ってくるとは限らない(リクエストの順番とレスポンスの順番が同期していない)

の2種類に分けられる。

前者を同期プロトコル、後者を非同期プロトコルと命名してみる。
HTTP や XML-RPC は同期プロトコルであり、JSON-RPC は非同期プロトコルである。TCPも非同期プロトコルだと言える。非同期プロトコルの特徴として、パケットの中にシーケンス番号(かそれに類するもの)が含まれている。


同期・非同期の違いはパイプライン化(リクエストを送ったあと、レスポンスを待たずに次のリクエストを送る)したときに出てくる。同期プロトコルではリクエストの順番通りにレスポンスが返ってくるが、非同期プロトコルでは順不同になる。
パイプライン化すると、2つ以上のリクエストが立て続けに発生したときに、2つ目以降のリクエストで ネットワーク や カーネル空間<=>ユーザー空間のアドレス変換 などの遅延分が隠蔽されて速くなる。負荷が高いときのパフォーマンスが向上する = スケーラビリティが良くなるので、高速なサーバーを実装する上ではぜひサポートしたい。


サーバーを実装することを考えたとき、マルチスレッドでイベント駆動型のアーキテクチャでは同期プロトコルのパイプラインは非常に対応しにくい。イベント駆動では「リクエストを処理しおわったら(=レスポンスを処理し終わったというイベントが発生したら)レスポンスを返す」という形になるので、単純に実装しただけではレスポンスを返す順番を保証できない。
リクエストを処理し終わっても一端キューに溜めておき、順番通りになったところでレスポンスを返すようにする必要がある。これを同期化と命名してみる。


一方クライアントが接続してくるごとにスレッドを立ち上げるアーキテクチャでは、普通に実装すれば同期プロトコルに対応する。


memcachedのテキストプロトコルは同期プロトコルだが、バイナリプロトコルは非同期プロトコルになる可能性がある*1。バイナリプロトコルにはヘッダに opaque というフィールドがあり、リクエストヘッダに入っている opaque の値をレスポンス時にそのまま返す。ここにシーケンス番号を入れればいい。


しかし libmemcached では opaque の値は常に 0 になっている。
memstored はマルチスレッドなイベント駆動型のサーバーで普通に実装しているので、リクエストをパイプライン化されるとlibmemcachedはたぶん混乱する。しかし幸いなことに(?)libmemcachedはパイプライン化しない(^_^;) *2


同期プロトコル vs 非同期プロトコル というのは一筋縄ではいかない問題だと思われる。
同期プロトコルと比べると、非同期プロトコルはクライアントの実装が複雑になる。これは嬉しくない。
逆に同期プロトコルでは、サーバーをマルチスレッドなイベント駆動型で実装しようとすると、実装が大変になる。クライアントごとにスレッドなモデルにするか、あるいはイベント駆動でもcoroutineを使えば良いのだが、どちらにしてもコンテキストスイッチのオーバーヘッドが少なからず発生する。


mpioで「汎用的な同期化ライブラリ」を実装すれば、HTTP や SMTP もサクサク実装できて嬉しかったりするだろうか。

*1:実際のところレスポンスの順番を保証しないと行けないのか、そうではないのかはちょっと良く分からない

*2:バイナリプロトコルで複数キーを同時にgetするときはまた別。これはパイプライン化で実現することが意図されている