sisfc 0.1.0 → 0.2.0
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.
- checksums.yaml +5 -5
- data/.projections.json +12 -0
- data/.travis.yml +6 -0
- data/README.md +37 -19
- data/Rakefile +4 -2
- data/TODO +5 -0
- data/bin/sisfc +29 -6
- data/examples/generator.R +3 -1
- data/examples/simulator.conf +80 -29
- data/lib/sisfc.rb +4 -4
- data/lib/sisfc/configuration.rb +73 -6
- data/lib/sisfc/data_center.rb +42 -29
- data/lib/sisfc/evaluation.rb +23 -15
- data/lib/sisfc/event.rb +9 -6
- data/lib/sisfc/generator.rb +14 -21
- data/lib/sisfc/latency_manager.rb +65 -0
- data/lib/sisfc/logger.rb +28 -0
- data/lib/sisfc/request.rb +42 -85
- data/lib/sisfc/service_type.rb +2 -0
- data/lib/sisfc/simulation.rb +234 -47
- data/lib/sisfc/sorted_array.rb +2 -0
- data/lib/sisfc/statistics.rb +37 -3
- data/lib/sisfc/support/dsl_helper.rb +2 -0
- data/lib/sisfc/version.rb +3 -1
- data/lib/sisfc/vm.rb +46 -27
- data/sisfc.gemspec +9 -5
- data/spec/minitest_helper.rb +9 -0
- data/{test/sisfc/configuration_test.rb → spec/sisfc/configuration_spec.rb} +4 -2
- data/spec/sisfc/data_center_spec.rb +19 -0
- data/{test/sisfc/evaluation_test.rb → spec/sisfc/evaluation_spec.rb} +5 -3
- data/{test/sisfc/generator_test.rb → spec/sisfc/generator_spec.rb} +21 -18
- data/spec/sisfc/latency_manager_spec.rb +13 -0
- data/spec/sisfc/reference_configuration.rb +534 -0
- data/spec/sisfc/request_spec.rb +19 -0
- metadata +115 -49
- data/test/sisfc/reference_configuration.rb +0 -191
- data/test/sisfc/request_test.rb +0 -13
- data/test/test_helper.rb +0 -4
data/lib/sisfc/service_type.rb
CHANGED
data/lib/sisfc/simulation.rb
CHANGED
@@ -1,9 +1,12 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative './data_center'
|
4
|
+
require_relative './event'
|
5
|
+
require_relative './generator'
|
6
|
+
require_relative './sorted_array'
|
7
|
+
require_relative './statistics'
|
8
|
+
require_relative './vm'
|
9
|
+
require_relative './latency_manager'
|
7
10
|
|
8
11
|
|
9
12
|
module SISFC
|
@@ -15,6 +18,9 @@ module SISFC
|
|
15
18
|
def initialize(opts = {})
|
16
19
|
@configuration = opts[:configuration]
|
17
20
|
@evaluator = opts[:evaluator]
|
21
|
+
|
22
|
+
# create latency manager
|
23
|
+
@latency_manager = LatencyManager.new(@configuration.latency_models)
|
18
24
|
end
|
19
25
|
|
20
26
|
|
@@ -33,28 +39,63 @@ module SISFC
|
|
33
39
|
# setup simulation start and current time
|
34
40
|
@current_time = @start_time = @configuration.start_time
|
35
41
|
|
36
|
-
# create data centers
|
37
|
-
|
42
|
+
# create data centers and store them in a repository
|
43
|
+
data_center_repository = Hash[
|
44
|
+
@configuration.data_centers.map do |k,v|
|
45
|
+
[ k, DataCenter.new(id: k, **v) ]
|
46
|
+
end
|
47
|
+
]
|
48
|
+
|
49
|
+
customer_repository = @configuration.customers
|
50
|
+
workflow_type_repository = @configuration.workflow_types
|
38
51
|
|
39
52
|
# initialize statistics
|
40
|
-
stats
|
41
|
-
|
53
|
+
stats = Statistics.new
|
54
|
+
per_workflow_and_customer_stats = Hash[
|
55
|
+
workflow_type_repository.keys.map do |wft_id|
|
56
|
+
[
|
57
|
+
wft_id,
|
58
|
+
Hash[
|
59
|
+
customer_repository.keys.map do |c_id|
|
60
|
+
[ c_id, Statistics.new(@configuration.custom_stats.find{|x| x[:customer_id] == c_id && x[:workflow_type_id] == wft_id } || {}) ]
|
61
|
+
end
|
62
|
+
]
|
63
|
+
]
|
64
|
+
end
|
65
|
+
]
|
66
|
+
reqs_received_per_workflow_and_customer = Hash[
|
67
|
+
workflow_type_repository.keys.map do |wft_id|
|
68
|
+
[ wft_id, Hash[customer_repository.keys.map {|c_id| [ c_id, 0 ]}] ]
|
69
|
+
end
|
70
|
+
]
|
42
71
|
|
43
72
|
# create VMs
|
44
73
|
@vms = []
|
45
74
|
vmid = 0
|
46
75
|
vm_allocation.each do |opts|
|
47
76
|
# setup service_time_distribution
|
48
|
-
|
77
|
+
stdist = @configuration.service_component_types[opts[:component_type]][:service_time_distribution]
|
49
78
|
|
50
79
|
# allocate the VMs
|
51
80
|
opts[:vm_num].times do
|
52
81
|
# create VM ...
|
53
|
-
|
82
|
+
# if [ "Financial Transaction Server", "RDBMS C", "Queue Manager" ].include? opts[:component_type]
|
83
|
+
# vm = VM.new(vmid, opts[:dc_id], opts[:vm_size], stdist, trace: true, notes: "ct #{opts[:component_type]}")
|
84
|
+
# else
|
85
|
+
# vm = VM.new(vmid, opts[:dc_id], opts[:vm_size], stdist)
|
86
|
+
# end
|
87
|
+
vm = VM.new(vmid, opts[:dc_id], opts[:vm_size], stdist)
|
54
88
|
# ... add it to the vm list ...
|
55
89
|
@vms << vm
|
56
90
|
# ... and register it in the corresponding data center
|
57
|
-
|
91
|
+
unless data_center_repository[opts[:dc_id]].add_vm(vm, opts[:component_type])
|
92
|
+
$stderr.puts "====== Unfeasible allocation at data center #{dc_id} ======"
|
93
|
+
$stderr.flush
|
94
|
+
# here we return Float::MAX instead of, e.g., Float::INFINITY,
|
95
|
+
# because the latter would break optimization tools. instead, we
|
96
|
+
# want to have a very high but comparable value.
|
97
|
+
return Float::MAX
|
98
|
+
end
|
58
99
|
# update vm id
|
59
100
|
vmid += 1
|
60
101
|
end
|
@@ -67,8 +108,8 @@ module SISFC
|
|
67
108
|
|
68
109
|
# generate first request
|
69
110
|
rg = RequestGenerator.new(@configuration.request_generation)
|
70
|
-
|
71
|
-
new_event(Event::
|
111
|
+
req_attrs = rg.generate
|
112
|
+
new_event(Event::ET_REQUEST_GENERATION, req_attrs, req_attrs[:generation_time], nil)
|
72
113
|
|
73
114
|
# schedule end of simulation
|
74
115
|
unless @configuration.end_time.nil?
|
@@ -80,54 +121,115 @@ module SISFC
|
|
80
121
|
warmup_threshold = @configuration.start_time + @configuration.warmup_duration.to_i
|
81
122
|
|
82
123
|
requests_being_worked_on = 0
|
83
|
-
|
124
|
+
requests_forwarded_to_other_dcs = 0
|
125
|
+
current_event = 0
|
84
126
|
|
85
127
|
# launch simulation
|
86
128
|
until @event_queue.empty?
|
87
129
|
e = @event_queue.shift
|
88
130
|
|
89
|
-
|
131
|
+
current_event += 1
|
90
132
|
# sanity check on simulation time flow
|
91
133
|
if @current_time > e.time
|
92
|
-
raise "Error: simulation time inconsistency for event #{
|
134
|
+
raise "Error: simulation time inconsistency for event #{current_event} " +
|
93
135
|
"e.type=#{e.type} @current_time=#{@current_time}, e.time=#{e.time}"
|
94
136
|
end
|
95
137
|
|
96
138
|
@current_time = e.time
|
97
139
|
|
98
140
|
case e.type
|
141
|
+
when Event::ET_REQUEST_GENERATION
|
142
|
+
req_attrs = e.data
|
143
|
+
|
144
|
+
# find closest data center
|
145
|
+
customer_location_id = customer_repository.dig(req_attrs[:customer_id], :location_id)
|
146
|
+
dc_at_customer_location = data_center_repository.values.find {|dc| dc.location_id == customer_location_id }
|
147
|
+
|
148
|
+
raise "No data center found at location id #{customer_location_id}!" unless dc_at_customer_location
|
149
|
+
|
150
|
+
# find first component name for requested workflow
|
151
|
+
workflow = workflow_type_repository[req_attrs[:workflow_type_id]]
|
152
|
+
first_component_name = workflow[:component_sequence][0][:name]
|
153
|
+
|
154
|
+
closest_dc = if dc_at_customer_location.has_vms_of_type?(first_component_name)
|
155
|
+
dc_at_customer_location
|
156
|
+
else
|
157
|
+
data_center_repository.values.select{|dc| dc.has_vms_of_type?(first_component_name) }&.sample
|
158
|
+
end
|
159
|
+
|
160
|
+
raise "Invalid configuration! No VMs of type #{first_component_name} found!" unless closest_dc
|
161
|
+
|
162
|
+
arrival_time = @current_time + @latency_manager.sample_latency_between(customer_location_id, closest_dc.location_id)
|
163
|
+
new_req = Request.new(req_attrs.merge!(initial_data_center_id: closest_dc.dcid,
|
164
|
+
arrival_time: arrival_time))
|
165
|
+
|
166
|
+
# schedule arrival of current request
|
167
|
+
new_event(Event::ET_REQUEST_ARRIVAL, new_req, arrival_time, nil)
|
168
|
+
|
169
|
+
# schedule generation of next request
|
170
|
+
req_attrs = rg.generate
|
171
|
+
new_event(Event::ET_REQUEST_GENERATION, req_attrs, req_attrs[:generation_time], nil)
|
172
|
+
|
99
173
|
when Event::ET_REQUEST_ARRIVAL
|
100
174
|
# get request
|
101
175
|
req = e.data
|
102
176
|
|
103
177
|
# find data center
|
104
|
-
data_center =
|
178
|
+
data_center = data_center_repository[req.data_center_id]
|
179
|
+
|
180
|
+
# update reqs_received_per_workflow_and_customer
|
181
|
+
reqs_received_per_workflow_and_customer[req.workflow_type_id][req.customer_id] += 1
|
105
182
|
|
106
183
|
# find next component name
|
107
|
-
workflow =
|
184
|
+
workflow = workflow_type_repository[req.workflow_type_id]
|
108
185
|
next_component_name = workflow[:component_sequence][req.next_step][:name]
|
186
|
+
# if [5,6,9].include? req.workflow_type_id
|
187
|
+
# $stderr.puts "received request for wf #{req.workflow_type_id} at dc #{req.data_center_id}"
|
188
|
+
# $stderr.puts "next component name is #{next_component_name}"
|
189
|
+
# $stderr.flush
|
190
|
+
# end
|
109
191
|
|
110
192
|
# get random vm providing next service component type
|
111
193
|
vm = data_center.get_random_vm(next_component_name)
|
194
|
+
# if [5,6,9].include? req.workflow_type_id
|
195
|
+
# $stderr.puts "next vm is #{vm.__id__}"
|
196
|
+
# $stderr.flush
|
197
|
+
# end
|
112
198
|
|
113
|
-
#
|
114
|
-
|
199
|
+
# schedule request forwarding to vm
|
200
|
+
new_event(Event::ET_REQUEST_FORWARDING, req, e.time, vm)
|
115
201
|
|
116
202
|
# update stats
|
117
203
|
if req.arrival_time > warmup_threshold
|
118
204
|
# increase the number of requests being worked on
|
119
205
|
requests_being_worked_on += 1
|
120
|
-
end
|
121
206
|
|
122
|
-
|
123
|
-
|
124
|
-
|
207
|
+
# increase count of received requests
|
208
|
+
stats.request_received
|
209
|
+
|
210
|
+
# increase count of received requests in per_workflow_and_customer_stats
|
211
|
+
per_workflow_and_customer_stats[req.workflow_type_id][req.customer_id].request_received
|
212
|
+
|
213
|
+
# if stats.received % 10_000 == 0
|
214
|
+
# $stderr.puts "#{Thread.current.__id__} sisfc: Received #{stats.received} requests."
|
215
|
+
# $stderr.puts "#{Thread.current.__id__} sisfc: Working on #{requests_being_worked_on} requests."
|
216
|
+
# $stderr.flush
|
217
|
+
# end
|
218
|
+
end
|
125
219
|
|
126
220
|
|
127
221
|
# Leave these events for when we add VM migration support
|
128
222
|
# when Event::ET_VM_SUSPEND
|
129
223
|
# when Event::ET_VM_RESUME
|
130
224
|
|
225
|
+
when Event::ET_REQUEST_FORWARDING
|
226
|
+
# get request
|
227
|
+
req = e.data
|
228
|
+
time = e.time
|
229
|
+
vm = e.destination
|
230
|
+
|
231
|
+
vm.new_request(self, req, time)
|
232
|
+
|
131
233
|
|
132
234
|
when Event::ET_WORKFLOW_STEP_COMPLETED
|
133
235
|
# retrieve request and vm
|
@@ -138,22 +240,103 @@ module SISFC
|
|
138
240
|
vm.request_finished(self, e.time)
|
139
241
|
|
140
242
|
# find data center and workflow
|
141
|
-
data_center =
|
142
|
-
workflow =
|
243
|
+
data_center = data_center_repository[req.data_center_id]
|
244
|
+
workflow = workflow_type_repository[req.workflow_type_id]
|
143
245
|
|
144
246
|
# check if there are other steps left to complete the workflow
|
145
247
|
if req.next_step < workflow[:component_sequence].size
|
146
248
|
# find next component name
|
147
249
|
next_component_name = workflow[:component_sequence][req.next_step][:name]
|
148
250
|
|
149
|
-
# get random
|
251
|
+
# get random VM providing next service component type
|
150
252
|
new_vm = data_center.get_random_vm(next_component_name)
|
151
253
|
|
152
|
-
#
|
153
|
-
|
254
|
+
# this is the request's time of arrival at the new VM
|
255
|
+
forwarding_time = e.time
|
256
|
+
|
257
|
+
# there might not be a VM of the type we need in the current data
|
258
|
+
# center, so look in the other data centers
|
259
|
+
unless new_vm
|
260
|
+
# get list of other data centers, randomly picked
|
261
|
+
other_dcs = data_center_repository.values.select{|x| x != data_center && x.has_vms_of_type?(next_component_name) }&.shuffle
|
262
|
+
other_dcs.each do |dc|
|
263
|
+
new_vm = dc.get_random_vm(next_component_name)
|
264
|
+
if new_vm
|
265
|
+
# need to update data_center_id of request
|
266
|
+
req.data_center_id = dc.dcid
|
267
|
+
|
268
|
+
# keep track of transmission time
|
269
|
+
transmission_time =
|
270
|
+
@latency_manager.sample_latency_between(data_center.location_id,
|
271
|
+
dc.location_id)
|
272
|
+
|
273
|
+
unless transmission_time >= 0.0
|
274
|
+
raise "Negative transmission time (#{transmission_time})!"
|
275
|
+
end
|
276
|
+
|
277
|
+
|
278
|
+
# if [5,6,9].include? req.workflow_type_id
|
279
|
+
# $stderr.puts "rerouting request for wf #{req.workflow_type_id} from dc #{data_center.dcid} to dc #{dc.dcid}"
|
280
|
+
# $stderr.puts "next component name is #{next_component_name}"
|
281
|
+
# $stderr.puts "transmission time is #{transmission_time}"
|
282
|
+
# $stderr.flush
|
283
|
+
# end
|
284
|
+
|
285
|
+
req.update_transfer_time(transmission_time)
|
286
|
+
forwarding_time += transmission_time
|
287
|
+
|
288
|
+
# update request's current data_center_id
|
289
|
+
req.data_center_id = dc.dcid
|
290
|
+
|
291
|
+
# keep track of number of requests forwarded to other data centers
|
292
|
+
requests_forwarded_to_other_dcs += 1
|
293
|
+
|
294
|
+
# we're done here
|
295
|
+
break
|
296
|
+
end
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
# make sure we actually found a VM
|
301
|
+
raise "Cannot find VM running a component of type " +
|
302
|
+
"#{next_component_name} in any data center!" unless new_vm
|
303
|
+
|
304
|
+
# if [5,6,9].include? req.workflow_type_id
|
305
|
+
# $stderr.puts "received request for wf #{req.workflow_type_id} at dc #{req.data_center_id}"
|
306
|
+
# $stderr.puts "next component name is #{next_component_name}"
|
307
|
+
# $stderr.puts "next vm is #{new_vm.__id__}"
|
308
|
+
# $stderr.flush
|
309
|
+
# end
|
310
|
+
|
311
|
+
# schedule request forwarding to vm
|
312
|
+
new_event(Event::ET_REQUEST_FORWARDING, req, forwarding_time, new_vm)
|
313
|
+
|
154
314
|
else # workflow is finished
|
315
|
+
# calculate transmission time
|
316
|
+
transmission_time =
|
317
|
+
@latency_manager.sample_latency_between(
|
318
|
+
# data center location
|
319
|
+
data_center_repository[req.data_center_id].location_id,
|
320
|
+
# customer location
|
321
|
+
customer_repository.dig(req.customer_id, :location_id)
|
322
|
+
)
|
323
|
+
|
324
|
+
# if [5,6,9].include? req.workflow_type_id
|
325
|
+
# $stderr.puts "closing request for wf #{req.workflow_type_id}"
|
326
|
+
# $stderr.puts "e.time is #{e.time}"
|
327
|
+
# $stderr.puts "transmission_time is #{transmission_time}"
|
328
|
+
# $stderr.flush
|
329
|
+
# end
|
330
|
+
|
331
|
+
unless transmission_time >= 0.0
|
332
|
+
raise "Negative transmission time (#{transmission_time})!"
|
333
|
+
end
|
334
|
+
|
335
|
+
# keep track of transmission time
|
336
|
+
req.update_transfer_time(transmission_time)
|
337
|
+
|
155
338
|
# schedule request closure
|
156
|
-
new_event(Event::ET_REQUEST_CLOSURE, req, e.time +
|
339
|
+
new_event(Event::ET_REQUEST_CLOSURE, req, e.time + transmission_time, nil)
|
157
340
|
end
|
158
341
|
|
159
342
|
|
@@ -171,7 +354,9 @@ module SISFC
|
|
171
354
|
|
172
355
|
# collect request statistics
|
173
356
|
stats.record_request(req)
|
174
|
-
|
357
|
+
|
358
|
+
# collect request statistics in per_workflow_and_customer_stats
|
359
|
+
per_workflow_and_customer_stats[req.workflow_type_id][req.customer_id].record_request(req)
|
175
360
|
end
|
176
361
|
|
177
362
|
|
@@ -184,22 +369,24 @@ module SISFC
|
|
184
369
|
|
185
370
|
# puts "========== Simulation Finished =========="
|
186
371
|
|
187
|
-
|
188
|
-
|
189
|
-
:served_requests => stats.n,
|
190
|
-
:queued_requests => requests_being_worked_on }
|
191
|
-
dc_kpis = dc_stats.map do |s|
|
192
|
-
{ :mttr => s.mean,
|
193
|
-
:served_requests => s.n, }
|
194
|
-
end
|
195
|
-
fitness = @evaluator.evaluate_business_impact(kpis, dc_kpis, vm_allocation)
|
372
|
+
costs = @evaluator.evaluate_business_impact(stats, per_workflow_and_customer_stats,
|
373
|
+
vm_allocation)
|
196
374
|
puts "====== Evaluating new allocation ======\n" +
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
375
|
+
"costs: #{costs}\n" +
|
376
|
+
"vm_allocation: #{vm_allocation.inspect}\n" +
|
377
|
+
"stats: #{stats.to_s}\n" +
|
378
|
+
"per_workflow_and_customer_stats: #{per_workflow_and_customer_stats.to_s}\n" +
|
379
|
+
"=======================================\n"
|
380
|
+
|
381
|
+
# we want to minimize the cost, so we define fitness as the opposite of
|
382
|
+
# the sum of all costs incurred
|
383
|
+
fitness = - costs.values.inject(0.0){|s,x| s += x }
|
202
384
|
end
|
203
385
|
|
386
|
+
private
|
387
|
+
def communication_latency_between(loc1, loc2)
|
388
|
+
@latency_manager.sample_latency_between(loc1.to_i, loc2.to_i)
|
389
|
+
end
|
390
|
+
|
204
391
|
end
|
205
392
|
end
|
data/lib/sisfc/sorted_array.rb
CHANGED
data/lib/sisfc/statistics.rb
CHANGED
@@ -1,19 +1,35 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative './request'
|
4
|
+
|
2
5
|
|
3
6
|
module SISFC
|
4
7
|
class Statistics
|
5
|
-
attr_reader :mean, :n
|
8
|
+
attr_reader :mean, :n, :received, :longer_than
|
9
|
+
alias_method :closed, :n
|
6
10
|
|
7
11
|
# see http://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Online_algorithm
|
8
|
-
|
12
|
+
# and https://www.johndcook.com/blog/standard_deviation/
|
13
|
+
def initialize(opts={})
|
9
14
|
@n = 0 # number of requests
|
10
15
|
@mean = 0.0
|
11
16
|
@m_2 = 0.0
|
17
|
+
@longer_than = init_counters_for_longer_than_stats(opts)
|
18
|
+
@received = 0
|
19
|
+
end
|
20
|
+
|
21
|
+
def request_received
|
22
|
+
@received += 1
|
12
23
|
end
|
13
24
|
|
14
25
|
def record_request(req)
|
15
26
|
# get new sample
|
16
27
|
x = req.ttr
|
28
|
+
raise "TTR #{x} for request #{req.rid} invalid!" unless x > 0.0
|
29
|
+
|
30
|
+
@longer_than.each_key do |k|
|
31
|
+
@longer_than[k] += 1 if x > k
|
32
|
+
end
|
17
33
|
|
18
34
|
# update counters
|
19
35
|
@n += 1
|
@@ -25,5 +41,23 @@ module SISFC
|
|
25
41
|
def variance
|
26
42
|
@m_2 / (@n - 1)
|
27
43
|
end
|
44
|
+
|
45
|
+
def to_s
|
46
|
+
"received: #{@received}, closed: #{@n}, " +
|
47
|
+
"(mean: #{@mean}, variance: #{variance}, longer_than: #{@longer_than.to_s})"
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
def init_counters_for_longer_than_stats(custom_kpis_config)
|
52
|
+
# prepare an infinite length enumerator that always returns zero
|
53
|
+
zeros = Enumerator.new(){|x| loop do x << 0 end }
|
54
|
+
|
55
|
+
Hash[
|
56
|
+
# wrap the values in custom_kpis_config[:longer_than] in an array
|
57
|
+
Array(custom_kpis_config[:longer_than]).
|
58
|
+
# and interval the numbers contained in that array with zeroes
|
59
|
+
zip(zeros) ]
|
60
|
+
end
|
61
|
+
|
28
62
|
end
|
29
63
|
end
|