mirror of
https://github.com/cloud-hypervisor/cloud-hypervisor.git
synced 2026-08-05 02:19:16 +00:00
vmm: implement functionality to send via multiple connections
Implements the functionality to send VM memory via multiple connections during a live migration. On-behalf-of: SAP sebastian.eydam@sap.com Signed-off-by: Sebastian Eydam <sebastian.eydam@cyberus-technology.de>
This commit is contained in:
@@ -90,6 +90,7 @@ mod pci_segment;
|
|||||||
pub mod seccomp_filters;
|
pub mod seccomp_filters;
|
||||||
mod serial_manager;
|
mod serial_manager;
|
||||||
mod sigwinch_listener;
|
mod sigwinch_listener;
|
||||||
|
mod sync_utils;
|
||||||
mod uffd;
|
mod uffd;
|
||||||
mod userfaultfd;
|
mod userfaultfd;
|
||||||
pub mod vm;
|
pub mod vm;
|
||||||
|
|||||||
@@ -5,12 +5,17 @@
|
|||||||
|
|
||||||
use std::io::{self, ErrorKind, Read, Write};
|
use std::io::{self, ErrorKind, Read, Write};
|
||||||
use std::net::{TcpListener, TcpStream};
|
use std::net::{TcpListener, TcpStream};
|
||||||
|
use std::num::NonZeroU32;
|
||||||
use std::os::fd::{AsFd, BorrowedFd};
|
use std::os::fd::{AsFd, BorrowedFd};
|
||||||
use std::os::unix::io::{AsRawFd, RawFd};
|
use std::os::unix::io::{AsRawFd, RawFd};
|
||||||
use std::os::unix::net::{UnixListener, UnixStream};
|
use std::os::unix::net::{UnixListener, UnixStream};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::result::Result;
|
use std::result::Result;
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
use std::sync::mpsc::{Receiver, Sender, SyncSender, TrySendError, channel, sync_channel};
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
use std::thread;
|
use std::thread;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::{Context, anyhow};
|
use anyhow::{Context, anyhow};
|
||||||
use log::{debug, error, info, warn};
|
use log::{debug, error, info, warn};
|
||||||
@@ -24,6 +29,7 @@ use vm_migration::protocol::{Command, MemoryRangeTable, Request, Response};
|
|||||||
use vm_migration::{MigratableError, Snapshot};
|
use vm_migration::{MigratableError, Snapshot};
|
||||||
use vmm_sys_util::eventfd::EventFd;
|
use vmm_sys_util::eventfd::EventFd;
|
||||||
|
|
||||||
|
use crate::sync_utils::Gate;
|
||||||
use crate::{GuestMemoryMmap, VmMigrationConfig};
|
use crate::{GuestMemoryMmap, VmMigrationConfig};
|
||||||
|
|
||||||
/// Transport-agnostic listener used to receive connections.
|
/// Transport-agnostic listener used to receive connections.
|
||||||
@@ -394,6 +400,339 @@ impl Drop for ReceiveAdditionalConnections {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The different kinds of messages we can send to memory sending threads.
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum SendMemoryThreadMessage {
|
||||||
|
/// A chunk of memory that the thread should send to the receiving side of the
|
||||||
|
/// live migration.
|
||||||
|
Memory(MemoryRangeTable),
|
||||||
|
/// A synchronization point after each iteration of sending memory. That way the
|
||||||
|
/// main thread knows when all memory is sent and acknowledged.
|
||||||
|
Gate(Arc<Gate>),
|
||||||
|
/// Sending memory is done and the threads are not needed anymore.
|
||||||
|
Disconnect,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The different kinds of messages the main thread can receive from a memory
|
||||||
|
/// sending thread.
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum SendMemoryThreadNotify {
|
||||||
|
/// A sending thread arrived at the gate. The main thread does not wait at the
|
||||||
|
/// gate, otherwise we could miss error messages.
|
||||||
|
Gate,
|
||||||
|
/// A sending thread encountered an error while sending memory.
|
||||||
|
Error,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This struct keeps track of additional threads we use to send VM memory.
|
||||||
|
pub(crate) struct SendAdditionalConnections {
|
||||||
|
guest_memory: GuestMemoryAtomic<GuestMemoryMmap>,
|
||||||
|
threads: Vec<thread::JoinHandle<Result<(), MigratableError>>>,
|
||||||
|
/// Sender to all workers. The receiver is shared by all workers.
|
||||||
|
message_tx: SyncSender<SendMemoryThreadMessage>,
|
||||||
|
/// If an error occurs in one of the memory sending threads, the main thread signals
|
||||||
|
/// this using this flag. Only the main thread checks this variable, the worker
|
||||||
|
/// threads will be stopped during cleanup.
|
||||||
|
worker_error: Arc<AtomicBool>,
|
||||||
|
/// After the main thread sent all memory chunks to the sender threads, it waits
|
||||||
|
/// until one of the workers notifies it. Either because an error occurred, or
|
||||||
|
/// because they arrived at the gate.
|
||||||
|
notify_rx: Receiver<SendMemoryThreadNotify>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SendAdditionalConnections {
|
||||||
|
/// How many requests can be queued for each connection before the main
|
||||||
|
/// thread has to wait for workers to catch up. This bounded [`SyncChannel`]
|
||||||
|
/// provides backpressure, so send_chunk() re-checks worker_error promptly
|
||||||
|
/// instead of queueing all memory descriptors up front and only noticing
|
||||||
|
/// failures at the next gate synchronization point.
|
||||||
|
const BUFFERED_REQUESTS_PER_THREAD: usize = 64;
|
||||||
|
|
||||||
|
/// The size of each chunk of memory to send.
|
||||||
|
///
|
||||||
|
/// We want to make this large, because each chunk is acknowledged and we wait
|
||||||
|
/// for the ack before sending the next chunk. The challenge is that if it is
|
||||||
|
/// _too_ large, we become more sensitive to network issues, like packet drops
|
||||||
|
/// in individual connections, because large amounts of data can pool when
|
||||||
|
/// throughput on one connection is temporarily reduced.
|
||||||
|
///
|
||||||
|
/// We can consider making this configurable, but a better network protocol that
|
||||||
|
/// doesn't require ACKs would be more efficient.
|
||||||
|
///
|
||||||
|
/// The best-case throughput per connection can be estimated via:
|
||||||
|
/// chunk_size / (chunk_size / throughput_per_connection + round_trip_time)
|
||||||
|
///
|
||||||
|
/// This chunk size together with eight connections is sufficient to saturate a 100G link.
|
||||||
|
const CHUNK_SIZE: u64 = 64 /* MiB */ << 20;
|
||||||
|
|
||||||
|
pub(crate) fn new(
|
||||||
|
destination: &str,
|
||||||
|
connections: NonZeroU32,
|
||||||
|
guest_memory: &GuestMemoryAtomic<GuestMemoryMmap>,
|
||||||
|
) -> Result<Self, MigratableError> {
|
||||||
|
let mut threads = Vec::new();
|
||||||
|
let configured_connections = connections.get();
|
||||||
|
let buffer_size = Self::BUFFERED_REQUESTS_PER_THREAD * configured_connections as usize;
|
||||||
|
let (message_tx, message_rx) = sync_channel::<SendMemoryThreadMessage>(buffer_size);
|
||||||
|
let worker_error = Arc::new(AtomicBool::new(false));
|
||||||
|
let (notify_tx, notify_rx) = channel::<SendMemoryThreadNotify>();
|
||||||
|
|
||||||
|
// If one connection is configured, we don't have to create any additional threads.
|
||||||
|
// In this case the main thread does the sending.
|
||||||
|
if configured_connections == 1 {
|
||||||
|
return Ok(Self {
|
||||||
|
guest_memory: guest_memory.clone(),
|
||||||
|
threads,
|
||||||
|
message_tx,
|
||||||
|
worker_error,
|
||||||
|
notify_rx,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let message_rx = Arc::new(Mutex::new(message_rx));
|
||||||
|
// If we use multiple threads to send memory, the main thread only distributes
|
||||||
|
// the memory chunks to the workers, but does not send memory anymore. Thus in
|
||||||
|
// this case we create one additional thread for each connection.
|
||||||
|
for n in 0..configured_connections {
|
||||||
|
let mut socket = send_migration_socket(destination)?;
|
||||||
|
let guest_memory = guest_memory.clone();
|
||||||
|
let message_rx = message_rx.clone();
|
||||||
|
let worker_error = worker_error.clone();
|
||||||
|
let notify_tx = notify_tx.clone();
|
||||||
|
|
||||||
|
let thread = thread::Builder::new()
|
||||||
|
.name(format!("migrate-send-memory-{n}"))
|
||||||
|
.spawn(move || {
|
||||||
|
Self::worker_send_memory(
|
||||||
|
&mut socket,
|
||||||
|
&guest_memory,
|
||||||
|
&message_rx,
|
||||||
|
&worker_error,
|
||||||
|
¬ify_tx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.inspect_err(|_| {
|
||||||
|
// If an error occurs here, we still do some light cleanup.
|
||||||
|
for _ in 0..threads.len() {
|
||||||
|
message_tx.send(SendMemoryThreadMessage::Disconnect).ok();
|
||||||
|
}
|
||||||
|
threads.drain(..).for_each(|thread| {
|
||||||
|
thread.join().ok();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.context("Error spawning send-memory thread")
|
||||||
|
.map_err(MigratableError::MigrateSend)?;
|
||||||
|
threads.push(thread);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
guest_memory: guest_memory.clone(),
|
||||||
|
threads,
|
||||||
|
message_tx,
|
||||||
|
worker_error,
|
||||||
|
notify_rx,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn worker_send_memory(
|
||||||
|
socket: &mut SocketStream,
|
||||||
|
guest_memory: &GuestMemoryAtomic<GuestMemoryMmap>,
|
||||||
|
message_rx: &Mutex<Receiver<SendMemoryThreadMessage>>,
|
||||||
|
worker_error: &AtomicBool,
|
||||||
|
notify_tx: &Sender<SendMemoryThreadNotify>,
|
||||||
|
) -> Result<(), MigratableError> {
|
||||||
|
info!("Spawned thread to send VM memory.");
|
||||||
|
loop {
|
||||||
|
// Every memory sending thread receives messages from the main thread through this
|
||||||
|
// channel. The lock is necessary to synchronize the multiple consumers. If the
|
||||||
|
// workers are very quick, lock contention could become a performance issue.
|
||||||
|
let message = message_rx
|
||||||
|
.lock()
|
||||||
|
.map_err(|_| MigratableError::MigrateSend(anyhow!("message_rx mutex is poisoned")))
|
||||||
|
.inspect_err(|_| {
|
||||||
|
worker_error.store(true, Ordering::Relaxed);
|
||||||
|
// We ignore errors during error handling.
|
||||||
|
notify_tx.send(SendMemoryThreadNotify::Error).ok();
|
||||||
|
})?
|
||||||
|
.recv()
|
||||||
|
.context("Error receiving message from main thread")
|
||||||
|
.map_err(MigratableError::MigrateSend)
|
||||||
|
.inspect_err(|_| {
|
||||||
|
worker_error.store(true, Ordering::Relaxed);
|
||||||
|
notify_tx.send(SendMemoryThreadNotify::Error).ok();
|
||||||
|
})?;
|
||||||
|
match message {
|
||||||
|
SendMemoryThreadMessage::Memory(table) => {
|
||||||
|
send_memory_ranges(guest_memory, &table, socket)
|
||||||
|
.inspect_err(|_| {
|
||||||
|
worker_error.store(true, Ordering::Relaxed);
|
||||||
|
notify_tx.send(SendMemoryThreadNotify::Error).ok();
|
||||||
|
})
|
||||||
|
.context("Error sending memory to receiver side")
|
||||||
|
.map_err(MigratableError::MigrateSend)?;
|
||||||
|
}
|
||||||
|
SendMemoryThreadMessage::Gate(gate) => {
|
||||||
|
notify_tx
|
||||||
|
.send(SendMemoryThreadNotify::Gate)
|
||||||
|
.context("Error sending gate notification to main thread")
|
||||||
|
.map_err(MigratableError::MigrateSend)
|
||||||
|
.inspect_err(|_| {
|
||||||
|
// Sending via `notify_tx` just failed, so we don't try to send another
|
||||||
|
// message via it.
|
||||||
|
worker_error.store(true, Ordering::Relaxed);
|
||||||
|
})?;
|
||||||
|
gate.wait();
|
||||||
|
}
|
||||||
|
SendMemoryThreadMessage::Disconnect => {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send memory via all connections that we have. `socket` is the original socket
|
||||||
|
/// that was used to connect to the destination. Returns Ok(true) if memory was
|
||||||
|
/// sent, Ok(false) if the given table was empty.
|
||||||
|
///
|
||||||
|
/// When this function returns, all memory has been sent and acknowledged.
|
||||||
|
pub(crate) fn send_memory(
|
||||||
|
&mut self,
|
||||||
|
table: MemoryRangeTable,
|
||||||
|
socket: &mut SocketStream,
|
||||||
|
) -> Result<bool, MigratableError> {
|
||||||
|
if table.regions().is_empty() {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we use only one connection, we send the memory directly.
|
||||||
|
if self.threads.is_empty() {
|
||||||
|
send_memory_ranges(&self.guest_memory, &table, socket)?;
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The chunk size is chosen to be big enough so that even very fast links need some
|
||||||
|
// milliseconds to send it.
|
||||||
|
for chunk in table.partition(Self::CHUNK_SIZE) {
|
||||||
|
self.send_chunk(chunk)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.wait_for_pending_data()?;
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send_chunk(&mut self, chunk: MemoryRangeTable) -> Result<(), MigratableError> {
|
||||||
|
let mut chunk = SendMemoryThreadMessage::Memory(chunk);
|
||||||
|
// [`Self::message_tx`] has a limited size, so we may have to retry sending the chunk
|
||||||
|
loop {
|
||||||
|
if self.worker_error.load(Ordering::Relaxed) {
|
||||||
|
return self.cleanup();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use try_send() so we can keep checking worker_error while the
|
||||||
|
// workers catch up. A blocking send() could wait forever if a
|
||||||
|
// worker failed and stopped making progress.
|
||||||
|
match self.message_tx.try_send(chunk) {
|
||||||
|
Ok(()) => {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
Err(TrySendError::Full(unsent_chunk)) => {
|
||||||
|
// The channel is full. We wait for a short time and retry.
|
||||||
|
thread::sleep(Duration::from_millis(10));
|
||||||
|
chunk = unsent_chunk;
|
||||||
|
}
|
||||||
|
Err(TrySendError::Disconnected(_)) => {
|
||||||
|
// The workers didn't disconnect for no reason, thus we do a cleanup.
|
||||||
|
return Err(self.cleanup().err().unwrap_or(MigratableError::MigrateSend(
|
||||||
|
anyhow!("All sending threads disconnected, but none returned an error?"),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wait until all data that is in-flight has actually been sent and acknowledged.
|
||||||
|
fn wait_for_pending_data(&mut self) -> Result<(), MigratableError> {
|
||||||
|
let gate = Arc::new(Gate::new());
|
||||||
|
for _ in 0..self.threads.len() {
|
||||||
|
self.message_tx
|
||||||
|
.send(SendMemoryThreadMessage::Gate(gate.clone()))
|
||||||
|
.context("Error sending gate message to workers")
|
||||||
|
.map_err(MigratableError::MigrateSend)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We cannot simply wait at the gate, otherwise we might miss it when a sender
|
||||||
|
// thread encounters an error. Thus we wait for the workers to notify us that
|
||||||
|
// they arrived at the gate.
|
||||||
|
let mut seen_threads = 0;
|
||||||
|
loop {
|
||||||
|
match self
|
||||||
|
.notify_rx
|
||||||
|
.recv()
|
||||||
|
.context("Error receiving message from workers")
|
||||||
|
.map_err(MigratableError::MigrateSend)?
|
||||||
|
{
|
||||||
|
SendMemoryThreadNotify::Gate => {
|
||||||
|
seen_threads += 1;
|
||||||
|
if seen_threads == self.threads.len() {
|
||||||
|
gate.open();
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SendMemoryThreadNotify::Error => {
|
||||||
|
// If an error occurred in one of the worker threads, we open
|
||||||
|
// the gate to make sure that no thread hangs. After that, we
|
||||||
|
// receive the error from Self::cleanup() and return it.
|
||||||
|
gate.open();
|
||||||
|
return self.cleanup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sends disconnect messages to all workers and joins them.
|
||||||
|
pub(crate) fn cleanup(&mut self) -> Result<(), MigratableError> {
|
||||||
|
// Send disconnect messages to all workers.
|
||||||
|
for _ in 0..self.threads.len() {
|
||||||
|
// All threads may have terminated, leading to a dropped receiver. Thus we ignore
|
||||||
|
// errors here.
|
||||||
|
self.message_tx
|
||||||
|
.try_send(SendMemoryThreadMessage::Disconnect)
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut first_err = Ok(());
|
||||||
|
self.threads.drain(..).for_each(|thread| {
|
||||||
|
let err = match thread.join() {
|
||||||
|
Ok(Ok(())) => None,
|
||||||
|
Ok(Err(e)) => Some(e),
|
||||||
|
Err(panic) => Some(MigratableError::MigrateSend(anyhow!(
|
||||||
|
"send-memory thread panicked: {panic:?}"
|
||||||
|
))),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(e) = err {
|
||||||
|
warn!("Error in send-memory thread: {e}");
|
||||||
|
|
||||||
|
if first_err.is_ok() {
|
||||||
|
first_err = Err(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
first_err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for SendAdditionalConnections {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if !self.threads.is_empty() {
|
||||||
|
warn!(
|
||||||
|
"SendAdditionalConnections was not cleaned up! Either cleanup() was never called (programming error) or it failed before completing."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Extract a UNIX socket path from a "unix:" migration URL.
|
/// Extract a UNIX socket path from a "unix:" migration URL.
|
||||||
fn socket_url_to_path(url: &str) -> Result<PathBuf, anyhow::Error> {
|
fn socket_url_to_path(url: &str) -> Result<PathBuf, anyhow::Error> {
|
||||||
url.strip_prefix("unix:")
|
url.strip_prefix("unix:")
|
||||||
|
|||||||
Reference in New Issue
Block a user