From 7931bc6b1e81493d87120bf8b269a15b88fa73dd Mon Sep 17 00:00:00 2001 From: Brad House Date: Thu, 10 Sep 2026 16:56:35 +0000 Subject: [PATCH 1/2] Give a system VM one NIC queue per CPU A system VM gets a single NIC queue however many CPUs it has, so every packet interrupt lands on CPU0. Adding CPUs to a router does not move more packets, which is the only reason to add them. Queue count is already a VM detail, nic.multiqueue.number, and the agent already turns it into . A user VM sets it three ways: deployVirtualMachine nicmultiqueuenumber=N updateVirtualMachine details[0].nic.multiqueue.number=N the Settings tab, listDetailOptions offers the key A system VM goes through none of them, so there is no way to set it at all. This gives one queue per CPU when nothing is set. 1 CPU no queues attribute, unchanged 4 CPU queues='4' 512 CPU queues='256', a tap device goes no higher The default offering is a single CPU, so a default install is unchanged. Only a resized system VM differs, and it differs the way the resize asked for. A queue number set on the VM still wins. Signed-off-by: Brad House --- .../cloud/hypervisor/HypervisorGuruBase.java | 34 ++++++++ .../com/cloud/hypervisor/KVMGuruTest.java | 78 +++++++++++++++++++ 2 files changed, 112 insertions(+) diff --git a/server/src/main/java/com/cloud/hypervisor/HypervisorGuruBase.java b/server/src/main/java/com/cloud/hypervisor/HypervisorGuruBase.java index d40b5b226986..d6ac247a1b72 100644 --- a/server/src/main/java/com/cloud/hypervisor/HypervisorGuruBase.java +++ b/server/src/main/java/com/cloud/hypervisor/HypervisorGuruBase.java @@ -16,6 +16,7 @@ // under the License. package com.cloud.hypervisor; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; @@ -73,6 +74,7 @@ import com.cloud.storage.Volume; import com.cloud.utils.Pair; import com.cloud.utils.component.AdapterBase; +import com.cloud.vm.VmDetailConstants; import com.cloud.vm.NicProfile; import com.cloud.vm.NicVO; import com.cloud.vm.UserVmManager; @@ -101,6 +103,8 @@ public abstract class HypervisorGuruBase extends AdapterBase implements Hypervis @Inject private DataCenterDao dcDao; @Inject + private static final int MAX_TAP_QUEUES = 256; + private NetworkOfferingDetailsDao networkOfferingDetailsDao; @Inject protected @@ -265,6 +269,34 @@ protected void addServiceOfferingExtraConfiguration(ServiceOffering offering, Vi } } + /** + * Gives a system VM one NIC queue per CPU. The guest only ever uses as many queues as it has + * CPUs, so this is what the CPU count already implies, and a system VM is only given more CPUs + * to move more packets. + * + * A user VM sets this per VM with deployVirtualMachine or updateVirtualMachine. A system VM + * goes through neither, so there is otherwise no way for it to get a queue at all. + * + * The default offering is a single CPU, so nothing changes until an operator resizes. + */ + protected void addDefaultNicQueuesForSystemVm(VirtualMachineTO to) { + if (to.getType() == null || !to.getType().isUsedBySystem()) { + return; + } + Map details = to.getDetails(); + if (details != null && details.containsKey(VmDetailConstants.NIC_MULTIQUEUE_NUMBER)) { + return; + } + // A tap device stops at 256 queues, and the host refuses the VM rather than trimming. + int queues = Math.min(to.getCpus(), MAX_TAP_QUEUES); + if (queues < 2) { + return; + } + Map updated = details == null ? new HashMap<>() : new HashMap<>(details); + updated.put(VmDetailConstants.NIC_MULTIQUEUE_NUMBER, String.valueOf(queues)); + to.setDetails(updated); + } + protected VirtualMachineTO toVirtualMachineTO(VirtualMachineProfile vmProfile) { ServiceOffering offering = serviceOfferingDao.findById(vmProfile.getId(), vmProfile.getServiceOfferingId()); VirtualMachine vm = vmProfile.getVirtualMachine(); @@ -332,6 +364,8 @@ protected VirtualMachineTO toVirtualMachineTO(VirtualMachineProfile vmProfile) { addExtraConfig(detailsInVm, to, vm.getAccountId(), vm.getHypervisorType()); } + addDefaultNicQueuesForSystemVm(to); + addServiceOfferingExtraConfiguration(offering, to); // Set GPU details diff --git a/server/src/test/java/com/cloud/hypervisor/KVMGuruTest.java b/server/src/test/java/com/cloud/hypervisor/KVMGuruTest.java index d94f9db0c99c..3da2fb5a46e8 100644 --- a/server/src/test/java/com/cloud/hypervisor/KVMGuruTest.java +++ b/server/src/test/java/com/cloud/hypervisor/KVMGuruTest.java @@ -29,6 +29,7 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.Mockito; @@ -51,6 +52,7 @@ import com.cloud.storage.dao.GuestOSDao; import com.cloud.storage.dao.GuestOSHypervisorDao; import com.cloud.utils.Pair; +import com.cloud.vm.VmDetailConstants; import com.cloud.vm.VirtualMachine; import com.cloud.vm.VirtualMachineProfile; @@ -506,4 +508,80 @@ public void testGetNullWhenVMThereIsNoInformationOfUsedHosts() { Assert.assertNull(clusterId); } + + private VirtualMachineTO systemVmTO(int cpus, Map details) { + VirtualMachineTO to = Mockito.mock(VirtualMachineTO.class); + Mockito.when(to.getType()).thenReturn(VirtualMachine.Type.DomainRouter); + Mockito.when(to.getCpus()).thenReturn(cpus); + Mockito.when(to.getDetails()).thenReturn(details); + return to; + } + + @SuppressWarnings("unchecked") + private Map capturedDetails(VirtualMachineTO to) { + ArgumentCaptor> captor = ArgumentCaptor.forClass(Map.class); + Mockito.verify(to).setDetails(captor.capture()); + return captor.getValue(); + } + + @Test + public void testSystemVmGetsAQueuePerCpu() { + VirtualMachineTO to = systemVmTO(4, null); + + guru.addDefaultNicQueuesForSystemVm(to); + + Assert.assertEquals("4", capturedDetails(to).get(VmDetailConstants.NIC_MULTIQUEUE_NUMBER)); + } + + @Test + public void testSystemVmQueuesStopAtTheTapLimit() { + VirtualMachineTO to = systemVmTO(512, null); + + guru.addDefaultNicQueuesForSystemVm(to); + + Assert.assertEquals("256", capturedDetails(to).get(VmDetailConstants.NIC_MULTIQUEUE_NUMBER)); + } + + @Test + public void testSingleCpuSystemVmIsLeftAlone() { + VirtualMachineTO to = systemVmTO(1, null); + + guru.addDefaultNicQueuesForSystemVm(to); + + Mockito.verify(to, Mockito.never()).setDetails(Mockito.anyMap()); + } + + @Test + public void testExistingDetailsAreKept() { + Map existing = new HashMap<>(); + existing.put(VmDetailConstants.ROOT_DISK_CONTROLLER, "virtio"); + VirtualMachineTO to = systemVmTO(4, existing); + + guru.addDefaultNicQueuesForSystemVm(to); + + Map details = capturedDetails(to); + Assert.assertEquals("4", details.get(VmDetailConstants.NIC_MULTIQUEUE_NUMBER)); + Assert.assertEquals("virtio", details.get(VmDetailConstants.ROOT_DISK_CONTROLLER)); + } + + @Test + public void testAQueueNumberAlreadySetWins() { + Map set = new HashMap<>(); + set.put(VmDetailConstants.NIC_MULTIQUEUE_NUMBER, "2"); + VirtualMachineTO to = systemVmTO(8, set); + + guru.addDefaultNicQueuesForSystemVm(to); + + Mockito.verify(to, Mockito.never()).setDetails(Mockito.anyMap()); + } + + @Test + public void testUserVmsAreNotTouched() { + VirtualMachineTO to = Mockito.mock(VirtualMachineTO.class); + Mockito.when(to.getType()).thenReturn(VirtualMachine.Type.User); + + guru.addDefaultNicQueuesForSystemVm(to); + + Mockito.verify(to, Mockito.never()).setDetails(Mockito.anyMap()); + } } From 0d63a7607840b2c5d3f6dc31b6c893fbffeaf7d7 Mon Sep 17 00:00:00 2001 From: Brad House Date: Thu, 10 Sep 2026 23:52:46 +0000 Subject: [PATCH 2/2] Restore the @Inject that MAX_TAP_QUEUES displaced MAX_TAP_QUEUES was declared between the @Inject and networkOfferingDetailsDao, so the annotation bound to the constant and the DAO was never injected. getNicDetails() dereferences it for every NIC, so every VM deploy failed with an NPE behind a 530 "Internal Server Error", which is what the simulator CI run was reporting. Move the constant above the injected fields and give the DAO its annotation back. Signed-off-by: Brad House --- .../main/java/com/cloud/hypervisor/HypervisorGuruBase.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/com/cloud/hypervisor/HypervisorGuruBase.java b/server/src/main/java/com/cloud/hypervisor/HypervisorGuruBase.java index d6ac247a1b72..36cd8be1905a 100644 --- a/server/src/main/java/com/cloud/hypervisor/HypervisorGuruBase.java +++ b/server/src/main/java/com/cloud/hypervisor/HypervisorGuruBase.java @@ -88,6 +88,8 @@ public abstract class HypervisorGuruBase extends AdapterBase implements HypervisorGuru, Configurable { + private static final int MAX_TAP_QUEUES = 256; + @Inject protected NicDao nicDao; @@ -103,8 +105,6 @@ public abstract class HypervisorGuruBase extends AdapterBase implements Hypervis @Inject private DataCenterDao dcDao; @Inject - private static final int MAX_TAP_QUEUES = 256; - private NetworkOfferingDetailsDao networkOfferingDetailsDao; @Inject protected