-
Notifications
You must be signed in to change notification settings - Fork 1.4k
NAS backup: run take-backup commands concurrently on the KVM host, bounded per host #12847
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: 4.20
Are you sure you want to change the base?
Changes from all commits
81f5dd2
b2cd0bf
4338c9c
b166bfd
e38c5c0
8514c35
da82917
f0096a6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -32,6 +32,7 @@ public class TakeBackupCommand extends Command { | |
| private List<String> volumePaths; | ||
| @LogLevel(LogLevel.Log4jLevel.Off) | ||
| private String mountOptions; | ||
| private boolean executeInSequence = false; | ||
|
|
||
| public TakeBackupCommand(String vmName, String backupPath) { | ||
| super(); | ||
|
|
@@ -89,6 +90,13 @@ public void setVolumePaths(List<String> volumePaths) { | |
|
|
||
| @Override | ||
| public boolean executeInSequence() { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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 |
||
| return true; | ||
| // Parallel by default: each backup uses its own per-VM on-NAS path, so concurrent | ||
| // runs do not contend. Operators can force sequential execution per zone via the | ||
| // backup.nas.parallel.execution.enabled setting. | ||
| return executeInSequence; | ||
| } | ||
|
|
||
| public void setExecuteInSequence(boolean executeInSequence) { | ||
| this.executeInSequence = executeInSequence; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| // Licensed to the Apache Software Foundation (ASF) under one | ||
| // or more contributor license agreements. See the NOTICE file | ||
| // distributed with this work for additional information | ||
| // regarding copyright ownership. The ASF licenses this file | ||
| // to you under the Apache License, Version 2.0 (the | ||
| // "License"); you may not use this file except in compliance | ||
| // with the License. You may obtain a copy of the License at | ||
| // | ||
| // http://www.apache.org/licenses/LICENSE-2.0 | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, | ||
| // software distributed under the License is distributed on an | ||
| // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
| // KIND, either express or implied. See the License for the | ||
| // specific language governing permissions and limitations | ||
| // under the License. | ||
| package org.apache.cloudstack.backup; | ||
|
|
||
| import org.junit.Assert; | ||
| import org.junit.Test; | ||
|
|
||
| public class TakeBackupCommandTest { | ||
|
|
||
| @Test | ||
| public void testExecuteInSequenceDefaultsToParallel() { | ||
| TakeBackupCommand command = new TakeBackupCommand("vm-1", "/backups/vm-1"); | ||
| // Default: run in parallel with other backup/delete commands. | ||
| Assert.assertFalse(command.executeInSequence()); | ||
| } | ||
|
|
||
| @Test | ||
| public void testExecuteInSequenceIsSettable() { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this tests the setter. worth testing that the setting actually reaches the command instead?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added |
||
| TakeBackupCommand command = new TakeBackupCommand("vm-1", "/backups/vm-1"); | ||
|
|
||
| command.setExecuteInSequence(true); | ||
| Assert.assertTrue(command.executeInSequence()); | ||
|
|
||
| command.setExecuteInSequence(false); | ||
| Assert.assertFalse(command.executeInSequence()); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,114 @@ | ||
| // Licensed to the Apache Software Foundation (ASF) under one | ||
| // or more contributor license agreements. See the NOTICE file | ||
| // distributed with this work for additional information | ||
| // regarding copyright ownership. The ASF licenses this file | ||
| // to you under the Apache License, Version 2.0 (the | ||
| // "License"); you may not use this file except in compliance | ||
| // with the License. You may obtain a copy of the License at | ||
| // | ||
| // http://www.apache.org/licenses/LICENSE-2.0 | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, | ||
| // software distributed under the License is distributed on an | ||
| // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
| // KIND, either express or implied. See the License for the | ||
| // specific language governing permissions and limitations | ||
| // under the License. | ||
| package org.apache.cloudstack.backup; | ||
|
|
||
| import java.lang.reflect.Field; | ||
| import java.util.concurrent.CountDownLatch; | ||
| import java.util.concurrent.TimeUnit; | ||
| import java.util.concurrent.atomic.AtomicBoolean; | ||
|
|
||
| import org.apache.cloudstack.framework.config.ConfigKey; | ||
| import org.junit.Assert; | ||
| import org.junit.Test; | ||
|
|
||
| import com.cloud.utils.exception.CloudRuntimeException; | ||
|
|
||
| public class NASBackupProviderTest { | ||
|
|
||
| private static void overrideDefaultConfigValue(final ConfigKey<?> key, final String value) throws Exception { | ||
| final Field field = ConfigKey.class.getDeclaredField("_defaultValue"); | ||
| field.setAccessible(true); | ||
| field.set(key, value); | ||
| } | ||
|
|
||
| @Test | ||
| public void executionPolicyReachesTheCommand() throws Exception { | ||
| final NASBackupProvider provider = new NASBackupProvider(); | ||
| final TakeBackupCommand command = new TakeBackupCommand("vm-1", "/backups/vm-1"); | ||
|
|
||
| overrideDefaultConfigValue(NASBackupProvider.NASBackupParallelExecution, "false"); | ||
| Assert.assertFalse(provider.applyExecutionPolicy(command, 1L)); | ||
| Assert.assertTrue("disabled setting must make the command sequential", command.executeInSequence()); | ||
|
|
||
| overrideDefaultConfigValue(NASBackupProvider.NASBackupParallelExecution, "true"); | ||
| Assert.assertTrue(provider.applyExecutionPolicy(command, 1L)); | ||
| Assert.assertFalse("enabled setting must let the command run concurrently", command.executeInSequence()); | ||
| } | ||
|
|
||
| @Test | ||
| public void perHostCapHoldsAndReleases() { | ||
| final NASBackupProvider provider = new NASBackupProvider(); | ||
| provider.acquireHostBackupSlot(7L, 2, 1); | ||
| provider.acquireHostBackupSlot(7L, 2, 1); | ||
| Assert.assertEquals(2, provider.getInFlightBackups(7L)); | ||
| try { | ||
| provider.acquireHostBackupSlot(7L, 2, 1); | ||
| Assert.fail("third backup on a host capped at 2 must not get a slot"); | ||
| } catch (CloudRuntimeException expected) { | ||
| Assert.assertTrue(expected.getMessage().contains("host 7")); | ||
| } | ||
| Assert.assertEquals(2, provider.getInFlightBackups(7L)); | ||
| provider.releaseHostBackupSlot(7L); | ||
| provider.acquireHostBackupSlot(7L, 2, 1); | ||
| Assert.assertEquals(2, provider.getInFlightBackups(7L)); | ||
| provider.releaseHostBackupSlot(7L); | ||
| provider.releaseHostBackupSlot(7L); | ||
| Assert.assertEquals(0, provider.getInFlightBackups(7L)); | ||
| } | ||
|
|
||
| @Test | ||
| public void hostsAreIndependent() { | ||
| final NASBackupProvider provider = new NASBackupProvider(); | ||
| provider.acquireHostBackupSlot(1L, 1, 1); | ||
| provider.acquireHostBackupSlot(2L, 1, 1); | ||
| Assert.assertEquals(1, provider.getInFlightBackups(1L)); | ||
| Assert.assertEquals(1, provider.getInFlightBackups(2L)); | ||
| provider.releaseHostBackupSlot(1L); | ||
| provider.releaseHostBackupSlot(2L); | ||
| } | ||
|
|
||
| @Test | ||
| public void waiterProceedsWhenASlotFrees() throws Exception { | ||
| final NASBackupProvider provider = new NASBackupProvider(); | ||
| provider.acquireHostBackupSlot(9L, 1, 10); | ||
| final CountDownLatch acquired = new CountDownLatch(1); | ||
| final AtomicBoolean failed = new AtomicBoolean(false); | ||
| final Thread waiter = new Thread(() -> { | ||
| try { | ||
| provider.acquireHostBackupSlot(9L, 1, 10); | ||
| acquired.countDown(); | ||
| } catch (CloudRuntimeException e) { | ||
| failed.set(true); | ||
| } | ||
| }); | ||
| waiter.start(); | ||
| Assert.assertFalse("waiter must block while the only slot is held", acquired.await(300, TimeUnit.MILLISECONDS)); | ||
| provider.releaseHostBackupSlot(9L); | ||
| Assert.assertTrue("waiter must get the slot once it is released", acquired.await(5, TimeUnit.SECONDS)); | ||
| Assert.assertFalse(failed.get()); | ||
| Assert.assertEquals(1, provider.getInFlightBackups(9L)); | ||
| provider.releaseHostBackupSlot(9L); | ||
| waiter.join(1000); | ||
| } | ||
|
|
||
| @Test | ||
| public void releaseOnUnknownHostIsHarmless() { | ||
| final NASBackupProvider provider = new NASBackupProvider(); | ||
| provider.releaseHostBackupSlot(404L); | ||
| Assert.assertEquals(0, provider.getInFlightBackups(404L)); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 bybackup.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.