NAS backup: run take-backup commands concurrently on the KVM host, bounded per host - #12847
NAS backup: run take-backup commands concurrently on the KVM host, bounded per host#12847jmsperu wants to merge 8 commits into
Conversation
Change executeInSequence() to return false for TakeBackupCommand and DeleteBackupCommand, allowing the KVM agent to process multiple backup/delete operations concurrently via its worker thread pool. Previously, all backup commands were serialized — a large VM backup (e.g. 100+ GB taking 2+ hours) would block all other backup and delete operations on the same host. Since each backup mounts its own temporary NFS directory and operates on independent VM disks, there is no shared state requiring serialization. Restore and PrepareForBackupRestoration commands remain sequential as they modify VM state that should not be concurrent. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## 4.20 #12847 +/- ##
============================================
+ Coverage 16.24% 16.36% +0.11%
- Complexity 13411 13597 +186
============================================
Files 5664 5669 +5
Lines 500463 501602 +1139
Branches 60779 60922 +143
============================================
+ Hits 81308 82086 +778
- Misses 410059 410313 +254
- Partials 9096 9203 +107
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
This PR changes CloudStack backup agent command scheduling by making the NAS backup take and delete commands eligible for parallel execution on the agent (instead of being forced through the single-threaded “in-sequence” queue).
Changes:
- Make
TakeBackupCommand.executeInSequence()returnfalse. - Make
DeleteBackupCommand.executeInSequence()returnfalse.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| core/src/main/java/org/apache/cloudstack/backup/TakeBackupCommand.java | Marks backup creation command as non-sequential (parallelizable) on the agent. |
| core/src/main/java/org/apache/cloudstack/backup/DeleteBackupCommand.java | Marks backup deletion command as non-sequential (parallelizable) on the agent. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
|
@JoaoJandre could you have a look at whether this change would affect KBOSS? |
@winterhazel KBOSS uses its own commands to send messages to the agent, thus this does not affect it. |
|
@jmsperu could you have a look at @abh1sar's comment at #12847 (comment)? |
|
Thanks for the ping @winterhazel — and @abh1sar's point at #12847 (comment) is well taken. DeleteBackup needs sequential execution because parallel deletes against the same NAS path can race on the cleanup of the chain (we've also seen the rm -rf vs mount/unmount interleave produce stuck mounts). Will restrict the parallelization to TakeBackup only in the next push — DeleteBackup keeps its current sequential behaviour. Thanks @JoaoJandre for the KBOSS confirmation too. |
Revert DeleteBackupCommand to executeInSequence()=true per @abh1sar's review. Backup creation is safe to parallelize across VMs because each VM writes to its own NAS path. Backup deletion is not safe to parallelize the same way: incremental chains share parent qcow2 files on the NAS, and concurrent deletions of sibling or descendant nodes can race on the underlying file references and the chain metadata. Until that's modeled explicitly, keep DeleteBackup serialized. TakeBackup parallelization stands.
|
@blueorangutan package |
|
@winterhazel a [SL] Jenkins job has been kicked to build packages. It will be bundled with KVM, XenServer and VMware SystemVM templates. I'll keep you posted as I make progress. |
|
Notes on the current red checks — all three appear unrelated to this change (which is a single
Happy to rebase to retrigger if a reviewer prefers a fresh CI run, but the change itself is unaffected by these matrices. Other 14 checks green, 8 still running. |
|
Packaging result [SF]: ✔️ el8 ✔️ el9 ✔️ el10 ✔️ debian ✔️ suse15. SL-JID 17997 |
|
@blueorangutan test |
|
@DaanHoogland a [SL] Trillian-Jenkins test job (ol8 mgmt + kvm-ol8) has been kicked to run smoke tests |
|
[SF] Trillian test result (tid-16181)
|
Per Daan's review on PR apache#12847, document inline why we return false from executeInSequence — the same reasoning previously only given in the PR thread. The comment explains the design intent (parallel dispatch with DeleteBackupCommand) and the safety case (per-VM on-NAS paths + per-script NFS mount lifecycle).
|
Does it make sense to add new global settings parameter so be able to edit this? |
…essential reasoning
…led setting (Zone, default true) to toggle parallel take-backup execution
|
@sbrueseke good suggestion — added a global setting |
…ng the executeInSequence toggle
|
@blueorangutan package |
|
@sbrueseke are you willing to test this? |
| @@ -89,6 +90,13 @@ public void setVolumePaths(List<String> volumePaths) { | |||
|
|
|||
| @Override | |||
| public boolean executeInSequence() { | |||
There was a problem hiding this comment.
if a host has 20 VMs on the same backup schedule, do all 20 get sent at once now? is there anything that caps how many run together on one host?
There was a problem hiding this comment.
Not any more. f0096a6 adds a per-host gate in the provider: at most backup.nas.parallel.max.per.host (default 2) take-backup commands are in flight on a host, and the rest wait on the management server, in the async job thread, bounded by backup.nas.parallel.queue.timeout (default 2 h). A wait that runs out fails cleanly without leaving a BackingUp row behind. So 20 VMs on one schedule on one host run two at a time.
| @@ -89,6 +90,13 @@ public void setVolumePaths(List<String> volumePaths) { | |||
|
|
|||
| @Override | |||
| public boolean executeInSequence() { | |||
There was a problem hiding this comment.
the agent has a small pool of worker threads and everything the mgmt server sends goes through it. if long backups fill it up, does anything else sent to that host wait behind them?
There was a problem hiding this comment.
That was the real gap, thank you. The cap is deliberately below the agent's default of 5 workers, so at least 3 workers stay free for start, stop, reboot and migrate at all times; an operator who raises the agent's workers can raise the cap to match. The waiting never happens on the agent side.
|
|
||
| ConfigKey<Boolean> NASBackupParallelExecution = new ConfigKey<>("Advanced", Boolean.class, | ||
| "backup.nas.parallel.execution.enabled", | ||
| "true", |
There was a problem hiding this comment.
should this default to false on 4.20? anyone upgrading gets the new behaviour without asking for it.
There was a problem hiding this comment.
With the per-host cap the default is safe by construction (3 of the 5 workers are always free), and a false default would leave the serialisation problem in place for everyone who does not read the release notes, so I have kept true. It is a one-character change and I will flip it if the maintainers prefer opt-in on 4.20; @DaanHoogland @abh1sar your call.
| "false", | ||
| "Enable volume attach/detach operations for VMs that are assigned to Backup Offerings.", true); | ||
|
|
||
| ConfigKey<Boolean> NASBackupParallelExecution = new ConfigKey<>("Advanced", Boolean.class, |
There was a problem hiding this comment.
why here and not in NASBackupProvider? it is a nas only setting and the provider already has an empty getConfigKeys.
There was a problem hiding this comment.
Agreed and moved. All three settings now live in NASBackupProvider and are published through its getConfigKeys(); BackupManager and BackupManagerImpl are back to their original shape.
| ConfigKey<Boolean> NASBackupParallelExecution = new ConfigKey<>("Advanced", Boolean.class, | ||
| "backup.nas.parallel.execution.enabled", | ||
| "true", | ||
| "Allow NAS take-backup commands to run in parallel with other backup and delete commands on the KVM host. Disable to force sequential execution.", |
There was a problem hiding this comment.
does this only affect other backup and delete commands? looks like it affects everything sent to that host, not just backups.
There was a problem hiding this comment.
Correct, and the description now says so: the command runs concurrently with every other command on the host, which is what executeInSequence=false means, bounded per host by the new cap. Restore and prepare-for-restore stay sequential because they touch VM state.
| } | ||
|
|
||
| @Test | ||
| public void testExecuteInSequenceIsSettable() { |
There was a problem hiding this comment.
this tests the setter. worth testing that the setting actually reaches the command instead?
There was a problem hiding this comment.
Added NASBackupProviderTest: it drives applyExecutionPolicy() with the setting at true and at false and asserts the command's executeInSequence() follows it, plus four tests on the per-host gate (the cap holds and releases, hosts are independent, a waiter proceeds when a slot frees, release on an unknown host is harmless). The setter test in core stays as the command-level check.
|
title and description still say the delete command is included. that was reverted, might be worth updating. |
|
All of @Damans227 comments are valid. Most important is takeBackup using up all of the 5 worker threads present on each kvm host. |
|
Thank you very much @Damans227. Should we go back to the drawing board? To me it looks like there are a lot of design questions, and right now it feels like patching holes in a carpet. Was there a design proposal in the first place? I would love to take a further look at it. Maybe we need to reconsider this feature to provide enterprise-level backup. What are your thoughts? @abh1sar @DaanHoogland |
… move the settings into the provider Parallel take-backup commands no longer compete for every agent worker thread: the NAS provider now holds a per-host slot for the whole agent round-trip and waits on the management server (the async job thread) when the host already has backup.nas.parallel.max.per.host (default 2) backups in flight, so start, stop, reboot and migrate commands are never queued behind backups. The wait is bounded by backup.nas.parallel.queue.timeout (default 7200 s) and fails cleanly with no backup row left behind. The settings are NAS-only, so they move from the BackupManager interface into NASBackupProvider.getConfigKeys(), and the enable flag's description now says what it really does: the command runs concurrently with every other command on the host, bounded per host, rather than only "with other backup commands". NASBackupProviderTest covers the setting reaching the command, the per-host cap holding and releasing, independence between hosts, a waiter proceeding once a slot frees, and release on an unknown host. Claude-Session: https://claude.ai/code/session_01MHXx4k6gi77ZCG5jx3wK4J Signed-off-by: James Peru <jmsperu@gmail.com>
|
@sbrueseke fair challenge, and the honest answer is that this PR started from a one-line observation (backups queue behind each other on a host) and grew review by review, which is exactly what "patching holes" looks like from outside. So here is the design in one place, now also in the PR description. Problem. Take-backup ran in sequence, so a host did one backup at a time and a single 2-hour VM delayed every other backup on that host. Change. Take-backup joins the agent's worker pool like start/stop already do. Unbounded, that is the problem @Damans227 and @abh1sar spotted: long backups could occupy every worker and stall start/stop/reboot/migrate on that host. The bound (pushed in f0096a6). The provider holds a per-host slot for each backup for the whole agent round-trip. When a host already has Why concurrent backups are safe. Each backup has its own temporary mount and its own QEMU block jobs on its own VM's disks; incremental state (#13074) is per VM. The shared resource is NAS bandwidth, which is what the per-host cap also throttles. Housekeeping in the same push. The settings moved into Open question for the maintainers: the default stays If the group would rather have a fuller design discussion before merging, I am happy to write this up as a discussion thread and hold the PR. My preference is to ship the bounded version: it is a smaller change than the unbounded serialisation problem it fixes, and the cap is the enterprise-grade part. |
Summary
TakeBackupCommandno longer executes in sequence on the KVM agent: NAS take-backup commands run concurrently with the host's other commands, the way start/stop already do.backup.nas.parallel.max.per.host(default 2) backups are in flight on one host; further backups wait, on the async job thread, up tobackup.nas.parallel.queue.timeout(default 7200 s).backup.nas.parallel.execution.enabled(zone, default true) is the switch back to strictly sequential behaviour.RestoreBackupCommandandPrepareForBackupRestorationCommandstay sequential (they change VM state).DeleteBackupCommandis unchanged (an earlier revision touched it; that was reverted).Problem
TakeBackupCommand.executeInSequence()wastrue, so a host ran one backup at a time and every backup queued behind the previous one. A single large VM (100+ GB, 2+ hours) delays every other backup on that host; on hosts with many VMs the schedule slips for the whole day and jobs time out.Design
Execution model. The KVM agent runs commands on a small worker pool (
workers, default 5). In-sequence commands run one at a time; everything else shares the pool. Moving take-backup out of the sequence queue fixes the queueing problem but, unbounded, would let long backups occupy every worker and stall start/stop/reboot/migrate for that host. So the change is only safe with a bound.The bound.
NASBackupProviderkeeps an in-flight counter per host. Before it sends a take-backup it takes one of the host's slots and holds it for the whole agent round-trip; when the host already hasmax.per.hostbackups running, the async job thread waits (never the agent). The default of 2 leaves at least 3 workers free on a default agent. The wait is capped byqueue.timeout; on timeout the backup fails with a clear message and no backup row is left behind.Why concurrent backups are safe on the agent. Each backup mounts its own temporary directory, runs its own QEMU block jobs against its own VM's disks, and shares no state with other backups. Incremental backups (#13074) keep bitmaps and checkpoints per VM. What backups do share is NAS bandwidth, which is why the per-host cap doubles as the throttle knob.
Controls. All three settings are zone-scoped, dynamic, and owned by the NAS provider (
getConfigKeys()), since they mean nothing to other providers.Out of scope. A cluster-wide scheduler (the backup schedule already staggers VMs), and rate limiting on the NAS itself.
Changes
core:TakeBackupCommand.executeInSequence()becomes a settable flag (default false).plugins/backup/nas: the three settings,applyExecutionPolicy(), the per-host gate (acquireHostBackupSlot/releaseHostBackupSlot), and the gate around the send intakeBackup().api/server: no NAS-specific settings any more.Tests
NASBackupProviderTest(new): the setting reaches the command; the per-host cap holds and releases; hosts are independent; a waiter proceeds once a slot frees; release on an unknown host is harmless.TakeBackupCommandTest: the command-level default and setter.🤖 Generated with Claude Code