概要
Redis.new したオブジェクトをインスタンス変数として保持した場合と毎回生成するような処理を比べた場合に redis-server に対するコネクションがどうなっているか挙動を確認してみました
環境
- macOS 10.14.5
- Ruby 2.6.2p47
- Redis 5.0.5
準備
gem "redis"
bundle install --path vendor
インスタンス変数にした場合
Redis.new
したオブジェクトをインスタンス変数にしてインスタンスメソッドで使う回すようにします
おそらくこれが最も一般的な使い方かなと思います
require 'redis'
class CRedis
def initialize
@client = Redis.new
end
def keys
@client.keys
end
def list
@client.client('list')
end
end
client = CRedis.new
3.times do
client.list.each { |c| p c['id'] }
client.keys.length
sleep 10
end
key の数をチェックしているだけです
チェックしたあとにクライアントの状態が変化しているか確認します
redis-server は接続しているクライアントそれぞれに id
を振り識別できるようにしています
id
はクライアントの接続/切断があるたびにインクリメントしていきます
試しに redis-cli client list
をたくさん実行するとどんどん id
が増えていくのが確認できると思います
これを実行して見ましょう
"49"
"49"
"49"
結果は同じ id
が表示され続けました
つまりインスタンス変数にした場合は redis-rb
では同じコネクションを使って接続していることがわかります (当然と言えば当然)
ちなみに上記を実行中に別のターミナルで確認コマンドとして以下を実行してみると以下のような結果が返ってきました
while true; do redis-cli client list; sleep 1; done
id=49 addr=127.0.0.1:59174 fd=8 name= age=5 idle=5 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=keys
id=50 addr=127.0.0.1:59175 fd=9 name= age=0 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=26 qbuf-free=32742 obl=0 oll=0 omem=0 events=r cmd=client
id=49 addr=127.0.0.1:59174 fd=8 name= age=6 idle=6 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=keys
id=51 addr=127.0.0.1:59176 fd=9 name= age=0 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=26 qbuf-free=32742 obl=0 oll=0 omem=0 events=r cmd=client
id=49 addr=127.0.0.1:59174 fd=8 name= age=7 idle=7 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=keys
id=52 addr=127.0.0.1:59177 fd=9 name= age=0 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=26 qbuf-free=32742 obl=0 oll=0 omem=0 events=r cmd=client
id=49 addr=127.0.0.1:59174 fd=8 name= age=8 idle=8 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=keys
id=53 addr=127.0.0.1:59178 fd=9 name= age=0 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=26 qbuf-free=32742 obl=0 oll=0 omem=0 events=r cmd=client
id=49 addr=127.0.0.1:59174 fd=8 name= age=9 idle=9 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=keys
id=54 addr=127.0.0.1:59179 fd=9 name= age=0 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=26 qbuf-free=32742 obl=0 oll=0 omem=0 events=r cmd=client
id=49 addr=127.0.0.1:59174 fd=8 name= age=10 idle=10 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=keys
id=55 addr=127.0.0.1:59180 fd=9 name= age=0 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=26 qbuf-free=32742 obl=0 oll=0 omem=0 events=r cmd=client
id=49 addr=127.0.0.1:59174 fd=8 name= age=11 idle=1 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=keys
id=56 addr=127.0.0.1:59181 fd=9 name= age=0 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=26 qbuf-free=32742 obl=0 oll=0 omem=0 events=r cmd=client
id=49 addr=127.0.0.1:59174 fd=8 name= age=12 idle=2 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=keys
id=57 addr=127.0.0.1:59182 fd=9 name= age=0 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=26 qbuf-free=32742 obl=0 oll=0 omem=0 events=r cmd=client
id=49 addr=127.0.0.1:59174 fd=8 name= age=13 idle=3 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=keys
id=58 addr=127.0.0.1:59183 fd=9 name= age=0 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=26 qbuf-free=32742 obl=0 oll=0 omem=0 events=r cmd=client
id=49 addr=127.0.0.1:59174 fd=8 name= age=14 idle=4 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=keys
id=59 addr=127.0.0.1:59184 fd=9 name= age=0 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=26 qbuf-free=32742 obl=0 oll=0 omem=0 events=r cmd=client
id=49
が redis-rb のクライアントになります
これの age フィールドが接続から切断までの時間になります
そして idel フィールドが接続はされているがコマンドが一切実行されていない時間です
確認用のスクリプトは 10 秒おきにコマンドを実行しているので idel=10
になると一度カウントがリセットされているのが確認できると思います
redis-server は idel 中のプロセスを kill しないのか
ここで疑問に思ったのは「この idel フィールドが無限に増え続けた場合に redis-server は切断したりするのか」ですが、どうやらデフォルトではしないようです
By default recent versions of Redis don't close the connection with the client if the client is idle for many seconds: the connection will remain open forever.
詳しく読んでいないので理由は不明ですがおそらくクライアント側はちゃんとルールを守って使ってくれるであろうという暗黙的な了解があるんだと思います
なので当然コネクションを張りっぱなしにしていると Too many open files や Out of memory エラーなどのカーネルエラーが発生するはずです
タイムアウトを設定してみる
とは言え運用するに辺りそういったクライアントが 100% 登場しないとは言い切れません
なのでちゃんとタイムアウトを設定することができます
timeout
という config
があるのでこれを設定します
redis-cli config get timeout
redis-cli config set timeout 5
これで再度先程のスクリプトを実行してみます
すると今度は id がカウントアップしているのが確認できると思います
"72"
"73"
"74"
idel が 5 秒以上あると強制的に切断されるので redis-rb
は内部的に新しいコネクションを作成して実行しているようです
コネクション切断エラーが出ると思ったのですがそうではないようです
ちなみに切断の条件が idel だと確認したのはタイムアウトの時間を 15 などにして実行してもすべて同じ id が表示されたからです
もし age の時間がタイムアウトの条件であれば 3 回目に表示される id は別の id が表示されることになります
実行するたびに Redis.new する場合では
次にインスタンス変数ではなく module メソッド内で毎回 Redis.new するような場合の挙動を確認してみます
require 'redis'
module MRedis
def keys
client = Redis.new
client.keys
end
def list
client = Redis.new
client.client('list')
end
module_function :keys, :list
end
3.times do
MRedis.list.each { |c| p c['id'] }
MRedis.keys.length
sleep 10
end
keys
, client list
を実行するたびに Redis.new
するようにしてみます
実行して挙動を確認しましょう
bundle exec ruby test2.rb
"85"
"86"
"87"
"88"
"89"
結果は上記のようになりました
1 回目は client list
分の id が表示されます
2 回目以降は keys
と client list
の 2 つ分が表示されます
2 回目以降に 2 つ表示されるのは前回の keys
のコネクションが残っており次の keys
が実行されるまでは破棄されないためです
つまりメソッド内で Redis.new
している場合はそのメソッドが再度コールされるまではコネクションが保持され続けることになります
そんな場合のために意図的に close
できます
なのでメソッド内で client を作成している場合などはちゃんと close
をコールしてあげるのがお作法かなと思います
module MRedis
def keys
client = Redis.new
ret = client.keys
client.close
ret
end
def list
client = Redis.new
ret = client.client('list')
client.close
ret
end
module_function :keys, :list
end
3.times do
MRedis.list.each { |c| p c['id'] }
MRedis.keys.length
sleep 10
end
これで実行するとちゃんと keys
コール後にコネクションが切断されており結果に表示されるのは client list
の id のみになるが確認できると思います
"131"
"133"
"135"
おまけ: クライアントに名前を付与するには
client list
の結果を見るとわかりますがクライアントには id 以外に name も付与できます
アプリを特定したい場合に便利です
client = Redis.new(:id => "hoge")
client.keys
sleep 5
bundle exec ruby test3.rb
でプロセスが実行中にクライアントを確認すると name が付与されているのが確認できます
:name
ではなく :id
を使うのが少し分かりづらいですが、、
id=139 addr=127.0.0.1:59480 fd=8 name=hoge age=1 idle=1 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=keys
最後に
redis-rb のクライアントのコネクションの挙動を確認してみました
基本はインスタンス変数として使うのが良いと思いますが場合によっては redis-cli
のようにコマンドごとに接続してもいいのではと思います
Web アプリのように session があり特定の時間コネクションをつないでいたほうがリソースを節約できる場合はインスタンス変数にし、バッチなど単発で処理しすぐに破棄する場合はコマンドごとに接続しても問題ないと思います
ただ接続/切断を短時間で頻繁に繰り返すような処理は避けたほうが良いと思います
ちゃんとやるのであれば redis-server 側でコネクション数と idel 時間を監視しておかしなクライアントがいた場合は修正する感じにするのが良いかなと思います