From: Dominik Rusovac <d.rusovac@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [PATCH pve-ha-manager 10/15] fix #7993: manager,usage: implement weighted auto rebalancing
Date: Wed, 21 Oct 2026 10:59:27 +0200 [thread overview]
Message-ID: <20261021085932.1196302-11-d.rusovac@proxmox.com> (raw)
In-Reply-To: <20261021085932.1196302-1-d.rusovac@proxmox.com>
Include ha-auto-rebalance-cpu-weight and ha-auto-rebalance-mem-weight in
load imbalance calculation and decision making.
The weights only play into decisions in the context of automatic
rebalancing. The bruteforce method optimizes the weighted load
imbalance. For the TOPSIS method ha-auto-rebalance-cpu-weight and
ha-auto-rebalance-mem-weight are assigned to the respective average cpu
and average memory criteria. The weights of the highest cpu and highest
memory criteria of TOPSIS are not independently configurable. Nodes to
start service are scored with the same fixed (TOPSIS) weights as before.
The weights also shape how sensitively the load balancer reacts to CPU
load changes relative to memory usage changes. This is to address: "In
our case CRS has not moved any VM around because the imbalance
percentage is low (7%). Eventhough the cluster is very imbalance [sic]
CPU wise, or memory wise alone." [0]
Skip load balancing with warning if both weights are 0.
[0] https://bugzilla.proxmox.com/show_bug.cgi?id=7993
Signed-off-by: Dominik Rusovac <d.rusovac@proxmox.com>
---
src/PVE/HA/Manager.pm | 47 ++++++++++++++++---
src/PVE/HA/Usage.pm | 21 +++++----
src/PVE/HA/Usage/Basic.pm | 14 ++++--
src/PVE/HA/Usage/Dynamic.pm | 27 +++++++----
src/PVE/HA/Usage/Static.pm | 27 +++++++----
.../log.expect | 13 +----
6 files changed, 102 insertions(+), 47 deletions(-)
diff --git a/src/PVE/HA/Manager.pm b/src/PVE/HA/Manager.pm
index 7a5a685..c6818e2 100644
--- a/src/PVE/HA/Manager.pm
+++ b/src/PVE/HA/Manager.pm
@@ -119,6 +119,12 @@ sub update_crs_scheduler_mode {
$self->{crs}->{auto_rebalance}->{margin} =
($crs_cfg->{'ha-auto-rebalance-margin'} // 10) / 100.0;
+ my $default_weight = 1;
+ $self->{crs}->{auto_rebalance}->{cpu_weight} = $crs_cfg->{'ha-auto-rebalance-cpu-weight'} // $default_weight;
+ # by default TOPSIS weights mem higher than cpu
+ $default_weight = 5 if ($self->{crs}->{auto_rebalance}->{method} eq 'topsis');
+ $self->{crs}->{auto_rebalance}->{mem_weight} = $crs_cfg->{'ha-auto-rebalance-mem-weight'} // $default_weight;
+
my $old_mode = $self->{crs}->{scheduler};
my $new_mode = $crs_cfg->{ha} || 'basic';
@@ -236,8 +242,20 @@ sub load_balance {
return if $crs->{scheduler} ne 'static' && $crs->{scheduler} ne 'dynamic';
return if $self->any_resource_motion_queued_or_running();
- my ($threshold, $method, $hold_duration, $margin) =
- $auto_rebalance_opts->@{qw(threshold method hold_duration margin)};
+ my (
+ $threshold, $method, $hold_duration, $margin, $cpu_weight, $mem_weight,
+ ) =
+ $auto_rebalance_opts->@{qw(threshold method hold_duration margin cpu_weight mem_weight)};
+
+ if ($cpu_weight + $mem_weight eq 0) {
+ $haenv->log(
+ 'warning',
+ "auto-rebalance weights are both set to 0, skipping load balancing -"
+ . " set at least one to a positive value to avoid constant load"
+ . " imbalance of 0",
+ );
+ return;
+ }
# with threshold and margin both at 0% any non-zero imbalance triggers a non-improving
# migration, churning the cluster forever - skip and warn instead of silently looping.
@@ -250,7 +268,7 @@ sub load_balance {
return;
}
- my $imbalance = $online_node_usage->calculate_node_imbalance();
+ my $imbalance = $online_node_usage->calculate_node_imbalance($cpu_weight, $mem_weight);
# do not load balance unless imbalance threshold has been exceeded
# consecutively for $hold_duration calls to load_balance();
@@ -268,15 +286,24 @@ sub load_balance {
my $result;
if ($method eq 'bruteforce') {
- $result = $online_node_usage->select_best_balancing_migration($candidates);
+ $result = $online_node_usage->select_best_balancing_migration(
+ $candidates,
+ $cpu_weight,
+ $mem_weight,
+ );
} elsif ($method eq 'topsis') {
- $result = $online_node_usage->select_best_balancing_migration_topsis($candidates);
+ $result = $online_node_usage->select_best_balancing_migration_topsis(
+ $candidates,
+ $cpu_weight,
+ $mem_weight,
+ );
}
# happens if $candidates is empty or $method isn't handled above
return if !$result;
- my ($migration, $target_imbalance) = $result->@{qw(migration imbalance)};
+ my ($migration, $target_imbalance, $score) = $result->@{qw(migration imbalance score)};
+ $target_imbalance = $score if $method eq 'bruteforce';
my $relative_change = ($imbalance - $target_imbalance) / $imbalance;
return if $relative_change < $margin;
@@ -311,7 +338,13 @@ sub flush_master_status {
$ms->{node_status} = $ns->{status};
$ms->{service_status} = $ss;
$ms->{timestamp} = $haenv->get_time();
- $ms->{imbalance} = $self->{online_node_usage}->calculate_node_imbalance();
+
+ my ($cpu_weight, $mem_weight) = (
+ $self->{crs}->{auto_rebalance}->{cpu_weight},
+ $self->{crs}->{auto_rebalance}->{mem_weight},
+ );
+ $ms->{imbalance} =
+ $self->{online_node_usage}->calculate_node_imbalance($cpu_weight, $mem_weight);
$haenv->write_manager_status($ms);
}
diff --git a/src/PVE/HA/Usage.pm b/src/PVE/HA/Usage.pm
index 659ab30..fbd8800 100644
--- a/src/PVE/HA/Usage.pm
+++ b/src/PVE/HA/Usage.pm
@@ -61,42 +61,47 @@ sub remove_service_usage {
}
sub calculate_node_imbalance {
- my ($self) = @_;
+ my ($self, $cpu_weight, $mem_weight) = @_;
die "implement in subclass";
}
sub score_best_balancing_migrations {
- my ($self, $migration_candidates, $limit) = @_;
+ my ($self, $migration_candidates, $limit, $cpu_weight, $mem_weight) = @_;
die "implement in subclass";
}
sub select_best_balancing_migration {
- my ($self, $migration_candidates) = @_;
+ my ($self, $migration_candidates, $cpu_weight, $mem_weight) = @_;
- my $migrations = $self->score_best_balancing_migrations($migration_candidates, 1);
+ my $migrations =
+ $self->score_best_balancing_migrations($migration_candidates, 1, $cpu_weight, $mem_weight);
return $migrations->[0];
}
sub score_best_balancing_migrations_topsis {
- my ($self, $migration_candidates, $limit) = @_;
+ my (
+ $self, $migration_candidates, $limit, $cpu_weight, $mem_weight,
+ ) = @_;
die "implement in subclass";
}
sub select_best_balancing_migration_topsis {
- my ($self, $migration_candidates) = @_;
+ my ($self, $migration_candidates, $cpu_weight, $mem_weight) = @_;
- my $migrations = $self->score_best_balancing_migrations_topsis($migration_candidates, 1);
+ my $migrations = $self->score_best_balancing_migrations_topsis(
+ $migration_candidates, 1, $cpu_weight, $mem_weight,
+ );
return $migrations->[0];
}
# Returns a hash with $nodename => $score pairs. A lower $score is better.
sub score_nodes_to_start_service {
- my ($self, $sid) = @_;
+ my ($self, $sid, $cpu_weight, $mem_weight) = @_;
die "implement in subclass";
}
diff --git a/src/PVE/HA/Usage/Basic.pm b/src/PVE/HA/Usage/Basic.pm
index 4dce9e1..57eecd3 100644
--- a/src/PVE/HA/Usage/Basic.pm
+++ b/src/PVE/HA/Usage/Basic.pm
@@ -67,19 +67,27 @@ sub remove_service_usage {
}
sub calculate_node_imbalance {
- my ($self) = @_;
+ my ($self, $cpu_weight, $mem_weight) = @_;
return 0.0;
}
sub score_best_balancing_migrations {
- my ($self, $migration_candidates, $limit) = @_;
+ my ($self, $migration_candidates, $limit, $cpu_weight, $mem_weight) = @_;
return [];
}
sub score_best_balancing_migrations_topsis {
- my ($self, $migration_candidates, $limit) = @_;
+ my (
+ $self,
+ $migration_candidates,
+ $limit,
+ $cpu_weight,
+ $max_cpu_weight,
+ $mem_weight,
+ $max_mem_weight,
+ ) = @_;
return [];
}
diff --git a/src/PVE/HA/Usage/Dynamic.pm b/src/PVE/HA/Usage/Dynamic.pm
index 76d0fea..bd03e89 100644
--- a/src/PVE/HA/Usage/Dynamic.pm
+++ b/src/PVE/HA/Usage/Dynamic.pm
@@ -105,20 +105,22 @@ sub remove_service_usage {
}
sub calculate_node_imbalance {
- my ($self) = @_;
+ my ($self, $cpu_weight, $mem_weight) = @_;
- my $node_imbalance = eval { $self->{scheduler}->calculate_node_imbalance() };
+ my $node_imbalance =
+ eval { $self->{scheduler}->calculate_node_imbalance($cpu_weight, $mem_weight) };
$self->{haenv}->log('warning', "unable to calculate dynamic node imbalance - $@") if $@;
return $node_imbalance // 0.0;
}
sub score_best_balancing_migrations {
- my ($self, $migration_candidates, $limit) = @_;
+ my ($self, $migration_candidates, $limit, $cpu_weight, $mem_weight) = @_;
my $migrations = eval {
- $self->{scheduler}
- ->score_best_balancing_migration_candidates($migration_candidates, $limit);
+ $self->{scheduler}->score_best_balancing_migration_candidates(
+ $migration_candidates, $limit, $cpu_weight, $mem_weight,
+ );
};
$self->{haenv}->log('warning', "unable to score best balancing migration - $@") if $@;
@@ -126,11 +128,14 @@ sub score_best_balancing_migrations {
}
sub score_best_balancing_migrations_topsis {
- my ($self, $migration_candidates, $limit) = @_;
+ my (
+ $self, $migration_candidates, $limit, $cpu_weight, $mem_weight,
+ ) = @_;
my $migrations = eval {
- $self->{scheduler}
- ->score_best_balancing_migration_candidates_topsis($migration_candidates, $limit);
+ $self->{scheduler}->score_best_balancing_migration_candidates_topsis(
+ $migration_candidates, $limit, $cpu_weight, $mem_weight,
+ );
};
$self->{haenv}->log('warning', "unable to score best balancing migration - $@") if $@;
@@ -140,9 +145,13 @@ sub score_best_balancing_migrations_topsis {
sub score_nodes_to_start_service {
my ($self, $sid) = @_;
+ my ($cpu_weight, $mem_weight) = (1, 5);
+
my $score_list = eval {
my $service_usage = get_service_usage($self, $sid);
- $self->{scheduler}->score_nodes_to_start_resource($service_usage);
+ $self->{scheduler}->score_nodes_to_start_resource(
+ $service_usage, $cpu_weight, $mem_weight,
+ );
};
$self->{haenv}
->log('err', "unable to score nodes according to dynamic usage for service '$sid' - $@")
diff --git a/src/PVE/HA/Usage/Static.pm b/src/PVE/HA/Usage/Static.pm
index e67d5f5..39308ff 100644
--- a/src/PVE/HA/Usage/Static.pm
+++ b/src/PVE/HA/Usage/Static.pm
@@ -112,20 +112,22 @@ sub remove_service_usage {
}
sub calculate_node_imbalance {
- my ($self) = @_;
+ my ($self, $cpu_weight, $mem_weight) = @_;
- my $node_imbalance = eval { $self->{scheduler}->calculate_node_imbalance() };
+ my $node_imbalance =
+ eval { $self->{scheduler}->calculate_node_imbalance($cpu_weight, $mem_weight) };
$self->{haenv}->log('warning', "unable to calculate static node imbalance - $@") if $@;
return $node_imbalance // 0.0;
}
sub score_best_balancing_migrations {
- my ($self, $migration_candidates, $limit) = @_;
+ my ($self, $migration_candidates, $limit, $cpu_weight, $mem_weight) = @_;
my $migrations = eval {
- $self->{scheduler}
- ->score_best_balancing_migration_candidates($migration_candidates, $limit);
+ $self->{scheduler}->score_best_balancing_migration_candidates(
+ $migration_candidates, $limit, $cpu_weight, $mem_weight,
+ );
};
$self->{haenv}->log('warning', "unable to score best balancing migration - $@") if $@;
@@ -133,11 +135,14 @@ sub score_best_balancing_migrations {
}
sub score_best_balancing_migrations_topsis {
- my ($self, $migration_candidates, $limit) = @_;
+ my (
+ $self, $migration_candidates, $limit, $cpu_weight, $mem_weight,
+ ) = @_;
my $migrations = eval {
- $self->{scheduler}
- ->score_best_balancing_migration_candidates_topsis($migration_candidates, $limit);
+ $self->{scheduler}->score_best_balancing_migration_candidates_topsis(
+ $migration_candidates, $limit, $cpu_weight, $mem_weight,
+ );
};
$self->{haenv}->log('warning', "unable to score best balancing migration - $@") if $@;
@@ -147,9 +152,13 @@ sub score_best_balancing_migrations_topsis {
sub score_nodes_to_start_service {
my ($self, $sid) = @_;
+ my ($cpu_weight, $mem_weight) = (1, 5);
+
my $score_list = eval {
my $service_usage = get_service_usage($self, $sid);
- $self->{scheduler}->score_nodes_to_start_service($service_usage);
+ $self->{scheduler}->score_nodes_to_start_service(
+ $service_usage, $cpu_weight, $mem_weight,
+ );
};
if (my $err = $@) {
$self->{haenv}->log(
diff --git a/src/test/test-crs-dynamic-auto-rebalance-topsis3/log.expect b/src/test/test-crs-dynamic-auto-rebalance-topsis3/log.expect
index 2f19c72..95163d4 100644
--- a/src/test/test-crs-dynamic-auto-rebalance-topsis3/log.expect
+++ b/src/test/test-crs-dynamic-auto-rebalance-topsis3/log.expect
@@ -53,7 +53,7 @@ info 25 node3/lrm: service status vm:107 started
info 120 cmdlist: execute service vm:105 set-dynamic-stats cpu 7.8 mem 7912
info 120 cmdlist: execute service vm:106 set-dynamic-stats cpu 5.7 mem 8192
info 120 cmdlist: execute service vm:107 set-dynamic-stats cpu 6.0 mem 8011
-info 160 node1/crm: auto rebalance - migrate vm:105 to node2 (expected change for imbalance from 60.2% to 29.8%)
+info 160 node1/crm: auto rebalance - migrate vm:105 to node2 (expected change for imbalance from 48.2% to 24.0%)
info 160 node1/crm: got crm command: migrate vm:105 node2
info 160 node1/crm: migrate service 'vm:105' to node 'node2'
info 160 node1/crm: service 'vm:105': state changed from 'started' to 'migrate' (node = node3, target = node2)
@@ -68,7 +68,7 @@ info 220 cmdlist: execute service vm:104 set-dynamic-stats cpu 6.7 mem 8
info 220 cmdlist: execute service vm:105 set-dynamic-stats cpu 1.8 mem 1201
info 220 cmdlist: execute service vm:106 set-dynamic-stats cpu 2.1 mem 1211
info 220 cmdlist: execute service vm:107 set-dynamic-stats cpu 0.9 mem 1191
-info 260 node1/crm: auto rebalance - migrate vm:103 to node3 (expected change for imbalance from 57.3% to 30.1%)
+info 260 node1/crm: auto rebalance - migrate vm:103 to node3 (expected change for imbalance from 58.4% to 27.4%)
info 260 node1/crm: got crm command: migrate vm:103 node3
info 260 node1/crm: migrate service 'vm:103' to node 'node3'
info 260 node1/crm: service 'vm:103': state changed from 'started' to 'migrate' (node = node2, target = node3)
@@ -77,13 +77,4 @@ info 263 node2/lrm: service vm:103 - end migrate to node 'node3'
info 280 node1/crm: service 'vm:103': state changed from 'migrate' to 'started' (node = node3)
info 285 node3/lrm: starting service vm:103
info 285 node3/lrm: service status vm:103 started
-info 340 node1/crm: auto rebalance - migrate vm:105 to node1 (expected change for imbalance from 30.1% to 17.0%)
-info 340 node1/crm: got crm command: migrate vm:105 node1
-info 340 node1/crm: migrate service 'vm:105' to node 'node1'
-info 340 node1/crm: service 'vm:105': state changed from 'started' to 'migrate' (node = node2, target = node1)
-info 343 node2/lrm: service vm:105 - start migrate to node 'node1'
-info 343 node2/lrm: service vm:105 - end migrate to node 'node1'
-info 360 node1/crm: service 'vm:105': state changed from 'migrate' to 'started' (node = node1)
-info 361 node1/lrm: starting service vm:105
-info 361 node1/lrm: service status vm:105 started
info 820 hardware: exit simulation - done
--
2.47.3
next prev parent reply other threads:[~2026-10-21 9:01 UTC|newest]
Thread overview: 17+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-21 8:59 [RFC PATCH-SERIES cluster/ha-manager/manager/proxmox{,-perl-rs} 00/15] fix #7993: implement weighted auto rebalancing Dominik Rusovac
2026-10-21 8:59 ` [PATCH proxmox 01/15] resource-scheduling: scheduler: fix typos Dominik Rusovac
2026-10-21 8:59 ` [PATCH proxmox 02/15] resource-scheduling: scheduler: abstract post-migration stats logic Dominik Rusovac
2026-10-21 12:49 ` Daniel Kral
2026-10-21 8:59 ` [PATCH proxmox 03/15] fix #7993: resource-scheduling: lift to weighted load balancing Dominik Rusovac
2026-10-21 8:59 ` [PATCH proxmox 04/15] workspace,resource-scheduling: add proptest crate Dominik Rusovac
2026-10-21 8:59 ` [PATCH proxmox 05/15] resource-scheduling: scheduler: proptest weights Dominik Rusovac
2026-10-21 8:59 ` [PATCH proxmox-perl-rs 06/15] fix #7993: pve-rs: resource-scheduling: add weights to pve bindings Dominik Rusovac
2026-10-21 8:59 ` [PATCH pve-manager 07/15] fix #7993: ui: form: crs options: add weights Dominik Rusovac
2026-10-21 8:59 ` [PATCH pve-manager 08/15] ui: form: crs options: make options advanced Dominik Rusovac
2026-10-21 8:59 ` [PATCH pve-ha-manager 09/15] manager: remove obsolete argument Dominik Rusovac
2026-10-21 8:59 ` Dominik Rusovac [this message]
2026-10-21 8:59 ` [PATCH pve-ha-manager 11/15] sim: hardware: enable datacenter config changes at runtime Dominik Rusovac
2026-10-21 8:59 ` [PATCH pve-ha-manager 12/15] manager: log changes to crs settings Dominik Rusovac
2026-10-21 8:59 ` [PATCH pve-ha-manager 13/15] test: add auto rebalance with weights test cases Dominik Rusovac
2026-10-21 8:59 ` [PATCH pve-ha-manager 14/15] manager: rename update_crs_scheduler_mode Dominik Rusovac
2026-10-21 8:59 ` [PATCH pve-cluster 15/15] fix #7993: datacenter config: add ha-auto-rebalance-{cpu,mem}-weight Dominik Rusovac
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261021085932.1196302-11-d.rusovac@proxmox.com \
--to=d.rusovac@proxmox.com \
--cc=pve-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.