alicorn 0.0.4 → 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.4
1
+ 0.0.5
data/alicorn.gemspec CHANGED
@@ -5,14 +5,14 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = "alicorn"
8
- s.version = "0.0.4"
8
+ s.version = "0.0.5"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Ben Somers"]
12
- s.date = "2012-05-11"
12
+ s.date = "2012-05-15"
13
13
  s.description = "Highly configurable dumb auto-scaler for managing unicorn web servers"
14
14
  s.email = "somers.ben@gmail.com"
15
- s.executables = ["alicorn"]
15
+ s.executables = ["alicorn", "alicorn_profiler"]
16
16
  s.extra_rdoc_files = [
17
17
  "LICENSE.txt",
18
18
  "README.rdoc"
@@ -27,8 +27,10 @@ Gem::Specification.new do |s|
27
27
  "VERSION",
28
28
  "alicorn.gemspec",
29
29
  "bin/alicorn",
30
+ "bin/alicorn_profiler",
30
31
  "lib/alicorn.rb",
31
32
  "lib/alicorn/dataset.rb",
33
+ "lib/alicorn/log_parser.rb",
32
34
  "lib/alicorn/scaler.rb",
33
35
  "test/helper.rb",
34
36
  "test/test_alicorn.rb"
data/bin/alicorn CHANGED
@@ -6,48 +6,52 @@ require 'alicorn'
6
6
  options = {}
7
7
  OptionParser.new do |opts|
8
8
  opts.banner = "Alicorn is a tool for auto-scaling unicorn worker counts.
9
- It relies on unicorn's built-in Raindrops middleware (so make sure you have
10
- enabled it). It computes rough load scores based on separately averaged
11
- data sets. , \n\nUsage: alicorn [options]"
9
+ It relies on unicorn's built-in Raindrops middleware (so make sure you have
10
+ enabled it). It computes rough load scores based on separately averaged
11
+ data sets. \n\nUsage: alicorn [options]"
12
12
  opts.on( '-h', '--help', 'Display this screen' ) do
13
13
  puts opts
14
14
  exit
15
15
  end
16
16
 
17
- opts.on("--min-workers N", Integer, "the minimum number of workers to scale down to") do |v|
17
+ opts.on("--min-workers [N]", Integer, "the minimum number of workers to scale down to") do |v|
18
18
  options[:min_workers] = v
19
19
  end
20
20
 
21
- opts.on("--max-workers N", Integer, "the maximum number of workers to scale up to") do |v|
21
+ opts.on("--max-workers [N]", Integer, "the maximum number of workers to scale up to") do |v|
22
22
  options[:max_workers] = v
23
23
  end
24
24
 
25
- opts.on("--upward-step-size N", Integer, "the number of workers to add when scaling up") do |v|
26
- options[:upward_step_size] = v
25
+ opts.on("--buffer [N]", Integer, "the number of extra workers to keep for safety's sake") do |v|
26
+ options[:buffer] = v
27
27
  end
28
28
 
29
- opts.on("--downward-step-size N", Integer, "the number of workers to remove when scaling down") do |v|
30
- options[:downward_step_size] = v
29
+ opts.on("--target-ratio [F]", Float, "the desired ratio of workers to busy workers") do |v|
30
+ options[:target_ratio] = v
31
31
  end
32
32
 
33
- opts.on("--upward-threshold (0-1)", Float, "the proportion of busy workers to trigger an upscale") do |v|
34
- options[:upward_threshold] = v
33
+ opts.on("--url [URL]", String, "raindrops URL to check, defaults to 127.0.0.1/_raindrops") do |v|
34
+ options[:url] = v
35
35
  end
36
36
 
37
- opts.on("--downward-threshold (0-1)", Float, "the proportion of busy workers to trigger a downscale") do |v|
38
- options[:downward_threshold] = v
37
+ opts.on("--sample-count [N]", Integer, "number of data points to check before scaling") do |v|
38
+ options[:sample_count] = v
39
39
  end
40
40
 
41
- opts.on("--url S", String, "raindrops URL to check, defaults to 127.0.0.1/_raindrops") do |v|
42
- options[:url] = v
41
+ opts.on("--delay [F]", Float, "delay between checks") do |v|
42
+ options[:delay] = v
43
43
  end
44
44
 
