mirror of
https://github.com/cloud-hypervisor/cloud-hypervisor.git
synced 2026-08-05 02:19:16 +00:00
vmm: migration: Converge based on user-provided downtime and timeout
Wire the new `downtime_ms`, `timeout_s`, and `timeout_strategy` fields from `VmSendMigrationData` into the precopy loop, replacing the previous hard-coded 5-iteration cap. Each iteration now evaluates three convergence criteria in order: - no dirty pages remain; - the estimated final-iteration downtime is within the configured budget - or the overall migration timeout has elapsed. On timeout, `TimeoutStrategy::Cancel` aborts and keeps the VM live on the source, while `TimeoutStrategy::Force` proceeds regardless of the downtime target. The convergence callback is updated to return a Result to propagate the cancel error cleanly up the call stack. With the recent changes [0], it is fairly easy to implement the new checks and operate on actual metrics. These changes are inspired by [1] but differ significantly in details. [0] https://github.com/cloud-hypervisor/cloud-hypervisor/pull/7799 [1] https://github.com/cloud-hypervisor/cloud-hypervisor/pull/7033 Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de> On-behalf-of: SAP philipp.schuster@sap.com
This commit is contained in:
committed by
Rob Bradford
parent
bbb0f083b0
commit
49e03c8bc5
@@ -40,9 +40,9 @@ pub struct MemoryMigrationContext {
|
||||
///
|
||||
/// Please note that this ignores any additional migration overhead and
|
||||
/// only looks at the memory transfer itself.
|
||||
estimated_downtime: Option<Duration>,
|
||||
pub estimated_downtime: Option<Duration>,
|
||||
/// Begin of the memory migration.
|
||||
migration_begin: Instant,
|
||||
pub migration_begin: Instant,
|
||||
/// Duration of the memory migration.
|
||||
///
|
||||
/// This is only `None` until the last iteration is finished.
|
||||
|
||||
115
vmm/src/lib.rs
115
vmm/src/lib.rs
@@ -13,6 +13,7 @@ use std::panic::AssertUnwindSafe;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::mpsc::{Receiver, RecvError, SendError, Sender};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
#[cfg(not(target_arch = "riscv64"))]
|
||||
use std::time::Instant;
|
||||
use std::{io, result, thread};
|
||||
@@ -48,8 +49,8 @@ use vmm_sys_util::signal::unblock_signal;
|
||||
use vmm_sys_util::sock_ctrl_msg::ScmSocket;
|
||||
|
||||
use crate::api::{
|
||||
ApiRequest, ApiResponse, RequestHandler, VmInfoResponse, VmReceiveMigrationData,
|
||||
VmSendMigrationData, VmmPingResponse,
|
||||
ApiRequest, ApiResponse, RequestHandler, TimeoutStrategy, VmInfoResponse,
|
||||
VmReceiveMigrationData, VmSendMigrationData, VmmPingResponse,
|
||||
};
|
||||
use crate::config::{MemoryRestoreMode, RestoreConfig, add_to_config};
|
||||
#[cfg(all(target_arch = "x86_64", feature = "guest_debug"))]
|
||||
@@ -1258,7 +1259,7 @@ impl Vmm {
|
||||
vm: &mut Vm,
|
||||
socket: &mut SocketStream,
|
||||
ctx: &mut MemoryMigrationContext,
|
||||
is_converged: impl Fn(&MemoryMigrationContext) -> bool,
|
||||
is_converged: impl Fn(&MemoryMigrationContext) -> result::Result<bool, MigratableError>,
|
||||
) -> result::Result<MemoryRangeTable /* remaining */, MigratableError> {
|
||||
loop {
|
||||
let iteration_begin = Instant::now();
|
||||
@@ -1271,7 +1272,7 @@ impl Vmm {
|
||||
};
|
||||
|
||||
ctx.update_metrics_before_transfer(iteration_begin, &iteration_table);
|
||||
if is_converged(ctx) {
|
||||
if is_converged(ctx)? {
|
||||
debug!("Precopy converged: {ctx}");
|
||||
break Ok(iteration_table);
|
||||
}
|
||||
@@ -1299,6 +1300,95 @@ impl Vmm {
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks whether the precopy memory migration has converged and it is safe
|
||||
/// to proceed to the final (paused) memory iteration.
|
||||
///
|
||||
/// Once this returns, the VM is expected to stop as soon as possible.
|
||||
///
|
||||
/// Convergence is reached when any of the following criteria is met:
|
||||
///
|
||||
/// 1. **No dirty pages remain** – the current iteration would transfer zero
|
||||
/// bytes.
|
||||
/// 2. **Downtime budget is met** – the estimated downtime for the final
|
||||
/// (paused) iteration is within the caller-specified
|
||||
/// [`VmSendMigrationData::downtime`] budget.
|
||||
/// 3. **Timeout** – the precopy phase has been running for at least
|
||||
/// [`VmSendMigrationData::timeout`]. The outcome depends on
|
||||
/// [`VmSendMigrationData::timeout_strategy`]:
|
||||
/// - [`TimeoutStrategy::Cancel`] – returns
|
||||
/// - [`TimeoutStrategy::Ignore`] – the migration completes despite not
|
||||
/// meeting the downtime budget.
|
||||
/// [`MigratableError::MigrateSend`] so the caller can abort the
|
||||
/// migration cleanly.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Ok(true)` – convergence criterion met; the caller should stop precopy
|
||||
/// iterations.
|
||||
/// * `Ok(false)` – not yet converged; the caller should run another
|
||||
/// dirty-page iteration.
|
||||
/// * `Err(_)` – the timeout was reached and [`TimeoutStrategy::Cancel`]
|
||||
/// is in effect.
|
||||
fn is_precopy_converged(
|
||||
ctx: &MemoryMigrationContext,
|
||||
send_data_migration: &VmSendMigrationData,
|
||||
) -> result::Result<bool, MigratableError> {
|
||||
if ctx.current_iteration_total_bytes == 0 {
|
||||
debug!("Precopy: No more memory to transfer");
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
// We currently ignore the time required to transfer the final
|
||||
// VM state (device state and vCPUs) and the time needed on the
|
||||
// receiver to create the VM and initialize its data structures
|
||||
// before execution can resume.
|
||||
//
|
||||
// Manual testing showed that migrating an idle VM on a modern
|
||||
// AMD CPU (CHV release build) adds ~5 ms of overhead when
|
||||
// scaling from 1 to 200 vCPUs. Given this small cost, we
|
||||
// deliberately avoid additional heuristics to estimate the
|
||||
// downtime more precisely - for now. Instead, we approximate
|
||||
// the downtime just by the transfer time of the final memory
|
||||
// delta.
|
||||
if let Some(memory_downtime) = ctx.estimated_downtime
|
||||
&& memory_downtime <= send_data_migration.downtime()
|
||||
{
|
||||
debug!(
|
||||
"Precopy: Target downtime can be met: {}ms <= {}ms",
|
||||
memory_downtime.as_millis(),
|
||||
send_data_migration.downtime().as_millis()
|
||||
);
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
// We check the beginning of the precopy migration and not the overall migration, and
|
||||
// this is fine: precopy takes the longest and the earlier steps are negligible.
|
||||
if ctx.migration_begin.elapsed() >= send_data_migration.timeout() {
|
||||
return match send_data_migration.timeout_strategy {
|
||||
TimeoutStrategy::Cancel => {
|
||||
let msg = format!(
|
||||
"Precopy: Timeout reached: {}s: migration didn't converge in time",
|
||||
send_data_migration.timeout().as_secs()
|
||||
);
|
||||
Err(MigratableError::MigrateSend(anyhow!("{msg}")))
|
||||
}
|
||||
TimeoutStrategy::Ignore => {
|
||||
info!(
|
||||
"Precopy: Pausing VM, ignoring target downtime ({}ms) due to timeout ({}s): Estimated downtime: {}ms",
|
||||
send_data_migration.downtime().as_millis(),
|
||||
send_data_migration.timeout().as_secs(),
|
||||
ctx.estimated_downtime
|
||||
.unwrap_or(Duration::from_secs(0))
|
||||
.as_millis()
|
||||
);
|
||||
Ok(true)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
/// Performs the memory migration including multiple iterations.
|
||||
///
|
||||
/// This includes:
|
||||
@@ -1308,19 +1398,18 @@ impl Vmm {
|
||||
fn do_memory_migration(
|
||||
vm: &mut Vm,
|
||||
socket: &mut SocketStream,
|
||||
// Used in next commit
|
||||
_send_data_migration: &VmSendMigrationData,
|
||||
send_data_migration: &VmSendMigrationData,
|
||||
) -> result::Result<(), MigratableError> {
|
||||
const MAX_ITERATIONS: usize = 5;
|
||||
|
||||
let mut ctx = MemoryMigrationContext::new();
|
||||
let is_converged = |ctx: &MemoryMigrationContext| {
|
||||
// TODO: Add check for configurable downtime and max migration time #7111
|
||||
ctx.iteration >= MAX_ITERATIONS || ctx.current_iteration_total_bytes == 0
|
||||
};
|
||||
|
||||
vm.start_dirty_log()?;
|
||||
let remaining = Self::do_memory_iterations(vm, socket, &mut ctx, is_converged)?;
|
||||
let remaining = Self::do_memory_iterations(
|
||||
vm,
|
||||
socket,
|
||||
&mut ctx,
|
||||
// We bind send_data_migration to the callback
|
||||
|ctx| Self::is_precopy_converged(ctx, send_data_migration),
|
||||
)?;
|
||||
vm.pause()?;
|
||||
|
||||
// Send last batch of dirty pages: final iteration
|
||||
|
||||
Reference in New Issue
Block a user