Skip to content

Commit f389570

Browse files
committed
feat(vm): hotplug cpu (phase 1) (#2147)
* feat(vm): hotplug cpu (phase 1) Simple implementation of CPU hotplug. - Change cpu cores setting from domain.resources/limits to domain.cpu fields. Change cores count instead of sockets. - Support old VMs to not require reboot on module update. - Requires changes in Kubevirt: deckhouse/3p-kubevirt#82 --------- Signed-off-by: Ivan Mikheykin <ivan.mikheykin@flant.com>
1 parent 8faa58f commit f389570

19 files changed

Lines changed: 507 additions & 140 deletions

File tree

build/components/versions.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ firmware:
33
libvirt: v10.9.0
44
edk2: stable202411
55
core:
6-
3p-kubevirt: dvp/set-memory-limits-while-hotplugging
6+
3p-kubevirt: v1.6.2-v12n.23
77
3p-containerized-data-importer: v1.60.3-v12n.18
88
distribution: 2.8.3
99
package:

images/virt-artifact/werf.inc.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ secrets:
1515
shell:
1616
install:
1717
- |
18-
echo rebuild 33
1918
echo "Git clone {{ $gitRepoName }} repository..."
2019
git clone --depth=1 $(cat /run/secrets/SOURCE_REPO)/{{ $gitRepoUrl }} --branch {{ $tag }} /src/kubevirt
2120

images/virtualization-artifact/pkg/controller/kvbuilder/kvvm.go

Lines changed: 112 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import (
2020
"fmt"
2121
"maps"
2222
"os"
23+
"strconv"
24+
"strings"
2325

2426
corev1 "k8s.io/api/core/v1"
2527
"k8s.io/apimachinery/pkg/api/resource"
@@ -52,6 +54,16 @@ const (
5254
EnableMemoryHotplugThreshold = 1 * 1024 * 1024 * 1024 // 1 Gi (no hotplug for VMs with less than 1Gi)
5355
)
5456

57+
const (
58+
// VCPUTopologyDynamicCoresAnnotation annotation indicates "distributed by sockets" or "dynamic cores number" VCPU topology.
59+
VCPUTopologyDynamicCoresAnnotation = "internal.virtualization.deckhouse.io/vcpu-topology-dynamic-cores"
60+
61+
CPUResourcesRequestsFractionAnnotation = "internal.virtualization.deckhouse.io/cpu-resources-requests-fraction"
62+
63+
// CPUMaxCoresPerSocket is a maximum number of cores per socket.
64+
CPUMaxCoresPerSocket = 16
65+
)
66+
5567
type KVVMOptions struct {
5668
EnableParavirtualization bool
5769
OsType v1alpha2.OsType
@@ -258,6 +270,17 @@ func (b *KVVM) SetTopologySpreadConstraint(topology []corev1.TopologySpreadConst
258270
}
259271

260272
func (b *KVVM) SetCPU(cores int, coreFraction string) error {
273+
// Support for VMs started with cpu configuration in requests-limits.
274+
// TODO delete this in the future (around 3-4 more versions after enabling cpu hotplug by default).
275+
if b.ResourceExists && isVMRunningWithCPUResources(b.Resource) {
276+
return b.setCPUNonHotpluggable(cores, coreFraction)
277+
}
278+
return b.setCPUHotpluggable(cores, coreFraction)
279+
}
280+
281+
// setCPUNonHotpluggable translates cpu configuration to requests and limit in KVVM.
282+
// Note: this is a first implementation, cpu hotplug is not compatible with this strategy.
283+
func (b *KVVM) setCPUNonHotpluggable(cores int, coreFraction string) error {
261284
domainSpec := &b.Resource.Spec.Template.Spec.Domain
262285
if domainSpec.CPU == nil {
263286
domainSpec.CPU = &virtv1.CPU{}
@@ -266,6 +289,7 @@ func (b *KVVM) SetCPU(cores int, coreFraction string) error {
266289
if err != nil {
267290
return err
268291
}
292+
269293
cpuLimit := GetCPULimit(cores)
270294
if domainSpec.Resources.Requests == nil {
271295
domainSpec.Resources.Requests = make(map[corev1.ResourceName]resource.Quantity)
@@ -284,6 +308,38 @@ func (b *KVVM) SetCPU(cores int, coreFraction string) error {
284308
return nil
285309
}
286310

311+
// setCPUHotpluggable translates cpu configuration to settings in domain.cpu field.
312+
// This field is compatible with memory hotplug.
313+
// Also, remove requests-limits for memory if any.
314+
// Note: we swap cores and sockets to bypass vm-validation webhook.
315+
func (b *KVVM) setCPUHotpluggable(cores int, coreFraction string) error {
316+
domainSpec := &b.Resource.Spec.Template.Spec.Domain
317+
if domainSpec.CPU == nil {
318+
domainSpec.CPU = &virtv1.CPU{}
319+
}
320+
321+
fraction, err := GetCPUFraction(coreFraction)
322+
if err != nil {
323+
return err
324+
}
325+
b.SetKVVMIAnnotation(CPUResourcesRequestsFractionAnnotation, strconv.Itoa(fraction))
326+
327+
socketsNeeded, coresPerSocketNeeded := vm.CalculateCoresAndSockets(cores)
328+
// Use "dynamic cores" hotplug strategy.
329+
// Workaround: swap cores and sockets in domainSpec to bypass vm-validator webhook.
330+
b.SetKVVMIAnnotation(VCPUTopologyDynamicCoresAnnotation, "")
331+
domainSpec.CPU.Cores = uint32(socketsNeeded)
332+
domainSpec.CPU.Sockets = uint32(coresPerSocketNeeded)
333+
domainSpec.CPU.MaxSockets = CPUMaxCoresPerSocket
334+
335+
// Remove CPU limits and requests if set by previous implementation.
336+
res := &b.Resource.Spec.Template.Spec.Domain.Resources
337+
delete(res.Requests, corev1.ResourceCPU)
338+
delete(res.Limits, corev1.ResourceCPU)
339+
340+
return nil
341+
}
342+
287343
// SetMemory sets memory in kvvm.
288344
// There are 2 possibilities to set memory:
289345
// 1. Use domain.memory.guest field: it enabled memory hotplugging, but not set resources.limits.
@@ -293,7 +349,7 @@ func (b *KVVM) SetCPU(cores int, coreFraction string) error {
293349
func (b *KVVM) SetMemory(memorySize resource.Quantity) {
294350
// Support for VMs started with memory size in requests-limits.
295351
// TODO delete this in the future (around 3-4 more versions after enabling memory hotplug by default).
296-
if b.ResourceExists && isVMRunningWithMemoryResources(b.Resource) {
352+
if b.ResourceExists && shouldKeepMemoryNonHotpluggable(b.Resource) {
297353
b.setMemoryNonHotpluggable(memorySize)
298354
return
299355
}
@@ -349,7 +405,7 @@ func (b *KVVM) setMemoryHotpluggable(memorySize resource.Quantity) {
349405
delete(res.Limits, corev1.ResourceMemory)
350406
}
351407

352-
func isVMRunningWithMemoryResources(kvvm *virtv1.VirtualMachine) bool {
408+
func isVMRunningWithCPUResources(kvvm *virtv1.VirtualMachine) bool {
353409
if kvvm == nil {
354410
return false
355411
}
@@ -359,10 +415,61 @@ func isVMRunningWithMemoryResources(kvvm *virtv1.VirtualMachine) bool {
359415
}
360416

361417
res := kvvm.Spec.Template.Spec.Domain.Resources
362-
_, hasMemoryRequests := res.Requests[corev1.ResourceMemory]
363-
_, hasMemoryLimits := res.Limits[corev1.ResourceMemory]
418+
_, hasCPURequests := res.Requests[corev1.ResourceCPU]
419+
_, hasCPULimits := res.Limits[corev1.ResourceCPU]
420+
421+
return hasCPURequests && hasCPULimits
422+
}
423+
424+
func shouldKeepMemoryNonHotpluggable(kvvm *virtv1.VirtualMachine) bool {
425+
if kvvm == nil {
426+
return false
427+
}
428+
429+
if kvvm.Status.PrintableStatus == virtv1.VirtualMachineStatusRunning || kvvm.Status.PrintableStatus == virtv1.VirtualMachineStatusMigrating {
430+
// Running or Migrating machines with memory resources should keep as non-hotpluggable.
431+
// Machines without memory resources should proceed as hotpluggable.
432+
res := kvvm.Spec.Template.Spec.Domain.Resources
433+
_, hasMemoryRequests := res.Requests[corev1.ResourceMemory]
434+
_, hasMemoryLimits := res.Limits[corev1.ResourceMemory]
364435

365-
return hasMemoryRequests && hasMemoryLimits
436+
return hasMemoryRequests && hasMemoryLimits
437+
}
438+
439+
// Proceed as hotpluggable if machine is not Running or Migrating.
440+
return false
441+
}
442+
443+
func GetCPUFraction(cpuFraction string) (int, error) {
444+
if cpuFraction == "" {
445+
return 100, nil
446+
}
447+
fraction := intstr.FromString(cpuFraction)
448+
value, _, err := getIntOrPercentValueSafely(&fraction)
449+
if err != nil {
450+
return 0, fmt.Errorf("invalid value for cpu fraction: %w", err)
451+
}
452+
return value, nil
453+
}
454+
455+
func getIntOrPercentValueSafely(intOrStr *intstr.IntOrString) (int, bool, error) {
456+
switch intOrStr.Type {
457+
case intstr.Int:
458+
return intOrStr.IntValue(), false, nil
459+
case intstr.String:
460+
s := intOrStr.StrVal
461+
if !strings.HasSuffix(s, "%") {
462+
return 0, false, fmt.Errorf("invalid type: string is not a percentage")
463+
}
464+
s = strings.TrimSuffix(intOrStr.StrVal, "%")
465+
466+
v, err := strconv.Atoi(s)
467+
if err != nil {
468+
return 0, false, fmt.Errorf("invalid value %q: %w", intOrStr.StrVal, err)
469+
}
470+
return v, true, nil
471+
}
472+
return 0, false, fmt.Errorf("invalid type: neither int nor percentage")
366473
}
367474

368475
func GetCPURequest(cores int, coreFraction string) (*resource.Quantity, error) {

0 commit comments

Comments
 (0)