45
- opts.on("--sample-count N", Integer, "number of data points to check before scaling") do |v|
46
- options[:sample_count] = v
45
+ opts.on("--log-path [PATH]", String, "path to write log file - leave blank for no logging") do |v|
46
+ options[:log_path] = v
47
47
  end
48
48
 
49
- opts.on("--delay (seconds)", Float, "delay between checks") do |v|
50
- options[:delay] = v
49
+ opts.on("-v", "--[no-]verbose", "turn on to write profiling info") do
50
+ options[:verbose] = true
51
+ end
52
+
53
+ opts.on("-d", "--[no-]dry-run", "turn on to disable actual scaling") do
54
+ options[:dry_run] = true
51
55
  end
52
56
 
53
57
  end.parse!
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env ruby
2
+ $:.push File.join(File.dirname(__FILE__),'..','lib')
3
+
4
+ require 'optparse'
5
+ require 'alicorn'
6
+ require 'alicorn/log_parser'
7
+
8
+
9
+ options = {}
10
+ OptionParser.new do |opts|
11
+ opts.banner = "Alicorn Profiler is a profiling tool to use for determining
12
+ optimal settings to run alicorn at. First, run the scaler in dry-run mode
13
+ to collect statistics about your application load. Then pull that log file
14
+ and feed it into this profiler. Experiment with the options until you find
15
+ settings that work well for you. Be aware that occasional 'Overloaded!'
16
+ situations are not necessarily bad; they just result in requests getting
17
+ queued. \n\nUsage: alicorn [options]"
18
+ opts.on( '-h', '--help', 'Display this screen' ) do
19
+ puts opts
20
+ exit
21
+ end
22
+
23
+ opts.on("--min-workers N", Integer, "the minimum number of workers to scale down to") do |v|
24
+ options[:min_workers] = v
25
+ end
26
+
27
+ opts.on("--max-workers N", Integer, "the maximum number of workers to scale up to") do |v|
28
+ options[:max_workers] = v
29
+ end
30
+
31
+ opts.on("--buffer N", Integer, "the number of extra workers to keep for safety's sake") do |v|
32
+ options[:buffer] = v
33
+ end
34
+
35
+ opts.on("--target-ratio F", Float, "the desired ratio of workers to busy workers") do |v|
36
+ options[:target_ratio] = v
37
+ end
38
+
39
+ opts.on("--log-path PATH", String, "location of the alicorn log file to read") do |v|
40
+ options[:log_path] = v
41
+ end
42
+ end.parse!
43
+
44
+ # Use these two variables to control the profiler
45
+ # The log file is the output of the sampling-mode alicorn
46
+ # The options are a stripped-down option set, including only
47
+ # the options that matter to the scaling algorithm.
48
+ @log_file = options[:log_path]
49
+ @options = { :min_workers => options[:min_workers],
50
+ :max_workers => options[:max_workers],
51
+ :target_ratio => options[:target_ration],
52
+ :buffer => options[:buffer],
53
+ :debug => true,
54
+ :dry_run => true}
55
+
56
+ @alp = Alicorn::LogParser.new(@log_file)
57
+ @alp.parse
58
+
59
+ scaler = Alicorn::Scaler.new(@options)
60
+ @worker_count = @options[:max_workers]
61
+
62
+ p "Testing #{@alp.samples.count} samples"
63
+ @alp.samples.each do |sample|
64
+ if sample[:active].max > @worker_count
65
+ p "Overloaded! Ran #{@worker_count} and got #{sample[:active].max} active"
66
+ end
67
+ sig = scaler.send(:auto_scale, sample, @worker_count)
68
+ @worker_count += 1 if sig == "TTIN"
69
+ @worker_count -= 1 if sig == "TTOU"
70
+ end
@@ -1,14 +1,17 @@
1
1
  class DataSet < Array
2
2
  def avg
3
+ return nil if empty?
3
4
  inject(:+) / size.to_i
4
5
  end
5
6
 
6
7
  def variance
8
+ return nil if empty?
7
9
  sum=self.inject(0){|acc,i|acc +(i-avg)**2}
8
10
  return(1/self.length.to_f*sum)
9
11
  end
10
12
 
11
13
  def stddev
14
+ return nil if empty?
12
15
  Math.sqrt(self.variance)
13
16
  end
14
17
  end
