The ProxLB Solver is a mathematically exact scheduler for Proxmox VE clusters. It uses Google's OR-Tools CP-SAT to find the provably global optimum for VM and Container placement, moving beyond simple greedy heuristics.
graph TD
A[Input: Cluster Snapshot] --> B{Validator}
B -- Conflict detected --> C[Status: RULE_CONFLICT]
B -- OK --> D[CP-SAT Modeling]
subgraph Solver [Optimization Loop]
D --> E[Solver: Find Optimum]
E -- Infeasible --> F[Status: INFEASIBLE]
E -- Optimal State --> G[Planner: Calculate Path]
G -- Blocked cycle? --> H{Reachable?}
H -- No --> I[Add No-Good Clause]
I --> E
H -- Yes --> J[Solution: Optimal & Executable]
end
The solver treats guest placement as an Integer Linear Programming (ILP) problem.
For every guest
-
$x_{i,j} = 1$ : Guest$i$ is assigned to node$j$ . -
$x_{i,j} = 0$ : Guest$i$ is not assigned to node$j$ .
Every guest must be assigned to exactly one node:
CP-SAT is an integer-only solver. To handle fractional values (like 0.5 CPU cores or 12.5% PSI), the solver internally scales all metrics by a factor of 10,000 (_LOAD_SCALE).
- 100% load is represented as 10,000.
- 0.5% PSI is represented as 50.
- This scaling explains the large objective values seen in technical solver logs.
The solver minimizes a weighted cost function:
-
Spread: The difference between the most and least utilized node (
$\text{Max} - \text{Min}$ ), scaled by total capacity. - MigrationCost: A weighted sum over all migrated guests — see §2 below.
-
Penalty: A massive malus (
$1{,}000{,}000$ ) for every violated soft constraint.
Migrating a guest has a real cost: RAM must be copied live over the network (dirty-page tracking); local disk requires a full sequential copy. The cost model reflects this:
The 256 MiB base unit gives enough granularity for the solver to distinguish between a 512 MiB guest (cost 2) and a 1 GiB guest (cost 4). The max(1, …) floor ensures tiny containers still have a non-zero weight.
The 4× local disk factor reflects that copying a local disk (LVM/ZFS) is significantly slower than a RAM live-migration.
ProxLB supports multiple optimization dimensions via the method parameter:
| Method | Balance objective | Use Case |
|---|---|---|
memory |
RAM allocation | Classic memory-based balancing (default). |
cpu |
CPU load (cores) | Throughput optimization. |
disk |
Local storage usage | Disk capacity balancing across nodes. |
cpu_psi |
CPU stall time (PSI) | Latency optimization (PVE 9+). |
cpu_smart |
CPU load + PSI | Balance of throughput and responsiveness. |
global_smart |
RAM + CPU + IO | Holistic cluster-wide optimization. |
The solver balances RAM by configured allocation, not actual RSS. This is correct for capacity planning — a VM configured with 4 GiB must be placed on a node that has 4 GiB reserved, regardless of its current usage.
- Used Mode (Default): Balances based on the actual measured CPU load (
cpu_used). - Assigned Mode: Balances based on the configured number of vCPUs. This ensures that reserved compute capacity is distributed evenly.
For PSI metrics, the solver uses an additive footprint model. It tries to spread these contributions so that the aggregate pressure on each node stays as low and uniform as possible.
Optimization is fine-tuned via three distinct tiers:
- Global Level (
w_global_*): Importance of resource pools (e.g., "RAM balance is 10x more important than IO"). - Resource Level (
w_*_usagevsw_*_psi): Weighting raw utilization against dynamic pressure stalls. - Guest Level (
priority):- Priority 3 (High): Footprint counts 3× towards the spread calculation.
- Priority 1 (Low): Footprint counts 1×.
- Effect: High-priority guests "force" their way onto nodes with the most free resources by artificially inflating their perceived load during the optimization phase.
Violations result in INFEASIBLE.
- Capacity: RAM, CPU cores (with overcommit), and named storage pools (ZFS, LVM).
- Pinning: Binding guests to specific hardware. Pinning is always hard.
- Maintenance: Nodes in maintenance mode are forbidden targets.
- Hard Rules: Affinity/Anti-Affinity marked as
hard: true.
The solver distinguishes between rules based on their origin:
| Origin | Type | Handling | Rationale |
|---|---|---|---|
pve |
Native HA | Atomic / Strict | Proxmox enforces these rules automatically. |
plb |
Internal Tags | Granular / Soft | ProxLB manages these; allows flexible transitions. |
- PVE Affinity (Atomic): Members of a native Proxmox affinity group are moved in the same execution step.
- PVE Anti-Affinity (Strict): The planner ensures partners never share a node even for a split second during transitions.
An optimal state is worthless if it cannot be executed (e.g., no buffer space for a swap). The Solver finds the target state, but the Planner determines the safe path to get there.
-
Dependency Analysis: The Planner builds a directed graph where an edge
$A \to B$ means "Guest A must move before Guest B can fit on its target node". - Step-by-Step Simulation: Migrations are grouped into execution steps. For every step, the Planner simulates node capacities to ensure that no host is oversubscribed even during the transition.
-
Cycle Breaking (Temp-Moves): Circular dependencies (e.g., Guest-A
$\leftrightarrow$ Guest-B swap) are detected. If a third "spare" node with sufficient capacity exists, the Planner inserts a temp-move (parking) to break the loop. - Atomic PVE Affinity: For native Proxmox affinity groups, the Planner picks one "trigger" guest for the API call. It assumes Proxmox will co-migrate the other group members atomically and accounts for their total resource footprint in that single step.
- Strict PVE Anti-Affinity: To satisfy native PVE anti-affinity, the Planner ensures that partners never share a node, even temporarily. One must fully vacate the target before the other is allowed to land.
- No-Good Feedback: If a dependency cycle is mathematically unbreakable (e.g., cluster is too full for parking), the Planner rejects the solution. The Solver is then re-triggered with a "No-Good" constraint to find the next-best reachable state.
The solver integrates with ProxLB via two operating modes:
The solver runs alongside ProxLB's built-in balancer without changing anything. It produces a structured JSONL log and an HTML report for validation.
The solver takes over execution. ProxLB's Balancing() class is still used for API calls, but the solver determines the plan. A feedback loop handles migration failures by pinning failed guests and re-solving.
The ProxLB Solver is tuned for Stability over Agility by default.
max_node_inflow(Default: 1): Only one guest at a time can migrate into a host. This prevents memory or CPU peaks that could trigger OOM on the target host.max_parallel_migrations(Default: 2): Limits simultaneous migrations cluster-wide.balanciness(Default: 3 — Moderate):- Level 1–2: Only moves guests for maintenance or hard rule violations.
- Level 3: Rebalances only if the spread exceeds ~15%.
- Level 5: Chases perfect balance.
method(Default:memory): RAM is usually the hardest bottleneck. Start with memory balancing before exploring CPU or Smart modes.cpu_overcommit(Default: 2.0): Allows assigning more vCPUs than physical cores exist.
make installmake testIntegration results and shadow-mode comparisons can be visualized as interactive reports:
make reportThis produces the following artifacts in the project root:
results.html: Full interactive report with sidebar, Mermaid dependency graphs, and per-run detail pages.results.md: A concise Markdown summary of all processed runs.results.xml: JUnit-compatible XML for CI/CD integration.
Individual run logs are stored as .jsonl files in the configured log_dir (e.g., /tmp/proxlb-solver-logs).
models.py: Strict type definitions for the cluster state.adapter.py: Bridge between ProxLB's runtime data and the solver models.solver.py: Mathematical model and CP-SAT integration.planner.py: Topological sort and dependency resolution for migrations.shadow.py: Non-intrusive "shadow mode" for live cluster observation.