'isolated' CPU allocation thread policy is guarantee that no vCPUs from other guests wouldn't be able to be placed on the cores of booted VM (In this case core is a set of sibling vCPUs). But we still able to boot VMs with 'dedicated' CPU allocation policy on these cores. This problem is actual for hosts without HyperThreading. In this case sets of siblings vCPUs are empty for each core but we are still trying to work with them as with HyperThreading cores. This causes the problem when one "isolated" core is used by several VMs. To fix it we must use method unpin_cpus_with_siblings() only if NUMA cell has siblings (i.e. has HyperThreading). For cells without HyperThreading CPU isolation is guaranteed by 'dedicated' CPU allocation policy. Closes-Bug: #1635674 Change-Id: I8f72187153c930cd941b7ee7e835a20ed0c0de03
286 lines
9.8 KiB
Python
286 lines
9.8 KiB
Python
# Copyright 2014 Red Hat Inc.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
from oslo_serialization import jsonutils
|
|
from oslo_utils import versionutils
|
|
|
|
from nova import exception
|
|
from nova.objects import base
|
|
from nova.objects import fields
|
|
from nova.virt import hardware
|
|
|
|
|
|
def all_things_equal(obj_a, obj_b):
|
|
for name in obj_a.fields:
|
|
set_a = obj_a.obj_attr_is_set(name)
|
|
set_b = obj_b.obj_attr_is_set(name)
|
|
if set_a != set_b:
|
|
return False
|
|
elif not set_a:
|
|
continue
|
|
|
|
if getattr(obj_a, name) != getattr(obj_b, name):
|
|
return False
|
|
return True
|
|
|
|
|
|
@base.NovaObjectRegistry.register
|
|
class NUMACell(base.NovaObject):
|
|
# Version 1.0: Initial version
|
|
# Version 1.1: Added pinned_cpus and siblings fields
|
|
# Version 1.2: Added mempages field
|
|
VERSION = '1.2'
|
|
|
|
fields = {
|
|
'id': fields.IntegerField(read_only=True),
|
|
'cpuset': fields.SetOfIntegersField(),
|
|
'memory': fields.IntegerField(),
|
|
'cpu_usage': fields.IntegerField(default=0),
|
|
'memory_usage': fields.IntegerField(default=0),
|
|
'pinned_cpus': fields.SetOfIntegersField(),
|
|
'siblings': fields.ListOfSetsOfIntegersField(),
|
|
'mempages': fields.ListOfObjectsField('NUMAPagesTopology'),
|
|
}
|
|
|
|
def __eq__(self, other):
|
|
return all_things_equal(self, other)
|
|
|
|
def __ne__(self, other):
|
|
return not (self == other)
|
|
|
|
@property
|
|
def free_cpus(self):
|
|
return self.cpuset - self.pinned_cpus or set()
|
|
|
|
@property
|
|
def free_siblings(self):
|
|
return [sibling_set & self.free_cpus
|
|
for sibling_set in self.siblings]
|
|
|
|
@property
|
|
def avail_cpus(self):
|
|
return len(self.free_cpus)
|
|
|
|
@property
|
|
def avail_memory(self):
|
|
return self.memory - self.memory_usage
|
|
|
|
def pin_cpus(self, cpus):
|
|
if cpus - self.cpuset:
|
|
raise exception.CPUPinningUnknown(requested=list(cpus),
|
|
cpuset=list(self.pinned_cpus))
|
|
if self.pinned_cpus & cpus:
|
|
raise exception.CPUPinningInvalid(requested=list(cpus),
|
|
free=list(self.cpuset -
|
|
self.pinned_cpus))
|
|
self.pinned_cpus |= cpus
|
|
|
|
def unpin_cpus(self, cpus):
|
|
if cpus - self.cpuset:
|
|
raise exception.CPUUnpinningUnknown(requested=list(cpus),
|
|
cpuset=list(self.pinned_cpus))
|
|
if (self.pinned_cpus & cpus) != cpus:
|
|
raise exception.CPUUnpinningInvalid(requested=list(cpus),
|
|
pinned=list(self.pinned_cpus))
|
|
self.pinned_cpus -= cpus
|
|
|
|
def pin_cpus_with_siblings(self, cpus):
|
|
# NOTE(snikitin): Empty siblings list means that HyperThreading is
|
|
# disabled on the NUMA cell and we must pin CPUs like normal CPUs.
|
|
if not self.siblings:
|
|
self.pin_cpus(cpus)
|
|
return
|
|
|
|
pin_siblings = set()
|
|
for sib in self.siblings:
|
|
if cpus & sib:
|
|
pin_siblings.update(sib)
|
|
self.pin_cpus(pin_siblings)
|
|
|
|
def unpin_cpus_with_siblings(self, cpus):
|
|
# NOTE(snikitin): Empty siblings list means that HyperThreading is
|
|
# disabled on the NUMA cell and we must unpin CPUs like normal CPUs.
|
|
if not self.siblings:
|
|
self.unpin_cpus(cpus)
|
|
return
|
|
|
|
pin_siblings = set()
|
|
for sib in self.siblings:
|
|
if cpus & sib:
|
|
pin_siblings.update(sib)
|
|
self.unpin_cpus(pin_siblings)
|
|
|
|
def _to_dict(self):
|
|
return {
|
|
'id': self.id,
|
|
'cpus': hardware.format_cpu_spec(
|
|
self.cpuset, allow_ranges=False),
|
|
'mem': {
|
|
'total': self.memory,
|
|
'used': self.memory_usage},
|
|
'cpu_usage': self.cpu_usage}
|
|
|
|
@classmethod
|
|
def _from_dict(cls, data_dict):
|
|
cpuset = hardware.parse_cpu_spec(
|
|
data_dict.get('cpus', ''))
|
|
cpu_usage = data_dict.get('cpu_usage', 0)
|
|
memory = data_dict.get('mem', {}).get('total', 0)
|
|
memory_usage = data_dict.get('mem', {}).get('used', 0)
|
|
cell_id = data_dict.get('id')
|
|
return cls(id=cell_id, cpuset=cpuset, memory=memory,
|
|
cpu_usage=cpu_usage, memory_usage=memory_usage,
|
|
mempages=[], pinned_cpus=set([]), siblings=[])
|
|
|
|
def can_fit_hugepages(self, pagesize, memory):
|
|
"""Returns whether memory can fit into hugepages size
|
|
|
|
:param pagesize: a page size in KibB
|
|
:param memory: a memory size asked to fit in KiB
|
|
|
|
:returns: whether memory can fit in hugepages
|
|
:raises: MemoryPageSizeNotSupported if page size not supported
|
|
"""
|
|
for pages in self.mempages:
|
|
if pages.size_kb == pagesize:
|
|
return (memory <= pages.free_kb and
|
|
(memory % pages.size_kb) == 0)
|
|
raise exception.MemoryPageSizeNotSupported(pagesize=pagesize)
|
|
|
|
|
|
@base.NovaObjectRegistry.register
|
|
class NUMAPagesTopology(base.NovaObject):
|
|
# Version 1.0: Initial version
|
|
# Version 1.1: Adds reserved field
|
|
VERSION = '1.1'
|
|
|
|
fields = {
|
|
'size_kb': fields.IntegerField(),
|
|
'total': fields.IntegerField(),
|
|
'used': fields.IntegerField(default=0),
|
|
'reserved': fields.IntegerField(default=0),
|
|
}
|
|
|
|
def obj_make_compatible(self, primitive, target_version):
|
|
super(NUMAPagesTopology, self).obj_make_compatible(primitive,
|
|
target_version)
|
|
target_version = versionutils.convert_version_to_tuple(target_version)
|
|
if target_version < (1, 1):
|
|
primitive.pop('reserved', None)
|
|
|
|
def __eq__(self, other):
|
|
return all_things_equal(self, other)
|
|
|
|
def __ne__(self, other):
|
|
return not (self == other)
|
|
|
|
@property
|
|
def free(self):
|
|
"""Returns the number of avail pages."""
|
|
if not self.obj_attr_is_set('reserved'):
|
|
# In case where an old compute node is sharing resource to
|
|
# an updated node we must ensure that this property is defined.
|
|
self.reserved = 0
|
|
return self.total - self.used - self.reserved
|
|
|
|
@property
|
|
def free_kb(self):
|
|
"""Returns the avail memory size in KiB."""
|
|
return self.free * self.size_kb
|
|
|
|
|
|
@base.NovaObjectRegistry.register
|
|
class NUMATopology(base.NovaObject):
|
|
# Version 1.0: Initial version
|
|
# Version 1.1: Update NUMACell to 1.1
|
|
# Version 1.2: Update NUMACell to 1.2
|
|
VERSION = '1.2'
|
|
|
|
fields = {
|
|
'cells': fields.ListOfObjectsField('NUMACell'),
|
|
}
|
|
|
|
@classmethod
|
|
def obj_from_primitive(cls, primitive, context=None):
|
|
if 'nova_object.name' in primitive:
|
|
obj_topology = super(NUMATopology, cls).obj_from_primitive(
|
|
primitive, context=context)
|
|
else:
|
|
# NOTE(sahid): This compatibility code needs to stay until we can
|
|
# guarantee that there are no cases of the old format stored in
|
|
# the database (or forever, if we can never guarantee that).
|
|
obj_topology = NUMATopology._from_dict(primitive)
|
|
return obj_topology
|
|
|
|
def _to_json(self):
|
|
return jsonutils.dumps(self.obj_to_primitive())
|
|
|
|
@classmethod
|
|
def obj_from_db_obj(cls, db_obj):
|
|
return cls.obj_from_primitive(
|
|
jsonutils.loads(db_obj))
|
|
|
|
def __len__(self):
|
|
"""Defined so that boolean testing works the same as for lists."""
|
|
return len(self.cells)
|
|
|
|
def _to_dict(self):
|
|
# TODO(sahid): needs to be removed.
|
|
return {'cells': [cell._to_dict() for cell in self.cells]}
|
|
|
|
@classmethod
|
|
def _from_dict(cls, data_dict):
|
|
return cls(cells=[
|
|
NUMACell._from_dict(cell_dict)
|
|
for cell_dict in data_dict.get('cells', [])])
|
|
|
|
|
|
@base.NovaObjectRegistry.register
|
|
class NUMATopologyLimits(base.NovaObject):
|
|
# Version 1.0: Initial version
|
|
VERSION = '1.0'
|
|
|
|
fields = {
|
|
'cpu_allocation_ratio': fields.FloatField(),
|
|
'ram_allocation_ratio': fields.FloatField(),
|
|
}
|
|
|
|
def to_dict_legacy(self, host_topology):
|
|
cells = []
|
|
for cell in host_topology.cells:
|
|
cells.append(
|
|
{'cpus': hardware.format_cpu_spec(
|
|
cell.cpuset, allow_ranges=False),
|
|
'mem': {'total': cell.memory,
|
|
'limit': cell.memory * self.ram_allocation_ratio},
|
|
'cpu_limit': len(cell.cpuset) * self.cpu_allocation_ratio,
|
|
'id': cell.id})
|
|
return {'cells': cells}
|
|
|
|
@classmethod
|
|
def obj_from_db_obj(cls, db_obj):
|
|
if 'nova_object.name' in db_obj:
|
|
obj_topology = cls.obj_from_primitive(db_obj)
|
|
else:
|
|
# NOTE(sahid): This compatibility code needs to stay until we can
|
|
# guarantee that all compute nodes are using RPC API => 3.40.
|
|
cell = db_obj['cells'][0]
|
|
ram_ratio = cell['mem']['limit'] / float(cell['mem']['total'])
|
|
cpu_ratio = cell['cpu_limit'] / float(len(hardware.parse_cpu_spec(
|
|
cell['cpus'])))
|
|
obj_topology = NUMATopologyLimits(
|
|
cpu_allocation_ratio=cpu_ratio,
|
|
ram_allocation_ratio=ram_ratio)
|
|
return obj_topology
|