#!/usr/bin/env python
from SimPy.Simulation import *
from itertools import cycle
import csv, random, sys
SERVERS = 2 # number of web servers
WORKERS_PER_SERVER = 25 # worker processes per web server
ACCEPT_QUEUE_LENGTH = 100 # maximum number of waiting requests
ARRIVAL_RATE_MIN = 20 # min arrival rate of web requests
ARRIVAL_RATE_MAX = 70 # max arrival rate of web requests
ARRIVAL_RATE_INC = 5 # arrival rate increment
CACHE_HIT_RATIO = 0.75 # fraction of rquests served from cache
CACHE_SERVE_RATE = 10 # rate for serving requests from cache
DB_SERVE_RATE = 0.5 # rate for serving data to the web server
DB_CONNECTIONS = 25 # number of allowed connections to the database
SIMULATION_STOP = 100 # time to stop simulation
SIMULATION_RUNS = 50 # number of simulation runs for point estimator
class LoadBalancer(object):
'''
This class represents a load balancer with a round-robin policy.
Calling assign() on an instance returns the next server resource
that a request will be assigned to.
'''
def __init__(self):
servers = [Resource(
capacity = WORKERS_PER_SERVER,
name = 'web server %d' % i,
unitName = 'worker'
) for i in xrange(SERVERS)]
self.servers = cycle(servers)
def assign(self):
return self.servers.next()
class Request(Process):
'''
Represents a web request. The handle method manages the process
execution model for an arbitrary web request.
'''
def __init__(self, name, server, database, wait_monitor, serve_monitor):
super(Request, self).__init__(name=name)
self.server = server
self.database = database
self.wait_monitor = wait_monitor
self.serve_monitor = serve_monitor
def handle(self):
# If the accept queue is full, this request is rejected
if len(self.server.waitQ) < ACCEPT_QUEUE_LENGTH:
arrival_time = now()
# Get assigned to a worker process
yield request, self, self.server
start_time = now()
self.wait_monitor.observe(start_time - arrival_time)
# If the requested page isn't cached, incur a database hit.
if random.random() >= CACHE_HIT_RATIO:
yield request, self, self.database
yield hold, self, random.expovariate(DB_SERVE_RATE)
yield release, self, self.database
# Always incur the cache serve rate. This is the time used
# to simply load and serve requested content.
yield hold, self, random.expovariate(CACHE_SERVE_RATE)
# Release the worker process
self.serve_monitor.observe(now() - start_time)
yield release, self, self.server
class RequestGenerator(Process):
'''Generates incoming web requests with exponential inter-arrival times'''
def __init__(self, name, arrival_rate, load_balancer):
super(RequestGenerator, self).__init__(name=name)
self.arrival_rate = arrival_rate
self.load_balancer = load_balancer
self.database = Resource(
capacity = DB_CONNECTIONS,
name = 'database',
unitName = 'connection'
)
# Monitors for data collection
self.wait_monitor = Monitor()
self.serve_monitor = Monitor()
def generate(self):
i = 0
while True:
yield hold, self, random.expovariate(self.arrival_rate)
r = Request(
'request %d' % i,
self.load_balancer.assign(),
self.database,
self.wait_monitor,
self.serve_monitor
)
activate(r, r.handle())
i += 1
if __name__ == '__main__':
out = csv.writer(sys.stdout, delimiter='\t')
out.writerow(['arrival rate', 'mean throughput', 'mean wait time', 'mean serve time'])
# Run our simulation for a variety of arrival rates
for arrival_rate in xrange(
ARRIVAL_RATE_MIN,
ARRIVAL_RATE_MAX + ARRIVAL_RATE_INC,
ARRIVAL_RATE_INC
):
throughput = []
wait_times = []
serve_time = []
for i in xrange(SIMULATION_RUNS):
initialize()
gen = RequestGenerator('req generator', arrival_rate, LoadBalancer())
activate(gen, gen.generate())
simulate(until=SIMULATION_STOP)
through = gen.serve_monitor.count() / float(SIMULATION_STOP)
throughput.append(through)
wait_times.append(gen.wait_monitor.mean())
serve_time.append(gen.serve_monitor.mean())
# Take final means as the means of means
mean_through = sum(throughput) / len(throughput)
mean_wait = sum(wait_times) / len(wait_times)
mean_serve = sum(serve_time) / len(serve_time)
out.writerow([arrival_rate, mean_through, mean_wait, mean_serve])