Re: [PATCH ha-manager v3 35/40] implement automatic rebalancing

Daniel Kral
2026-03-31T13:50:23Z

On Mon Mar 30, 2026 at 4:30 PM CEST, Daniel Kral wrote:
> +sub load_balance {
> + my ($self) = @_;
> +
> + my ($crs, $haenv, $online_node_usage) = $self->@{qw(crs haenv online_node_usage)};
> + my ($auto_rebalance_opts) = $crs->{auto_rebalance};
> +
> + return if !$auto_rebalance_opts->{enable};
> + return if $crs->{scheduler} ne 'static' && $crs->{scheduler} ne 'dynamic';

We do not implement the load balancing related methods for
PVE::HA::Usage::Basic.

If for some reason recompute_online_node_usage() fallbacks to use
PVE::HA::Usage::Basic instead of the selected 'static' or 'dynamic' crs
mode, then this guarantee here is wrong and the first call (here
$online_node_usage->calculate_node_imbalance()) will fail.

recompute_online_node_usage() could easily fail e.g. if for 'dynamic'
one node does not have pvestatd running.
PVE::HA::Usage::Dynamic::add_node() will fail then, because there's no
recent node usage data for all nodes to correctly represent the cluster
node usage.

I'll fix this either by changing the $self->{crs}->{mode} value as we
fallback or at least having sensible implementations for Basic as well,
such as 0.0 for node_imbalance and empty lists for the
score_best_balancing_migrations{,_topsis}() methods, or even both.

> + 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 $imbalance = $online_node_usage->calculate_node_imbalance();
> +
> + # do not load balance unless imbalance threshold has been exceeded
> + # consecutively for $hold_duration calls to load_balance()
> + if ($imbalance < $threshold) {
> + $self->{sustained_imbalance_round} = 0;
> + return;
> + } else {
> + $self->{sustained_imbalance_round}++;
> + return if $self->{sustained_imbalance_round} < $hold_duration;
> + $self->{sustained_imbalance_round} = 0;
> + }
> +
> + my $candidates = $self->get_resource_migration_candidates();
> +
> + my $result;
> + if ($method eq 'bruteforce') {
> + $result = $online_node_usage->select_best_balancing_migration($candidates);
> + } elsif ($method eq 'topsis') {
> + $result = $online_node_usage->select_best_balancing_migration_topsis($candidates);
> + }
> +
> + # happens if $candidates is empty or $method isn't handled above
> + return if !$result;
> +
> + my ($migration, $target_imbalance) = $result->@{qw(migration imbalance)};
> +
> + my $relative_change = ($imbalance - $target_imbalance) / $imbalance;
> + return if $relative_change < $margin;
> +
> + my ($sid, $source, $target) = $migration->@{qw(sid source-node target-node)};
> +
> + my (undef, $type, $id) = $haenv->parse_sid($sid);
> + my $task = $type eq 'vm' ? "migrate" : "relocate";
> + my $cmd = "$task $sid $target";
> +
> + my $target_imbalance_str = int(100 * $target_imbalance + 0.5) / 100;
> + $haenv->log(
> + 'info',
> + "auto rebalance - $task $sid to $target (expected target imbalance: $target_imbalance_str)",
> + );
> +
> + $self->queue_resource_motion($cmd, $task, $sid, $target);
> +}