memcachedのstats slabsの項目名は直感的でない。
例えば、
free_chunksは、delete等されて再利用可能なチャンク数
free_chunks_endが最後にアロケートされたページで一度もsetされていないチャンク数
とかは、memcachedのソースを読んでやっと意味がわかった。
また、チューニングの際に必要な、アラインメントによる無駄領域の合計がぱっと見でわからなかったりする。
とういことで、
memcachedのstats slabs統計情報を解析するRubyスクリプトを張り付けておく。
# テキストプロトコルにしか対応してません。
# バイナリプロトコル専用のmemachedへは、rubyのmemcachedライブラリを使うように改造すりゃできる。
起動オプション
Usage:
./better_stats_slabs.rb -d /path_to_dir
or ./better_stats_slabs.rb -f /path_to_file
or ./better_stats_slabs.rb -h localhost:11211,localhost11222
Byte表示 オプションなし(デフォルト)
KiloByte表示 -k
MegaByte表示 -m
GigaByte表示 -g
出力結果
出力説明
STAT1..の行はスラブクラスごとの情報、下のtotalは全体の情報。
数字は全部サイズ。(byte, kB, MB GBの切り替えはオプションで可能)
空き領域が0のスラブクラスの先頭にはアスタリスクを表示している。
どのスラブでout of memoryが発生しているか、が分かる。
never_usedは、free_chunks_endのサイズ合計であり、最後に割り当てられたスラブ(ページ)の空き領域
reusableは、free_chunksのサイズ合計であり、delete済みのチャンク数合計
free_totalは、never_usedと、reusableの合計。つまり、そのスラブの空き領域合計サイズ。
free_totalが0だと、次にそのスラブクラスにsetした際にmallocされるってこと。(-Lオプションつけない場合)
item_sizeは、アラインメントを意識したアイテムの平均サイズの合計
wastedは、アラインメントを意識したアイテムの平均無駄サイズの合計
item_size + wastedが使用中チャンクサイズの合計です。
チューニング時にgrowth_factorを決める際に参考にできる。
total mallocは、スラブ用領域に割り当てられたサイズの合計、
total free sizeは空き領域合計
total ued sizeは使用中チャンクサイズ合計
items sizeの割合は、スラブ用メモリ領域全体のうちのアイテム本体のサイズが占める割合
wasted sizeの割合は、スラブ用メモリ領域全体のうちの無駄領域のサイズが占める割合
ソース
- #!/usr/bin/ruby
- # encoding: UTF-8
- def comma(number)
- number.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\1,').reverse
- end
- def percent(numerator, denominator)
- (denominator == 0 ? "- " : (100 * numerator / denominator).to_s).rjust(3)
- end
- alias :org_printf :printf
- def printf(fmt, *args)
- org_printf(fmt, *args.map{|a| a.is_a?(Numeric) ? a / $options[:unit_scale] : a})
- end
- def stats_slabs(host_port)
- host, port = host_port.split(":").first, host_port.split(":").last
- lines = []
- begin
- TCPSocket.open(host, port) do |s|
- s.puts "stats slabs"
- while "END" != (line = s.gets.chop)
- lines << line
- end
- end
- rescue => e
- puts "counld not connect to #{host_port}"
- puts e.message
- end
- lines
- end
- def parse(lines)
- chunks_stats = []
- lines.each do |line|
- case line
- when /^STAT (\d+).*:chunk_size (\d+)/
- chunks_stats << {no: $1, size: $2.to_i,
- previous_size: chunks_stats.last ? chunks_stats.last[:size].to_i : 0 }
- when /^STAT.*:total_chunks (\d+)/
- chunks_stats.last[:total_chunks] = $1.to_i
- when /^STAT.*:used_chunks (\d+)/
- chunks_stats.last[:used_chunks] = $1.to_i
- when /^STAT.*:free_chunks_end (\d+)/
- chunks_stats.last[:free_chunks_end] = $1.to_i
- when /^STAT.*:free_chunks (\d+)/
- chunks_stats.last[:free_chunks] = $1.to_i
- end
- end
- chunks_stats
- end
- def calc_and_print(chunks_stats)
- total_malloced = 0 # total size of all chunks.
- total_free_size = 0 # total size of free chunks.
- total_used_size = 0 # total size of used chunks.
- total_items_size_about = 0 # total size of items.
- total_waste_size_about = 0 # total size of wasted spaces by align.
- puts "(unit: #{$options[:unit]})"
- chunks_stats.each_with_index do |stat, i|
- total_malloced += (stat[:size] * stat[:total_chunks])
- # The number of free chunks in a subclass = free_chunks_end + free_chunks
- sum_unused_size = stat[:size] * (stat[:free_chunks_end]) # chunks has not used yet.
- sum_reusable_size = stat[:size] * (stat[:free_chunks]) # deleted chunks.
- sum_free_size = sum_unused_size + sum_reusable_size
- total_free_size += sum_free_size
- used_size = stat[:size] * stat[:used_chunks]
- total_used_size += used_size
- # The average of wasted size(unused space) of a chunk.
- agv_wasted_size_per_chunk = (stat[:size] - stat[:previous_size] ) / 2
- sum_wasted_size = agv_wasted_size_per_chunk * stat[:used_chunks]
- total_waste_size_about += sum_wasted_size
- sum_items_size = (stat[:size] - agv_wasted_size_per_chunk) * stat[:used_chunks]
- total_items_size_about += sum_items_size
- printf(" #{sum_free_size == 0 ? "*" : " "} STAT %2d(#{stat[:size].to_s.rjust(4)}) | never_used: %10d reusable: %10d free_total: %10d | item_size : %10d (#{percent(sum_items_size, used_size)}%%) wasted: %10d (#{percent(sum_wasted_size, used_size)}%%)\n", stat[:no], sum_unused_size, sum_reusable_size, sum_free_size, sum_items_size, sum_wasted_size)
- end
- puts ""
- printf(" total malloced : %11d #{$options[:unit]}\n", total_malloced)
- printf(" total free size : %11d #{$options[:unit]}(#{percent(total_free_size, total_malloced)}%%)\n", total_free_size)
- printf(" total used size : %11d #{$options[:unit]}(#{percent(total_used_size, total_malloced)}%%)\n", total_used_size)
- printf(" items size ≈ %12d #{$options[:unit]}(#{percent(total_items_size_about, total_malloced)}%%)\n", total_items_size_about)
- printf(" wasted size ≈ %12d #{$options[:unit]}(#{percent(total_waste_size_about, total_malloced)}%%)\n", total_waste_size_about)
- end
- def parse_calc_print(header, lines)
- chunks_stats = parse(lines)
- puts "-" * 5 + header + "-" * 130
- return if chunks_stats.empty?
- calc_and_print chunks_stats
- end
- Usage = "Usage:
- ./better_stats_slabs.rb -d /path_to_dir
- or ./better_stats_slabs.rb -f /path_to_file
- or ./better_stats_slabs.rb -h localhost:11211,localhost11222
- Byte ./better_stats_slabs.rb -d /path_to_dir
- KiloByte ./better_stats_slabs.rb -d /path_to_dir -k
- MegaByte ./better_stats_slabs.rb -d /path_to_dir -m
- GigaByte ./better_stats_slabs.rb -d /path_to_dir -g
- "
- $options = {unit: "Byte", unit_scale: 1}
- require 'optparse'
- OptionParser.new{|opt|
- Version = "0.1"
- opt.banner = Usage
- opt.on("-k", "kB", "KilloByte") do
- $options[:unit] = "kB"
- $options[:unit_scale] = 1024
- end
- opt.on("-m", "MB", "MegaByte") do
- $options[:unit] = "MB"
- $options[:unit_scale] = 1024 ** 2
- end
- opt.on("-g", "GB", "GigaByte") do
- $options[:unit] = "GB"
- $options[:unit_scale] = 1024 ** 3
- end
- opt.on("-f filepath", "a file which had saved 'stats slabs'.") do |file|
- $options[:file] = file
- end
- opt.on("-d dir_path", "a dir path wihch has 'stats slabs' files.") do |dir|
- $options[:dir] = dir
- end
- opt.on("-h localhost:11211,localhost11222", "Host:Port of memcached process. ex. localhost:11211,localhost:11212") do |hs|
- $options[:hosts_and_ports] = hs
- end
- opt.parse!(ARGV)
- }
- if file = $options[:file]
- lines = File.readlines(File.expand_path file)
- parse_calc_print(file, lines)
- elsif dir = $options[:dir]
- Dir.glob("#{dir}/*") do |path|
- lines= File.readlines(File.expand_path path)
- parse_calc_print(path, lines)
- end
- elsif hs = $options[:hosts_and_ports]
- require 'socket'
- host_port_array = hs.split(",").map{|h| h.strip}
- host_port_array.each do |h|
- lines = stats_slabs(h)
- parse_calc_print(h, lines)
- end
- else
- puts Usage
- end