RESP (REdis Serialization Protocol)の調査


はじめに

下記の資料を参考にしている。

大半が英訳となっているが、英語に難があるので意訳という形でごまかしている。

途中に出てくる例は、上記資料の例を引用している。

Redis プロトコルを読む目的

Redis の Pub/Sub コマンドを実装するために必要な情報を収集するため。

RESPとは

RESPとは、 REdis Serialization Protocol の略。RESPは、次の事項を実現している。

  • シンプルな実装
  • 高速にパース
  • 人間でも読める

RESP の立ち位置。数字が大きくなるに連れて、物理層からアプリケーション層に近づく感じ。

  1. TCP/IP
  2. RESP
  3. Redis コマンド

ネットワークレイヤ

TCP/6379 ポートを使用。もしくはUnixソケットなどのTCPと同等のストリームで使用される必要がる。

リクエスト-レスポンスモデル

サーバは、クライアントから要求に対して処理が完了するまで結果を返さない。
ただし、下記2つの例外がある。

1.Redisはパイプラインをサポートしているので、クライアントは複数のコマンドを一度に送信してからレスポンスを待つ。
2. クライアントが Pub/Sub チャンネルを購読する時、プッシュプロトコルに変化し、クライアントはコマンドを送信しなくなる。 サーバが素早くメッセージを送信するため。

以上2つを除けばシンプルなリクエスト-レスポンスモデル。

RESP の詳細

RESP は、次のデータ型をシリアライズするプロトコル。

  • 文字列
  • エラー
  • 整数
  • 長い文字列(Bulk String)
  • 配列

クライアントは、 コマンドをRESP のBulk Stringの配列としてサーバに送っている。
サーバは、RESPの型のうち1つで応答する

RESPの型は、先頭バイトで判別できる

  • 先頭バイト + は、文字列用
  • 先頭バイト - は、エラー用
  • 先頭バイト : は、整数用
  • 先頭バイト $ は、Bulk String用
  • 先頭バイト * は、配列用

以上に加えて、後述するNull値が存在する。
RESPの部品は、"\r\n" (CRLF) で終了する。

文字列

文字列には、 "\r" (CR)や "\n" (LF) を含まれない。改行が許されていない。

使用用途は、小さなオーバーヘッドでバイナリではない文字列を送るために使用する。例えば、サーバが "OK"レスポンスを返す場合は、5バイトの文字列になる。

+OK\r\n

エラー

実のところエラーは、文字列と変わりないが、 + の代わりに - で始まる。簡単な例。

-Error message\r\n

エラーを受け取ったら、クライアントは、例外として扱ったら良い。

整数

: で始まって数字が続き、CRLFで終了する。例えば、 ":0\r\n" や ":1000\r\n" 。
符号付き64bit整数値が保証うされている。
真偽値で利用される。1なら真、0なら偽。

Bulk String

512MBを上限とする単一バイナリ。
ここで言う512MBとは、512*1024*1024=536870912bytesである。redis-3.2.5のソースコードを参照した。

Bulk String は、次の方法でエンコードされる。

  • "$" で始まり、構成するバイト数が付き、CRLFで終わる。
  • 文字列データ
  • CRLF で終わる

文字列 foobar は、次のようにエンコードされる。

$6\r\nfoobar\r\n

Null値も存在する。 長さは-1で、データ無し。

$-1\r\n

Null Bulk String と呼ばれる。
クライアントライブラリは、空文字列ではなく、nilオブジェクトを返すと良い。RubyだったらNil、CだったらNULLかフラグで示すと良い。

配列

クライアントは、配列を使ってRedisサーバへコマンドを送る。Redisコマンドは、クライアントを配列を使って集合の要素を返す。例えば、LRANGEコマンドは、リストの要素を返す。
配列は次のフォーマットを使う。

  • "*" で始まり、10進数で表記された要素数が付き、CRLFで終わる
  • 続く型は、すべて配列の要素

空配列は、次の通り。

*0\r\n

