Skip to content

Instantly share code, notes, and snippets.

@fchauvel
Created March 11, 2017 10:50
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save fchauvel/e8059c21686d73b18180ab4d62cc5b07 to your computer and use it in GitHub Desktop.
Save fchauvel/e8059c21686d73b18180ab4d62cc5b07 to your computer and use it in GitHub Desktop.
A minimal model of emergent auto scaling
#!/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