mctop 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in mctop.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Etsy
2
+
3
+ Permission is hereby granted, free of charge, to any person
4
+ obtaining a copy of this software and associated documentation
5
+ files (the "Software"), to deal in the Software without
6
+ restriction, including without limitation the rights to use,
7
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the
9
+ Software is furnished to do so, subject to the following
10
+ conditions:
11
+
12
+ The above copyright notice and this permission notice shall be
13
+ included in all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
+ OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,68 @@
1
+ # mctop
2
+
3
+ Inspired by "top", mctop passively sniffs the network traffic passing in and out of a
4
+ server's network interface and tracks the keys responding to memcache get commands. The output
5
+ is presented on the terminal and allows sorting by total calls, requests/sec and
6
+ bandwidth.
7
+
8
+ You can read more detail about why this tool evovled over on our
9
+ [code as craft](http://codeascraft.etsy.com/2012/12/13/mctop-a-tool-for-analyzing-memcache-get-traffic) blog.
10
+
11
+ mctop depends on the [ruby-pcap](https://rubygems.org/gems/ruby-pcap) gem, if you don't have
12
+ this installed you'll need to ensure you have the development pcap libraries (libpcap-devel
13
+ package on most linux distros) to build the native gem.
14
+
15
+ ## How it works
16
+
17
+ mctop sniffs network traffic collecting memcache `VALUE` responses and calculates from
18
+ traffic statistics for each key seen. It currently reports on the following metrics per key:
19
+
20
+ * **calls** - the number of times the key has been called since mctop started
21
+ * **objsize** - the size of the object stored for that key
22
+ * **req/sec** - the number of requests per second for the key
23
+ * **bw (kbps)** - the estimated netowrk bandwidth consumed by this key in kilobits-per-second
24
+
25
+ ## Getting it running
26
+
27
+ the quickest way to get it running is to:
28
+
29
+ * ensure you have libpcap-devel installed
30
+ * git clone this repo
31
+ * in the top level directory of this repo `bundle install` (this will install the deps)
32
+ * then either:
33
+ * install it locally `rake install`; or
34
+ * run it from the repo (good for hacking) `sudo ./bin/mctop --help`
35
+
36
+ ## Command line options
37
+
38
+ Usage: mctop [options]
39
+ -i, --interface=NIC Network interface to sniff (required)
40
+ -d, --discard=THRESH Discard keys with request/sec rate below THRESH
41
+ -r, --refresh=MS Refresh the stats display every MS milliseconds
42
+ -h, --help Show usage info
43
+
44
+ ## User interface commands
45
+
46
+ The following key commands are available in the console UI:
47
+
48
+ * `C` - sort by number of calls
49
+ * `S` - sort by object size
50
+ * `R` - sort by requests/sec
51
+ * `B` - sort by bandwidth
52
+ * `T` - toggle sorting by ascending / descending order
53
+ * `Q` - quits
54
+
55
+ ## Status bar
56
+
57
+ The following details are displayed in the status bar
58
+
59
+ * `sort mode` - the current sort mode and ordering
60
+ * `keys` - total number of keys in the metrics table
61
+ * `packets` - packets received and dropped by libpcap (% is percentage of packets dropped)
62
+ * `rt` - the time taken to sort and render the stats
63
+
64
+ ## Known issues / Gotchas
65
+
66
+ ### ruby-pcap drops packets at high volume
67
+ from my testing the ruby-pcap native interface to libpcap struggles to keep up with high packet rates (in what we see on a production memcache instance) you can keep an eye on the packets recv/drop and loss percentage on the status bar at the bottom of the UI to get an idea of the packet
68
+
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/bin/mctop ADDED
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # mctop - A command line memcached traffic analyzer
4
+ #
5
+ # Author:: Marcus Barczak (<marcus@etsy.com>)
6
+
7
+ $:.unshift File.join(File.dirname(__FILE__),'..','lib')
8
+
9
+ require 'cmdline'
10
+ require 'sniffer'
11
+ require 'ui'
12
+
13
+ @config = CmdLine.parse(ARGV)
14
+
15
+ # instantiate a sniffer and user interface object
16
+ sniffer = MemcacheSniffer.new(@config)
17
+ ui = UI.new(@config)
18
+
19
+ # set default display options
20
+ sort_mode = :reqsec
21
+ sort_order = :desc
22
+ done = false
23
+
24
+ # trap most of the typical signals
25
+ %w[ INT QUIT HUP KILL ].each do |sig|
26
+ Signal.trap(sig) do
27
+ puts "** Caught signal #{sig} - exiting"
28
+ done = true
29
+ end
30
+ end
31
+
32
+ # kick the sniffer thread off
33
+ sniff_thread = Thread.new { sniffer.start }
34
+
35
+ # main loop
36
+ until done do
37
+ ui.header
38
+ ui.footer
39
+ ui.render_stats(sniffer, sort_mode, sort_order)
40
+ refresh
41
+
42
+ key = ui.input_handler
43
+ case key
44
+ when /[Qq]/
45
+ done = true
46
+ when /[Cc]/
47
+ sort_mode = :calls
48
+ when /[Ss]/
49
+ sort_mode = :objsize
50
+ when /[Rr]/
51
+ sort_mode = :reqsec
52
+ when /[Bb]/
53
+ sort_mode = :bw
54
+ when /[Tt]/
55
+ if sort_order == :desc
56
+ sort_order = :asc
57
+ else
58
+ sort_order = :desc
59
+ end
60
+ end
61
+ end
62
+
63
+ ## cleanup
64
+ ui.done
65
+ sniffer.done
66
+
67
+ ## if sniffer thread doesn't join immediately kill it off the
68
+ ## capture.each loop blocks if no packets have been seen
69
+ if sniff_thread.join(0)
70
+ sniff_thread.kill
71
+ end
data/lib/cmdline.rb ADDED
@@ -0,0 +1,51 @@
1
+ require 'optparse'
2
+ require 'pcap'
3
+
4
+ class CmdLine
5
+ def self.parse(args)
6
+ @config = {}
7
+
8
+ opts = OptionParser.new do |opt|
9
+ opt.on('-i', '--interface=NIC', 'Network interface to sniff (required)') do |nic|
10
+ @config[:nic] = nic
11
+ end
12
+
13
+ opt.on '-d', '--discard=THRESH', Float, 'Discard keys with request/sec rate below THRESH' do |discard_thresh|
14
+ @config[:discard_thresh] = discard_thresh
15
+ end
16
+
17
+ opt.on '-r', '--refresh=MS', Float, 'Refresh the stats display every MS milliseconds' do |refresh_rate|
18
+ @config[:refresh_rate] = refresh_rate
19
+ end
20
+
21
+ opt.on_tail '-h', '--help', 'Show usage info' do
22
+ puts opts
23
+ exit
24
+ end
25
+ end
26
+
27
+ opts.parse!
28
+
29
+ # bail if we're not root
30
+ unless Process::Sys.getuid == 0
31
+ puts "** ERROR: needs to run as root to capture packets"
32
+ exit 1
33
+ end
34
+
35
+ # we need need a nic to listen on
36
+ unless @config.has_key?(:nic)
37
+ puts "** ERROR: You must specify a network interface to listen on"
38
+ puts opts
39
+ exit 1
40
+ end
41
+
42
+ # we can't do 'any' interface just yet due to weirdness with ruby pcap libs
43
+ if @config[:nic] =~ /any/i
44
+ puts "** ERROR: can't bind to any interface due to odd issues with ruby-pcap"
45
+ puts opts
46
+ exit 1
47
+ end
48
+
49
+ @config
50
+ end
51
+ end
data/lib/sniffer.rb ADDED
@@ -0,0 +1,55 @@
1
+ require 'pcap'
2
+
3
+ class MemcacheSniffer
4
+ attr_accessor :metrics, :semaphore
5
+
6
+ def initialize(config)
7
+ @source = config[:nic]
8
+
9
+ @metrics = {}
10
+ @metrics[:calls] = {}
11
+ @metrics[:objsize] = {}
12
+ @metrics[:reqsec] = {}
13
+ @metrics[:bw] = {}
14
+ @metrics[:stats] = { :recv => 0, :drop => 0 }
15
+
16
+ @semaphore = Mutex.new
17
+ end
18
+
19
+ def start
20
+ cap = Pcap::Capture.open_live(@source, 1500)
21
+
22
+ @metrics[:start_time] = Time.new.to_f
23
+
24
+ @done = false
25
+
26
+ cap.setfilter('port 11211')
27
+ cap.loop do |packet|
28
+ @metrics[:stats] = cap.stats
29
+
30
+ # parse key name, and size from VALUE responses
31
+ if packet.raw_data =~ /VALUE (\S+) \S+ (\S+)/
32
+ key = $1
33
+ bytes = $2
34
+
35
+ @semaphore.synchronize do
36
+ if @metrics[:calls].has_key?(key)
37
+ @metrics[:calls][key] += 1
38
+ else
39
+ @metrics[:calls][key] = 1
40
+ end
41
+
42
+ @metrics[:objsize][key] = bytes.to_i
43
+ end
44
+ end
45
+
46
+ break if @done
47
+ end
48
+
49
+ cap.close
50
+ end
51
+
52
+ def done
53
+ @done = true
54
+ end
55
+ end
data/lib/ui.rb ADDED
@@ -0,0 +1,161 @@
1
+ require 'curses'
2
+
3
+ include Curses
4
+
5
+ class UI
6
+ def initialize(config)
7
+ init_screen
8
+ cbreak
9
+ curs_set(0)
10
+
11
+ # set keyboard input timeout - sneaky way to manage refresh rate
12
+ Curses.timeout = config.has_key?(:refresh_rate) ? config[:refresh_rate] : 500
13
+
14
+ if can_change_color?
15
+ start_color
16
+ init_pair(0, COLOR_WHITE, COLOR_BLACK)
17
+ init_pair(1, COLOR_WHITE, COLOR_BLUE)
18
+ init_pair(2, COLOR_WHITE, COLOR_RED)
19
+ end
20
+
21
+ @stat_cols = %w[ calls objsize req/sec bw(kbps) ]
22
+ @stat_col_width = 10
23
+ @key_col_width = 0
24
+
25
+ # we will delete any keys from the metrics table whose req/sec rate is below discard_rate
26
+ @discard_thresh = config.has_key?(:discard_thresh) ? config[:discard_thresh] : 0
27
+
28
+ @commands = {
29
+ 'Q' => "quit",
30
+ 'C' => "sort by calls",
31
+ 'S' => "sort by size",
32
+ 'R' => "sort by req/sec",
33
+ 'B' => "sort by bandwidth",
34
+ 'T' => "toggle sort order (asc|desc)"
35
+ }
36
+ end
37
+
38
+ def header
39
+ # pad stat columns to @stat_col_width
40
+ @stat_cols = @stat_cols.map { |c| sprintf("%#{@stat_col_width}s", c) }
41
+
42
+ # key column width is whatever is left over
43
+ @key_col_width = cols - (@stat_cols.length * @stat_col_width)
44
+
45
+ attrset(color_pair(1))
46
+ setpos(0,0)
47
+ addstr(sprintf "%-#{@key_col_width}s%s", "memcache key", @stat_cols.join)
48
+ end
49
+
50
+ def footer
51
+ footer_text = @commands.map { |k,v| "#{k}:#{v}" }.join(' | ')
52
+ setpos(lines-1, 0)
53
+ attrset(color_pair(2))
54
+ addstr(sprintf "%-#{cols}s", footer_text)
55
+ end
56
+
57
+ def render_stats(sniffer, sort_mode, sort_order = :desc)
58
+ render_start_t = Time.now.to_f * 1000
59
+
60
+ # subtract header + footer lines
61
+ maxlines = lines - 3
62
+ offset = 1
63
+
64
+ # calculate packet loss ratio
65
+ if sniffer.metrics[:stats][:recv] > 0
66
+ loss = (sniffer.metrics[:stats][:drop].to_f / sniffer.metrics[:stats][:recv].to_f) * 100
67
+ else
68
+ loss = 0
69
+ end
70
+
71
+ # construct and render footer stats line
72
+ setpos(lines-2,0)
73
+ attrset(color_pair(2))
74
+ header_summary = sprintf "%-28s %-14s %-30s",
75
+ "sort mode: #{sort_mode.to_s} (#{sort_order.to_s})",
76
+ "keys: #{sniffer.metrics[:calls].keys.count}",
77
+ "packets (recv/dropped): #{sniffer.metrics[:stats][:recv]} / #{sniffer.metrics[:stats][:drop]} (#{loss.round(2)}%)"
78
+ addstr(sprintf "%-#{cols}s", header_summary)
79
+
80
+ # reset colours for main key display
81
+ attrset(color_pair(0))
82
+
83
+ top = []
84
+
85
+ sniffer.semaphore.synchronize do
86
+ # we may have seen no packets received on the sniffer thread
87
+ return if sniffer.metrics[:start_time].nil?
88
+
89
+ elapsed = Time.now.to_f - sniffer.metrics[:start_time]
90
+
91
+ # iterate over all the keys in the metrics hash and calculate some values
92
+ sniffer.metrics[:calls].each do |k,v|
93
+ reqsec = v / elapsed
94
+
95
+ # if req/sec is <= the discard threshold delete those keys from
96
+ # the metrics hash - this is a hack to manage the size of the
97
+ # metrics hash in high volume environments
98
+ if reqsec <= @discard_thresh
99
+ sniffer.metrics[:calls].delete(k)
100
+ sniffer.metrics[:objsize].delete(k)
101
+ sniffer.metrics[:reqsec].delete(k)
102
+ sniffer.metrics[:bw].delete(k)
103
+ else
104
+ sniffer.metrics[:reqsec][k] = v / elapsed
105
+ sniffer.metrics[:bw][k] = ((sniffer.metrics[:objsize][k] * sniffer.metrics[:reqsec][k]) * 8) / 1000
106
+ end
107
+ end
108
+
109
+ top = sniffer.metrics[sort_mode].sort { |a,b| a[1] <=> b[1] }
110
+ end
111
+
112
+ unless sort_order == :asc
113
+ top.reverse!
114
+ end
115
+
116
+ for i in 0..maxlines-1
117
+ if i < top.length
118
+ k = top[i][0]
119
+ v = top[i][1]
120
+
121
+ # if the key is too wide for the column truncate it and add an ellipsis
122
+ if k.length > @key_col_width
123
+ display_key = k[0..@key_col_width-4]
124
+ display_key = "#{display_key}..."
125
+ else
126
+ display_key = k
127
+ end
128
+
129
+ # render each key
130
+ line = sprintf "%-#{@key_col_width}s %9.d %9.d %9.2f %9.2f",
131
+ display_key,
132
+ sniffer.metrics[:calls][k],
133
+ sniffer.metrics[:objsize][k],
134
+ sniffer.metrics[:reqsec][k],
135
+ sniffer.metrics[:bw][k]
136
+ else
137
+ # we're not clearing the display between renders so erase past
138
+ # keys with blank lines if there's < maxlines of results
139
+ line = " "*cols
140
+ end
141
+
142
+ setpos(1+i, 0)
143
+ addstr(line)
144
+ end
145
+
146
+ # print render time in status bar
147
+ runtime = (Time.now.to_f * 1000) - render_start_t
148
+ attrset(color_pair(2))
149
+ setpos(lines-2, cols-18)
150
+ addstr(sprintf "rt: %8.3f (ms)", runtime)
151
+ end
152
+
153
+ def input_handler
154
+ getch
155
+ end
156
+
157
+ def done
158
+ nocbreak
159
+ close_screen
160
+ end
161
+ end
data/mctop.gemspec ADDED
@@ -0,0 +1,19 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+
5
+ Gem::Specification.new do |gem|
6
+ gem.name = "mctop"
7
+ gem.version = "0.0.3"
8
+ gem.authors = ["Marcus Barczak"]
9
+ gem.email = ["marcus@etsy.com"]
10
+ gem.description = %q{mctop - a realtime memcache key analyzer}
11
+ gem.summary = %q{mctop - an interactive terminal app for analyzing memcache key activity breaking it out by requests per second, calls, and estimated bandwidth make sure you have the libpcap development libraries installed for the dependencies}
12
+ gem.homepage = "https://github.com/etsy/mctop/"
13
+
14
+ gem.files = `git ls-files`.split($/)
15
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
16
+ gem.require_paths = ["lib"]
17
+
18
+ gem.add_runtime_dependency 'ruby-pcap', '~> 0.7.8'
19
+ end
metadata ADDED
@@ -0,0 +1,74 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mctop
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.3
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Marcus Barczak
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-12-13 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: ruby-pcap
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 0.7.8
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: 0.7.8
30
+ description: mctop - a realtime memcache key analyzer
31
+ email:
32
+ - marcus@etsy.com
33
+ executables:
34
+ - mctop
35
+ extensions: []
36
+ extra_rdoc_files: []
37
+ files:
38
+ - .gitignore
39
+ - Gemfile
40
+ - LICENSE
41
+ - README.md
42
+ - Rakefile
43
+ - bin/mctop
44
+ - lib/cmdline.rb
45
+ - lib/sniffer.rb
46
+ - lib/ui.rb
47
+ - mctop.gemspec
48
+ homepage: https://github.com/etsy/mctop/
49
+ licenses: []
50
+ post_install_message:
51
+ rdoc_options: []
52
+ require_paths:
53
+ - lib
54
+ required_ruby_version: !ruby/object:Gem::Requirement
55
+ none: false
56
+ requirements:
57
+ - - ! '>='
58
+ - !ruby/object:Gem::Version
59
+ version: '0'
60
+ required_rubygems_version: !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ! '>='
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ requirements: []
67
+ rubyforge_project:
68
+ rubygems_version: 1.8.23
69
+ signing_key:
70
+ specification_version: 3
71
+ summary: mctop - an interactive terminal app for analyzing memcache key activity breaking
72
+ it out by requests per second, calls, and estimated bandwidth make sure you have
73
+ the libpcap development libraries installed for the dependencies
74
+ test_files: []