Skip to content

Instantly share code, notes, and snippets.

@alexnavis
Last active January 7, 2022 16:02
Show Gist options
  • Save alexnavis/2665e05715f4355ec55bebd623d8ca8f to your computer and use it in GitHub Desktop.
Save alexnavis/2665e05715f4355ec55bebd623d8ca8f to your computer and use it in GitHub Desktop.
Redis Big Keys, Dump Debug Info with Humanized Size Information to Export CSV
# Intention to dump from live redis. Use rdb-tools to dump from a file.
# To know the usage run: >> ruby redis_debug.rb
require 'redis' # needs redis gem to work
require 'csv'
class RedisDebug
DEFAULT_OPTS = { redis: {host: 'localhost', port: '6379', db: 0, timeout: 100},
batch_size: 2000,
dry_run: false,
filter_top: nil,
report_dir: 'log',
file_prefix: 'redis_keys_size'
}
def self.get_info(opts = {})
opts = DEFAULT_OPTS.merge(opts)
redis = Redis.new(opts[:redis])
all_keys = redis.keys '*'
all_keys = opts[:dry_run] ? all_keys.take(10) : all_keys
puts "Total keys: #{all_keys.size}, fetching keys in batch of size '#{opts[:batch_size]}' => #{all_keys.size/opts[:batch_size]} batches"
results = all_keys.each_slice(opts[:batch_size]).reduce([]) do |acc, slice|
results = self.debug(redis, slice)
acc.concat(results)
putc ".#{acc.count}."
acc
end
sorted_results = results.sort_by { |debug_info| -debug_info.size }.take(opts[:filter_top] || results.count)
puts "\nCompleted results count: #{sorted_results.count}"
sorted_results
end
def self.print_info(opts = {})
opts = DEFAULT_OPTS.merge(opts)
results = get_info opts
file_name = "#{opts[:report_dir]}/#{opts[:file_prefix]}#{Time.now.strftime('%Y-%m-%d_%H_%M_%S')}.csv"
CSV.open(file_name, 'wb') do |csv|
csv << ['key', 'humanized size', 'bytes', 'encoding', 'refcount', 'lru', 'lru_seconds_idle', 'address']
results.each do |info|
csv << [info.key, info.humanize_size, info.size, info.encoding, info.refcount, info.lru, info.lru_seconds_idle, info.address]
end
end
puts "Regenerated report in : #{File.absolute_path(file_name)}"
end
def self.debug(redis, keys)
return {} if keys.nil?
results = redis.pipelined do
keys.each { |key| redis.debug(:object, key) }
end
keys.zip(results).map do |(key, value)|
value = value.nil? ? {} : value.split(' ').drop(1).map { |item| item.split(':') }.to_h
DebugInfo.new key, convert_integer_values(value)
end
end
def self.convert_integer_values(value)
value.map do |k, v|
v = v.to_i if v.match(/^\d+$/)
[k, v]
end.to_h
end
end
class DebugInfo
attr_reader :values, :key
def initialize(key, values)
@key = key
@values = values
end
def size
values[:serializedlength.to_s]
end
def address
values[:at.to_s]
end
def refcount
values[:refcount.to_s]
end
def encoding
values[:encoding.to_s]
end
def lru
values[:lru.to_s]
end
def lru_seconds_idle
values[:lru_seconds_idle.to_s]
end
def humanize_size
{
'B' => 1024,
'KB' => 1024 * 1024,
'MB' => 1024 * 1024 * 1024,
'GB' => 1024 * 1024 * 1024 * 1024,
'TB' => 1024 * 1024 * 1024 * 1024 * 1024
}.each_pair { |e, s| return "#{(size.to_f / (s / 1024)).round(2)}#{e}" if size < s }
end
def values
@values
end
end
opts = ARGV[0].nil? ? nil : eval(ARGV[0])
if opts.nil? || !opts.is_a?(Hash)
puts 'usage >>>>>> ruby lib/redis_debug.rb <ruby hash>. e.g., ruby lib/redis_debug.rb {dry_run: true}'
puts "\tall options (with default) >> '#{RedisDebug::DEFAULT_OPTS}'"
puts "\tminimal options >> '#{{redis: {port: '1111', dry_run: true}}}'"
exit
end
RedisDebug.print_info(opts)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment