Skip to content

Commit 74012b6

Browse files
committed
WIP: Shard affinity for k8s workloads
1 parent aa99d4b commit 74012b6

File tree

2 files changed

+228
-33
lines changed

2 files changed

+228
-33
lines changed

nova/scheduler/filters/shard_filter.py

Lines changed: 100 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@
1919
from oslo_log import log as logging
2020

2121
import nova.conf
22+
from nova import context as nova_context
23+
from nova.objects.instance import Instance
24+
from nova.objects.instance import InstanceList
2225
from nova.scheduler import filters
2326
from nova.scheduler import utils
2427
from nova import utils as nova_utils
@@ -28,6 +31,9 @@
2831
CONF = nova.conf.CONF
2932

3033
_SERVICE_AUTH = None
34+
GARDENER_PREFIX = "kubernetes.io-cluster-shoot--garden--"
35+
KKS_PREFIX = "kubernikus:kluster"
36+
HANA_PREFIX = "hana_"
3137

3238

3339
class ShardFilter(filters.BaseHostFilter):
@@ -114,11 +120,61 @@ def _get_shards(self, project_id):
114120

115121
return self._PROJECT_SHARD_CACHE.get(project_id)
116122

117-
def host_passes(self, host_state, spec_obj):
123+
def _get_k8s_cluster_instances(self, spec_obj):
124+
"""If the instance will be part of a K8S cluster, it returns
125+
the list of all other instances that are already part of it,
126+
if any.
127+
"""
128+
k8s_filter = self._k8s_instance_query_filter(spec_obj)
129+
130+
if not k8s_filter:
131+
return []
132+
133+
k8s_filter['project_id'] = spec_obj.project_id
134+
135+
return InstanceList.get_by_filters(
136+
nova_context.get_admin_context(), filters=k8s_filter,
137+
expected_attrs=['flavor', 'metadata', 'tags'])
138+
139+
def _k8s_instance_query_filter(self, spec_obj):
140+
elevated = nova_context.get_admin_context()
141+
instance = Instance.get_by_uuid(elevated, spec_obj.instance_uuid,
142+
expected_attrs=['tags', 'metadata'])
143+
144+
# Kubernikus
145+
kks_tag = next((t.tag for t in instance.tags
146+
if t.tag.startswith(KKS_PREFIX)), None)
147+
if kks_tag:
148+
return {'tags': [kks_tag]}
149+
150+
# Gardener
151+
gardener_meta = {k: v for k, v in instance.metadata.items()
152+
if k.starswith(GARDENER_PREFIX)}
153+
if gardener_meta:
154+
return {'metadata': gardener_meta}
155+
156+
return None
157+
158+
def filter_all(self, filter_obj_list, spec_obj):
159+
"""Yield objects that pass the filter.
160+
161+
Can be overridden in a subclass, if you need to base filtering
162+
decisions on all objects. Otherwise, one can just override
163+
_filter_one() to filter a single object.
164+
"""
118165
# Only VMware
119166
if utils.is_non_vmware_spec(spec_obj):
120-
return True
167+
for obj in filter_obj_list:
168+
yield obj
169+
return
170+
171+
k8s_instances = self._get_k8s_cluster_instances(spec_obj)
172+
173+
for obj in filter_obj_list:
174+
if self._host_passes(obj, spec_obj, k8s_instances):
175+
yield obj
121176

177+
def _host_passes(self, host_state, spec_obj, k8s_instances):
122178
host_shard_aggrs = [aggr for aggr in host_state.aggregates
123179
if aggr.name.startswith(self._SHARD_PREFIX)]
124180

@@ -148,18 +204,58 @@ def host_passes(self, host_state, spec_obj):
148204
if self._ALL_SHARDS in shards:
149205
LOG.debug('project enabled for all shards %(project_shards)s.',
150206
{'project_shards': shards})
151-
return True
152207
elif host_shard_names & set(shards):
153208
LOG.debug('%(host_state)s shard %(host_shard)s found in project '
154209
'shards %(project_shards)s.',
155210
{'host_state': host_state,
156211
'host_shard': host_shard_names,
157212
'project_shards': shards})
158-
return True
159213
else:
160214
LOG.debug('%(host_state)s shard %(host_shard)s not found in '
161215
'project shards %(project_shards)s.',
162216
{'host_state': host_state,
163217
'host_shard': host_shard_names,
164218
'project_shards': shards})
165219
return False
220+
221+
if not utils.request_is_resize(spec_obj):
222+
# K8S orchestrators are only creating or deleting nodes,
223+
# therefore we shouldn't infer with resize/migrate requests.
224+
return self._host_passes_k8s(host_state, host_shard_names,
225+
spec_obj, k8s_instances)
226+
227+
return True
228+
229+
def _host_passes_k8s(self, host_state, host_shard_names, spec_obj,
230+
k8s_instances):
231+
"""Instances of a K8S cluster must end up on the same shard.
232+
The K8S cluster is identified by the metadata or tags set
233+
by the orchestrator (Gardener or Kubernikus).
234+
"""
235+
if not k8s_instances:
236+
# There are no instances in the cluster, yet.
237+
# We allow any shard for the first instance.
238+
return True
239+
240+
def _is_hana(flavor):
241+
return flavor.name.startswith(HANA_PREFIX)
242+
243+
def _is_same_category(instance, flavor):
244+
"""Check whether instance is from the flavor's family."""
245+
if _is_hana(flavor):
246+
return _is_hana(instance.flavor)
247+
return True
248+
249+
def _instance_matches(instance):
250+
if spec_obj.availability_zone:
251+
if (instance.availability_zone !=
252+
spec_obj.availability_zone):
253+
return False
254+
return _is_same_category(instance, spec_obj.flavor)
255+
256+
k8s_hosts = set([i.host for i in k8s_instances
257+
if _instance_matches(i)])
258+
259+
return any(agg.name in host_shard_names and
260+
set(agg.hosts) & k8s_hosts
261+
for agg in host_state.aggregates)

0 commit comments

Comments
 (0)