alicorn 0.0.4 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: