Skip to content

Commit ccbae16

Browse files
authored
feat(scheduler): allow for node overprovisioning (#47)
`ProxmoxCluster.spec.SchedulerHints.MemoryAdjustment=300` allows to (theoretically) allocate 300% of a host's memory for VMs (use with caution - enabling memory ballooning highly recommended) `ProxmoxCluster.spec.SchedulerHints.MemoryAdjustment=95` allows to limit memory allocation to 95% of a host's memory `ProxmoxCluster.spec.SchedulerHints.MemoryAdjustment=0` entirely disables scheduling memory constraints
1 parent 5e2c0b6 commit ccbae16

File tree

9 files changed

+193
-65
lines changed

9 files changed

+193
-65
lines changed

api/v1alpha1/proxmoxcluster_types.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2222
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
2323

24+
"k8s.io/utils/ptr"
2425
ipamicv1 "sigs.k8s.io/cluster-api-ipam-provider-in-cluster/api/v1alpha2"
2526
)
2627

@@ -44,6 +45,11 @@ type ProxmoxClusterSpec struct {
4445
// +optional
4546
AllowedNodes []string `json:"allowedNodes,omitempty"`
4647

48+
// SchedulerHints allows to influence the decision on where a VM will be scheduled. For example by applying a multiplicator
49+
// to a node's resources, to allow for overprovisioning or to ensure a node will always have a safety buffer.
50+
// +optional
51+
SchedulerHints *SchedulerHints `json:"schedulerHints,omitempty"`
52+
4753
// IPv4Config contains information about available IPV4 address pools and the gateway.
4854
// this can be combined with ipv6Config in order to enable dual stack.
4955
// either IPv4Config or IPv6Config must be provided.
@@ -63,6 +69,28 @@ type ProxmoxClusterSpec struct {
6369
DNSServers []string `json:"dnsServers"`
6470
}
6571

72+
// SchedulerHints allows to pass the scheduler instructions to (dis)allow over- or enforce underprovisioning of resources.
73+
type SchedulerHints struct {
74+
// MemoryAdjustment allows to adjust a node's memory by a given percentage.
75+
// For example, setting it to 300 allows to allocate 300% of a host's memory for VMs,
76+
// and setting it to 95 limits memory allocation to 95% of a host's memory.
77+
// Setting it to 0 entirely disables scheduling memory constraints.
78+
// By default 100% of a node's memory will be used for allocation.
79+
// +optional
80+
MemoryAdjustment *uint64 `json:"memoryAdjustment,omitempty"`
81+
}
82+
83+
// GetMemoryAdjustment returns the memory adjustment percentage to use within the scheduler.
84+
func (sh *SchedulerHints) GetMemoryAdjustment() uint64 {
85+
memoryAdjustment := uint64(100)
86+
87+
if sh != nil {
88+
memoryAdjustment = ptr.Deref(sh.MemoryAdjustment, 100)
89+
}
90+
91+
return memoryAdjustment
92+
}
93+
6694
// ProxmoxClusterStatus defines the observed state of ProxmoxCluster.
6795
type ProxmoxClusterStatus struct {
6896
// Ready indicates that the cluster is ready.

api/v1alpha1/zz_generated.deepcopy.go

Lines changed: 25 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/crd/bases/infrastructure.cluster.x-k8s.io_proxmoxclusters.yaml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,22 @@ spec:
131131
x-kubernetes-validations:
132132
- message: IPv6Config addresses must be provided
133133
rule: self.addresses.size() > 0
134+
schedulerHints:
135+
description: SchedulerHints allows to influence the decision on where
136+
a VM will be scheduled. For example by applying a multiplicator
137+
to a node's resources, to allow for overprovisioning or to ensure
138+
a node will always have a safety buffer.
139+
properties:
140+
memoryAdjustment:
141+
description: MemoryAdjustment allows to adjust a node's memory
142+
by a given percentage. For example, setting it to 300 allows
143+
to allocate 300% of a host's memory for VMs, and setting it
144+
to 95 limits memory allocation to 95% of a host's memory. Setting
145+
it to 0 entirely disables scheduling memory constraints. By
146+
default 100% of a node's memory will be used for allocation.
147+
format: int64
148+
type: integer
149+
type: object
134150
required:
135151
- dnsServers
136152
type: object

internal/service/scheduler/vmscheduler.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,13 @@ func (err InsufficientMemoryError) Error() string {
4747
func ScheduleVM(ctx context.Context, machineScope *scope.MachineScope) (string, error) {
4848
client := machineScope.InfraCluster.ProxmoxClient
4949
allowedNodes := machineScope.InfraCluster.ProxmoxCluster.Spec.AllowedNodes
50+
schedulerHints := machineScope.InfraCluster.ProxmoxCluster.Spec.SchedulerHints
5051
locations := machineScope.InfraCluster.ProxmoxCluster.Status.NodeLocations.Workers
5152
if util.IsControlPlaneMachine(machineScope.Machine) {
5253
locations = machineScope.InfraCluster.ProxmoxCluster.Status.NodeLocations.ControlPlane
5354
}
5455

55-
return selectNode(ctx, client, machineScope.ProxmoxMachine, locations, allowedNodes)
56+
return selectNode(ctx, client, machineScope.ProxmoxMachine, locations, allowedNodes, schedulerHints)
5657
}
5758

5859
func selectNode(
@@ -61,10 +62,11 @@ func selectNode(
6162
machine *infrav1.ProxmoxMachine,
6263
locations []infrav1.NodeLocation,
6364
allowedNodes []string,
65+
schedulerHints *infrav1.SchedulerHints,
6466
) (string, error) {
6567
byMemory := make(sortByAvailableMemory, len(allowedNodes))
6668
for i, nodeName := range allowedNodes {
67-
mem, err := client.GetReservableMemoryBytes(ctx, nodeName)
69+
mem, err := client.GetReservableMemoryBytes(ctx, nodeName, schedulerHints.GetMemoryAdjustment())
6870
if err != nil {
6971
return "", err
7072
}
@@ -119,7 +121,7 @@ func selectNode(
119121
}
120122

121123
type resourceClient interface {
122-
GetReservableMemoryBytes(context.Context, string) (uint64, error)
124+
GetReservableMemoryBytes(context.Context, string, uint64) (uint64, error)
123125
}
124126

125127
type nodeInfo struct {

internal/service/scheduler/vmscheduler_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import (
2727

2828
type fakeResourceClient map[string]uint64
2929

30-
func (c fakeResourceClient) GetReservableMemoryBytes(_ context.Context, nodeName string) (uint64, error) {
30+
func (c fakeResourceClient) GetReservableMemoryBytes(_ context.Context, nodeName string, _ uint64) (uint64, error) {
3131
return c[nodeName], nil
3232
}
3333

@@ -62,7 +62,7 @@ func TestSelectNode(t *testing.T) {
6262

6363
client := fakeResourceClient(availableMem)
6464

65-
node, err := selectNode(context.Background(), client, proxmoxMachine, locations, allowedNodes)
65+
node, err := selectNode(context.Background(), client, proxmoxMachine, locations, allowedNodes, &infrav1.SchedulerHints{})
6666
require.NoError(t, err)
6767
require.Equal(t, expectedNode, node)
6868

@@ -82,7 +82,7 @@ func TestSelectNode(t *testing.T) {
8282

8383
client := fakeResourceClient(availableMem)
8484

85-
node, err := selectNode(context.Background(), client, proxmoxMachine, locations, allowedNodes)
85+
node, err := selectNode(context.Background(), client, proxmoxMachine, locations, allowedNodes, &infrav1.SchedulerHints{})
8686
require.ErrorAs(t, err, &InsufficientMemoryError{})
8787
require.Empty(t, node)
8888

pkg/proxmox/client.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ type Client interface {
3737

3838
GetTask(ctx context.Context, upID string) (*proxmox.Task, error)
3939

40-
GetReservableMemoryBytes(ctx context.Context, nodeName string) (uint64, error)
40+
GetReservableMemoryBytes(ctx context.Context, nodeName string, nodeMemoryAdjustment uint64) (uint64, error)
4141

4242
ResizeDisk(ctx context.Context, vm *proxmox.VirtualMachine, disk, size string) error
4343

pkg/proxmox/goproxmox/api_client.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -181,13 +181,17 @@ func (c *APIClient) GetTask(ctx context.Context, upID string) (*proxmox.Task, er
181181
}
182182

183183
// GetReservableMemoryBytes returns the memory that can be reserved by a new VM, in bytes.
184-
func (c *APIClient) GetReservableMemoryBytes(ctx context.Context, nodeName string) (uint64, error) {
184+
func (c *APIClient) GetReservableMemoryBytes(ctx context.Context, nodeName string, nodeMemoryAdjustment uint64) (uint64, error) {
185185
node, err := c.Client.Node(ctx, nodeName)
186186
if err != nil {
187187
return 0, fmt.Errorf("cannot find node with name %s: %w", nodeName, err)
188188
}
189189

190-
reservableMemory := node.Memory.Total
190+
reservableMemory := uint64(float64(node.Memory.Total) / 100 * float64(nodeMemoryAdjustment))
191+
192+
if nodeMemoryAdjustment == 0 {
193+
return node.Memory.Total, nil
194+
}
191195

192196
vms, err := node.VirtualMachines(ctx)
193197
if err != nil {

pkg/proxmox/goproxmox/api_client_test.go

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,53 @@ func newJSONResponder(status int, data any) httpmock.Responder {
4848

4949
func TestProxmoxAPIClient_GetReservableMemoryBytes(t *testing.T) {
5050
tests := []struct {
51-
name string
52-
maxMem uint64
53-
expect uint64
51+
name string
52+
maxMem uint64 // memory size of already provisioned guest
53+
expect uint64 // expected available memory of the host
54+
nodeMemoryAdjustment uint64 // factor like 1.0 to multiply host memory with for overprovisioning
5455
}{
55-
{name: "under zero", maxMem: 29, expect: 1},
56-
{name: "exact zero", maxMem: 30, expect: 0},
57-
{name: "over zero", maxMem: 31, expect: 0},
56+
{
57+
name: "under zero - no overprovisioning",
58+
maxMem: 29,
59+
expect: 1,
60+
nodeMemoryAdjustment: 100,
61+
},
62+
{
63+
name: "exact zero - no overprovisioning",
64+
maxMem: 30,
65+
expect: 0,
66+
nodeMemoryAdjustment: 100,
67+
},
68+
{
69+
name: "over zero - no overprovisioning",
70+
maxMem: 31,
71+
expect: 0,
72+
nodeMemoryAdjustment: 100,
73+
},
74+
{
75+
name: "under zero - overprovisioning",
76+
maxMem: 58,
77+
expect: 2,
78+
nodeMemoryAdjustment: 200,
79+
},
80+
{
81+
name: "exact zero - overprovisioning",
82+
maxMem: 30 * 2,
83+
expect: 0,
84+
nodeMemoryAdjustment: 200,
85+
},
86+
{
87+
name: "over zero - overprovisioning",
88+
maxMem: 31 * 2,
89+
expect: 0,
90+
nodeMemoryAdjustment: 200,
91+
},
92+
{
93+
name: "scheduler disabled",
94+
maxMem: 100,
95+
expect: 30,
96+
nodeMemoryAdjustment: 0,
97+
},
5898
}
5999

60100
for _, test := range tests {
@@ -104,7 +144,7 @@ func TestProxmoxAPIClient_GetReservableMemoryBytes(t *testing.T) {
104144
httpmock.RegisterResponder(http.MethodGet, `=~/nodes/test/lxc`,
105145
newJSONResponder(200, proxmox.Containers{}))
106146

107-
reservable, err := client.GetReservableMemoryBytes(context.Background(), "test")
147+
reservable, err := client.GetReservableMemoryBytes(context.Background(), "test", test.nodeMemoryAdjustment)
108148
require.NoError(t, err)
109149
require.Equal(t, test.expect, reservable)
110150
})

0 commit comments

Comments
 (0)