rubyでmemcachedのキーの一覧を表示する。また特定データを削除する等。
なんで作ろうと思ったか
C#のmemcachedセッションライブラリがバグってて無期限データ入れやがったー。
そのまま放置して130万件にもなりやがった。
くそー 消したい。flush_allすると全データ消えて既存ユーザーが強制timeout扱いなるからそれは避けたい。
なので無期限データのみ消したいんだー 絶対ー!!
アイテム一覧表示 実行結果
$ util.print_items
stats
{"pid"=>"16348",
"uptime"=>"585047",
"time"=>"1361858174",
"version"=>"1.4.7",
"libevent"=>"1.4.13-stable",
"pointer_size"=>"64",
"rusage_user"=>"1.210815",
"rusage_system"=>"5.016237",
"curr_connections"=>"24",
"total_connections"=>"1554",
"connection_structures"=>"34",
"cmd_get"=>"16466",
"cmd_set"=>"25559",
"cmd_flush"=>"0",
"get_hits"=>"16424",
"get_misses"=>"42",
"delete_misses"=>"0",
"delete_hits"=>"4",
"incr_misses"=>"0",
"incr_hits"=>"0",
"decr_misses"=>"0",
"decr_hits"=>"0",
"cas_misses"=>"0",
"cas_hits"=>"0",
"cas_badval"=>"0",
"auth_cmds"=>"0",
"auth_errors"=>"0",
"bytes_read"=>"783051598",
"bytes_written"=>"396680001",
"limit_maxbytes"=>"67108864",
"accepting_conns"=>"1",
"listen_disabled_num"=>"0",
"threads"=>"4",
"conn_yields"=>"0",
"bytes"=>"111905",
"curr_items"=>"7",
"total_items"=>"25559",
"evictions"=>"0",
"reclaimed"=>"27"}
stats items
{5=>
{"number"=>"2",
"age"=>"583060",
"evicted"=>"0",
"evicted_nonzero"=>"0",
"evicted_time"=>"0",
"outofmemory"=>"0",
"tailrepairs"=>"0",
"reclaimed"=>"11"},
6=>
{"number"=>"1",
"age"=>"582577",
"evicted"=>"0",
"evicted_nonzero"=>"0",
"evicted_time"=>"0",
"outofmemory"=>"0",
"tailrepairs"=>"0",
"reclaimed"=>"14"},
7=>
{"number"=>"1",
"age"=>"502636",
"evicted"=>"0",
"evicted_nonzero"=>"0",
"evicted_time"=>"0",
"outofmemory"=>"0",
"tailrepairs"=>"0",
"reclaimed"=>"0"},
28=>
{"number"=>"3",
"age"=>"505667",
"evicted"=>"0",
"evicted_nonzero"=>"0",
"evicted_time"=>"0",
"outofmemory"=>"0",
"tailrepairs"=>"0",
"reclaimed"=>"1"}}
server_start_time:2013-02-19 20:25:27 +0900
item_number:5 key:foo_development:_session_id:f0df468194448c505e26b99143e5d65c bytes:107 time:2013-02-26 20:55:22 +0900
item_number:5 key:foo_development:_session_id:f7450243f80ca59fbb9ffc18049fedc0 bytes:107 time:2013-02-26 20:23:07 +0900
item_number:6 key:foo_development:_session_id:e2674611e09369f8b5bc62ba95c59b92 bytes:165 time:2013-02-26 20:15:04 +0900
item_number:7 key:hoge_e15atqhotfpm4sbnbm3dwa3g bytes:269 time:2013-02-26 16:02:41 +0900
item_number:28 key:hoge_qmeysujdltvz22s2ahtpmrye bytes:36546 time:2013-02-27 14:55:44 +0900
item_number:28 key:hoge_3nsu2trne4j0vyrfqg202lqe bytes:37387 time:2013-02-26 21:04:37 +0900
item_number:28 key:hoge_lwbr2fcdzx21wgbroczjtghq bytes:36536 time:2013-02-26 16:53:29 +0900
2013-02-26 14:54:50 +0900 infinity_count:0 normal_count:7 expire_count:0
ソース
# coding: utf-8 STDOUT.sync = true require 'rubygems' require 'pp' gem 'memcache_do', '>=0.1.1' gem 'dalli', '>=2.6.2' require 'memcache_do' require 'dalli' class MemcacheStats def initialize(host='localhost', port='11211') @host = host @port = port @dc = Dalli::Client.new("#{host}:#{port}") end def list stats_items_hash.each do |no, item| stats_cachedump(no).lines do |line| next if line.chomp == 'END' hoge, key, bytes, second = line.match(/^ITEM ([^ ]*) \[(\d*) b; (\d*)/).to_a second = second.to_i time = Time.at(second) data = {:item_number => no, :key => key, :bytes => bytes, :time => time} yield(data) end end end def exec(cmd) MemcacheDo.exec(cmd, @host, @port) end def stats exec 'stats' end def stats_hash raw = stats hash = {} raw.lines do |line| # p line line.chomp! res = line.match(/^STAT (\w+) ([^ ]+)/) if res hash[res[1]] = res[2] end end hash end def stats_items exec 'stats items' end def stats_cachedump(item_number) exec "stats cachedump #{item_number} 0" end def stats_items_hash raw = stats_items hash = {} raw.lines do |line| # p line line.chomp! res = line.match(/^STAT items:(\d+):(\w+) ([^ ]+)/) if res key_num = res[1].to_i hash[key_num] = {} unless hash.has_key? key_num hash[key_num][res[2]] = res[3] end end hash end def delete(key) @dc.delete key end def dalli(key) @dc end end if $0 == __FILE__ class MemcacheUtil def initialize(host='localhost') @m = MemcacheStats.new(host) end # アイテム一覧表示 def print_items m = @m infinity_count = 0 normal_count = 0 expire_count = 0 # stats取得 # puts m.stats stats = m.stats_hash puts "stats" pp stats # stats_items取得 #puts m.stats_items stats_items = m.stats_items_hash puts "stats items" pp stats_items # サーバー開始時間を計算 server_start_time = Time.at(stats["time"].to_i - stats["uptime"].to_i) puts "server_start_time:#{server_start_time}" # データ一覧取得 m.list do |data| item_number = data[:item_number] key = data[:key] bytes = data[:bytes] time = data[:time] puts "item_number:#{item_number} key:#{key} bytes:#{bytes} time:#{time}" if time == server_start_time # 無期限キャッシュの有効期限はサーバー開始時間と一緒 infinity_count += 1 elsif time < Time.now # 期限切れデータ。どうもstats cachedumpに期限切れてるのがでてくる expire_count += 1 else normal_count += 1 end if key == "hogehoge" # 簡易データ表示(先頭にヘッダーぽいのついてるのを除去してないです) p m.exec("get #{key}") # rubyのデータなら下記のようにm.dalliでDalliClientのインスタンス返すのでご自由にどぞ。 #m.dalli.get(key) end end puts "#{Time.now} infinity_count:#{infinity_count} normal_count:#{normal_count} expire_count:#{expire_count}" end # 無期限データ削除&有効期限切れデータ削除 def delete_infinity_and_expire_data m = @m puts "#{Time.now} start process" loop do infinity_count = 0 normal_count = 0 expire_count = 0 # stats取得 # puts m.stats stats = m.stats_hash puts "stats" pp stats # stats_items取得 #puts m.stats_items stats_items = m.stats_items_hash puts "stats items" pp stats_items # サーバー開始時間を計算 server_start_time = Time.at(stats["time"].to_i - stats["uptime"].to_i) puts "server_start_time:#{server_start_time}" # データ一覧取得 m.list do |data| item_number = data[:item_number] key = data[:key] bytes = data[:bytes] time = data[:time] puts "item_number:#{item_number} key:#{key} bytes:#{bytes} time:#{time}" if (time == server_start_time) # 無期限キャッシュの有効期限はサーバー開始時間と一緒 infinity_count += 1 m.delete key elsif time < Time.now # 期限切れデータ。どうもstats cachedumpに期限切れてるのがでてくる expire_count += 1 m.delete key else normal_count += 1 end end puts "#{Time.now} infinity_count:#{infinity_count} normal_count:#{normal_count} expire_count:#{expire_count}" # 削除対象が無くなるまで繰り返す。 # stats cachedumpは1MBしか返さない制限あるのでゴミデータは削除しないと消したいデータが出てこない場合がある。なので期限切れデータを消した後もループで再実行している。 if infinity_count == 0 && expire_count == 0 break end end puts "#{Time.now} end process" end end util = MemcacheUtil.new('192.168.xxx.xxx') util.print_items # コメント外して使ってください。 # 無期限データ削除&有効期限切れデータ削除 #util.delete_infinity_and_expire_data end
注意点
stats cachedumpだとキー一覧等のデータが1MBまでしか返ってこないとか。もしアクティブなデータがいっぱい入っている場合は無理そう。
また有効期限切れデータも返ってくるので、今回は有効期限切れデータがあった場合は削除するような処理にした。
参考
http://blog.elijaa.org/index.php?post/2010/12/24/Understanding-Memcached-stats-cachedump-command
http://lzone.de/dump+memcache+keys
そのうちコマンドstats cachedumpが消えるかもとかもいってるみたい。まー微妙な仕様なので新しいのができると期待。