大量データのインサート¶
しばしば Redis インスタンスは、数百万のキーをできるだけ高速に作成するため、既存の、またはユーザーが生成する大量のデータを、短時間でロードする必要があります。
これは mass insertion と呼ばれます。このドキュメントでは、Redis にできるだけ高速にデータを投入する方法について、情報を提供することを目的とします。
プロトコルを使うんだ, Luke¶
大量データの投入に、標準の Redis クライアントを使うことは、いくつかの理由により良い方針ではありません: コマンドをひとつひとつ送信するナイーブなアプローチは、毎回のコマンドの度にラウンドトリップが発生するため低速です。パイプライニングを使うことも可能ですが、大量のレコードを追加するためには、書き込みコマンドを発行する一方で、同時にできるだけ高速に応答を読み込まなければいけません。
ごく少数のクライアントしかノンブロッキングI/Oをサポートしていません。また、すべてのクライアントが、スループット最大化のための効率的な応答のパースを実現できているわけではありません。こうした理由により、Redis に大量のデータをインポートする際に望ましい方法は、生の Redis プロトコルが書かれたテキスト・ファイルを生成し、データ追加に必要なコマンドを呼び出すことです。
たとえば、私が ‘KeyN -> ValueN’ というフォーマットのキーを数十億個含むような、大きなデータ・セットを生成する必要がある場合、以下のような Redis プロトコル形式のファイルを作成します:
SET Key0 Value0
SET Key1 Value1
...
SET KeyN ValueN
ファイルを作成したら、残りの仕事は、 Redis へファイルの内容をできるだけ高速に送り込むことです。以前のやり方は、以下のように ‘netcat’ を使うことでした。
(cat data.txt; sleep 10) | nc localhost 6379 > /dev/null
しかしこの方法は、大量のデータをインポートするときに信頼できるやり方とは言えません。netcat はすべてのデータが転送されたか実際のところを知らず、またエラー検知もできないためです。github の Redis の unstable branch [訳注: 2014/6 時点では stable version の 2.8 にマージ済み] に含まれる ‘redis-cli’ ユーティリティでは、mass insertion のため pipe mode をサポートするようになりました。
pipe モードを使うには、以下のようにコマンドを実行します:
cat data.txt | redis-cli --pipe
応答はこのようになります:
All data transferred. Waiting for the last reply...
Last reply received from server.
errors: 0, replies: 1000000
redis-cli ユーティリティは、Redis インスタンスから受信したエラーを標準出力にリダイレクトします。
Redis プロトコルを生成する¶
Redis プロトコルの生成およびパースは非常にシンプルで、 ここにドキュメント化 されています。しかし、mass insertion のためのプロトコルを生成するにあたり、すべてのプロトコルの詳細まで理解する必要はありません。各コマンドが、以下のように表現されることだけを理解しておいてください:
*<args><cr><lf>
$<len><cr><lf>
<arg0><cr><lf>
<arg1><cr><lf>
...
<argN><cr><lf>
ここで、’<cr>’ は ”t” (ASCII 文字コード 13) を、’<lf>’ は “n” (ASCII 文字コード 10) を表します。
たとえば SET key value というコマンドは、以下のプロトコルで表現されます:
*3<cr><lf>
$3<cr><lf>
SET<cr><lf>
$3<cr><lf>
key<cr><lf>
$5<cr><lf>
value<cr><lf>
または引用符で囲った文字列で表現すると:
"*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n"
mass insertion のために必要なファイルは、上記の方法で表現された 1 つ 1 つのコマンドから構成されます。
以下は、妥当なプロトコルを生成する Ruby 関数です。
def gen_redis_proto(*cmd)
proto = ""
proto << "*"+cmd.length.to_s+"\r\n"
cmd.each{|arg|
proto << "$"+arg.to_s.bytesize.to_s+"\r\n"
proto << arg.to_s+"\r\n"
}
proto
end
puts gen_redis_proto("SET","mykey","Hello World!").inspect
この関数を使うと、前述のキー・バリューペアは以下のプログラムで簡単に生成できます:
(0...1000).each{|n|
STDOUT.write(gen_redis_proto("SET","Key#{n}","Value#{n}"))
}
このプログラムの実行結果をパイプで直接 redis-cli に送れば、大量データをインポートするセッションを実行できます。
$ ruby proto.rb | redis-cli --pipe
All data transferred. Waiting for the last reply...
Last reply received from server.
errors: 0, replies: 1000
pipe モードが内部でどのように動作しているか¶
redis-cli の pipe モードの内部で必要な魔法は、netcat と同程度に高速で、かつ、サーバーから送信される応答を理解できる、というものです。
これは以下の方法で実現されています:
redis-cli –pipe は可能なかぎり高速にデータをサーバーへ送信する。
それと同時に、得られた受信データを読み、パースを試みる。
標準入力からのデータ入力が終わったら、ランダムな 20 バイトの文字列とともに、特殊な ECHO コマンドを送信する: これが最後のコマンドだとわかっているため、受信データをチェックし、同じ 20 バイトからなる bulk reply であるかのマッチングが可能。
この特殊コマンドが送信されたら、コードは応答と 20 バイト文字列とのマッチングを始める。もしマッチする応答が見つかったら、成功とともに終了する。
このトリックを使うことで、いくつのコマンドを送信したか把握しておくために、サーバーへ送信するプロトコルをパースする必要がなくなり、応答のパースを行うだけで済むようになります。
一方応答については、受け取ったすべての応答をパースするため、最終的には、 mass insert セッション中に転送されたコマンドの数をユーザーに提示することができます。