def first_fit(ordering=None): """ First-Fit heuristic (hosts each VM in the first server with enough resources). For pragmatism purposes, we can define the way VMs are ordered. By default, we can select "Increasing" or "Decreasing". If we don't pass the ordering parameter, the function will define a placement for VMs based on the ordering provided by the input file. Parameters ========== ordering : String Ordering of VMs to be used by the First-Fit heuristic """ # Ordering VMs before selecting the VM placement vms = vms_ordering(ordering=ordering) for vm in vms: for server in Server.all(): if server.has_capacity_to_host(vm): server.virtual_machines.append(vm) server.cpu_demand += vm.cpu_demand server.memory_demand += vm.memory_demand server.disk_demand += vm.disk_demand vm.server = server break
def collect_metrics(self): """ Stores relevant events that occur during the simulation. """ # Collecting Server metrics servers = [] for server in Server.all(): server_data = { 'server': server, 'occupation_rate': server.occupation_rate(), 'cpu_capacity': server.cpu_capacity, 'memory_capacity': server.memory_capacity, 'disk_capacity': server.disk_capacity, 'cpu_demand': server.cpu_demand, 'memory_demand': server.memory_demand, 'disk_demand': server.disk_demand, 'virtual_machines': server.virtual_machines, 'updated': server.updated, 'update_step': server.update_step } servers.append(server_data) # Collecting VirtualMachine metrics virtual_machines = [] for vm in VirtualMachine.all(): vm_data = { 'cpu_demand': vm.cpu_demand, 'memory_demand': vm.memory_demand, 'disk_demand': vm.disk_demand, 'server_update_status': vm.server.updated, 'migrations': vm.migrations } virtual_machines.append(vm_data) # Creating the structure to accommodate simulation metrics self.metrics.append({'maintenance_step': self.maintenance_step, 'simulation_step': self.env.now, 'servers': servers, 'virtual_machines': virtual_machines})
def greedy_least_batch(): """ Maintenance strategy proposed in [1]. It is designed to minimize the number of maintenance steps necessary to update the data center. References ========== [1] Zheng, Zeyu, et al. "Least maintenance batch scheduling in cloud data center networks." IEEE communications letters 18.6 (2014): 901-904. """ # Patching servers nonupdated servers that are not hosting VMs servers_to_patch = Server.ready_to_patch() if len(servers_to_patch) > 0: servers_patch_duration = [] for server in servers_to_patch: patch_duration = server.update() servers_patch_duration.append(patch_duration) # As servers are updated simultaneously, we don't need to call the function # that quantifies the server maintenance duration for each server being patched yield SimulationEnvironment.first().env.timeout(max(servers_patch_duration)) # Migrating VMs else: servers_being_emptied = [] # Sorts the servers to empty based on their occupation rate (ascending) servers_to_empty = sorted(Server.nonupdated(), key=lambda sv: sv.occupation_rate()) for server in servers_to_empty: # We consider as candidate hosts for the VMs every server # not being emptied in the current iteration candidate_servers = [cand_server for cand_server in Server.all() if cand_server not in servers_being_emptied and cand_server != server] vms = [vm for vm in server.virtual_machines] if Server.can_host_vms(candidate_servers, vms): for _ in range(len(server.virtual_machines)): vm = vms.pop(0) # Sorting servers by update status (updated ones first) and demand (more occupied ones first) candidate_servers = sorted(candidate_servers, key=lambda cand_server: (-cand_server.updated, -cand_server.occupation_rate())) # Using a First-Fit strategy to select a candidate host for each VM for cand_server in candidate_servers: if cand_server.has_capacity_to_host(vm): yield SimulationEnvironment.first().env.timeout(vm.migrate(cand_server)) break if len(server.virtual_machines) == 0: servers_being_emptied.append(server)
def describe_simulation_scenario(): """ Describes the simulation scenario with all created objects and its relationships """ occupation_rate = 0 for server in Server.all(): occupation_rate += server.occupation_rate() occupation_rate = occupation_rate / Server.count() print(f'DATASET NAME: {OUTPUT_FILE_NAME}.json') print(f'DATA CENTER OCCUPATION: {round(occupation_rate)}% ({occupation_rate})')
def random_fit(): """ Migrates VMs to random Server objects with resources to host them. """ for vm in VirtualMachine.all(): random_server = random.choice(Server.all()) while not random_server.has_capacity_to_host(vm): random_server = random.choice(Server.all()) # Assigning the random server to host the VM random_server.virtual_machines.append(vm) # Computing VM demand to its host server random_server.cpu_demand += vm.cpu_demand random_server.memory_demand += vm.memory_demand random_server.disk_demand += vm.disk_demand # Assigning the server as the VM host vm.server = random_server
def best_fit(ordering=None): """ Best-Fit heuristic (tries to allocate each VM inside the server that has the maximum load, but still has resources to host the VM). For pragmatism purposes, we can define the way VMs are ordered. By default, we can select "Increasing" or "Decreasing". If we don't pass the ordering parameter, the function will define a placement for VMs based on the ordering provided by the input file. Parameters ========== ordering : String Ordering of VMs to be used by the Best-Fit heuristic """ # Ordering VMs before selecting the VM placement vms = vms_ordering(ordering=ordering) for vm in vms: # As the Best-Fit heuristic consists of choosing the server with maximum # load that still has resources to host the VM, we choose for sorting servers # according to their demand (descending), and then we pick the first server # in that list with resources to host the VM servers = sorted(Server.all(), key=lambda sv: (-sv.cpu_demand, -sv.memory_demand, -sv.disk_demand)) for server in servers: if server.has_capacity_to_host(vm): server.virtual_machines.append(vm) server.cpu_demand += vm.cpu_demand server.memory_demand += vm.memory_demand server.disk_demand += vm.disk_demand vm.server = server break
def worst_fit_like(): """ Worst-Fit-like maintenance strategy (presented by Severo et al.) ==================================================================== Note: We use the term "empty" to refer to servers that are not hosting VMs. The maintenance process is divided in two tasks: (i) Patching empty servers (lines 25-35) (ii) Migrating VMs to empty more servers (lines 40-72) When choosing which servers will host the VMs, this strategy uses the Worst-Fit Decreasing heuristic. """ # Patching servers nonupdated servers that are not hosting VMs servers_to_patch = Server.ready_to_patch() if len(servers_to_patch) > 0: servers_patch_duration = [] for server in servers_to_patch: patch_duration = server.update() servers_patch_duration.append(patch_duration) # As servers are updated simultaneously, we don't need to call the function # that quantifies the server maintenance duration for each server being patched yield SimulationEnvironment.first().env.timeout( max(servers_patch_duration)) # (ii) Migrating VMs to empty more servers else: servers_being_emptied = [] # Getting the list of servers that still need to receive the patch servers_to_empty = Server.nonupdated() for server in servers_to_empty: # We consider as candidate hosts for the VMs all Server # objects not being emptied in the current maintenance step candidate_servers = [ cand_server for cand_server in Server.all() if cand_server not in servers_being_emptied and cand_server != server ] # Sorting VMs by its demand (decreasing) vms = [vm for vm in server.virtual_machines] vms = sorted(vms, key=lambda vm: -vm.demand()) for _ in range(len(server.virtual_machines)): vm = vms.pop(0) # Sorting servers (bins) to align with Worst-Fit's idea, # which is prioritizing servers with less space remaining candidate_servers = sorted( candidate_servers, key=lambda cand: cand.occupation_rate()) # Migrating VMs using the Worst-Fit heuristic for cand_server in candidate_servers: if cand_server.has_capacity_to_host(vm): # Migrating the VM and storing the migration duration to allow future analysis yield SimulationEnvironment.first().env.timeout( vm.migrate(cand_server)) break if len(server.virtual_machines) == 0: servers_being_emptied.append(server)
def vulnerability_surface(env, maintenance_data): """ Maintenance strategy proposed by Severo et al. 2020 =================================================== Note: We use the term "empty" to refer to servers that are not hosting VMs When choosing which servers to empty, this heuristic prioritizes servers that take less time to be emptied, which can be achieved by having a small number of VMs or by hosting small VMs (that will take a negligible time to be migrated). The maintenance process is divided in two tasks: (i) Patching empty servers (lines 31-38) (ii) Migrating VMs to empty more servers (lines 42-90) Parameters ========== env : SimPy.Environment Used to quantity the amount of simulation time spent by the migration maintenance_data : List Object that will be filled during the maintenance, storing metrics on each maintenance step """ while len(Server.nonupdated()) > 0: # Patching servers nonupdated servers that are not hosting VMs servers_to_patch = Server.ready_to_patch() if len(servers_to_patch) > 0: for server in servers_to_patch: server.updated = True # As servers are updated simultaneously, we don't need to call the function # that quantifies the server maintenance duration for each server being patched yield env.process(server_update(env, constants.PATCHING_TIME)) # Migrating VMs servers_being_emptied = [] # Sorts the servers to empty based on its update score. This score considers # the amount of time needed to migrate all VMs hosted by the server servers_to_empty = sorted( Server.nonupdated(), key=lambda cand_server: cand_server.update_cost()) migrations_data = [ ] # Stores data on the migrations performed to allow future analysis for server in servers_to_empty: vms_to_migrate = len(server.virtual_machines) servers_checked = 0 # We consider as candidate hosts for the VMs every server # not being emptied in the current iteration candidate_servers = [ cand_server for cand_server in Server.all() if cand_server not in servers_being_emptied and cand_server != server ] while len(server.virtual_machines ) > 0 and servers_checked < vms_to_migrate * len( candidate_servers): # Sorting VMs by its demand (decreasing) vms = sorted( server.virtual_machines, key=lambda vm: (-vm.cpu_demand, -vm.memory_demand, -vm.disk_demand)) vm = server.virtual_machines[0] # Sorting servers by update status (updated ones first) and demand (decreasing) candidate_servers = sorted( candidate_servers, key=lambda cand_server: (-cand_server.updated, -cand_server.cpu_demand, -cand_server.memory_demand, -cand_server.disk_demand)) if Server.can_host_vms(candidate_servers, vms): # Using a First-Fit Decreasing strategy to select a candidate host for each VM for cand_server in candidate_servers: servers_checked += 1 if cand_server.has_capacity_to_host(vm): # Migrating the VM and storing the migration duration to allow future analysis migration_duration = yield env.process( vm.migrate(env, cand_server)) migrations_data.append({ 'origin': server, 'destination': cand_server, 'vm': vm, 'duration': migration_duration }) break else: break if len(server.virtual_machines) == 0: servers_being_emptied.append(server) # Collecting metrics gathered in the current maintenance step (i.e., outer while loop iteration) maintenance_data.append( collect_metrics(env, 'VS Heuristic', servers_to_patch, servers_being_emptied, migrations_data))
def first_fit(env, maintenance_data): """ First-Fit like maintenance strategy (presented by Severo et al. 2020) ==================================================================== Note: We use the term "empty" to refer to servers that are not hosting VMs. The maintenance process is divided in two tasks: (i) Patching empty servers (lines 29-36) (ii) Migrating VMs to empty more servers (lines 40-73) When choosing which servers will host the VMs, this strategy uses the First-Fit heuristic. Parameters ========== env : SimPy.Environment Used to quantity the amount of simulation time spent by the migration maintenance_data : List Object that will be filled during the maintenance, storing metrics on each maintenance step """ while len(Server.nonupdated()) > 0: # (i) Patching servers nonupdated servers that are not hosting VMs (lines 30-37) servers_to_patch = Server.ready_to_patch() if len(servers_to_patch) > 0: for server in servers_to_patch: server.updated = True # As servers are updated simultaneously, we don't need to call the function # that quantifies the server maintenance duration for each server being patched yield env.process(server_update(env, constants.PATCHING_TIME)) # (ii) Migrating VMs to empty more servers (lines 41-74) servers_being_emptied = [] # Getting the list of servers that still need to receive the patch servers_to_empty = Server.nonupdated() migrations_data = [ ] # Stores data on the migrations performed to allow future analysis for server in servers_to_empty: candidate_servers = [ cand_server for cand_server in Server.all() if cand_server != server and cand_server not in servers_being_emptied ] vms_to_migrate = len(server.virtual_machines) servers_checked = 0 while len(server.virtual_machines ) > 0 and servers_checked <= vms_to_migrate * len( candidate_servers): vm = server.virtual_machines[0] # Migrating VMs using the First-Fit heuristic, which suggests the # migration of VMs to the first server that has resources to host it for cand_server in candidate_servers: servers_checked += 1 if cand_server.has_capacity_to_host(vm): # Migrating the VM and storing the migration duration to allow future analysis migration_duration = yield env.process( vm.migrate(env, cand_server)) migrations_data.append({ 'origin': server, 'destination': cand_server, 'vm': vm, 'duration': migration_duration }) break if len(server.virtual_machines) == 0: servers_being_emptied.append(server) # Collecting metrics gathered in the current maintenance step (i.e., outer while loop iteration) maintenance_data.append( collect_metrics(env, 'First-Fit', servers_to_patch, servers_being_emptied, migrations_data))
def collect_metrics(env, strategy, servers_patched, servers_being_emptied, migrations_data): """ Gather metrics from the current maintenance step. Supported metrics: - Simulation steps - Number of servers being updated - Number of servers being emptied - Number of updated servers - Number of nonupdated servers - Vulnerability Surface (Severo et al. 2020) - Number of VM migrations - Overall migrations duration (amount of time spent with migrations in the current step) - Longer migration - Shorter migration - Average migration duration - Servers occupation rate - Servers consolidation rate Parameters ========== env : SimPy.Environment Used to quantity the amount of simulation time spent by the migration strategy : String Name of the used maintenance strategy servers_patched : List List of servers updated in the current maintenance step servers_being_emptied : List List of servers being emptied in the current maintenance step migrations_data : List Information on each migration performed in the current maintenance step Returns ======= output : Dictionary List of metrics collected during the current maintenance step """ output = {} # Number of simulation steps output['simulation_steps'] = env.now # Name of the used maintenance strategy output['strategy'] = strategy # Other simulation metrics output['metrics'] = {} # Number of updated and nonupdated servers output['metrics']['updated_servers'] = len(Server.updated()) output['metrics']['nonupdated_servers'] = len(Server.nonupdated()) # Vulnerability Surface (Severo et al. 2020) = Number of non-updated servers * Elapsed time output['metrics']['vulnerability_surface'] = env.now * output['metrics'][ 'nonupdated_servers'] # Gathering VM migration metrics output['metrics']['vm_migrations'] = 0 output['metrics']['migrations_duration'] = 0 output['metrics']['longer_migration'] = 0 output['metrics']['shorter_migration'] = 0 output['metrics']['avg_migration_duration'] = 0 if len(migrations_data) > 0: # Number of VM migrations performed in this interval output['metrics']['vm_migrations'] = len(migrations_data) # Time spent performing VM migrations migrations_duration = sum(migr['duration'] for migr in migrations_data) output['metrics']['migrations_duration'] = migrations_duration # Longer migration output['metrics']['longer_migration'] = max( migr['duration'] for migr in migrations_data) # Shorter migration output['metrics']['shorter_migration'] = min( migr['duration'] for migr in migrations_data) # Average migration duration output['metrics'][ 'avg_migration_duration'] = migrations_duration / len( migrations_data) # Gathering server-related metrics # Occupation rate aggregated_occupation_rate = sum(sv.occupation_rate() for sv in Server.all()) output['metrics']['occupation_rate'] = aggregated_occupation_rate / len( Server.used_servers()) # Consolidation rate output['metrics']['consolidation_rate'] = Server.consolidation_rate() # Servers being updated output['metrics']['servers_being_updated'] = len(servers_patched) # Servers being emptied output['metrics']['servers_being_emptied'] = len(servers_being_emptied) return (output)
TOPOLOGY = fnss.topologies.datacenter.fat_tree_topology(k=8) ###################### ## CREATING OBJECTS ## ###################### create_servers() create_virtual_machines() ################################################################################### ## MAPPING ORIGINAL GRAPH NODES THAT REPRESENT HOSTS TO PYTHON OBJECTS (SERVERS) ## ################################################################################### servers = [sv for sv in Server.all()] new_nodes = [] for node in TOPOLOGY.nodes(data=True): if node[1]['type'] == 'host': new_nodes.append(servers.pop(0)) else: new_nodes.append(node[0]) TOPOLOGY = map_graph_nodes_to_objects(TOPOLOGY, new_nodes) # Updating created object's topology property for server in Server.all(): server.topology = TOPOLOGY
def salus(): """ Salus is the Roman goddess of safety. This maintenance strategy was proposed by Severo et al. in 2020. Note: We use the term "empty" to refer to servers that are not hosting VMs When choosing which servers to empty, this heuristic prioritizes servers with a smaller update cost, which takes into account multiple factors such as server capacity and server's patch duration. The maintenance process is divided in two tasks: (i) Patching empty servers (lines 28-36) (ii) Migrating VMs to empty more servers (lines 41-74) """ # Patching servers nonupdated servers that are not hosting VMs servers_to_patch = Server.ready_to_patch() if len(servers_to_patch) > 0: servers_patch_duration = [] for server in servers_to_patch: patch_duration = server.update() servers_patch_duration.append(patch_duration) # As servers are updated simultaneously, we don't need to call the function # that quantifies the server maintenance duration for each server being patched yield SimulationEnvironment.first().env.timeout(max(servers_patch_duration)) # Migrating VMs else: servers_being_emptied = [] # Sorts the servers to empty based on its update score. This score considers the amount of time # needed to update the server (including VM migrations to draining the server) and its capacity servers_to_empty = sorted(Server.nonupdated(), key=lambda sv: (sv.maintenance_duration() * (1/(sv.capacity()+1))) ** (1/2)) for server in servers_to_empty: # We consider as candidate hosts for the VMs all Server # objects not being emptied in the current maintenance step candidate_servers = [cand_server for cand_server in Server.all() if cand_server not in servers_being_emptied and cand_server != server] # Sorting VMs by its demand (decreasing) vms = [vm for vm in server.virtual_machines] vms = sorted(vms, key=lambda vm: -vm.demand()) if Server.can_host_vms(candidate_servers, vms): for _ in range(len(server.virtual_machines)): vm = vms.pop(0) # Sorting servers by update status (updated ones first) and demand (decreasing) candidate_servers = sorted(candidate_servers, key=lambda sv: (-sv.updated, -sv.occupation_rate())) # Using a Best-Fit Decreasing strategy to select a candidate host for each VM for cand_server in candidate_servers: if cand_server.has_capacity_to_host(vm): yield SimulationEnvironment.first().env.timeout(vm.migrate(cand_server)) break if len(server.virtual_machines) == 0: servers_being_emptied.append(server)
def load_dataset(cls, input_file): """ Creates simulation objects according to data from a JSON input file Parameters ========== file : string Path location of the JSON input file initial_edge_node_connection : boolean, optional Informs if the input file provides information on which clients are initially connected to edge nodes initial_placement : boolean, optional Informs if the input file provides information on the services initial placement """ with open(f'data/{input_file}.json', 'r') as read_file: data = json.load(read_file) read_file.close() # Informing the simulation environment what's the dataset that will be used during the simulation Simulator.environment.dataset = input_file ########################## # SIMULATION COMPONENTS ## ########################## # Servers for server_data in data['servers']: # Creating object server = Server(id=None, cpu=None, memory=None, disk=None, updated=None) # Defining object attributes server.id = server_data['id'] server.cpu_capacity = server_data['cpu_capacity'] server.memory_capacity = server_data['memory_capacity'] server.disk_capacity = server_data['disk_capacity'] server.updated = server_data['updated'] server.patch_duration = server_data['patch_duration'] server.sanity_check_duration = server_data['sanity_check_duration'] # Virtual Machines for vm_data in data['virtual_machines']: # Creating object vm = VirtualMachine(id=None, cpu=None, memory=None, disk=None) # Defining object attributes vm.id = vm_data['id'] vm.cpu_demand = vm_data['cpu_demand'] vm.memory_demand = vm_data['memory_demand'] vm.disk_demand = vm_data['disk_demand'] # Initial Placement server = Server.find_by_id(vm_data['server']) server.cpu_demand += vm.cpu_demand server.memory_demand += vm.memory_demand server.disk_demand += vm.disk_demand vm.server = server server.virtual_machines.append(vm) ###################### ## Network Topology ## ###################### topology = FatTree() # Creating links and nodes for link in data['network_topology']: # Creating nodes if link['nodes'][0]['type'] == 'Server': node_1 = Server.find_by_id(link['nodes'][0]['id']) else: node_1 = link['nodes'][0]['id'] # Creating node 1 if it doesn't exists yet if node_1 not in topology: topology.add_node(node_1) for key, value in link['nodes'][0]['data'].items(): topology.nodes[node_1][key] = value if link['nodes'][1]['type'] == 'Server': node_2 = Server.find_by_id(link['nodes'][1]['id']) else: node_2 = link['nodes'][1]['id'] # Creating node 2 if it doesn't exists yet if node_2 not in topology: topology.add_node(node_2) for key, value in link['nodes'][1]['data'].items(): topology.nodes[node_2][key] = value # Creating link if it wasn't created yet if not topology.has_edge(node_1, node_2): topology.add_edge(node_1, node_2) # Adding attributes to the link topology[node_1][node_2]['bandwidth'] = link['bandwidth'] # Assigning 'topology' and 'simulation_environment' attributes to created objects objects = Server.all() + VirtualMachine.all() for obj in objects: obj.topology = topology obj.simulation_environment = Simulator.environment
def show_metrics(dataset, heuristic, output_file=None): """ Presents information and metrics of the performed placement and optionally stores these results into an output CSV file. Currently, this method outputs the following metrics: - Servers occupation rate - Servers consolidation rate Parameters ========== dataset : String Name of the used dataset heuristic : STring Name of the used placement heuristic output_file : String Optional parameters regarding the name of the output CSV file """ # Servers occupation rate occupation_rate = sum(sv.occupation_rate() for sv in Server.all()) / len(Server.used_servers()) # Servers consolidation rate consolidation_rate = Server.consolidation_rate() # Prints out the placement metrics print( '========================\n== SIMULATION RESULTS ==\n========================' ) print(f'Dataset: "{dataset}"') print(f'Placement Strategy: {heuristic}\n') print(f'Consolidation Rate: {consolidation_rate}') print(f'Occupation Rate: {occupation_rate}\n') print('Placement:') for server in Server.all(): vms = [vm.id for vm in server.virtual_machines] print(f'SV_{server.id}. VMs: {vms}') # If the output_file parameter was provided, stores the placement results into a CSV file if output_file: with open(output_file, mode='w') as csv_file: output_writer = csv.writer(csv_file, delimiter='\t', quotechar='"', quoting=csv.QUOTE_MINIMAL) # Creating header output_writer.writerow([ 'Dataset', 'Strategy', 'No. of Servers', 'No. of VMs', 'Occupation Rate', 'Consolidation Rate' ]) # Creating body output_writer.writerow([ dataset, heuristic, Server.count(), VirtualMachine.count(), occupation_rate, consolidation_rate ])