Skip to content

Commit 9c27033

Browse files
committed
hardware: Reject requests for no hyperthreads on hosts with HT
Attempting to boot an instance with 'hw:cpu_policy=dedicated' will result in a request from nova-scheduler to placement for allocation candidates with $flavor.vcpu 'PCPU' inventory. Similarly, booting an instance with 'hw:cpu_thread_policy=isolate' will result in a request for allocation candidates with 'HW_CPU_HYPERTHREADING=forbidden', i.e. hosts without hyperthreading. This has been the case since the cpu-resources feature was implemented in Train. However, as part of that work and to enable upgrades from hosts that predated Train, we also make a second request for candidates with $flavor.vcpu 'VCPU' inventory. The idea behind this is that old compute nodes would only report 'VCPU' and should be useable, and any new compute nodes that got caught up in this second request could never actually be scheduled to since there wouldn't be enough cores from 'ComputeNode.numa_topology.cells.[*].pcpuset' available to schedule to, resulting in rejection by the 'NUMATopologyFilter'. However, if a host was rejected in the first query because it reported the 'HW_CPU_HYPERTHREADING' trait, it could get picked up by the second query and would happily be scheduled to, resulting in an instance consuming 'VCPU' inventory from a host that properly supported 'PCPU' inventory. The solution is simply, though also a huge hack. If we detect that the host is using new style configuration and should be able to report 'PCPU', check if the instance asked for no hyperthreading and whether the host has it. If all are True, reject the request. Change-Id: Id39aaaac09585ca1a754b669351c86e234b89dd9 Signed-off-by: Stephen Finucane <[email protected]> Closes-Bug: #1889633
1 parent 737e0c0 commit 9c27033

File tree

4 files changed

+99
-16
lines changed

4 files changed

+99
-16
lines changed

nova/tests/functional/libvirt/test_numa_servers.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -383,12 +383,7 @@ def test_create_server_with_isolate_thread_policy_fails(self):
383383
}
384384
flavor_id = self._create_flavor(vcpu=2, extra_spec=extra_spec)
385385

386-
# FIXME(stephenfin): This should go to error status since there should
387-
# not be a host available
388-
expected_usage = {
389-
'DISK_GB': 20, 'MEMORY_MB': 2048, 'PCPU': 0, 'VCPU': 2,
390-
}
391-
self._run_build_test(flavor_id, expected_usage=expected_usage)
386+
self._run_build_test(flavor_id, end_status='ERROR')
392387

