Files
seaweedfs/weed/topology/balancer/disk_fullness.go
T
Chris Lu 77bf2a3ab0 volume.balance: gate on real physical disk usage (fixes #10160) (#10162)
* shell: add volume.balance -byDiskUsage to balance by actual data

The default balancer ranks servers by slot density, dividing used volumes by
MaxVolumeCount. When MaxVolumeCount is configured higher than the disk can hold,
a physically near-full server looks nearly empty and gets picked as the move
target, so balancing drains less-full servers onto an already-full one.

-byDiskUsage ranks servers by the actual data they hold (sum of volume sizes)
instead, so the fullest-by-data server is treated as full and balancing drains
it. It assumes comparable disk sizes per disk type and still respects each
server's free volume slots. Default behavior is unchanged.

* plumb physical disk usage into topology, gate volume.balance on it

Volume servers now report each disk's filesystem total/free bytes in the
heartbeat, and the master stores them in DiskInfo. volume.balance uses them to
skip any move target whose disk is already near full (-maxDiskUsagePercent,
default 90), so an over-configured maxVolumeCount can no longer make a
physically full server look empty and get drained onto. The gate judges each
server against its own disk, so heterogeneous disk sizes are fine; servers that
do not report bytes fall back to slot-only behavior.

Rust seaweed-volume mirrors the heartbeat reporting.

* admin: report real physical disk capacity when volume servers provide it

The dashboard estimated server capacity as maxVolumeCount * volumeSizeLimit,
which overstates it when maxVolumeCount is set higher than the disk holds.
Prefer the filesystem capacity now reported per disk, falling back to the
estimate for servers that do not report it.

* worker: gate automatic balance on physical disk fullness too

The maintenance balance worker selects the least slot-utilized server as the
move destination, so an over-configured maxVolumeCount makes a physically full
server look empty and get drained onto — the same defect as the shell command.
Now that DiskInfo carries real disk bytes, skip any destination whose disk is
at/above 90% used (per server, against its own disk); a full server can still be
a source. When every candidate destination is full, create no tasks. Servers
that do not report disk bytes are not gated.

* balance: share the physical-disk-fullness gate between shell and worker

The shell volume.balance command and the maintenance balance worker each grew
their own copy of the disk-fullness gate (targetDiskTooFull / destinationDiskTooFull)
and a maxDiskUsagePercent=90 constant. Pull both into weed/topology/balancer
(DiskTooFullAfter + DefaultMaxDiskUsagePercent) so the policy has one home and the
two balancers can't drift.

* balance: harden the physical-disk gate

Guard against a nil DiskInfo in the byte/slot lookups. Let a zero disk-capacity
report clear previously stored bytes (0 means "not reported" for bytes, unlike
maxVolumeCount), so a server that stops reporting falls back to slot-only instead
of trusting stale capacity. In the worker, charge each planned move's bytes to
its destination within a detection cycle so the gate sees a target fill up rather
than only its heartbeat-time free space. Note the per-location capacity summing
assumes one location per filesystem (the used ratio the gate relies on stays
correct regardless; absolute capacity can over-report).
2026-06-30 19:31:12 -07:00

34 lines
1.6 KiB
Go

// Package balancer holds balancing policy shared by the shell volume.balance
// command and the maintenance balance worker so the two implementations do not
// drift. It is dependency-light and takes plain values, so both the raw
// master_pb world (shell) and the ActiveTopology world (worker) can call it.
package balancer
// DefaultMaxDiskUsagePercent is the default physical-disk high-water mark for
// balancing: a move target whose disk is at or above this used percentage is
// skipped. It guards against an over-configured maxVolumeCount making a
// physically full server look under-utilized by slot count.
const DefaultMaxDiskUsagePercent = 90
// DiskTooFullAfter reports whether landing incomingBytes on a disk would push its
// physical used space to or above maxUsagePercent of totalBytes. It judges the
// disk against its own total, so heterogeneous disk sizes are handled correctly.
//
// It returns false (no opinion) when the gate is disabled (maxUsagePercent <= 0
// or >= 100) or the disk reports no capacity (totalBytes == 0), so callers fall
// back to slot-only behavior for servers that do not report disk bytes.
//
// Pass incomingBytes = 0 to test current fullness, or a volume's worth of bytes
// to test fullness after the move.
func DiskTooFullAfter(totalBytes, freeBytes, incomingBytes uint64, maxUsagePercent int) bool {
if maxUsagePercent <= 0 || maxUsagePercent >= 100 || totalBytes == 0 {
return false
}
var used uint64
if totalBytes > freeBytes {
used = totalBytes - freeBytes
}
usedAfter := float64(used) + float64(incomingBytes)
return usedAfter*100 >= float64(totalBytes)*float64(maxUsagePercent)
}