Skip to content

Commit d8404d3

Browse files
committed
SCAN instead of KEYS and sanitization for encoding
1 parent 5b0dc04 commit d8404d3

1 file changed

Lines changed: 78 additions & 49 deletions

File tree

redis-audit.rb

Lines changed: 78 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,22 @@
22

33
# Copyright (c) 2012, Simon Maynard
44
# http://snmaynard.com
5-
#
6-
# Permission is hereby granted, free of charge, to any person obtaining a
7-
# copy of this software and associated documentation files (the "Software"),
8-
# to deal in the Software without restriction, including without limitation
9-
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
10-
# and/or sell copies of the Software, and to permit persons to whom the
5+
#
6+
# Permission is hereby granted, free of charge, to any person obtaining a
7+
# copy of this software and associated documentation files (the "Software"),
8+
# to deal in the Software without restriction, including without limitation
9+
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
10+
# and/or sell copies of the Software, and to permit persons to whom the
1111
# Software is furnished to do so, subject to the following conditions:
1212
#
13-
# The above copyright notice and this permission notice shall be included
13+
# The above copyright notice and this permission notice shall be included
1414
# in all copies or substantial portions of the Software.
1515
#
16-
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17-
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18-
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19-
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20-
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
16+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20+
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
2121
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
2222

2323
require 'bundler/setup'
@@ -27,8 +27,8 @@
2727

2828
# Container class for stats around a key group
2929
class KeyStats
30-
attr_accessor :total_instances,
31-
:total_idle_time,
30+
attr_accessor :total_instances,
31+
:total_idle_time,
3232
:total_serialized_length,
3333
:total_expirys_set,
3434
:min_serialized_length,
@@ -37,63 +37,65 @@ class KeyStats
3737
:max_idle_time,
3838
:max_ttl,
3939
:sample_keys
40-
40+
4141
def initialize
4242
@total_instances = 0
4343
@total_idle_time = 0
4444
@total_serialized_length = 0
4545
@total_expirys_set = 0
46-
46+
4747
@min_serialized_length = nil
4848
@max_serialized_length = nil
4949
@min_idle_time = nil
5050
@max_idle_time = nil
5151
@max_ttl = nil
52-
52+
5353
@sample_keys = {}
54+
55+
@has_scrub = RUBY_VERSION.to_f >= 2.1
5456
end
55-
57+
5658
def add_stats_for_key(key, type, idle_time, serialized_length, ttl)
5759
@total_instances += 1
5860
@total_idle_time += idle_time
5961
@total_expirys_set += 1 if ttl != nil
6062
@total_serialized_length += serialized_length
61-
63+
6264
@min_idle_time = idle_time if @min_idle_time.nil? || @min_idle_time > idle_time
6365
@max_idle_time = idle_time if @max_idle_time.nil? || @max_idle_time < idle_time
6466
@min_serialized_length = serialized_length if @min_serialized_length.nil? || @min_serialized_length > serialized_length
6567
@max_serialized_length = serialized_length if @max_serialized_length.nil? || @max_serialized_length < serialized_length
6668
@max_ttl = ttl if ttl != nil && ( @max_ttl == nil || @max_ttl < ttl )
67-
69+
6870
@sample_keys[key] = type if @sample_keys.count < 10
6971
end
7072
end
7173

