-
Notifications
You must be signed in to change notification settings - Fork 4
/
recommender.py
208 lines (177 loc) · 8.25 KB
/
recommender.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
DEFAULT_NAMESPACE="default"
DELTA = 0.2
# Select the VPAs that choose the current clever recommender
def selects_recommender(vpas, recommender_name):
selected_vpas = []
for vpa in vpas["items"]:
vpa_spec = vpa["spec"]
if "recommenders" not in vpa_spec.keys():
continue
else:
print("VPA {} has chosen {} recommenders".format(vpa["metadata"]["name"], len(vpa_spec["recommenders"])))
print(vpa_spec)
for recommender in vpa_spec["recommenders"]:
if recommender["name"] == recommender_name:
selected_vpas.append(vpa)
return selected_vpas
# Check if all container CPU requests are the same and get the consistent value.
# If some container requests are larger than others, is_consistent would be False.
def get_consistent_max_val(request_dict):
max_val = -1
consistent_cnt = 0
for pod in request_dict.keys():
for container in request_dict[pod].keys():
if request_dict[pod][container] > max_val:
max_val = request_dict[pod][container]
consistent_cnt += 1
is_consistent = True
if consistent_cnt > 1:
is_consistent = False
return is_consistent, max_val
# Only check the default CPU request. If not existed, it will use 1 core by default.
def get_vpa_detailed_info(corev1, vpa):
# Get the VPA spec
vpa_spec = vpa["spec"]
# example target_ref {'apiVersion': 'apps/v1', 'kind': 'Deployment', 'name': 'hamster'}
target_ref = vpa_spec["targetRef"]
print(target_ref)
# Retrieve the target pods
if "namespace" in target_ref.keys():
target_namespace = target_ref["namespace"]
else:
target_namespace = DEFAULT_NAMESPACE
# Get the target containers
target_pods = corev1.list_namespaced_pod(namespace=target_namespace, label_selector="app=" + target_ref["name"])
# Retrieve the target containers
vpa_pod_nodes = {}
all_container_cpu_requests = {}
for pod in target_pods.items:
all_container_cpu_requests[pod.metadata.name] = {}
vpa_pod_nodes[pod.metadata.name] = pod.spec.node_name
for container in pod.spec.containers:
# print(container.name)
# obtain the CPU request and convert it to int
cur_request = str2resource("cpu", container.resources.requests["cpu"])
all_container_cpu_requests[pod.metadata.name][container.name] = cur_request
# Get the maximum default request if there are many containers.
is_consistent, max_cpu_val = get_consistent_max_val(all_container_cpu_requests)
vpa_container_cpu_request = max_cpu_val
if not is_consistent:
print("Warning: the containers managed by {} do not have consistent CPU requests!", vpa["metadata"]["name"])
return vpa_container_cpu_request, vpa_pod_nodes
# resource2str converts a resource (CPU, Memory) value to a string
def resource2str(resource, value):
if resource.lower() == "cpu":
if value < 1:
return str(int(value * 1000)) + "m"
else:
return str(value)
# Memory is in bytes
else:
if value < 1024:
return str(value) + "B"
elif value < 1024 * 1024:
return str(int(value / 1024)) + "k"
elif value < 1024 * 1024 * 1024:
return str(int(value / 1024 / 1024)) + "Mi"
else:
return str(int(value / 1024 / 1024 / 1024)) + "Gi"
# Convert a resource (CPU, Memory) string to a float value
def str2resource(resource, value):
if type(value) is str:
if resource.lower() == "cpu":
if value[-1] == "m":
return float(value[:-1]) / 1000
else:
return float(value)
else:
if value[-1].lower() == "b":
return float(value[:-1])
elif value[-1].lower() == "k":
return float(value[:-1]) * 1024
elif value[-2:].lower() == "mi":
return float(value[:-2]) * 1024 * 1024
elif value[-2:].lower() == "gi":
return float(value[:-2]) * 1024 * 1024 * 1024
else:
return float(value)
else:
return value
def bound_var(var, min_value, max_value):
if var < min_value:
return min_value
elif var > max_value:
return max_value
else:
return var
# Find the nodes with frequency changes in the last iteration
def find_node_with_frequency_changes(cur_node_frequencies, prev_node_frequencies):
node_with_frequency_changes = []
for node in cur_node_frequencies.keys():
# TODO: compare frequencies
if node not in prev_node_frequencies.keys():
node_with_frequency_changes.append(node)
else:
if cur_node_frequencies[node] == prev_node_frequencies[node]:
continue
else:
node_with_frequency_changes.append(node)
return node_with_frequency_changes
def get_recommendation(vpa, corev1, node_frequencies, max_node_frequencies, vpa_default_request):
"""
This function takes a VPA and returns a list of recommendations
"""
# Get the VPA spec
vpa_spec = vpa["spec"]
# example target_ref {'apiVersion': 'apps/v1', 'kind': 'Deployment', 'name': 'hamster'}
target_ref = vpa_spec["targetRef"]
print(target_ref)
# Retrieve the target pods
if "namespace" in target_ref.keys():
target_namespace = target_ref["namespace"]
else:
target_namespace = DEFAULT_NAMESPACE
# Get the target pods
target_pods = corev1.list_namespaced_pod(namespace=target_namespace, label_selector="app=" + target_ref["name"])
# Get the target container traces
recommendations = []
# Get uncapped target
uncapped_targets = {}
for pod in target_pods.items:
pod_node = pod.spec.node_name
node_frequency = node_frequencies[pod_node]
max_node_frequency = max_node_frequencies[pod_node]
for container in pod.spec.containers:
container_name = container.name
uncapped_target = vpa_default_request * float(max_node_frequency) / float(node_frequency)
if container_name not in uncapped_targets.keys():
uncapped_targets[container_name] = uncapped_target
else:
uncapped_targets[container_name] = max(uncapped_target, uncapped_targets[container_name])
for containerPolicy in vpa_spec["resourcePolicy"]["containerPolicies"]:
controlled_resources = containerPolicy["controlledResources"]
max_allowed = containerPolicy["maxAllowed"]
min_allowed = containerPolicy["minAllowed"]
for resource in controlled_resources:
if resource != "cpu":
continue
else:
for container_name in uncapped_targets.keys():
container_recommendation = {"containerName": container_name, "lowerBound": {}, "target": {},
"uncappedTarget": {}, "upperBound": {}}
uncapped_target = uncapped_targets[container_name]
lower_bound = uncapped_target * (1 - DELTA)
upper_bound = uncapped_target * (1 + DELTA)
# If the target is below the lowerbound, set it to the lowerbound
min_allowed_value = str2resource(resource, min_allowed[resource])
max_allowed_value = str2resource(resource, max_allowed[resource])
target = bound_var(uncapped_target, min_allowed_value, max_allowed_value)
lower_bound = bound_var(lower_bound, min_allowed_value, max_allowed_value)
upper_bound = bound_var(upper_bound, min_allowed_value, max_allowed_value)
# Convert CPU/Memory values to millicores/bytes
container_recommendation["lowerBound"][resource] = resource2str(resource, lower_bound)
container_recommendation["target"][resource] = resource2str(resource, target)
container_recommendation["uncappedTarget"][resource] = resource2str(resource, uncapped_target)
container_recommendation["upperBound"][resource] = resource2str(resource, upper_bound)
recommendations.append(container_recommendation)
return recommendations