393388
def test_create_server_with_pcpu(self):
394389
"""Create a server using an explicit 'resources:PCPU' request.

nova/tests/unit/virt/test_hardware.py

Lines changed: 65 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3749,10 +3749,13 @@ def test_get_pinning_host_siblings_large_instance_odd_fit(self):
37493749
got_topo = objects.VirtCPUTopology(sockets=1, cores=5, threads=1)
37503750
self.assertEqualTopology(got_topo, inst_pin.cpu_topology)
37513751

3752+
# TODO(stephenfin): Remove when we drop support for vcpu_pin_set
37523753
def test_get_pinning_isolate_policy_too_few_fully_free_cores(self):
37533754
host_pin = objects.NUMACell(
37543755
id=0,
3755-
cpuset=set(),
3756+
# Simulate a legacy host with vcpu_pin_set configuration,
3757+
# meaning cpuset == pcpuset
3758+
cpuset=set([0, 1, 2, 3]),
37563759
pcpuset=set([0, 1, 2, 3]),
37573760
memory=4096,
37583761
memory_usage=0,
@@ -3774,10 +3777,13 @@ def test_get_pinning_isolate_policy_too_few_fully_free_cores(self):
37743777

37753778
self.assertIsNone(inst_pin)
37763779

3780+
# TODO(stephenfin): Remove when we drop support for vcpu_pin_set
37773781
def test_get_pinning_isolate_policy_no_fully_free_cores(self):
37783782
host_pin = objects.NUMACell(
37793783
id=0,
3780-
cpuset=set(),
3784+
# Simulate a legacy host with vcpu_pin_set configuration,
3785+
# meaning cpuset == pcpuset
3786+
cpuset=set([0, 1, 2, 3]),
37813787
pcpuset=set([0, 1, 2, 3]),
37823788
memory=4096,
37833789
memory_usage=0,
@@ -3799,10 +3805,13 @@ def test_get_pinning_isolate_policy_no_fully_free_cores(self):
37993805

38003806
self.assertIsNone(inst_pin)
38013807

3808+
# TODO(stephenfin): Remove when we drop support for vcpu_pin_set
38023809
def test_get_pinning_isolate_policy_fits(self):
38033810
host_pin = objects.NUMACell(
38043811
id=0,
3805-
cpuset=set(),
3812+
# Simulate a legacy host with vcpu_pin_set configuration,
3813+
# meaning cpuset == pcpuset
3814+
cpuset=set([0, 1, 2, 3]),
38063815
pcpuset=set([0, 1, 2, 3]),
38073816
memory=4096,
38083817
memory_usage=0,
@@ -3825,10 +3834,13 @@ def test_get_pinning_isolate_policy_fits(self):
38253834
got_topo = objects.VirtCPUTopology(sockets=1, cores=2, threads=1)
38263835
self.assertEqualTopology(got_topo, inst_pin.cpu_topology)
38273836

3837+
# TODO(stephenfin): Remove when we drop support for vcpu_pin_set
38283838
def test_get_pinning_isolate_policy_fits_ht_host(self):
38293839
host_pin = objects.NUMACell(
38303840
id=0,
3831-
cpuset=set(),
3841+
# Simulate a legacy host with vcpu_pin_set configuration,
3842+
# meaning cpuset == pcpuset
3843+
cpuset=set([0, 1, 2, 3]),
38323844
pcpuset=set([0, 1, 2, 3]),
38333845
memory=4096,
38343846
memory_usage=0,
@@ -3852,10 +3864,13 @@ def test_get_pinning_isolate_policy_fits_ht_host(self):
38523864
got_topo = objects.VirtCPUTopology(sockets=1, cores=2, threads=1)
38533865
self.assertEqualTopology(got_topo, inst_pin.cpu_topology)
38543866

3867+
# TODO(stephenfin): Remove when we drop support for vcpu_pin_set
38553868
def test_get_pinning_isolate_policy_fits_w_usage(self):
38563869
host_pin = objects.NUMACell(
38573870
id=0,
3858-
cpuset=set(),
3871+
# Simulate a legacy host with vcpu_pin_set configuration,
3872+
# meaning cpuset == pcpuset
3873+
cpuset=set([0, 1, 2, 3, 4, 5, 6, 7]),
38593874
pcpuset=set([0, 1, 2, 3, 4, 5, 6, 7]),
38603875
memory=4096,
38613876
memory_usage=0,
@@ -3879,6 +3894,38 @@ def test_get_pinning_isolate_policy_fits_w_usage(self):
38793894
got_topo = objects.VirtCPUTopology(sockets=1, cores=2, threads=1)
38803895
self.assertEqualTopology(got_topo, inst_pin.cpu_topology)
38813896

3897+
# TODO(stephenfin): Remove when we drop support for vcpu_pin_set
3898+
@mock.patch.object(hw, 'LOG')
3899+
def test_get_pinning_isolate_policy_bug_1889633(self, mock_log):
3900+
host_pin = objects.NUMACell(
3901+
id=0,
3902+
cpuset={0, 1, 4, 5},
3903+
pcpuset={2, 3, 6, 7},
3904+
memory=4096,
3905+
memory_usage=0,
3906+
pinned_cpus=set(),
3907+
siblings=[{0, 4}, {1, 5}, {2, 6}, {3, 7}],
3908+
mempages=[],
3909+
)
3910+
inst_pin = objects.InstanceNUMACell(
3911+
cpuset=set(),
3912+
pcpuset={0, 1},
3913+
memory=2048,
3914+
cpu_policy=fields.CPUAllocationPolicy.DEDICATED,
3915+
cpu_thread_policy=fields.CPUThreadAllocationPolicy.ISOLATE,
3916+
)
3917+
limits = objects.NUMATopologyLimits(
3918+
cpu_allocation_ratio=2, ram_allocation_ratio=2,
3919+
)
3920+
3921+
# host has hyperthreads, which means no NUMA topology should be built
3922+
inst_topo = hw._numa_fit_instance_cell(host_pin, inst_pin, limits)
3923+
self.assertIsNone(inst_topo)
3924+
self.assertIn(
3925+
'Host supports hyperthreads, but instance requested no',
3926+
mock_log.warning.call_args[0][0],
3927+
)
3928+
38823929

38833930
class CPUPinningTestCase(test.NoDBTestCase, _CPUPinningTestCaseBase):
38843931
def test_host_numa_fit_instance_to_host_single_cell(self):
@@ -4898,11 +4945,14 @@ def test_isolate_full_usage(self):
48984945

48994946
self.assertEqual(set([0, 1]), host_topo.cells[0].pinned_cpus)
49004947

4948+
# TODO(stephenfin): Remove when we drop support for vcpu_pin_set
49014949
def test_isolate_w_isolate_thread_alloc(self):
49024950
host_topo = objects.NUMATopology(cells=[
49034951
objects.NUMACell(
49044952
id=0,
4905-
cpuset=set(),
4953+
# Simulate a legacy host with vcpu_pin_set configuration,
4954+
# meaning cpuset == pcpuset
4955+
cpuset=set([0, 1, 2, 3, 4, 5]),
49064956
pcpuset=set([0, 1, 2, 3, 4, 5]),
49074957
memory=2048,
49084958
cpu_usage=0,
@@ -4925,11 +4975,14 @@ def test_isolate_w_isolate_thread_alloc(self):
49254975
self.assertEqual({0: 0, 1: 2}, inst_topo.cells[0].cpu_pinning)
49264976
self.assertEqual(set([4]), inst_topo.cells[0].cpuset_reserved)
49274977

4978+
# TODO(stephenfin): Remove when we drop support for vcpu_pin_set
49284979
def test_isolate_w_isolate_thread_alloc_usage(self):
49294980
host_topo = objects.NUMATopology(cells=[
49304981
objects.NUMACell(
49314982
id=0,
4932-
cpuset=set(),
4983+
# Simulate a legacy host with vcpu_pin_set configuration,
4984+
# meaning cpuset == pcpuset
4985+
cpuset=set([0, 1, 2, 3, 4, 5]),
49334986
pcpuset=set([0, 1, 2, 3, 4, 5]),
49344987
memory=2048,
49354988
cpu_usage=0,
@@ -5038,11 +5091,14 @@ def test_asymmetric_host(self):
50385091
self.assertEqual({0: 2, 1: 3}, inst_topo.cells[0].cpu_pinning)
50395092
self.assertEqual(set([1]), inst_topo.cells[0].cpuset_reserved)
50405093

5094+
# TODO(stephenfin): Remove when we drop support for vcpu_pin_set
50415095
def test_asymmetric_host_w_isolate_thread_alloc(self):
50425096
host_topo = objects.NUMATopology(cells=[
50435097
objects.NUMACell(
50445098
id=0,
5045-
cpuset=set(),
5099+
# Simulate a legacy host with vcpu_pin_set configuration,
5100+
# meaning cpuset == pcpuset
5101+
cpuset=set([1, 2, 3, 4, 5]),
50465102
pcpuset=set([1, 2, 3, 4, 5]),
50475103
memory=2048,
50485104
cpu_usage=0,
@@ -5052,8 +5108,7 @@ def test_asymmetric_host_w_isolate_thread_alloc(self):
50525108
mempages=[objects.NUMAPagesTopology(
50535109
size_kb=4, total=524288, used=0)])])
50545110
inst_topo = objects.InstanceNUMATopology(
5055-
emulator_threads_policy=(
5056-
fields.CPUEmulatorThreadsPolicy.ISOLATE),
5111+
emulator_threads_policy=fields.CPUEmulatorThreadsPolicy.ISOLATE,
50575112
cells=[objects.InstanceNUMACell(
50585113
id=0,
50595114
cpuset=set(), pcpuset=set([0, 1]), memory=2048,

nova/virt/hardware.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -918,6 +918,30 @@ def _get_reserved(sibling_set, vcpus_pinning, num_cpu_reserved=0,
918918
'for the isolate policy without this.')
919919
return
920920

921+
# TODO(stephenfin): Drop this when we drop support for 'vcpu_pin_set'
922+
# NOTE(stephenfin): This is total hack. We're relying on the fact that
923+
# the libvirt driver, which is the only one that currently supports
924+
# pinned CPUs, will set cpuset and pcpuset to the same value if using
925+
# legacy configuration, i.e. 'vcpu_pin_set', as part of
926+
# '_get_host_numa_topology'. They can't be equal otherwise since
927+
# 'cpu_dedicated_set' and 'cpu_shared_set' must be disjoint. Therefore,
928+
# if these are equal, the host that this NUMA cell corresponds to is
929+
# using legacy configuration and it's okay to use the old, "pin a core
930+
# and reserve its siblings" implementation of the 'isolate' policy. If
931+
# they're not, the host is using new-style configuration and we've just
932+
# hit bug #1889633
933+
if threads_per_core != 1 and host_cell.pcpuset != host_cell.cpuset:
934+
LOG.warning(
935+
"Host supports hyperthreads, but instance requested no "
936+
"hyperthreads. This should have been rejected by the "
937+
"scheduler but we likely got here due to the fallback VCPU "
938+
"query. Consider setting '[workarounds] "
939+
"disable_fallback_pcpu_query' to 'True' once hosts are no "
940+
"longer using 'vcpu_pin_set'. Refer to bug #1889633 for more "
941+
"information."
942+
)
943+
return
944+
921945
pinning = _get_pinning(
922946
1, # we only want to "use" one thread per core
923947
sibling_sets[threads_per_core],
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
fixes:
3+
- |
4+
An issue that could result in instances with the ``isolate`` thread policy
5+
(``hw:cpu_thread_policy=isolate``) being scheduled to hosts with SMT
6+
(HyperThreading) and consuming ``VCPU`` instead of ``PCPU`` has been
7+
resolved. See `bug #1889633`__ for more information.
8+
9+
.. __: https://bugs.launchpad.net/nova/+bug/1889633

0 commit comments

Comments
 (0)