pipelining でコマンド実行を速くしよう with Ruby

redis

redis は client/server モデルを採用している TCP サーバーなので複数のコマンドを実行したい時に、 コマンドを送る・結果を受け取る・次のコマンドを送る...とやっていたら全てのコマンドが完了するのに、単純化して RTT(Round Trip Time) * リクエスト数 の時間がかかる。 これは パイプライニング(Pipelining) によって高速化できる。

Pipelining

Pipelining とは、サーバーからのレスポンスを待たずに複数のリクエストを送り、最後にまとめてレスポンスを受け取るという手法である。

画像 - https://ja.wikipedia.org/wiki/HTTP%E3%83%91%E3%82%A4%E3%83%97%E3%83%A9%E3%82%A4%E3%83%B3#/media/%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB:HTTP_pipelining2_ja.svg

wikipedia の顔像から分かるようにリクエスト毎にレスポンスを待たなくてよく全体の処理時間が短縮される。 pipelining で処理が早くなる理由としてレスポンスを待たなくてよくなること以外にも、socket I/O の回数を減らせる read()write() syscall は kernel mode で動作し、redis の処理は user mode で処理される。 pipelining を使わない時、毎リクエスト毎に 2 つのモードを行き来する必要があり、そのスイッチングコストが大きい。 それが pipelining を使うと、1 回の read()・write() だけで済むので、処理時間が短縮される。

注意点! pipelining を使ってコマンドを送る際、1 回に送るコマンドを制限したほうがよい。 というのも最大で、コマンド分のレスポンスを追加でメモリに持っておく必要があるから。 10,000 くらいまでにしておくのがよさそう。

Ruby 実装

100,000 の string 型をローカルの redis に set する。 redis には予め flushall をしておいて、実行時間も計測する。

pipelining を使わない実装

実装

require 'redis'

redis = Redis.new
100000.times do |n|
  redis.set("key_#{n}", "value_#{n}")
end

実行時間

real    0m5.038s
user    0m3.015s
sys     0m1.264s

pipelining を使った実装

実装

require 'redis'

redis = Redis.new
redis.pipelined do
  100000.times do |n|
    redis.set("key_#{n}", "value_#{n}")
  end
end

実行時間

real    0m1.857s
user    0m1.196s
sys     0m0.472s

まとめ

複数のコマンドを redis に対して発行する場合には pipelining を使おう! コマンドの数によっては 3 倍以上速くなる!

参照