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
2323require 'bundler/setup'
2727
2828# Container class for stats around a key group
2929class 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
7072end
7173
7274class 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