odeの開発メモ日記

プログラマーやってます。今までの道のり ポケコンbasic→C(DirectX5 ネットやろうぜ)→Perl(ちょろっと)→Java→C#→Ruby→Android(Java)

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
実行結果

delete_infinity_and_expire_dataを実行して
5,6時間経過で約130万件が約300件に!
アクティブセッション少なっ!

意地で作ったけど、memcached再起動すれば簡単かつ安全なのが泣ける。
そもそもmemcaechedのデータは消えていいようなキャッシュをいれる目的なんだから良いinterfaceもutilもないんだろうなー。
もしかしたらmemcached互換サーバーな永続性あるやつのほうがutil優れてたりして。

注意点

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が消えるかもとかもいってるみたい。まー微妙な仕様なので新しいのができると期待。

rubyメモ書き

  • memcache_doのexecでdeleteはできなかった。ソース見たらレスポンスにENDが返ってくるデータ用らしく恐らくsetも無理。
  • dalliのgetを使うとrubyのデシリアライズしようとするのでruby専用になっている、恐らくdalliのsetもだめだろう。プレーンテキストをやりとりするserializer作ればいいのかも。

実行環境

ruby 1.9.3p374
memcached version 1.4.7