"foo" と "bar" のBulk Stringを2つ持つ配列は次のようにエンコードされる

*2\r\n$3\r\nfoo\r\n$3\r\nbar\r\n

3つの整数を持つ配列は次のようにエンコードされる

*3\r\n:1\r\n:2\r\n:3\r\n

同じ型でなくても複数の型を持つ配列も可能。

*5\r\n
:1\r\n
:2\r\n
:3\r\n
:4\r\n
$6\r\n
foobar\r\n

見やすくなるように改行している。

Null配列は -1 でカウントしている。

*-1\r\n

クライアントライブラリAPIは、空配列ではなくNullオブジェクトを返すと良い。空配列とと条件を分けるため。実例として、 BLPOPコマンドのタイムアウト条件がある。

配列の配列も可能。

*2\r\n
 *3\r\n
  :1\r\n
  :2\r\n
  :3\r\n
 *2\r\n
  +Foo\r\n
  -Bar\r\n

見やすいように改行とインデントを含めた。実際は含まれない。

Null要素を持つ配列

例えば、

*3\r\n
$3\r\n
foo\r\n
$-1\r\n
$3\r\n
bar\r\n

2番目の要素が Null。クライアントライブラリは次のように返す。

["foo",nil,"bar"]

Redisサーバへコマンド送信

以上で、Redis のクライアントライブラリを実装できる。ここからは、クライアントとサーバどのように動くか示す。

  • クライアントは、RedisサーバへBulk Stringから成る配列を送る
  • Redisサーバは応答として、正当なデータをクライアントへ返答する。

クライアントが、mylistキーに含まれるリストの長さを取得するためのコマンド LLEN mylist 送ると、サーバは、次のように整数で返答する。(Cはクライアント、Sはサーバ)

C: *2\r\n$4\r\nLLEN\r\n$6\r\nmylist\r\n 
S: :48293\r\n

複数コマンドとパイプライン

クライアントは、複数コマンドを出すために同じ接続を使い回すことができる。パイプラインは、クライアントが1つの書き込み(write)操作で複数コマンド送信をサポートしている。

パイプライン

この章では、https://redis.io/topics/pipeliningRedis Pipelineを参照した。

このパイプライン方式は、POP3プロトコルでもすでにサポートされている。メールをサーバから劇的に早くダウンロードするために使われている。

Redisはパイプラインを初期からサポートしているので、どのRedisバージョンでもパイプラインを利用できる。この例では、netcatを使った例

(printf "PING\r\nPING\r\nPING\r\n"; sleep 1) | nc localhost 6379
+PONG
+PONG
+PONG

すべてのコールに RTT(Round Trip Time) のコストをは楽事無く、1度に3コマンドを呼び出せる。

重要!: クライアントが、パイプラインを使ってコマンドを送っている間、サーバは、メモリを使って強制的に返答をキューする。なので、パイプラインを使ってたくさんのコマンドを送信する必要があれば、程よい数で送ってやると良い。10,000コマンド送って、返答を読んだ後に再度10,000コマンドを送るなど。全部送るのと速度はほぼ変わらないが、メモリ確保容量が異なってくる。

インラインコマンド

たまに、telnet の手打ちでRedisサーバへコマンド送信する必要がある。

Redisは、インラインコマンドフォーマットと呼ばれる特別なコマンドを用意している。

次の例は、インラインコマンドを使ったサーバ/クライアント間の会話である。

C: PING
S: +PONG

次の例は、もう一つの整数を返すインラインコマンド。

C: EXISTS somekey
S: :0

基本的に、telnetセッションにおいてスペースで分割された引数を簡単にかける。*で始まるコマンドでなく統一されていないのものの、 Redisは、この条件を検知できコマンドをパースできる。

まとめ

速そう。あと、Redisコマンドはリストを送って結果を受け取っているのでLispな感じがした。

Pub/Subコマンドは、パイプラインの仕組みを使ってい実現している。実装できそうな気がしてきた。まずは、PING/PONGコマンドの対応から着手した方が良いかも。