small refactoring as preparation for overprovisioning RM
[osm/vim-emu.git] / src / emuvim / dcemulator / resourcemodel / upb / simple.py
1 """
2 Playground for resource models created by University of Paderborn.
3 """
4 import time
5 import json
6 import logging
7 from emuvim.dcemulator.resourcemodel import BaseResourceModel
8
9 LOG = logging.getLogger("rm.upb.simple")
10 LOG.setLevel(logging.DEBUG)
11
12 CPU_PERIOD = 1000000
13
14
15 class UpbSimpleCloudDcRM(BaseResourceModel):
16 """
17 This will be an example resource model that limits the overall
18 resources that can be deployed per data center.
19 No over provisioning. Resources are fixed throughout entire container
20 lifetime.
21 """
22
23 def __init__(self, max_cu=32, max_mu=1024,
24 deactivate_cpu_limit=False,
25 deactivate_mem_limit=False):
26 """
27 Initialize model.
28 :param max_cu: Maximum number of compute units available in this DC.
29 :param max_mu: Maximum memory of entire dc.
30 :return:
31 """
32 self.dc_max_cu = max_cu
33 self.dc_max_mu = max_mu
34 self.dc_alloc_cu = 0
35 self.dc_alloc_mu = 0
36 self.deactivate_cpu_limit = deactivate_cpu_limit
37 self.deactivate_mem_limit = deactivate_mem_limit
38 self.single_cu = 0
39 self.single_mu = 0
40 super(self.__class__, self).__init__()
41
42 def allocate(self, d):
43 """
44 Allocate resources for the given container.
45 Defined by d.flavor_name
46 :param d: container
47 :return:
48 """
49 self._allocated_compute_instances[d.name] = d
50 if not self.deactivate_cpu_limit:
51 self._allocate_cpu(d)
52 if not self.deactivate_mem_limit:
53 self._allocate_mem(d)
54 self._apply_limits()
55
56 def _allocate_cpu(self, d):
57 """
58 Actually allocate (bookkeeping)
59 :param d: container
60 :return:
61 """
62 fl_cu = self._get_flavor(d).get("compute")
63 # check for over provisioning
64 if self.dc_alloc_cu + fl_cu > self.dc_max_cu:
65 raise Exception("Not enough compute resources left.")
66 self.dc_alloc_cu += fl_cu
67
68 def _allocate_mem(self, d):
69 """
70 Actually allocate (bookkeeping)
71 :param d: container
72 :return:
73 """
74 fl_mu = self._get_flavor(d).get("memory")
75 # check for over provisioning
76 if self.dc_alloc_mu + fl_mu > self.dc_max_mu:
77 raise Exception("Not enough memory resources left.")
78 self.dc_alloc_mu += fl_mu
79
80 def free(self, d):
81 """
82 Free resources allocated to the given container.
83 :param d: container
84 :return:
85 """
86 del self._allocated_compute_instances[d.name]
87 if not self.deactivate_cpu_limit:
88 self._free_cpu(d)
89 if not self.deactivate_mem_limit:
90 self._free_mem(d)
91 self._apply_limits()
92
93 def _free_cpu(self, d):
94 """
95 Free resources.
96 :param d: container
97 :return:
98 """
99 self.dc_alloc_cu -= self._get_flavor(d).get("compute")
100
101 def _free_mem(self, d):
102 """
103 Free resources.
104 :param d: container
105 :return:
106 """
107 self.dc_alloc_mu -= self._get_flavor(d).get("memory")
108
109 def _apply_limits(self):
110 """
111 Recalculate real resource limits for all allocated containers and apply them
112 to their cgroups.
113 We have to recalculate for all to allow e.g. overprovisioning models.
114 :return:
115 """
116 for d in self._allocated_compute_instances.itervalues():
117 if not self.deactivate_cpu_limit:
118 self._apply_cpu_limits(d)
119 if not self.deactivate_mem_limit:
120 self._apply_mem_limits(d)
121
122 def _apply_cpu_limits(self, d):
123 """
124 Calculate real CPU limit (CFS bandwidth) and apply.
125 :param d: container
126 :return:
127 """
128 number_cu = self._get_flavor(d).get("compute")
129 # calculate cpu time fraction of a single compute unit
130 self.single_cu = self._compute_single_cu()
131 # calculate cpu time fraction for container with given flavor
132 cpu_time_percentage = self.single_cu * number_cu
133 # calculate input values for CFS scheduler bandwidth limitation
134 cpu_period, cpu_quota = self._calculate_cpu_cfs_values(cpu_time_percentage)
135 # apply limits to container if changed
136 if d.cpu_period != cpu_period or d.cpu_quota != cpu_quota:
137 LOG.debug("Setting CPU limit for %r: cpu_quota = cpu_period * limit = %f * %f = %f" % (
138 d.name, cpu_period, cpu_time_percentage, cpu_quota))
139 d.updateCpuLimit(cpu_period=int(cpu_period), cpu_quota=int(cpu_quota))
140
141 def _compute_single_cu(self):
142 """
143 Calculate percentage of CPU time of a singe CU unit.
144 :return:
145 """
146 # get cpu time fraction for entire emulation
147 e_cpu = self.registrar.e_cpu
148 # calculate
149 return float(e_cpu) / sum([rm.dc_max_cu for rm in list(self.registrar.resource_models)])
150
151 def _calculate_cpu_cfs_values(self, cpu_time_percentage):
152 """
153 Calculate cpu period and quota for CFS
154 :param cpu_time_percentage: percentage of overall CPU to be used
155 :return: cpu_period, cpu_quota
156 """
157 # (see: https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt)
158 # Attention minimum cpu_quota is 1ms (micro)
159 cpu_period = CPU_PERIOD # lets consider a fixed period of 1000000 microseconds for now
160 cpu_quota = cpu_period * cpu_time_percentage # calculate the fraction of cpu time for this container
161 # ATTENTION >= 1000 to avoid a invalid argument system error ... no idea why
162 if cpu_quota < 1000:
163 cpu_quota = 1000
164 LOG.warning("Increased CPU quota to avoid system error.")
165 return cpu_period, cpu_quota
166
167 def _apply_mem_limits(self, d):
168 """
169 Calculate real mem limit and apply.
170 :param d: container
171 :return:
172 """
173 number_mu = self._get_flavor(d).get("memory")
174 # get memory amount for entire emulation
175 e_mem = self.registrar.e_mem
176 # calculate amount of memory for a single mu
177 self.single_mu = float(e_mem) / sum([rm.dc_max_mu for rm in list(self.registrar.resource_models)])
178 # calculate mem for given flavor
179 mem_limit = self.single_mu * number_mu
180 mem_limit = self._calculate_mem_limit_value(mem_limit)
181 # apply to container if changed
182 if d.mem_limit != mem_limit:
183 LOG.debug("Setting MEM limit for %r: mem_limit = %f MB" % (d.name, mem_limit/1024/1024))
184 d.updateMemoryLimit(mem_limit=mem_limit)
185
186 def _calculate_mem_limit_value(self, mem_limit):
187 """
188 Calculate actual mem limit as input for cgroup API
189 :param mem_limit: abstract mem limit
190 :return: concrete mem limit
191 """
192 # ATTENTION minimum mem_limit per container is 4MB
193 if mem_limit < 4:
194 mem_limit = 4
195 LOG.warning("Increased MEM limit because it was less than 4.0 MB.")
196 # to byte!
197 return int(mem_limit*1024*1024)
198
199 def get_state_dict(self):
200 """
201 Return the state of the resource model as simple dict.
202 Helper method for logging functionality.
203 :return:
204 """
205 # collect info about all allocated instances
206 allocation_state = dict()
207 for k, d in self._allocated_compute_instances.iteritems():
208 s = dict()
209 s["cpu_period"] = d.cpu_period
210 s["cpu_quota"] = d.cpu_quota
211 s["cpu_shares"] = d.cpu_shares
212 s["mem_limit"] = d.mem_limit
213 s["memswap_limit"] = d.memswap_limit
214 allocation_state[k] = s
215 # final result
216 r = dict()
217 r["e_cpu"] = self.registrar.e_cpu
218 r["e_mem"] = self.registrar.e_mem
219 r["dc_max_cu"] = self.dc_max_cu
220 r["dc_max_mu"] = self.dc_max_mu
221 r["dc_alloc_cu"] = self.dc_alloc_cu
222 r["dc_alloc_mu"] = self.dc_alloc_mu
223 r["single_cu_percentage"] = self.single_cu
224 r["single_mu_percentage"] = self.single_mu
225 r["allocation_state"] = allocation_state
226 return r
227
228 def _get_flavor(self, d):
229 """
230 Get flavor assigned to given container.
231 Identified by d.flavor_name.
232 :param d: container
233 :return:
234 """
235 if d.flavor_name not in self._flavors:
236 raise Exception("Flavor %r does not exist" % d.flavor_name)
237 return self._flavors.get(d.flavor_name)
238
239 def _write_log(self, d, path, action):
240 """
241 Helper to log RM info for experiments.
242 :param d: container
243 :param path: log path
244 :param action: allocate or free
245 :return:
246 """
247 if path is None:
248 return
249 # we have a path: write out RM info
250 l = dict()
251 l["t"] = time.time()
252 l["container_state"] = d.getStatus()
253 l["action"] = action
254 l["rm_state"] = self.get_state_dict()
255 # append to logfile
256 with open(path, "a") as f:
257 f.write("%s\n" % json.dumps(l))