7274
class RedisAudit
7375
@@key_regex = /^(.*):(.*)$/
7476
@@debug_regex = /serializedlength:(\d*).*lru_seconds_idle:(\d*)/
75-
77+
7678
# Configure regular expressions here if you need to guarantee that certain keys are grouped together
7779
@@key_group_regex_list = []
78-
80+
7981
def initialize(redis, sample_size)
8082
@redis = redis
8183
@keys = Hash.new {|h,k| h[k] = KeyStats.new}
8284
@sample_size = sample_size
8385
@dbsize = 0
8486
end
85-
87+
8688
def audit_keys
8789
@dbsize = @redis.dbsize.to_i
88-
90+
8991
if @sample_size == 0 || @sample_size.nil?
9092
@sample_size = (0.1 * @dbsize).to_i
9193
end
92-
94+
9395
if @sample_size < @dbsize
9496
puts "Sampling #{@sample_size} keys..."
9597
sample_progress = @sample_size/10
96-
98+
9799
@sample_size.times do |index|
98100
key = @redis.randomkey
99101
audit_key(key)
@@ -103,9 +105,10 @@ def audit_keys
103105
end
104106
else
105107
sample_progress = @dbsize/10
106-
108+
107109
puts "Getting a list of all #{@dbsize} keys..."
108-
keys = @redis.keys("*")
110+
keys = fetch_all_keys
111+
109112
puts "Auditing #{@dbsize} keys..."
110113
keys.each_with_index do |key, index|
111114
audit_key(key)
@@ -115,7 +118,22 @@ def audit_keys
115118
end
116119
end
117120
end
118-
121+
122+
def fetch_all_keys
123+
keys = []
124+
cursor = 0
125+
batch_size = 1000
126+
127+
loop do
128+
puts '.'
129+
cursor, keys_batch = @redis.scan(cursor, match: "*", count: batch_size)
130+
keys.push(*keys_batch) if keys_batch.size > 0
131+
break if cursor.to_i == 0
132+
end
133+
134+
keys
135+
end
136+
119137
def audit_key(key)
120138
pipeline = @redis.pipelined do
121139
@redis.debug("object", key)
@@ -131,34 +149,34 @@ def audit_key(key)
131149
rescue Redis::CommandError
132150
$stderr.puts "Skipping key #{key}"
133151
end
134-
152+
135153
# This function defines what keys are grouped together. Currently it looks for a key that
136-
# matches at least a third of the key from the start, and groups those together. It also
137-
# removes any numbers as they are (generally) ids.
154+
# matches at least a third of the key from the start, and groups those together. It also
155+
# removes any numbers as they are (generally) ids.
138156
def group_key(key, type)
139157
@@key_group_regex_list.each_with_index do |regex, index|
140158
return "#{regex.to_s}:#{type}" if regex.match(key)
141159
end
142-
160+
143161
# This makes the odds of finding a correct match higher, as mostly these are ids
144-
key = key.delete("0-9")
145-
162+
key = normalize_key(key)
163+
146164
matching_key = nil
147165
length_of_best_match = 0
148166
threshold = key.length / 3
149167
matching_portion = nil
150168
key_codepoints = key.codepoints.to_a
151-
169+
152170
@keys.keys.each do |current_key|
153171
next if matching_key && !current_key.start_with?(matching_portion) # we know it wont be longer
154172
length_of_match = 0
155-
173+
156174
current_key.each_codepoint.with_index do |codepoint, index|
157175
next if index < length_of_best_match
158176
break unless key_codepoints[index] == codepoint
159177
length_of_match += 1
160178
end
161-
179+
162180
# Minimum length of match is 1/3 of the new key length
163181
if length_of_match >= threshold && length_of_match > length_of_best_match && @@key_regex.match(current_key)[2] == type
164182
matching_key = current_key
@@ -172,26 +190,37 @@ def group_key(key, type)
172190
return "#{key}:#{type}"
173191
end
174192
end
175-
193+
194+
def normalize_key(key)
195+
scrubbed_key =
196+
if @has_scrub
197+
key.scrub
198+
else
199+
key.chars.select(&:valid_encoding?).join
200+
end
201+
202+
scrubbed_key.delete("0-9")
203+
end
204+
176205
def output_duration(seconds)
177206
m, s = seconds.divmod(60)
178207
h, m = m.divmod(60)
179208
d, h = h.divmod(24)
180-
209+
181210
output = []
182211
output << "#{d} days" if d != 0
183212
output << "#{h} hours" if h != 0
184213
output << "#{m} minutes" if m != 0
185214
output << "#{s} seconds" if s != 0
186215
return "0 seconds" if output.count == 0
187-
return output.join(", ")
216+
return output.join(", ")
188217
end
189-
218+
190219
def output_bytes(bytes)
191220
kb, b = bytes.divmod(1024)
192221
mb, kb = kb.divmod(1024)
193222
gb, mb = mb.divmod(1024)
194-
223+
195224
if gb != 0
196225
result = ((gb + mb/1024.0)*100).round()/100.0
197226
return "#{result} GB"
@@ -205,11 +234,11 @@ def output_bytes(bytes)
205234
return "#{b} bytes"
206235
end
207236
end
208-
237+
209238
def output_stats
210239
complete_serialized_length = @keys.map {|key, value| value.total_serialized_length }.reduce(:+)
211240
sorted_keys = @keys.keys.sort{|a,b| @keys[a].total_serialized_length <=> @keys[b].total_serialized_length}
212-
241+
213242
if complete_serialized_length == 0 || complete_serialized_length.nil?
214243
complete_serialized_length = 0
215244
end
@@ -224,7 +253,7 @@ def output_stats
224253
key_fields = @@key_regex.match(key)
225254
common_key = key_fields[1]
226255
common_type = key_fields[2]
227-
256+
228257
puts "=============================================================================="
229258
puts "Found #{value.total_instances} keys containing #{common_type}s, like:"
230259
puts "\e[0;33m#{value.sample_keys.keys.join(", ")}\e[0m"
@@ -235,7 +264,7 @@ def output_stats
235264
else
236265
puts "\e[0;1;4m#{make_proportion_percentage(value.total_expirys_set/value.total_instances.to_f)}\e[0m of these keys expire (#{value.total_expirys_set}), with maximum ttl of #{output_duration(value.max_ttl)}"
237266
end
238-
267+
239268
puts "Average last accessed time: \e[0;1;4m#{output_duration(value.total_idle_time/value.total_instances)}\e[0m - (Max: #{output_duration(value.max_idle_time)} Min:#{output_duration(value.min_idle_time)})"
240269
puts
241270
end
@@ -253,7 +282,7 @@ def output_stats
253282
:width => 50
254283
}]
255284
format = summary_columns.map{|c| "%-#{c[:width]}s" }.join(' | ')
256-
285+
257286
puts "=============================================================================="
258287
puts "Summary"
259288
puts
@@ -266,7 +295,7 @@ def output_stats
266295
end
267296
puts format.tr(' |', '-+') % summary_columns.map{|c| '-'*c[:width] }
268297
end
269-
298+
270299
def make_proportion_percentage(value)
271300
return "#{(value * 10000).round/100.0}%"
272301
end

0 commit comments

Comments
 (0)