インメモリKVSのRedisについて

* wiki(Tech Note)のページ追加
このエントリーの内容を整理してwikiにもRedis関連のページを追加しました。
Redis – Tech Note


Redis is an open source, advanced key-value store. It is often referred to as a data structure server since keys can contain strings, hashes, lists, sets and sorted sets.

インメモリKVSのRedisを少し触ってみました。

さくらVPS(CentOS 5.6)にインストールしようと思ったのですが、yumのレポジトリだと今は古いバージョンしか入れられないようなのでソースからビルドしました。手順は公式の通りに。(参照: Download – Redis)
また、/etc/init.d で動作させるスクリプトは同梱されていなかったので以下からお借りしました。
A CentOS initscript for Redis — Gist

Redisの利用実績

Who’s using Redis?
公式のアナウンスによると、githubやdigg、stackoverflowなど多くの有名サイトで利用実績があるようなので、導入検討時の良い説得材料になりそうです^^

Redis data types (型について)

Redisは基本の文字列型の他にリストやセット、ハッシュなどの様々な型がありますが、それぞれの型に対するコマンドがatomicに動作するという特徴を持ちます (参照: Command reference – Redis)。つまり、ユーザー側でCAS操作をぺたぺた書いてその戻り値をいちいち気にする必要はありません。

String (文字列)

まずは基本の文字列型から試してみます。整数文字列をセットしてインクリメント/デクリメント操作をすると内部で整数値(符号付き64bit値)として扱われます。また、バイナリセーフなのでどんな種類のデータでも保存できます。JPEGイメージとかでもOK。
[shell]
$ redis-cli ## Redisシェルを起動
redis> set foo bar
OK
redis> get foo
“bar”
redis> type counter ## キーの型を調べる
string
redis> set counter 0 ## 数値文字列をセット
OK
redis> incr counter ## 値をインクリメント
(integer) 1
redis> incr counter
(integer) 2
redis> get counter
“2”
redis> decr counter ## 値をデクリメント
(integer) 1
redis> get counter
“1”
redis> setex mykey 10 foobar ## expires付きで値をセット (10 sec)
OK
redis> get mykey ## 指定した秒数の間は値を取得できる
“foobar”
redis> get mykey ## 指定した秒数を超えると値は削除される
(nil)
[/shell]
v2.2以降ではLRU (Least Recently Used)をサポートしているのでmemcachedのような使い方もできます。

List (リスト)

次はリスト型。公式のチュートリアルでTwitterクローンを制作してますが、そこで使われているようにユースケースとしては時系列データを扱うのに良さそうです。
[shell]
redis> rpush messages “Hello how are you?” ## リストの末尾(right)に値をpush
(integer) 1
redis> rpush messages “Fine thanks. I’m having fun with Redis”
(integer) 2
redis> rpush messages “I should look into this NOSQL thing ASAP”
(integer) 3
redis> lrange messages 0 -1 ## リストの先頭から末尾までの値を取得
1) “Hello how are you?”
2) “Fine thanks. I’m having fun with Redis”
3) “I should look into this NOSQL thing ASAP”
redis> lpush messages “Long time no see” ## リストの先頭(left)に値をpush
(integer) 4
redis> lrange messages 0 -1
1) “Long time no see”
2) “Hello how are you?”
3) “Fine thanks. I’m having fun with Redis”
4) “I should look into this NOSQL thing ASAP”
redis> rpop messages ## リストの末尾から値をpop
“I should look into this NOSQL thing ASAP”
redis> lpop messages ## リストの先頭から値をpop
“Long time no see”
redis> llen messages ## リストの要素数を返す
(integer) 2
[/shell]
複数のプロセスから1つのリストにどんどん値をpushしていっても安心で、rpush/lpushなどのList型のコマンドはatomicな操作になっています。

* RPOPLPUSH(srckey, dstkey)
個人的に面白いなと思ったList型のコマンドを別途紹介。このコマンドは srckey に対応するリストの末尾要素を削除して、その要素を dstkey に対応するリストの先頭にpushします。ひとつのリストをバックアップとして使ってメッセージキューを安全に実装するときなどに使えます。また、srckey と dstkey が同じ場合はローテーションすることになります。
[shell]
redis> lrange src 0 -1 ## リスト src は [“a”, “b”, “c”]
1) “a”
2) “b”
3) “c”
redis> lrange dst 0 -1 ## リスト dst は [“d”, “e”, “f”]
1) “d”
2) “e”
3) “f”
redis> rpoplpush src dst ## srcからpopしてdstにpush
“c” ## popした要素が返る
redis> lrange src 0 -1
1) “a”
2) “b”
redis> lrange dst 0 -1
1) “c”
2) “d”
3) “e”
4) “f”
redis> rpoplpush dst dst ## リストローテーション
“f”
redis> lrange dst 0 -1
1) “f”
2) “c”
3) “d”
4) “e”
[/shell]

Set/Sorted Set (集合/ソート済み集合)

Setは順不同の集合型。和集合や積集合などの各種集合演算が利用可能で、メンバの重複を許可しません。ソーシャル系のサービスだとユーザー(コミュニティ)のつながりを調べたりするのに使えそうです。また、Sorted Setでは任意のスコアで値がソートされるのでいろいろな用途が考えられると思います。
[shell]
redis> sadd A a ## セットに値を追加
(integer) 1
redis> sadd A b
(integer) 1
redis> sadd A c
(integer) 1
redis> sadd A d
(integer) 1
redis> smembers A ## セット内のメンバを取得
1) “c”
2) “d”
3) “a”
4) “b”
redis> sadd B a ## もうひとつセットを作る
(integer) 1
redis> sadd B b
(integer) 1
redis> sadd B d
(integer) 1
redis> sadd B e
redis> scard B ## セット内のメンバ数を返す
(integer) 4
redis> sinter A B ## 積集合 A ∩ B
1) “d”
2) “a”
3) “b”
redis> sunion A B ## 和集合 A ∪ B
1) “b”
2) “c”
3) “d”
4) “e”
5) “a”
redis> sdiff A B ## 差集合 A \ B
1) “c”
redis> sunionstore P A B ## 和集合を新たなセットとして作る
(integer) 5
redis> sinterstore Q A B ## 積集合を新たなセットとして作る
(integer) 3
redis> sdiff P Q ## 対象差 A Δ B
1) “c”
2) “e”

## Sorted Set
redis> zadd hackers 1940 “Alan Kay”
(integer) 1
redis> zadd hackers 1953 “Richard Stallman”
(integer) 1
redis> zadd hackers 1965 “Yukihiro Matsumoto”
(integer) 1
redis> zadd hackers 1916 “Claude Shannon”
(integer) 1
redis> zadd hackers 1969 “Linus Torvalds”
(integer) 1
redis> zadd hackers 1912 “Alan Turing”
(integer) 1
redis> zrange hackers 0 -1 ## 指定した範囲のメンバをスコアの昇順で返す
1) “Alan Turing”
2) “Claude Shannon”
3) “Alan Kay”
4) “Richard Stallman”
5) “Yukihiro Matsumoto”
6) “Linus Torvalds”
redis> zrevrange hackers 0 -1 ## 降順
1) “Linus Torvalds”
2) “Yukihiro Matsumoto”
3) “Richard Stallman”
4) “Alan Kay”
5) “Claude Shannon”
6) “Alan Turing”
[/shell]

Hash (ハッシュ)

最後はHash型。ハッシュ形式のデータを保存できます。
[shell]
redis> hset user:ryo job engineer ## ハッシュのフィールドに値をセット
(integer) 1
redis> hset user:ryo age 26
(integer) 1
redis> hget user:ryo job ## ハッシュのフィールドの値を返す
“engineer”
redis> hkeys user:ryo ## ハッシュ内に存在する全てのフィールドを返す
1) “job”
2) “age”
redis> hvals user:ryo ## ハッシュ内に存在する全ての値を返す
1) “engineer”
2) “26”
[/shell]
Hash型では効率的にメモリを使用するために設定ファイルでパラメータを指定できるようになっていましたが、いまいちよくわかりません。zipmap ってなんだろう。。

# Hashes are encoded in a special way (much more memory efficient) when they
# have at max a given numer of elements, and the biggest element does not
# exceed a given threshold. You can configure this limits with the following
# configuration directives.
hash-max-zipmap-entries 512
hash-max-zipmap-value 64

以上で、それぞれのデータタイプ毎に提供されたコマンドの使い方を簡単におさらいしました。Redisには他にもたくさんのコマンドが提供されているのでいろいろ調べておきたいと思います。

Persistence (永続性)

Redisはインメモリで動作しますが、非同期でディスクにも書き出すため永続性を備えています。ディスクに書き出すタイミングは設定ファイル(redis.conf)で指定できます。デフォルトでは以下のように設定されていて、この辺りは運用しながら調整する必要がありそうです。

## 以下をコメントアウトするとディスクには一切書き出されない
## その場合はmemcachedと同様にインメモリKVSとして振る舞う
save 900 1    ## 1回更新があったら900秒(15分)後
save 300 10    ## 10回更新があったら300秒間(5分)後
save 60 10000    ## 10000回更新があったら60秒間(1分)後

rdbcompression yes  ## ダンプファイルをLZFで圧縮するかどうか
dbfilename dump.rdb    ## ダンプファイル名
dir ./    ## ファイルが書き出される場所

このようにRedisの永続化機能はスナップショットを書き出す方式なので、もしRedisサーバが落ちたときは最新のデータが失われる可能性があります。これを防ぐために append only file と呼ばれるファイルに更新コマンドを書き出すこともできます。

append only file

デフォルトでは無効になっているので利用するために設定ファイルを編集します。

appendonly yes

これだけ。デフォルトのファイル名は appendonly.aof になっています。ここで、set mykey foo を実行したときのappend only fileの内容は以下のようになりました。このようにプロトコルがシンプルなので簡単に読めます($の後ろの数字はバイト数)。

*2
$6
SELECT
$1
0
*3
$3
set
$5
mykey
$3
foo

同期には fsync() を使っているようですが、どのタイミングで同期するのかも指定できます。

# appendfsync always  ## すべての更新コマンド実行時に同期、遅いが安全
appendfsync everysec  ## 毎秒同期、最悪で1秒間でデータが失われる可能性がある、デフォルト
# appendfsync no  ##データの同期はOSに任せる、安全ではないが高速

Virtual memory (仮想メモリ)

RedisではLinuxカーネルを参考にして独自に仮想メモリ機構を取り入れているみたいです。つまりRAMからあふれたデータをswap outしてディスクに書き出してくれます。実装初期のバージョンでは不安定でよく壊れていたようですが、現在は良くなっていると思われます(※ 厳密な検証はしてません)。この仮想メモリに関連するパラメータもいくつかあるので、内部動作も含めて調べておきたいと思います。ただ、弊社のサービスで使うDBサーバは(お金の力を使って)数十GBのメモリを載せてたりするので、仮想メモリはあんまり使わないかもしれません。

Replication (レプリケーション)

RedisにはTokyo Tyrantなどと同様にマスター/スレーブのレプリケーションをサポートしています。利用するのは簡単で、スレーブとして利用したいRedisの設定ファイルに項目を追加するだけです。

port 6380  ## スレーブは6380ポートで動作させる
slaveof localhost 6379  ## slaveof という項目にレプリケーション対象のホスト名とポート番号を指定

レプリケーションが動作しているか確認してみます。
[shell]
$ redis-server /etc/redis.conf ## localhost:6379
$ redis-server /etc/redis_slave.conf ## localhost:6380
$ redis-cli -p 6379 -h localhost ## マスターに接続
redis> set mykey foo ## 値をセット
OK
redis> get mykey
“foo”

$ redis-cli -p 6380 -h localhost ## スレーブに接続
redis> get mykey ## 確認
“foo”
[/shell]
大丈夫そうです。ログでは以下のように書き出されていました。

## マスター側ログ
[15876] 12 Jun 15:52:33 * Slave ask for synchronization
[15876] 12 Jun 15:52:33 * Starting BGSAVE for SYNC
[15876] 12 Jun 15:52:33 * Background saving started by pid 23083
[23083] 12 Jun 15:52:33 * DB saved on disk
[15876] 12 Jun 15:52:33 * Background saving terminated with success
[15876] 12 Jun 15:52:33 * Synchronization with slave succeeded

## スレーブ側ログ
[23082] 12 Jun 15:52:33 * Connecting to MASTER...
[23082] 12 Jun 15:52:33 * MASTER <-> SLAVE sync started: SYNC sent
[23082] 12 Jun 15:52:33 * MASTER <-> SLAVE sync: receiving 34 bytes from master
[23082] 12 Jun 15:52:33 * MASTER <-> SLAVE sync: Loading DB in memory
[23082] 12 Jun 15:52:33 * MASTER <-> SLAVE sync: Finished with success

Hashing (データ分散)

イマドキのNoSQLデータベースクライアントにおいては珍しいことではないのですが、Redisクライアントでも Consistent Hashing によって複数サーバにデータを振り分けています(対応していないクライアントライブラリもある)。近々Rubyコミュニティの小さな集まりに参加する予定なので、ここではリハビリも兼ねてRubyクライアントを使って確認してみます。公式のレコメンドに従って redis-rb を使いました (MRI 1.9.2p0 (2010-08-18 revision 29036))。
[ruby]
#!/bin/env ruby

require ‘rubygems’
require ‘redis’
require ‘redis/distributed’

## Redisサーバは4台
## Redisクラスではなく、Redis::Distributedクラスを使う
r = Redis::Distributed.new %w(redis://localhost:6379 redis://localhost:6380 redis://localhost:6381 redis://localhost:6382)
r.flushdb

(1..20).each do |n|
r.set(“key#{n}”, “value#{n}”)
end

## Redis::Distributed@ring はHashRingクラスのインスタンス
r.ring.nodes.each do |node|
p “#{node.client.host}:#{node.client.port} => #{node.keys(“*”).join(‘, ‘)}”
end

r.flushdb
[/ruby]
* 出力結果

"localhost:6379 => key12, key10, key11, key13, key6"
"localhost:6380 => key20, key7"
"localhost:6381 => key19, key5, key14, key15, key16, key17, key18"
"localhost:6382 => key2, key3, key4, key8, key1, key9"

キーが偏ってますね。HashRingクラスの実装を見てみると仮想ノードのデフォルト数は160になっていました。これを倍の320に設定して試してみます。

"localhost:6379 => key12, key10, key11, key13, key6"
"localhost:6380 => key20, key4, key14, key15, key7, key16, key17"
"localhost:6381 => key9, key5, key1, key19, key8"
"localhost:6382 => key2, key3, key18"

たいして変わらなかった。。でもとりあえずキーが分散することは確認できました。

Pipelining (パイプライニング)

Redisではパイプライニングにも対応しているらしいので確認してみました。複数のコマンドをサーバーの応答を待たずに一括で送信します。
[shell]
$ (echo -en “PING\r\nPING\r\nPING\r\n”; sleep 1) | nc localhost 6379
+PONG
+PONG
+PONG
[/shell]
さくらVPS(いちばん安いやつ)上のRedis Server v2.2.9で公式のサンプルコードを使ってベンチマークを取ってみます。パイプライニングの有無で処理時間を計測しています (MRI 1.9.2p0 (2010-08-18 revision 29036))。
[ruby]
#!/bin/env ruby

require ‘rubygems’
require ‘redis’

def bench(descr)
start = Time.now
yield
puts “#{descr} #{Time.now-start} seconds”
end

def without_pipelining
r = Redis.new
10000.times {
r.ping
}
end

def with_pipelining
r = Redis.new
r.pipelined {
10000.times {
r.ping
}
}
end

bench(“without pipelining”) {
without_pipelining
}
bench(“with pipelining”) {
with_pipelining
}
[/ruby]
* 結果

without pipelining 2.295884 seconds  ## パイプライニング無し
with pipelining 0.443958 seconds  ## パイプライニング有り

公式のベンチマーク結果と似たような差が出ました。パイプラインを使用したほうが5倍ほど速かったです。

Pub/Sub

RedisではPublish-Subscribeモデルのメッセージング機能をサポートしています。
* subscriber.rb
[ruby]
#!/bin/env ruby

require ‘rubygems’
require ‘redis’

begin
r = Redis.new:host => ‘localhost’, :port => 6379
puts ‘starting subscribe …’
r.subscribe ‘pubsub-test’ do |sub|
sub.message do |ch, msg|
puts “channel: #{ch}”
puts “message: #{msg}”
end
end
rescue SocketError, Errno::ECONNREFUSED => e
puts e.message
rescue RuntimeError => e
puts e.message
rescue Interrupt
puts ‘stopping subscribe …’
end
[/ruby]
* 動作確認
[shell]
## subscriberを起動
$ ruby subscriber.rb
starting subscribe …

## メッセージをpublish
$ redis-cli publish ‘pubsub-test’ ‘my message’
(integer) 1

## subscriberがメッセージを受信
starting subscribe …
channel: pubsub-test
message: my message
## Ctrl-C で止める
stopping subscribe …
[/shell]
使うだけなら簡単ですね。

——————–
以上、Redisについてちょっと調べてみました。クライアントライブラリは多くの言語から提供されていますが、作りの質が言語によってかなり差があるように見えるので検証時は注意した方が良さそうです。また、データセンターをまたいで利用したときの検証も行っていきたいです。リアルタイム性が必要なサービスで十分使えるなら、例えば映像配信インフラにおいて視聴ステータスの管理やプレイヤーからのハートビートをさばく部分などに使えるでしょうか。ちょっと試してみようと思います。

これ以上さくらVPSでがんばるのは大変なので会社のデータセンター使わせてもらおう。今夏の節電対策で西日本にがばっと移設したところもあるので好都合かもしれない。

あわせて読む:

4 Thoughts

  1. Thanks for finally writing about >インメモリKVSのRedisについて
    ? Rest Term <Loved it!

  2. One or two of regarding me: co-workers call myself NICK as well as in online
    you will find me underneath redhat01 login name.

    I do certainly not name me personally a hacker, however I love to review program code and discover vulnerabilites on game titles and different applications.

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です