Skip to content

Commit 8faa58f

Browse files
committed
feat(vm): memory hotplug (phase 1)
Simple implementation of the memory hotplug support. - Change memory configuration: set it in domain.memory.guest instead of setting requests and limits in domain.resources. - Support old VMs to not require reboot on module update. - Support for enabling feature gates explicitly in ModuleConfig. Dev notes: - Adding detector of kvvm.spec changes to require reboot if kvbuilder changes something. - New on-demand vmop migration prefixed as "hotplug-resource-". Signed-off-by: Ivan Mikheykin <ivan.mikheykin@flant.com>
1 parent 1d89f59 commit 8faa58f

24 files changed

Lines changed: 702 additions & 115 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: v1.6.2-v12n.21
6+
3p-kubevirt: dvp/set-memory-limits-while-hotplugging
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: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ secrets:
1515
shell:
1616
install:
1717
- |
18+
echo rebuild 33
1819
echo "Git clone {{ $gitRepoName }} repository..."
1920
git clone --depth=1 $(cat /run/secrets/SOURCE_REPO)/{{ $gitRepoUrl }} --branch {{ $tag }} /src/kubevirt
2021

images/virtualization-artifact/pkg/common/annotations/annotations.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,9 +90,10 @@ const (
9090
AnnVMRestartRequested = AnnAPIGroupV + "/vm-restart-requested"
9191

9292
// AnnVMOPWorkloadUpdate is an annotation on vmop that represents a vmop created by workload-updater controller.
93-
AnnVMOPWorkloadUpdate = AnnAPIGroupV + "/workload-update"
94-
AnnVMOPWorkloadUpdateImage = AnnAPIGroupV + "/workload-update-image"
95-
AnnVMOPWorkloadUpdateNodePlacementSum = AnnAPIGroupV + "/workload-update-node-placement-sum"
93+
AnnVMOPWorkloadUpdate = AnnAPIGroupV + "/workload-update"
94+
AnnVMOPWorkloadUpdateImage = AnnAPIGroupV + "/workload-update-image"
95+
AnnVMOPWorkloadUpdateNodePlacementSum = AnnAPIGroupV + "/workload-update-node-placement-sum"
96+
AnnVMOPWorkloadUpdateHotplugResourcesSum = AnnAPIGroupV + "/workload-update-hotplug-resources-sum"
9697
// AnnVMRestore is an annotation on a resource that indicates it was created by the vmrestore controller; the value is the UID of the `VirtualMachineRestore` resource.
9798
AnnVMRestore = AnnAPIGroupV + "/vmrestore"
9899
// AnnVMOPEvacuation is an annotation on vmop that represents a vmop created by evacuation controller

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

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import (
3333
"github.com/deckhouse/virtualization-controller/pkg/common/array"
3434
"github.com/deckhouse/virtualization-controller/pkg/common/resource_builder"
3535
"github.com/deckhouse/virtualization-controller/pkg/common/vm"
36+
"github.com/deckhouse/virtualization-controller/pkg/featuregates"
3637
"github.com/deckhouse/virtualization/api/core/v1alpha2"
3738
)
3839

@@ -46,6 +47,9 @@ const (
4647

4748
// GenericCPUModel specifies the base CPU model for Features and Discovery CPU model types.
4849
GenericCPUModel = "qemu64"
50+
51+
MaxMemorySizeForHotplug = 256 * 1024 * 1024 * 1024 // 256 Gi (safely limit to not overlap somewhat conservative 38 bit physical address space)
52+
EnableMemoryHotplugThreshold = 1 * 1024 * 1024 * 1024 // 1 Gi (no hotplug for VMs with less than 1Gi)
4953
)
5054

5155
type KVVMOptions struct {
@@ -280,7 +284,25 @@ func (b *KVVM) SetCPU(cores int, coreFraction string) error {
280284
return nil
281285
}
282286

287+
// SetMemory sets memory in kvvm.
288+
// There are 2 possibilities to set memory:
289+
// 1. Use domain.memory.guest field: it enabled memory hotplugging, but not set resources.limits.
290+
// 2. Explicitly set limits and requests in domain.resources. No hotplugging in this scenario.
291+
//
292+
// (1) is a new approach, and (2) should be respected for Running VMs started by previous version of the controller.
283293
func (b *KVVM) SetMemory(memorySize resource.Quantity) {
294+
// Support for VMs started with memory size in requests-limits.
295+
// TODO delete this in the future (around 3-4 more versions after enabling memory hotplug by default).
296+
if b.ResourceExists && isVMRunningWithMemoryResources(b.Resource) {
297+
b.setMemoryNonHotpluggable(memorySize)
298+
return
299+
}
300+
b.setMemoryHotpluggable(memorySize)
301+
}
302+
303+
// setMemoryNonHotpluggable translates memory size to requests and limits in KVVM.
304+
// Note: this is a first implementation, memory hotplug is not compatible with this strategy.
305+
func (b *KVVM) setMemoryNonHotpluggable(memorySize resource.Quantity) {
284306
res := &b.Resource.Spec.Template.Spec.Domain.Resources
285307
if res.Requests == nil {
286308
res.Requests = make(map[corev1.ResourceName]resource.Quantity)
@@ -292,6 +314,57 @@ func (b *KVVM) SetMemory(memorySize resource.Quantity) {
292314
res.Limits[corev1.ResourceMemory] = memorySize
293315
}
294316

317+
// setMemoryHotpluggable translates memory size to settings in domain.memory field.
318+
// This field is compatible with memory hotplug.
319+
// Also, remove requests-limits for memory if any.
320+
func (b *KVVM) setMemoryHotpluggable(memorySize resource.Quantity) {
321+
domain := &b.Resource.Spec.Template.Spec.Domain
322+
323+
currentMaxGuest := int64(-1)
324+
if domain.Memory != nil && domain.Memory.MaxGuest != nil {
325+
currentMaxGuest = domain.Memory.MaxGuest.Value()
326+
}
327+
328+
domain.Memory = &virtv1.Memory{
329+
Guest: &memorySize,
330+
}
331+
332+
// Set maxMemory to enable hotplug for mem size >= 1Gi.
333+
hotplugThreshold := resource.NewQuantity(EnableMemoryHotplugThreshold, resource.BinarySI)
334+
if featuregates.Default().Enabled(featuregates.HotplugMemoryWithLiveMigration) {
335+
if memorySize.Cmp(*hotplugThreshold) >= 0 {
336+
maxMemory := resource.NewQuantity(MaxMemorySizeForHotplug, resource.BinarySI)
337+
domain.Memory.MaxGuest = maxMemory
338+
}
339+
}
340+
// Set maxGuest to 0 if hotplug is disabled now (mem size < 1Gi) and maxGuest was previously set.
341+
// Zero value is just a flag to patch memory and remove maxGuest before updating kvvm.
342+
if memorySize.Cmp(*hotplugThreshold) == -1 && currentMaxGuest > 0 {
343+
domain.Memory.MaxGuest = resource.NewQuantity(0, resource.BinarySI)
344+
}
345+
346+
// Remove memory limits and requests if set by previous implementation.
347+
res := &b.Resource.Spec.Template.Spec.Domain.Resources
348+
delete(res.Requests, corev1.ResourceMemory)
349+
delete(res.Limits, corev1.ResourceMemory)
350+
}
351+
352+
func isVMRunningWithMemoryResources(kvvm *virtv1.VirtualMachine) bool {
353+
if kvvm == nil {
354+
return false
355+
}
356+
357+
if kvvm.Status.PrintableStatus != virtv1.VirtualMachineStatusRunning {
358+
return false
359+
}
360+
361+
res := kvvm.Spec.Template.Spec.Domain.Resources
362+
_, hasMemoryRequests := res.Requests[corev1.ResourceMemory]
363+
_, hasMemoryLimits := res.Limits[corev1.ResourceMemory]
364+
365+
return hasMemoryRequests && hasMemoryLimits
366+
}
367+
295368
func GetCPURequest(cores int, coreFraction string) (*resource.Quantity, error) {
296369
if coreFraction == "" {
297370
return GetCPULimit(cores), nil
@@ -484,7 +557,7 @@ func (b *KVVM) SetProvisioning(p *v1alpha2.Provisioning) error {
484557
}
485558
}
486559

487-
func (b *KVVM) SetOsType(osType v1alpha2.OsType) error {
560+
func (b *KVVM) SetOSType(osType v1alpha2.OsType) error {
488561
switch osType {
489562
case v1alpha2.Windows:
490563
// Need for `029-use-OFVM_CODE-for-linux.patch`

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -119,25 +119,25 @@ func TestSetAffinity(t *testing.T) {
119119
}
120120
}
121121

122-
func TestSetOsType(t *testing.T) {
122+
func TestSetOSType(t *testing.T) {
123123
name := "test-name"
124124
namespace := "test-namespace"
125125

126126
t.Run("Change from Windows to Generic should remove TPM", func(t *testing.T) {
127127
builder := NewEmptyKVVM(types.NamespacedName{Name: name, Namespace: namespace}, KVVMOptions{})
128128

129-
err := builder.SetOsType(v1alpha2.Windows)
129+
err := builder.SetOSType(v1alpha2.Windows)
130130
if err != nil {
131-
t.Fatalf("SetOsType(Windows) failed: %v", err)
131+
t.Fatalf("SetOSType(Windows) failed: %v", err)
132132
}
133133

134134
if builder.Resource.Spec.Template.Spec.Domain.Devices.TPM == nil {
135135
t.Error("TPM should be present after setting Windows OS")
136136
}
137137

138-
err = builder.SetOsType(v1alpha2.GenericOs)
138+
err = builder.SetOSType(v1alpha2.GenericOs)
139139
if err != nil {
140-
t.Fatalf("SetOsType(GenericOs) failed: %v", err)
140+
t.Fatalf("SetOSType(GenericOs) failed: %v", err)
141141
}
142142

143143
if builder.Resource.Spec.Template.Spec.Domain.Devices.TPM != nil {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ func ApplyVirtualMachineSpec(
9999
if err := kvvm.SetRunPolicy(vm.Spec.RunPolicy); err != nil {
100100
return err
101101
}
102-
if err := kvvm.SetOsType(vm.Spec.OsType); err != nil {
102+
if err := kvvm.SetOSType(vm.Spec.OsType); err != nil {
103103
return err
104104
}
105105
if err := kvvm.SetBootloader(vm.Spec.Bootloader); err != nil {

images/virtualization-artifact/pkg/controller/reconciler/reconciler.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package reconciler
1919
import (
2020
"context"
2121
"errors"
22+
"fmt"
2223
"reflect"
2324
"strings"
2425
"time"
@@ -102,7 +103,8 @@ handlersLoop:
102103
switch {
103104
case err == nil: // OK.
104105
case errors.Is(err, ErrStopHandlerChain):
105-
log.Debug("Handler chain execution stopped")
106+
msg := fmt.Sprintf("Handler %s stopped chain execution", name)
107+
log.Debug(msg)
106108
result = MergeResults(result, res)
107109
break handlersLoop
108110
case k8serrors.IsConflict(err):

0 commit comments

Comments
 (0)