@@ -0,0 +1,36 @@
1
+ module Alicorn
2
+ class LogParser
3
+ attr_accessor :filename, :samples, :calling, :active, :queued, :calling_avg, :active_avg, :queued_avg
4
+
5
+ def initialize(file = "alicorn.log")
6
+ self.filename = file
7
+ self.samples, self.calling, self.active, self.queued, self.calling_avg, self.active_avg, self.queued_avg = [], [], [], [], [], [], []
8
+ end
9
+
10
+ def parse
11
+ f = File.open(filename)
12
+ f.each do |line|
13
+ if line.match(/^"Sampling/)
14
+ @sample_hash = {} # this will reset every sample
15
+ elsif line.match(/^"calling:\[(.+)\]"/)
16
+ data = $1.split(", ").map(&:to_i)
17
+ calling << data
18
+ @sample_hash[:calling] = data
19
+ elsif line.match(/^"calling avg:([\d]+)"/)
20
+ data = $1.to_i
21
+ calling_avg << data
22
+ @sample_hash[:calling_avg] = data
23
+ elsif line.match(/^"active:\[(.+)\]"/)
24
+ data = $1.split(", ").map(&:to_i)
25
+ active << data
26
+ @sample_hash[:active] = data
27
+ elsif line.match(/^"active avg:([\d]+)"/)
28
+ data = $1.to_i
29
+ active_avg << data
30
+ @sample_hash[:active_avg] = data
31
+ samples << @sample_hash
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -1,86 +1,103 @@
1
1
  require 'curl'
2
+ require 'logger'
2
3
  require 'alicorn/dataset'
3
4
 
4
- class Alicorn::Scaler
5
- attr_accessor :min_workers, :max_workers, :upward_step_size,
6
- :downward_step_size, :upward_threshold, :downward_threshold, :raindrops_url,
7
- :delay, :sample_count, :app_name, :master_pid, :worker_count, :calling,
8
- :writing, :active, :queued
5
+ module Alicorn
6
+ class Scaler
7
+ attr_accessor :min_workers, :max_workers, :target_ratio, :buffer,
8
+ :raindrops_url, :delay, :sample_count, :app_name, :dry_run,
9
+ :master_pid, :worker_count, :logger
9
10
 
10
- def initialize(options)
11
- self.min_workers = options[:min_workers] || 1
12
- self.max_workers = options[:max_workers]
13
- self.upward_threshold = options[:upward_threshold]
14
- self.downward_threshold = options[:downward_threshold]
15
- self.upward_step_size = options[:upward_step_size] || 1
16
- self.downward_step_size = options[:downward_step_size] || 1
17
- self.raindrops_url = options[:url] || "http://127.0.0.1/_raindrops"
18
- self.delay = options[:delay] || 1
19
- self.sample_count = options[:sample_count] || 30
20
- self.app_name = options[:app_name] || "unicorn"
21
- self.master_pid = find_master_pid(app_name)
22
- self.worker_count = find_worker_count(app_name)
23
- self.calling = DataSet.new
24
- self.writing = DataSet.new
25
- self.active = DataSet.new
26
- self.queued = DataSet.new
27
- end
11
+ def initialize(options)
12
+ self.min_workers = options[:min_workers] || 1
13
+ self.max_workers = options[:max_workers]
14
+ self.target_ratio = options[:target_ratio] || 1.3
15
+ self.buffer = options[:buffer] || 0
16
+ self.raindrops_url = options[:url] || "http://127.0.0.1/_raindrops"
17
+ self.delay = options[:delay] || 1
18
+ self.sample_count = options[:sample_count] || 30
19
+ self.app_name = options[:app_name] || "unicorn"
20
+ self.dry_run = options[:dry_run]
21
+ log_path = options[:log_path] || "/dev/null"
28
22
 
29
- def scale!
30
- collect_data
31
- upscale or downscale
32
- end
23
+ self.logger = Logger.new(log_path)
24
+ logger.level = options[:verbose] ? Logger::DEBUG : Logger::WARN
25
+ end
33
26
 
34
- protected
27
+ def scale!
28
+ master_pid = find_master_pid
29
+ worker_count = find_worker_count
30
+ data = collect_data
35
31
 
36
- # planned algorithm: maintain worker count at 1.3*active
37
- def upscale
38
- # return false if worker_count >= max_workers
39
- # return false if calling.all? { |sample| sample == 0 }
40
- end
32
+ sig = auto_scale(data, worker_count)
33
+ send_signal(sig) if sig
34
+ end
41
35
 
42
- def downscale
43
- # return false if worker_count <= min_workers
44
- end
36
+ protected
45
37
 
46
- private
38
+ def auto_scale(data, worker_count)
39
+ # Calculate target
40
+ target = data[:active].max * target_ratio + buffer
47
41
 
48
- def find_master_pid(app_name)
49
- end
42
+ # Check hard thresholds
43
+ target = max_workers if max_workers and target > max_workers
44
+ target = min_workers if target < min_workers
45
+ target = target.ceil
50
46
 
51
- def find_worker_count(app_name)
52
- end
47
+ logger.debug "target calculated at: #{target}, worker count at #{worker_count}"
48
+ if target > worker_count
49
+ logger.debug "scaling up!" unless dry_run
50
+ return "TTIN"
51
+ elsif target < worker_count
52
+ logger.debug "scaling down!" unless dry_run
53
+ return "TTOU"
54
+ end
55
+ end
53
56
 
54
- def collect_data
55
- p "Sampling #{sample_count} times"
56
- sample_count.times do
57
- raindrops = get_raindrops(raindrops_url)
58
- calling << $1.to_i if raindrops.detect { |line| line.match(/calling: ([0-9]+)/) }
59
- writing << $1.to_i if raindrops.detect { |line| line.match(/writing: ([0-9]+)/) }
60
- active << $1.to_i if raindrops.detect { |line| line.match(/active: ([0-9]+)/) }
61
- queued << $1.to_i if raindrops.detect { |line| line.match(/queued: ([0-9]+)/) }
62
- sleep(delay)
57
+ def find_master_pid
63
58
  end
64
- p "Collected:"
65
- p "calling:#{calling}"
66
- p "calling avg:#{calling.avg}"
67
- p "calling stddev:#{calling.stddev}"
68
- p "writing:#{writing}"
69
- p "writing avg:#{writing.avg}"
70
- p "writing stddev:#{writing.stddev}"
71
- p "active:#{active}"
72
- p "active avg:#{active.avg}"
73
- p "active stddev:#{active.stddev}"
74
- p "queued:#{queued}"
75
- p "queued avg:#{queued.avg}"
76
- p "queued stddev:#{queued.stddev}"
77
- end
78
59
 
79
- def get_raindrops(url)
80
- Curl::Easy.http_get(url).body_str.split("\n")
81
- end
82
-
83
- def send_signal(sig)
84
- Process.kill(sig, master_pid)
60
+ def find_worker_count
61
+ 14
62
+ end
63
+
64
+ private
65
+
66
+ def collect_data
67
+ logger.debug "Sampling #{sample_count} times at #{Time.now}"
68
+ calling, writing, active, queued = DataSet.new, DataSet.new, DataSet.new, DataSet.new
69
+ sample_count.times do
70
+ raindrops = get_raindrops(raindrops_url)
71
+ calling << $1.to_i if raindrops.detect { |line| line.match(/calling: ([0-9]+)/) }
72
+ writing << $1.to_i if raindrops.detect { |line| line.match(/writing: ([0-9]+)/) }
73
+ active << $1.to_i if raindrops.detect { |line| line.match(/active: ([0-9]+)/) }
74
+ queued << $1.to_i if raindrops.detect { |line| line.match(/queued: ([0-9]+)/) }
75
+ sleep(delay)
76
+ end
77
+ logger.debug "Collected:"
78
+ logger.debug "calling:#{calling}"
79
+ logger.debug "calling avg:#{calling.avg}"
80
+ logger.debug "calling stddev:#{calling.stddev}"
81
+ logger.debug "writing:#{writing}"
82
+ logger.debug "writing avg:#{writing.avg}"
83
+ logger.debug "writing stddev:#{writing.stddev}"
84
+ logger.debug "active:#{active}"
85
+ logger.debug "active avg:#{active.avg}"
86
+ logger.debug "active stddev:#{active.stddev}"
87
+ logger.debug "queued:#{queued}"
88
+ logger.debug "queued avg:#{queued.avg}"
89
+ logger.debug "queued stddev:#{queued.stddev}"
90
+ {:calling => calling, :writing => writing, :active => active, :queued => queued}
91
+ end
92
+
93
+ def get_raindrops(url)
94
+ Curl::Easy.http_get(url).body_str.split("\n")
95
+ end
96
+
97
+ def send_signal(sig)
98
+ return false if dry_run
99
+ return sig
100
+ # Process.kill(sig, master_pid)
101
+ end
85
102
  end
86
103
  end
data/test/helper.rb CHANGED
@@ -12,7 +12,8 @@ require 'shoulda'
12
12
 
13
13
  $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
14
14
  $LOAD_PATH.unshift(File.dirname(__FILE__))
15
- require 'unicorn_rider'
15
+ require 'alicorn'
16
+ require 'script/al'
16
17
 
17
18
  class Test::Unit::TestCase
18
19
  end
data/test/test_alicorn.rb CHANGED
@@ -1,7 +1,9 @@
1
1
  require 'helper'
2
2
 
3
- class TestUnicornRider < Test::Unit::TestCase
3
+ class TestScaler < Test::Unit::TestCase
4
+
4
5
  should "probably rename this file and start testing for real" do
5
6
  flunk "hey buddy, you should probably rename this file and start testing for real"
6
7
  end
8
+
7
9
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: alicorn
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.4
4
+ version: 0.0.5
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-05-11 00:00:00.000000000 Z
12
+ date: 2012-05-15 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: curb
16
- requirement: &70142969894040 !ruby/object:Gem::Requirement
16
+ requirement: &70095244082940 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ~>
@@ -21,10 +21,10 @@ dependencies:
21
21
  version: 0.8.0
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *70142969894040
24
+ version_requirements: *70095244082940
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: shoulda
27
- requirement: &70142969893260 !ruby/object:Gem::Requirement
27
+ requirement: &70095244082440 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ~>
@@ -32,10 +32,10 @@ dependencies:
32
32
  version: 3.0.1
33
33
  type: :development
34
34
  prerelease: false
35
- version_requirements: *70142969893260
35
+ version_requirements: *70095244082440
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: rdoc
38
- requirement: &70142969892540 !ruby/object:Gem::Requirement
38
+ requirement: &70095244081720 !ruby/object:Gem::Requirement
39
39
  none: false
40
40
  requirements:
41
41
  - - ~>
@@ -43,10 +43,10 @@ dependencies:
43
43
  version: '3.12'
44
44
  type: :development
45
45
  prerelease: false
46
- version_requirements: *70142969892540
46
+ version_requirements: *70095244081720
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: bundler
49
- requirement: &70142969891780 !ruby/object:Gem::Requirement
49
+ requirement: &70095244081000 !ruby/object:Gem::Requirement
50
50
  none: false
51
51
  requirements:
52
52
  - - ~>
@@ -54,10 +54,10 @@ dependencies:
54
54
  version: 1.1.3
55
55
  type: :development
56
56
  prerelease: false
57
- version_requirements: *70142969891780
57
+ version_requirements: *70095244081000
58
58
  - !ruby/object:Gem::Dependency
59
59
  name: jeweler
60
- requirement: &70142969891080 !ruby/object:Gem::Requirement
60
+ requirement: &70095244080240 !ruby/object:Gem::Requirement
61
61
  none: false
62
62
  requirements:
63
63
  - - ~>
@@ -65,11 +65,12 @@ dependencies:
65
65
  version: 1.8.3
66
66
  type: :development
67
67
  prerelease: false
68
- version_requirements: *70142969891080
68
+ version_requirements: *70095244080240
69
69
  description: Highly configurable dumb auto-scaler for managing unicorn web servers
70
70
  email: somers.ben@gmail.com
71
71
  executables:
72
72
  - alicorn
73
+ - alicorn_profiler
73
74
  extensions: []
74
75
  extra_rdoc_files:
75
76
  - LICENSE.txt
@@ -84,8 +85,10 @@ files:
84
85
  - VERSION
85
86
  - alicorn.gemspec
86
87
  - bin/alicorn
88
+ - bin/alicorn_profiler
87
89
  - lib/alicorn.rb
88
90
  - lib/alicorn/dataset.rb
91
+ - lib/alicorn/log_parser.rb
89
92
  - lib/alicorn/scaler.rb
90
93
  - test/helper.rb
91
94
  - test/test_alicorn.rb
@@ -104,7 +107,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
104
107
  version: '0'
105
108
  segments:
106
109
  - 0
107
- hash: 2272190929466153493
110
+ hash: -2775814352740060929
108
111
  required_rubygems_version: !ruby/object:Gem::Requirement
109
112
  none: false
110
113
  requirements: