この記事はmruby adevent calendar 24日目の記事です。
mrubyでRedisを操作するmruby-redisは以前から開発していたのですが、最近Pipelining対応のPRを頂きPipelining対応したのでそれの性能を確認してみました。また、その結果簡単に作れるようになった分散型インメモリジョブキューであるDisqueのmrubyクライアントであるmruby-disqueの紹介をします。
RedisとhiredisのPipelining
mruby-redisはhiredisというCライブラリを利用しており、hiredisでは効率的にRedisとIOできるようにPipelining機能をサポートしています。RedisとhiredisにおけるPipeliningを簡単に説明すると、
- 通常のRedisアクセスはクライアントからメソッドを発行するとRedisプロトコルを通じてReplyを待つ
- Pipeliningの場合は、クライアントからメソッド発行するとRedisへのinputバッファにRedisのAPIをキューイングする
- Reply用のAPIを叩くと、inputバッファにキューイングしたメソッドをまとめてRedisに投げることでソケット
write()
をまとめる write()
後はread()
しながらreplyを待ち結果をバッファに貯めこむ事によりソケットのread()
をまとめられる- 上記のような動作によりRedisとのソケットI/Oのためのシステムコール数を少なくし効率化できる機能がPipelining
という動きになります。これによって、ソケットI/Oが効率化されて高速化される事が予想されます。
Pipeliningのベンチマーク
ということで、早速ngx_mrubyを経由してベンチマークをとってみましょう。
まずは以下のようにngx_mrubyを設定します。
location /redis1 { mruby_content_handler_code ' redis = Userdata.new.redis redis.queue(:set, "mruby-redis-test:foo", "0") redis.queue(:set, "mruby-redis-test:foo", "BAR1") redis.queue(:get, "mruby-redis-test:foo") Nginx.rputs redis.bulk_reply[2] '; } location /redis2 { mruby_content_handler_code ' redis = Userdata.new.redis redis["mruby-redis-test:foo"] = "0" redis["mruby-redis-test:foo"] = "BAR2" Nginx.rputs redis["mruby-redis-test:foo"] '; }
環境は以下です。
- MacBook Pro (Retina, 13-inch, Late 2013)
- CPU 2.8 GHz Intel Core i7
- Memory 16 GB 1600 MHz DDR3
nginxのworkerは一つになるように以下のようなnginx.conf
を設定し、Redisはbrewでいれたものをデフォルトで起動しました
ベンチマーク結果は以下となります。
$ ab -k -c 100 -n 100000 http://127.0.0.1:58080/redis1 Requests per second: 13182.81 [#/sec] (mean)
$ ab -k -c 100 -n 100000 http://127.0.0.1:58080/redis2 Requests per second: 7297.13 [#/sec] (mean)
pipeliningを使った方が倍ぐらい速いことがわかりました。また、reply
メソッドは結果が来るまでまつような処理になっており、queueはひたすらタスクを内部的にキューイングしています。
この例で行くと、/redis2
へのアクセスはsocketに対するread()
とwrite()
がメソッド単位で実行されているのに対し、/redis1
ではPipeliningにより、メソッド自体は4つ呼ばれていますが、内部的にRedisと通信しているソケットにおいてはread()
とwrite()
がそれぞれ1回しか実行されていないと考えられます(システムコールの数は確認していないので間違えているかも...)
Disqueのクライアントも作ってみた
この流れでRedisと同じ作者が作られているDisqueという、分散型in-memoryジョブキューというのがあり、これはRedisプロトコルによってDisqueとお喋りができるので大変便利なミドルウェアです。つまりクライアント的には、Redisのプロトコルを喋る汎用的なクライアントを実装しておけば、それがそのままDisqueと通信することができるという事になります。
今回のPipelining処理実装は、各Redis API毎にメソッドを定義しておらず、APIと引数のstringをそのままhiredisのライブラリを経由してRedisと通信するような汎用的な実装になっているため、Disqueへの通信も簡単にできることになります。
ということで、mruby-redisを使えばRubyだけでmruby-disqueを使ってみました。
使い方は大体以下の通りで、
assert("Disque#addjob") do d = Disque.new job_id = d.addjob "q1", "box", "0" assert_not_equal(nil, job_id) d.deljob job_id end assert("Disque#getjob") do d = Disque.new d.addjob "q1", "box", "0" d.addjob "q1", "cat", "0" job1 = d.getjob "q1" job2 = d.getjob "q1" assert_equal("box", job1.job_name) assert_equal("cat", job2.job_name) d.deljob job1.job_id, job2.job_id end assert("Disque#run_command") do d = Disque.new job_id = d.addjob "q1", "box", "0" qlen = d.run_command :qlen, "q1" assert_equal(1, qlen) d.deljob job_id end
Disqueを起動後に、Disque#addjob
でジョブを追加し、Disque#getjob
でジョブを取得する、というのが基本的な使い方です。また、Disque#run_command
を使うと、全てのDisque APIを汎用的に使う事が可能で、例えば上記の例のようにd.run_command :qlen, "q1"
などとすると、キューq1にあるキューの数を取得するQLEN
APIを実行することができます。
まとめ
ということで、今回はmrubyのRedisクライアントmruby-redisのPipelining対応と、その結果簡単に作る事ができたmruby-disqueの紹介でした。今後は、Redisクライアントの非同期(コールバック)対応等によってより多くの面で使えるように改善していきたいと思っております。