文章目录
- Multifd迁移特点
- Multifd实现原理
- 数据结构
- MultiFDInit_t
- MultiFDPacket_t
- MultiFDPages_t
- MultiFDSendParams
- multifd_send_state
- 发送原理
- 迁移线程
- multifd线程
- 核心流程
- 连接建立
- 迁移准备
- 迁移迭代
- Multifd性能测试
Multifd迁移特点
- 我们知道QEMU的内存迁移分为pre-copy,post-copy,这通过内存拷贝的时间段来区分,当内存拷贝在目的虚机启动之前完成,称为pre-copy,反之称为post-copy。这两种迁移拷贝内存数据都在迁移线程中完成。multifd则不同,它发送内存数据在专门的线程中完成,每个线程都通过socket连接到目的端,建立一个发送通道,所有线程可以并行发送内存数据。
- multifd迁移与普通迁移的主要区别在于以下两点:
- 连接建立方式
普通迁移可以有两种连接建立方式,一是libvirt服务进程与对端发起连接,然后将fd传递给QEMU,QEMU的迁移线程拿到此fd后,向fd写入数据,发起迁移。二是QEMU负责建立socket连接,libvirt传递给QEMU的是对端访问地址。对于multifd来说,它只能使用第二种方式,QEMU负责建立socket连接,并且除了建立迁移线程的socket连接,还需要为每个发送线程建立socket连接。在建立连接阶段,普通迁移只需要建立主迁移通道(migration channel),使用一个socket连接,multifd则除主迁移通道之外,还建立了多个侧通道(side channel),每个侧通道维护一个socket连接,每个线程维护一个侧通道。侧通道代替主迁移通道发送内存数据,而主通道在multifd中的作用大大弱化,仅仅是向对端发送已拷贝的内存数据。普通迁移在QEMU建立好与目的端的连接以后,就启动迁移线程发起迁移。而multifd在这之前,还需要启动multfd线程,同时初始化一些数据结构,这也是由于连接建立方式不同导致的。 - 内存拷贝方式
普通迁移的内存拷贝只发生在主迁移通道中,所有RAMBlock包含的host内存页都通过此通道发送,发送动作都在迁移线程中完成。multifd迁移的内存拷贝则在侧通道中完成,当迁移线程逐RAMBlock寻找脏页成功后,不会将RAMBlock中包含的内存脏页交给主通道,而是将脏页地址保存起来,交给空闲的multifd线程,由侧通道发送出去,然后主迁移线程继续寻找脏页,成功后交给另一个空闲的multifd线程发送出去。对比来看,普通迁移时迁移线程除了要负责脏页查找,还负责脏页发送,而multifd迁移时将脏页的发送交给了multifd线程,主线程只负责脏页查找。
Multifd实现原理
数据结构
MultiFDInit_t
- multifd迁移流的开头通过一个初始包标识自己,这个包在每个multfd线程开始发送数据前传输给对端,用于确认迁移方式。
typedef struct {
uint32_t magic;
uint32_t version;
unsigned char uuid[16]; /* QemuUUID */
uint8_t id;
uint8_t unused1[7]; /* Reserved for future use */
uint64_t unused2[4]; /* Reserved for future use */
} __attribute__((packed)) MultiFDInit_t;
MultiFDPacket_t
- multifd线程一次性会发送多个物理页,最多可以累计至128个(4k页大小),
MultiFDPacket_t
用于记录multifd线程本次发送物理页的元数据,这些元数据作为数据,在内存页数据发送之前,提前发送到对端。multifd线程会把MultiFDPacket_t
结构作为流数据一起发送到对端。
typedef struct {
uint32_t magic;
uint32_t version;
uint32_t flags;
/* maximum number of allocated pages */
/* 每个页作为一个iov向量写入侧通道的流中
* 多个页就有多个iov,multifd累计一定数量的内存页之后一起发送
* pages_alloc表示最大可累计的内存页数,QEMU在迁移准备阶段
* 会分配相应大小的iov数组 */
uint32_t pages_alloc;
/* 已经累计的内存页数目 */
uint32_t pages_used;
/* size of the next packet that contains pages */
/* 侧通道的流中既包含元数据又包含数据
* next_packet_size用于指示侧通道流中
* 下一次包含packet的位置 */
uint32_t next_packet_size;
/* 迁移线程每次交给multifd发送的内存页,都是许多个
* QEMU将一次性传输的多个内存页定义成一个packet
* multifd每发送一次就记录一下packet的数目
* packet_num用于记录发送包的数目 */
uint64_t packet_num;
uint64_t unused[4]; /* Reserved for future use */
/* 存放发送的内存页所在的RAMBlock的idstr */
char ramblock[256];
uint64_t offset[];
} __attribute__((packed)) MultiFDPacket_t;
MultiFDPages_t
-
MultiFDPages_t
用于维护multifd线程一次性要发送的host物理页,multfd线程每次发送的物理页都属于同一个RAMBlock
,同一个RAMBlock
的物理页累计到128个页再发送(页大小4k),不同的RAMBlock
的物理页需要分开发送。 - 因为每次multifd线程发送的物理页都属于同一个
RAMBlock
,因此MultiFDPages_t
中设计了一个block结构指向发送物理页所属的RAMBlock
。 - 如果连续发送同一个
RAMBlock
的物理页,需要累计128个物理页才能发送,因此发送的iov向量是个数组,初始化的数组大小为128,iov[]数组每个元素指向一个物理页。同理,还需要一个offset
来保存iov[]数组中每个对应物理页在RAMBlock
中的偏移。iov
和offset
域设计的目的在此。
typedef struct {
/* number of used pages */
/* iov数组中已经存放了物理页地址的元素个数 */
uint32_t used;
/* number of allocated pages */
/* iov数组的大小 */
uint32_t allocated;
/* global number of generated multifd packets */
uint64_t packet_num;
/* offset of each page */
/* iov数组每个元素指向的物理页在RAMBlock的偏移 */
ram_addr_t *offset;
/* pointer to each page */
/* 每个元素指向同一个RAMBlock的物理页 */
struct iovec *iov;
/* iov数组中指向的所有物理页所属的RAMBlock */
RAMBlock *block;
} MultiFDPages_t;
- 从上面三个数据结构可以大致勾勒出侧通道中发送的数据格式,如下图所示:
MultiFDSendParams
- Multifd的特点是启动多个线程,同时和对端建立socket连接并发送数据,每个线程的socket连接被QEMU封装成一个channel,channel需要发送的内存被维护在
MultiFDPages_t
中,每个multifd发送数据需要的所有参数被封装成MultiFDSendParams
,它的核心数据就是一个channel和要发送的内存页,分别用c和pages表示。 -
MultiFDSendParams
有部分信息用于线程同步,sem字段用于通知multfd线程发送数据,mutex字段用于保护MultiFDSendParams
,因为迁移线程和multifd线程都要修改这个结构。quit字段用于指示multifd线程是否停止工作
typedef struct {
/* this fields are not changed once the thread is created */
/* channel number */
uint8_t id;
/* channel thread name */
/* multifd发送线程名字,格式:multifdsend_%d */
char *name;
/* tls hostname */
char *tls_hostname;
/* channel thread id */
/* 指向multifd线程 */
QemuThread thread;
/* communication channel */
/* multifd数据发往的通道 */
QIOChannel *c;
/* sem where to wait for more work */
/* multifd睡眠在此信号量,直到被其它线程唤醒,开始工作,发送数据 */
QemuSemaphore sem;
/* this mutex protects the following parameters */
/* 以下的字段可能被迁移线程和multifd线程同时修改,mutex用于保护以下字段 */
QemuMutex mutex;
/* is this channel thread running */
bool running;
/* should this thread finish */
bool quit;
/* thread has work to do */
/* 当前multifd线程是否 */
int pending_job;
/* array of pages to sent */
/* 迁移线程准备好pages之后,与multifd线程交换指针
* multifd指向迁移线程准备好的pages,然后发送内存数据 */
MultiFDPages_t *pages;
/* packet allocated len */
uint32_t packet_len;
/* pointer to the packet */
MultiFDPacket_t *packet;
/* multifd flags for each packet */
uint32_t flags;
/* size of the next packet that contains pages */
uint32_t next_packet_size;
/* global number of generated multifd packets */
uint64_t packet_num;
/* thread local variables */
/* packets sent through this channel */
/* 记录通过此channel发送的物理页累积大小 */
uint64_t num_packets;
/* pages sent through this channel */
uint64_t num_pages;
/* syncs main thread and channels */
/* 迁移线程迭代发送数据时,每次结束时需要等待multifd线程完成发送
* 这时迁移线程睡眠在sem_sync,当multifd线程发送完成后通过
* sem_sync唤醒迁移线程 */
QemuSemaphore sem_sync;
/* used for compression methods */
void *data;
} MultiFDSendParams;
multifd_send_state
-
multifd_send_state
是一个全局变量,它指向一个全局结构体,主要维护multifd线程的全局信息,包括每个fd线程的参数、迁移线程准备要发送的内存页、fd线程的状态等。
struct {
/* fd线程发送内存需要的参数数组
* multifdsend_0对应params[0]
* multifdsend_1对应params[1],以此类推
* */
MultiFDSendParams *params;
/* array of pages to sent */
/* 迁移线程找到脏页后封装成pages,临时存放在该字段
* 当需要让fd线程发送这些pages时,让fd线程指向的pages
* 与该字段指向的pages交换 */
MultiFDPages_t *pages;
/* global number of generated multifd packets */
uint64_t packet_num;
/* send channels ready */
/* 当fd线程中有任意一个准备好之后,会唤醒等待在该信号量
* 上的迁移线程,让迁移线程继续工作 */
QemuSemaphore channels_ready;
/*
* Have we already run terminate threads. There is a race when it
* happens that we got one error while we are exiting.
* We will use atomic operations. Only valid values are 0 and 1.
*/
int exiting;
/* multifd ops */
MultiFDMethods *ops;
} *multifd_send_state;
发送原理
- 在介绍multifd流程之前,我们先尝试分析multifd迁移的发送原理,如果单独理解发送原理有问题,再跳到后面一节看迁移的核心流程。multifd迁移,顾名思义,多fd迁移,这里的fd就是创建socket连接或者其它连接返回的fd,总之,就是有多个通道可以并发地发送数据。每个通道专门启动了一个线程来负责发送数据。multifd发送示意图如下:
- 所有multifd线程发送需要的信息被封装成params数组,数组每个元素对应一个multifd线程需要的信息,param的核心数据是pages,即要发送的内存页指针,每个multifd线程都维护了这样一个信息,同时还有一个全局的公共的pages指针,指向迁移线程搜集的需要发送的内存页。
- 如果上层在内存迁移时指定N个multifd线程用于发送内存数据(libvirt通过–parallel-connections=N指定),那么multifd线程就有N个,指向内存页指针的pages有N+1个(N个属于multifd线程,1个全局的但只有迁移线程会操作)。
- 如上图所示,当迁移线程进入迭代迁移阶段,每次迭代可以概括成两步,第一步时搜集可以发送的脏页内存,第二部发送脏页内存,迁移线程在第一步中将脏页内存的地址放到全局变量multifd_send_state的pages字段。然后查找空闲的multifd线程,将其pages指针与全局变量multifd_send_state的pages指针交换,这样空闲的multifd线程就获取了需要发送的物理页,然后迁移线程唤醒空闲multifd线程,开始工作。当进入下一次迭代时,迁移线程将新找到的脏页内存地址又放入multifd_send_state的pages字段,查找新的空闲multifd线程,继续发送物理页。
- 由上面的分析可知,multifd_send_state中的pages字段会依次与空闲multifd线程的pages字段交换内容,不断地在变换。迁移线程负责查找内存脏页,multifd线程负责发送脏页,当所有multifd都忙起来的时候,内存迁移发送数据的能力达到最大值,这时迁移线程如果再找到新的内存脏页,需要等待multifd线程空闲之后才能再次发送数据。
迁移线程
- 迁移线程迭代查找脏页内存,触发multifd线程发送内存数据的流程如下:
/* 迁移线程入口点 */
migration_thread
migration_iteration_run
qemu_savevm_state_iterate
se->ops->save_live_iterate
/* 迁移迭代的入口点 */
ram_save_iterate
ram_find_and_save_block
ram_save_host_page
ram_save_target_page
ram_save_multifd_page
multifd_queue_page
-
multifd_queue_page
函数将找到的脏页内存地方存放到multifd_send_state
全局变量,分析其具体实现。
/* 入参:
* f,迁移线程的主通道,在multifd中只用来已传输的数据长度
* block,内存脏页所在的RAMBlock,迁移线程是逐block查找的脏页内存
* offset,内存脏页通常只占用RAMBlock的一小段4k区间,offset表示内存脏页的起始地址 */
int multifd_queue_page(QEMUFile *f, RAMBlock *block, ram_addr_t offset)
{
/* 首先获取全局变量multifd_send_state中的pages字段 */
MultiFDPages_t *pages = multifd_send_state->pages;
/* pages字段存放的是与multifd线程pages交换的值,或者初始值
* 如果block为空,表示之前还没有发送过内存页 */
if (!pages->block) {
pages->block = block;
}
/* 如果当前迁移线程要发送的内存页与前一次发送的内存页所在
* RAMBlock相同,将内存页地址保存到iov[]的地址中,长度设置为页大小 */
if (pages->block == block) {
/* 保存发送页在RAMBlock中的偏移 */
pages->offset[pages->used] = offset;
/* 保存发送页的HVA */
pages->iov[pages->used].iov_base = block->host + offset;
/* 设置页大小 */
pages->iov[pages->used].iov_len = qemu_target_page_size();
/* 每填充一个页到iov数组,增加一次使用计数 */
pages->used++;
/* 如果累计的页没有到最大值,直接返回,这里的最大值为128个物理页,下文有介绍 */
if (pages->used < pages->allocated) {
return 1;
}
}
/* 有两种情况可以触发multifd线程发送物理页:
* 1. 当前发送的物理页所在RAMBlock与前一次发送的物理页所在RAMBlock不同
* 2. 当前发送的物理页个数累计达到了128个 */
if (multifd_send_pages(f) < 0) {
return -1;
}
/* 进入到这里只有一种情况
* 当前物理页所在RAMBlock与前一次发送的物理页所在RAMBlock互不相同
* 需要再次调用multifd_queue_page,将本次要发送的物理页真正地入队 */
if (pages->block != block) {
return multifd_queue_page(f, block, offset);
}
return 1;
}
-
multifd_send_pages
的主要工作是遍历所有multifd线程,查找空闲的线程,将要发送的内存页地址传递给空闲线程,然后唤醒它,使其工作,流程如下:
static int multifd_send_pages(QEMUFile *f)
{
/* 本次查找空闲线程的起始值,每次加1 */
static int next_channel;
MultiFDSendParams *p = NULL; /* make happy gcc */
MultiFDPages_t *pages = multifd_send_state->pages;
uint64_t transferred;
/* 首先睡眠在channels_ready信号量上,等待空闲的线程将自己唤醒 */
qemu_sem_wait(&multifd_send_state->channels_ready);
/*
* next_channel can remain from a previous migration that was
* using more channels, so ensure it doesn't overflow if the
* limit is lower now.
*/
next_channel %= migrate_multifd_channels();
/* 遍历所有multifd线程使用的连接通道,查找空闲的multifd线程 */
for (i = next_channel;; i = (i + 1) % migrate_multifd_channels()) {
p = &multifd_send_state->params[i];
qemu_mutex_lock(&p->mutex);
/* 如果线程没有任务,说明其空闲,找到目标 */
if (!p->pending_job) {
p->pending_job++;
next_channel = (i + 1) % migrate_multifd_channels();
break;
}
qemu_mutex_unlock(&p->mutex);
}
assert(!p->pages->used);
assert(!p->pages->block);
/* multifd线程发送的内存页被封装成packet的概念
* 每发送一次,增加packet的计数 */
p->packet_num = multifd_send_state->packet_num++;
/* 将全局变量中的页指针和找到的multifd空闲线程的内存页指针交换
* 通过这种方式,multifd线程得到了要发送的内存页
* 而全局变量中的页指针作为了一个中转站
* 用于存放迁移线程准备好的,需要让multifd线程发送的页指针 */
multifd_send_state->pages = p->pages;
p->pages = pages;
transferred = ((uint64_t) pages->used) * qemu_target_page_size()
+ p->packet_len;
qemu_file_update_transfer(f, transferred);
ram_counters.multifd_bytes += transferred;
ram_counters.transferred += transferred;
qemu_mutex_unlock(&p->mutex);
/* 所有准备工作完成之后,唤醒等待在sem上的multifd线程,让其开始工作 */
qemu_sem_post(&p->sem);
return 1;
}
multifd线程
- multifd线程被创建出来之后,它的工作很简单,可以概括为:睡眠、被唤醒、发送数据、继续睡眠。循环如此,直到被通知停止工作。
static void *multifd_send_thread(void *opaque)
{
MultiFDSendParams *p = opaque;
Error *local_err = NULL;
int ret = 0;
uint32_t flags = 0;
/* 发送初始化包到对端,表明自己开始进行multifd数据发送 */
multifd_send_initial_packet(p, &local_err)
/* initial packet */
p->num_packets = 1;
/* 无限循环 */
while (true) {
/* 睡眠在sem信号量上,直到迁移线程准备好要发送的数据,将自己唤醒 */
qemu_sem_wait(&p->sem);
/* multifd线程被唤醒,如果被告知结束工作,则退出循环 */
if (qatomic_read(&multifd_send_state->exiting)) {
break;
}
qemu_mutex_lock(&p->mutex);
/* 如果迁移线程标记有等待的任务,则进入工作流程 */
if (p->pending_job) {
if (used) {
ret = multifd_send_state->ops->send_prepare(p, used,
&local_err);
}
/* 填充packet的元数据 */
multifd_send_fill_packet(p);
p->flags = 0;
p->num_packets++;
p->num_pages += used;
p->pages->used = 0;
p->pages->block = NULL;
qemu_mutex_unlock(&p->mutex);
/* 发送packet的元数据 */
ret = qio_channel_write_all(p->c, (void *)p->packet,
p->packet_len, &local_err);
/* 发送内存页 */
if (used) {
ret = multifd_send_state->ops->send_write(p, used, &local_err);
if (ret != 0) {
break;
}
}
qemu_mutex_lock(&p->mutex);
p->pending_job--;
qemu_mutex_unlock(&p->mutex);
/* 发送结束,将同步等待在sem_sync信号量上的线程唤醒 */
if (flags & MULTIFD_FLAG_SYNC) {
qemu_sem_post(&p->sem_sync);
}
/* 发送结束,将等待在channels_ready上的线程唤醒
* 表明自己准备好了,可以进行下一轮的数据传输 */
qemu_sem_post(&multifd_send_state->channels_ready);
}
}
......
}
核心流程
连接建立
- multifd建立了两次连接,第一个是迁移命令下发的初始阶段,建立的主迁移通道,第二个是迁移准备阶段,建立的侧通道。初始阶段建立主迁移通道的代码路径如下:
/* 上层发起内存迁移触发迁移命令 */
qmp_migrate
socket_start_outgoing_migration
socket_start_outgoing_migration_internal
/* 将对端的socket连接地址存放到全局变量outgoing_args中
* 建立侧通道连接时可以使用该地址 */
outgoing_args.saddr = saddr
/* 创建一个线程异步地创建socket连接,然后将其封装成一个QIOChannelSocket
* 它的父类是QIOChannel,QEMU通过操作QIOChannel来向对端发送内存数据
* 该函数首先创建socket连接,同时将该socket_outgoing_migration函数封装成
* 一个任务,当socket连接创建完成后,创建一个线程来执行该任务,因此
* socket_outgoing_migration在最后会在一个单独创建的线程中被执行 */
qio_channel_socket_connect_async(sioc,
saddr,
socket_outgoing_migration,
data,
socket_connect_data_free,
NULL);
qio_task_run_in_thread(task,
qio_channel_socket_connect_worker,
addrCopy,
(GDestroyNotify)qapi_free_SocketAddress,
context)
/* 创建socket连接 */
qio_channel_socket_connect_worker
qio_channel_socket_connect_sync
socket_connect
qio_channel_socket_set_fd
/* 创建线程异步执行qio_task_thread_worker */
qio_channel_socket_connect_async
qio_task_run_in_thread
qemu_thread_create(&thread,
"io-task-worker",
qio_task_thread_worker,
task,
QEMU_THREAD_DETACHED);
/* 设置socket连接完成之后需要回调的任务 */
qio_task_thread_worker
/* qio_task_thread_result为socket连接完成后要回调的函数
* 传入的task中有要执行的任务 */
g_source_set_callback(task->thread->completion,
qio_task_thread_result, task, NULL)
/* 执行迁移函数socket_outgoing_migration */
qio_task_thread_result
qio_task_complete(task)
task->func(task, task->opaque) <=> socket_outgoing_migration
- 迁移准备阶段,建立侧通道的代码路径如下:
/* 迁移发起入口点 */
socket_outgoing_migration
migration_channel_connect
migrate_fd_connect
/* 迁移前multifd相关准备 */
multifd_save_setup
qemu_thread_create(&s->thread, "live_migration", migration_thread, s,
QEMU_THREAD_JOINABLE);
/* multifd设置入口 */
multifd_save_setup
socket_send_channel_create(multifd_new_send_channel_async, p)
qio_channel_socket_connect_async(sioc, outgoing_args.saddr,
f, data, NULL, NULL)
qio_task_run_in_thread(task,
qio_channel_socket_connect_worker,
addrCopy,
(GDestroyNotify)qapi_free_SocketAddress,
context)
迁移准备
- multifd迁移的准备工作主要在
multifd_save_setup
函数中完成,它负责初始化每个multifd线程需要的参数,创建multifd线程。如下:
int multifd_save_setup(Error **errp)
{
/* 默认累计发送内存页的上限,128页,当同一个RAMBlock中累计到128个物理页
* multifd线程才开始传输数据,在这之前只是将要发送的内存页的地址存放在iov[]数组中 */
uint32_t page_count = MULTIFD_PACKET_SIZE / qemu_target_page_size();
/* 如果没有启用multifd迁移,直接返回 */
if (!migrate_use_multifd()) {
return 0;
}
s = migrate_get_current();
/* 获取上层设置的multifd线程数,默认为2 */
thread_count = migrate_multifd_channels();
/* 为全局数据结构multifd_send_state分配空间 */
multifd_send_state = g_malloc0(sizeof(*multifd_send_state));
/* 为每个multifd线程分配对应的params数据结构 */
multifd_send_state->params = g_new0(MultiFDSendParams, thread_count);
/* 初始化全局变量中的pages指针 */
multifd_send_state->pages = multifd_pages_init(page_count);
qemu_sem_init(&multifd_send_state->channels_ready, 0);
qatomic_set(&multifd_send_state->exiting, 0);
multifd_send_state->ops = multifd_ops[migrate_multifd_compression()];
/* 针对每个multifd线程对应的params参数 */
for (i = 0; i < thread_count; i++) {
MultiFDSendParams *p = &multifd_send_state->params[i];
qemu_mutex_init(&p->mutex);
qemu_sem_init(&p->sem, 0);
qemu_sem_init(&p->sem_sync, 0);
p->quit = false;
p->pending_job = 0;
p->id = i;
p->pages = multifd_pages_init(page_count);
p->packet_len = sizeof(MultiFDPacket_t)
+ sizeof(uint64_t) * page_count;
p->packet = g_malloc0(p->packet_len);
p->packet->magic = cpu_to_be32(MULTIFD_MAGIC);
p->packet->version = cpu_to_be32(MULTIFD_VERSION);
p->name = g_strdup_printf("multifdsend_%d", i);
p->tls_hostname = g_strdup(s->hostname);
/* 创建socket连接,同时创建对应的multifd线程,专门负责传输数据 */
socket_send_channel_create(multifd_new_send_channel_async, p);
}
......
}
迁移迭代
/* 迁移迭代入口点 */
migration_iteration_run
qemu_savevm_state_iterate
se->ops->save_live_iterate <=> ram_save_iterate
/* 查找脏页然后异步发送*/
ram_find_and_save_block
/* 遍历所有RAMBlock,查找其中可以发送的内存脏页 */
do {
/* 查找包含脏页的block */
find_dirty_block
/* 异步发送脏页内容 */
ram_save_host_page
} while (!pages && again)
/* 等待multifd线程发送脏页结束,然后进入下一次迭代 */
multifd_send_sync_main
Multifd性能测试