Created
March 11, 2017 10:50
-
-
Save fchauvel/e8059c21686d73b18180ab4d62cc5b07 to your computer and use it in GitHub Desktop.
A minimal model of emergent auto scaling
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
from copy import deepcopy | |
import matplotlib.pyplot as plt | |
class ServerFactory: | |
def __init__(self, service_time, budget, living_cost, margin, reproduction_cost): | |
self._service_time = service_time | |
self._budget = budget | |
self._living_cost = living_cost | |
self._margin = margin | |
self._reproduction_cost = reproduction_cost | |
def one_server(self, simulation, living_cost): | |
return Server(simulation, | |
self._service_time, | |
0, | |
living_cost, | |
self._margin, | |
self._reproduction_cost) | |
class Server: | |
def __init__(self, simulation, service_time, budget, living_cost, margin, reproduction_cost): | |
self._simulation = simulation | |
self._money = budget | |
self._service_time = service_time | |
self._living_cost = living_cost | |
self._margin = margin | |
self._reproduction_cost = reproduction_cost | |
self._arrival_rate = 0 | |
self._queue = 0 | |
def send(self, arrival_rate): | |
self._arrival_rate = arrival_rate | |
def update(self): | |
processed = self._processed_requests() | |
self._money += processed * self._margin - self._living_cost | |
self._queue = max(0, self._queue + self._arrival_rate - processed) | |
if self._can_reproduce(): | |
self._money -= self._reproduction_cost | |
self._simulation.birth(self._living_cost) | |
if self._is_dead(): | |
self._simulation.death_of(self) | |
def _processed_requests(self): | |
return min(self._arrival_rate + self._queue, self._service_time) | |
@property | |
def response_time(self): | |
return (self._queue + 1) / self._service_time | |
def _can_reproduce(self): | |
return self._money > self._reproduction_cost | |
def _is_dead(self): | |
return self._money < 0 | |
class Observation: | |
def __init__(self): | |
self._server_counts = [] | |
self._flow_times = [] | |
def record(self, server_count, flow_time): | |
self._server_counts.append(server_count) | |
self._flow_times.append(flow_time) | |
class Simulation: | |
def __init__(self, factory, arrival_rate, duration, observer): | |
self._duration = duration | |
self._create = factory | |
self._arrival_rate = arrival_rate | |
self._observer = observer | |
self._servers = [self._create.one_server(self, factory._living_cost)] | |
self._new_servers = [] | |
self._dead_servers = [] | |
def birth(self, living_cost): | |
self._new_servers.append(self._create.one_server(self, living_cost)) | |
def death_of(self, server): | |
self._dead_servers.append(server) | |
def simulate(self): | |
for each_step in range(0, self._duration): | |
flow_time = self._split_workload() | |
self._process_requests() | |
self._servers = self._update_servers() | |
self._observer.record(len(self._servers), flow_time) | |
return self._observer | |
def _split_workload(self): | |
minimum = min(s.response_time for s in self._servers) | |
champions = [each for each in self._servers if | |
each.response_time == minimum] | |
share = self._arrival_rate / len(champions) | |
for each_server in self._servers: | |
if each_server in champions: | |
each_server.send(share) | |
else: | |
each_server.send(0) | |
return minimum | |
def _process_requests(self): | |
for each_server in self._servers: | |
each_server.update() | |
def _update_servers(self): | |
servers = [each for each in self._servers if | |
each not in self._dead_servers] | |
servers.extend(self._new_servers) | |
self._dead_servers = [] | |
self._new_servers = [] | |
return servers | |
class Settings: | |
def __init__(self, service_time, budget, arrival_rate, duration, living_cost, margin, reproduction_cost): | |
self._arrival_rate = arrival_rate | |
self._service_time = service_time | |
self._budget = budget | |
self._duration = duration | |
self._living_cost = living_cost | |
self._margin = margin | |
self._reproduction_cost = reproduction_cost | |
@property | |
def carrying_capacity(self): | |
return self._arrival_rate * (self._margin / self._living_cost) | |
@property | |
def optimal_server_count(self): | |
return self._arrival_rate / self._service_time | |
def set(self, key, value): | |
result = deepcopy(self) | |
field_name = "_" + key | |
assert getattr(result, field_name), "No field named " + field_name | |
setattr(result, field_name, value) | |
return result | |
def to_simulation(self): | |
factory = ServerFactory(service_time=self._service_time, | |
budget=self._budget, | |
living_cost=self._living_cost, | |
margin=self._margin, | |
reproduction_cost=self._reproduction_cost) | |
simulation = Simulation(factory=factory, | |
duration=self._duration, | |
arrival_rate=self._arrival_rate, | |
observer=Observation()) | |
return simulation | |
class View: | |
def __init__(self, settings): | |
self._template = settings | |
self._ylabel = "Unknown" | |
def show(self, results): | |
plt.ylabel(self._ylabel) | |
plt.xlabel("Simulated Time") | |
handles = [] | |
for cost in sorted(results.keys()): | |
line, = plt.plot(list(range(0, self._template._duration)), | |
self.get_data(cost, results), label=self.make_label( | |
cost)) | |
handles.append(line) | |
self.show_extras(plt) | |
plt.legend(handles=handles, loc=1, frameon=False) | |
plt.show() | |
def get_data(self, key, results): | |
pass | |
def make_label(self, cost): | |
return "%.2f" % cost | |
def show_extras(self, plt): | |
pass | |
class ViewServerCount(View): | |
def __init__(self, settings): | |
super().__init__(settings) | |
self._ylabel = "Server Count" | |
def get_data(self, key, results): | |
return results[key]._server_counts | |
def show_extras(self, plt): | |
self._show_carrying_capacity(plt) | |
self._show_minimum(plt) | |
def _show_minimum(self, plt): | |
optimum = self._template.optimal_server_count | |
plt.plot((0, self._template._duration), (optimum, optimum), 'k--') | |
def _show_carrying_capacity(self, plt): | |
capacity = self._template.carrying_capacity | |
plt.plot((0, self._template._duration), (capacity, capacity), 'k-.') | |
class ViewResponseTime(View): | |
def __init__(self, settings): | |
super().__init__(settings) | |
self._ylabel = "Response Time" | |
def get_data(self, key, results): | |
return results[key]._flow_times | |
def show_extras(self, plt): | |
plt.yscale('log') | |
class Sensitivity: | |
def __init__(self, settings, view, parameter, values): | |
self._template = settings | |
self._view = view | |
self._parameter = parameter | |
self._values = values | |
def run(self): | |
results = {} | |
for each_value in self._values: | |
settings = self._template.set(self._parameter, each_value) | |
simulation = settings.to_simulation() | |
results[each_value] = simulation.simulate() | |
self._view.show(results) | |
settings = Settings( | |
arrival_rate=50, | |
service_time=2, | |
budget=1, | |
duration=1000, | |
living_cost=2, | |
margin=1.2, | |
reproduction_cost=10 | |
) | |
print("Optimum: ", settings.optimal_server_count) | |
print("Capacity: ", settings.carrying_capacity) | |
response_time = ViewResponseTime(settings) | |
server_count = ViewServerCount(settings) | |
#experiment = Sensitivity(settings, server_count, "reproduction_cost", [5, 10, 20, 30]) | |
experiment = Sensitivity(settings, response_time, "living_cost", [1, 1.5, 1,.75, 2, 2.25]) | |
#experiment = Sensitivity(settings, response_time, "reproduction_cost", [5, 10, 20, 30]) | |
experiment.run() | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment