2019年6月29日土曜日

redis-rb でコネクションがどう扱われているか簡単に確認してみた

概要

Redis.new したオブジェクトをインスタンス変数として保持した場合と毎回生成するような処理を比べた場合に redis-server に対するコネクションがどうなっているか挙動を確認してみました

環境

  • macOS 10.14.5
  • Ruby 2.6.2p47
    • redis-rb 4.1.2
  • Redis 5.0.5

準備

  • bundle init
  • vim Gemfile
gem "redis"
  • bundle install --path vendor

インスタンス変数にした場合

Redis.new したオブジェクトをインスタンス変数にしてインスタンスメソッドで使う回すようにします
おそらくこれが最も一般的な使い方かなと思います

  • vim test.rb
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 が増えていくのが確認できると思います

これを実行して見ましょう

  • bundle exec ruby test.rb
"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
    • => 0
  • redis-cli config set timeout 5
    • => OK

これで再度先程のスクリプトを実行してみます
すると今度は id がカウントアップしているのが確認できると思います

"72"
"73"
"74"

idel が 5 秒以上あると強制的に切断されるので redis-rb は内部的に新しいコネクションを作成して実行しているようです
コネクション切断エラーが出ると思ったのですがそうではないようです

ちなみに切断の条件が idel だと確認したのはタイムアウトの時間を 15 などにして実行してもすべて同じ id が表示されたからです
もし age の時間がタイムアウトの条件であれば 3 回目に表示される id は別の id が表示されることになります

実行するたびに Redis.new する場合では

次にインスタンス変数ではなく module メソッド内で毎回 Redis.new するような場合の挙動を確認してみます

  • vim test2.rb
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 回目以降は keysclient 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 も付与できます
アプリを特定したい場合に便利です

  • vim test3.rb
client = Redis.new(:id => "hoge")
client.keys
sleep 5
  • bundle exec ruby test3.rb

でプロセスが実行中にクライアントを確認すると name が付与されているのが確認できます
:name ではなく :id を使うのが少し分かりづらいですが、、

  • redis-cil client list
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 時間を監視しておかしなクライアントがいた場合は修正する感じにするのが良いかなと思います

0 件のコメント:

コメントを投稿