├── README ├── autoperf.rb ├── make_replay_log.rb ├── replay_sample.conf └── requests_sample.conf /README: -------------------------------------------------------------------------------- 1 | For updated version, see: https://github.com/jmervine/autoperf 2 | 3 | -------------------------------------------------------------- 4 | 5 | Autoperf is a ruby driver for httperf, designed to help you automate load and performance testing of any web application - for a single end point, or through log replay. More: http://www.igvita.com/2008/09/30/load-testing-with-log-replay 6 | 7 | To get started, first download & install httperf: 8 | http://www.hpl.hp.com/research/linux/httperf/ 9 | 10 | Next, either run a simple test straight from the command line (man httperf), or create 11 | an execution plan for autoperf. If you want to replay an access log from your production 12 | environment, follow these steps: 13 | 14 | # grab last 10000 lines from nginx log, and extract a certain pattern (if needed) 15 | tail -n 10000 nginx.log | grep "__pattern__" > requests 16 | 17 | # extract the request path (ex. /homepage) from the log file 18 | awk '{print $7}' requests > requests_path 19 | 20 | # replace newlines with null terminators (httperf format) 21 | tr "\n" "\0" < requests_path > replay_log 22 | 23 | Next, configure your execution plan (see sample.conf), and run autoperf: 24 | ruby autoperf.rb -c sample.conf 25 | 26 | Sample output: 27 | +-----------------------------------------------------------------------------+ 28 | | rate | conn/s | req/s | replies/s avg | errors | 5xx status | net io (KB/s) | 29 | +-----------------------------------------------------------------------------+ 30 | | 100 | 99.9 | 99.9 | 99.7 | 0 | 0 | 45.4 | 31 | | 120 | 119.7 | 119.7 | 120.0 | 0 | 0 | 54.4 | 32 | | 140 | 139.3 | 139.3 | 138.0 | 0 | 0 | 63.6 | 33 | |> 160 | 151.9 | 151.9 | 147.0 | 0 | 0 | 69.3 | 34 | | 180 | 132.2 | 129.8 | 137.4 | 27 | 0 | 59.6 | 35 | | 200 | 119.8 | 117.6 | 139.9 | 31 | 14 | 53.9 | 36 | +-----------------------------------------------------------------------------+ 37 | 38 | If your server uses caching, making it pointless to run the same requests over 39 | and over, you can use different requests for each run. 40 | 41 | # Create 10 1000-line files (xa, xb, xc etc) 42 | split -a 1 requests_path 43 | 44 | # Convert to null-terminated strings 45 | for x in x?; do tr "\n" "\0" < $x > $x.nul; done 46 | 47 | # run as before, but use the `wlog` line instead of `httperf_wlog` in the conf file 48 | ruby autoperf.rb -c sample.conf 49 | -------------------------------------------------------------------------------- /autoperf.rb: -------------------------------------------------------------------------------- 1 | #-- 2 | # Copyright (C)2008 Ilya Grigorik 3 | # You can redistribute this under the terms of the Ruby license 4 | #++ 5 | 6 | require 'rubygems' 7 | require 'optparse' 8 | require 'ruport' 9 | 10 | class AutoPerf 11 | def initialize(opts = {}) 12 | @conf = {} 13 | OptionParser.new do |opts| 14 | opts.banner = "Usage: autoperf.rb [-c config]" 15 | 16 | opts.on("-c", "--config [string]", String, "configuration file") do |v| 17 | @conf = parse_config(v) 18 | end 19 | end.parse! 20 | 21 | run() 22 | end 23 | 24 | def parse_config(config_file) 25 | raise Errno::EACCES, "#{config_file} is not readable" unless File.readable?(config_file) 26 | 27 | conf = {} 28 | open(config_file).each { |line| 29 | line.chomp 30 | unless (/^\#/.match(line)) 31 | if(/\s*=\s*/.match(line)) 32 | param, value = line.split(/\s*=\s*/, 2) 33 | var_name = "#{param}".chomp.strip 34 | value = value.chomp.strip 35 | new_value = '' 36 | if (value) 37 | if value =~ /^['"](.*)['"]$/ 38 | new_value = $1 39 | else 40 | new_value = value 41 | end 42 | else 43 | new_value = '' 44 | end 45 | conf[var_name] = new_value 46 | end 47 | end 48 | } 49 | 50 | if conf['wlog'] 51 | conf['wlog'] = Dir[conf['wlog']].sort 52 | end 53 | 54 | return conf 55 | end 56 | 57 | def benchmark(conf) 58 | httperf_opt = conf.keys.grep(/httperf/).collect {|k| "--#{k.gsub(/httperf_/, '')} #{conf[k]}"}.join(" ") 59 | if conf['wlog'] 60 | wlog = conf['wlog'].shift 61 | conf['wlog'].push wlog 62 | wlog_opt = "--wlog n,#{wlog}" 63 | end 64 | httperf_cmd = "httperf --hog --server=#{conf['host']} --uri=#{conf['uri']} --port=#{conf['port']} #{httperf_opt} #{wlog_opt}" 65 | 66 | res = Hash.new("") 67 | IO.popen("#{httperf_cmd} 2>&1") do |pipe| 68 | puts "\n#{httperf_cmd}" 69 | 70 | while((line = pipe.gets)) 71 | res['output'] += line 72 | 73 | case line 74 | when /^Total: .*replies (\d+)/ then res['replies'] = $1 75 | when /^Connection rate: (\d+\.\d)/ then res['conn/s'] = $1 76 | when /^Request rate: (\d+\.\d)/ then res['req/s'] = $1 77 | when /^Reply time .* response (\d+\.\d)/ then res['reply time'] = $1 78 | when /^Net I\/O: (\d+\.\d)/ then res['net io (KB/s)'] = $1 79 | when /^Errors: total (\d+)/ then res['errors'] = $1 80 | when /^Reply rate .*min (\d+\.\d) avg (\d+\.\d) max (\d+\.\d) stddev (\d+\.\d)/ then 81 | res['replies/s min'] = $1 82 | res['replies/s avg'] = $2 83 | res['replies/s max'] = $3 84 | res['replies/s stddev'] = $4 85 | when /^Reply status: 1xx=\d+ 2xx=\d+ 3xx=\d+ 4xx=\d+ 5xx=(\d+)/ then res['5xx status'] = $1 86 | end 87 | end 88 | end 89 | 90 | return res 91 | end 92 | 93 | def run 94 | results = {} 95 | report = Table(:column_names => ['rate', 'conn/s', 'req/s', 'replies/s avg', 96 | 'errors', '5xx status', 'net io (KB/s)']) 97 | 98 | (@conf['low_rate'].to_i..@conf['high_rate'].to_i).step(@conf['rate_step'].to_i) do |rate| 99 | results[rate] = benchmark(@conf.merge({'httperf_rate' => rate})) 100 | report << results[rate].merge({'rate' => rate}) 101 | 102 | puts report.to_s 103 | puts results[rate]['output'] if results[rate]['errors'].to_i > 0 || results[rate]['5xx status'].to_i > 0 104 | end 105 | end 106 | end 107 | 108 | trap("INT") { 109 | puts "Terminating tests." 110 | Process.exit 111 | } 112 | 113 | AutoPerf.new() 114 | -------------------------------------------------------------------------------- /make_replay_log.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # Make replay log by extracting the URI from an Apache CLF log file 3 | # replacing string 'match' by 'substitute' and concatenating them 4 | # together by replacing \n by \0 5 | 6 | progname = File.basename($0) 7 | 8 | if ARGV.size < 2 9 | puts "Usage: ruby #{progname} 'match' 'substitute' file1 [file2 [file3]] > output_file" 10 | puts "Usage: cat logfile | ruby #{progname} 'match' 'substitute' > output_file" 11 | puts 12 | puts "Example: cat /var/log/access_log | ruby #{progname} '/app' '/' > replay_log" 13 | exit 14 | end 15 | 16 | match = ARGV.shift 17 | sub = ARGV.shift 18 | 19 | ARGF.each do |line| 20 | request = line.split('"')[1] 21 | next if request.nil? 22 | 23 | uri = request.split[1] 24 | next if uri.nil? 25 | 26 | begin 27 | uri[match] = sub 28 | rescue IndexError 29 | # simply output line that don't contain the 'replace' string 30 | ensure 31 | print uri.chomp + "\0" 32 | end 33 | end -------------------------------------------------------------------------------- /replay_sample.conf: -------------------------------------------------------------------------------- 1 | # Autoperf Configuration File 2 | 3 | # The host, URI (relative to the document root) and port to test. 4 | host = localhost 5 | #uri = /homepage 6 | port = 80 7 | 8 | # The 'rate' is the number of number of connections to open per second. 9 | # A series of tests will be conducted, starting at low rate, 10 | # increasing by rate step, and finishing at high_rate. 11 | low_rate = 100 12 | high_rate = 500 13 | rate_step = 50 14 | 15 | 16 | # httperf options 17 | 18 | # wlog specifies a replay log file (null terminated requests paths) 19 | # 'n' prefix tells httperf to stop after all requests in the file 20 | # have been replayed 21 | httperf_wlog = n,replay_log 22 | 23 | # Autoperf can generate different wlog instructions for every run if 24 | # you set wlog (not httperf_wlog) to a glob pattern of the files you 25 | # want to use. 26 | # wlog = x?.nul 27 | 28 | # num-conns is the total number of connections to make during a test 29 | # num-calls is the number of requests per connection (if keep alive is supported) 30 | # The product of num_call and rate is the the approximate number of 31 | # requests per second that will be attempted. 32 | httperf_num-conns = 50 33 | httperf_num-calls = 1 34 | 35 | # timeout sets the maximimum time (in seconds) that httperf will wait 36 | # for replies from the web server. If the timeout is exceeded, the 37 | # reply concerned is counted as an error. 38 | httperf_timeout = 5 39 | 40 | # add-header adds an HTTP header 41 | # If your test server is using HTTP basic auth, add a header like the following. 42 | # To figure out what it should be use "curl -u user:password -v ..." 43 | # httperf_add-header = '"Authorization: Basic AbC123xYz456==\n"' 44 | -------------------------------------------------------------------------------- /requests_sample.conf: -------------------------------------------------------------------------------- 1 | # Autoperf Configuration File 2 | 3 | # The host, URI (relative to the document root) and port to test. 4 | host = localhost 5 | uri = /homepage 6 | port = 80 7 | 8 | # The 'rate' is the number of number of connections to open per second. 9 | # A series of tests will be conducted, starting at low rate, 10 | # increasing by rate step, and finishing at high_rate. 11 | low_rate = 100 12 | high_rate = 500 13 | rate_step = 50 14 | 15 | 16 | # httperf options 17 | 18 | # wlog specifies a replay log file (null terminated requests paths) 19 | # 'n' prefix tells httperf to stop after all requests in the file 20 | # have been replayed 21 | # httperf_wlog = n,requests_httperf 22 | 23 | # Autoperf can generate different wlog instructions for every run if 24 | # you set wlog (not httperf_wlog) to a glob pattern of the files you 25 | # want to use. 26 | # wlog = x?.nul 27 | 28 | # num-conns is the total number of connections to make during a test 29 | # num-calls is the number of requests per connection (if keep alive is supported) 30 | # The product of num_call and rate is the the approximate number of 31 | # requests per second that will be attempted. 32 | httperf_num-conns = 50 33 | httperf_num-calls = 1 34 | 35 | # timeout sets the maximimum time (in seconds) that httperf will wait 36 | # for replies from the web server. If the timeout is exceeded, the 37 | # reply concerned is counted as an error. 38 | httperf_timeout = 5 39 | 40 | # add-header adds an HTTP header 41 | # If your test server is using HTTP basic auth, add a header like the following. 42 | # To figure out what it should be use "curl -u user:password -v ..." 43 | # httperf_add-header = '"Authorization: Basic AbC123xYz456==\n"' --------------------------------------------------------------------------------