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
结构作为流数据一起发送到对端。
/* This value needs to be a multiple of qemu_target_page_size() */
/* 一个 multifd 包大小为 512k, 如果每个offset[]数组元素描述一个 4k 页的地址
* 则 offset[] 数组大小为 512 / 4 = 128 个数组,当 offset 数组满,则发送数据
*/
#define MULTIFD_PACKET_SIZE (512 * 1024)
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 性能测试
说明
multifd 相比标准迁移可以更好的利用带宽,因此在 10Gb 或 25Gb 网络环境更能发挥优势。我们的测试分别在 10Gb 和 1Gb 网络环境进行,主要对比 multifd 和标准迁移,并说明适用场景。 测试虚机规格:32C64G 测试负载我们使用 sysbench 跑 mysql 数据库,命令如下:
sysbench oltp_read_write --mysql-host=localhost --mysql-user=root --mysql-password='root@123HC!r0cks' --mysql-db=sbtest --report-interval=1 --threads=64 --tables=10 --table-size=10000000 --time=300
基准测试
网络总带宽 万兆网卡标定带宽:Speed: 10000 Mb/s = 9.76 Gb/s = 1250 MB/s = 1.22 GB/s 实测带宽:9.40 Gb/s = 9625 Mb/s = 1203 MB/s = 1.17 GB/s 千兆网卡标定带宽:Speed: 1000Mb/s = 0.97 Gb/s = 125 MB/s = 0.122 GB/s 实测带宽:952 Mb/s = 119 MB/s 非迁移情况下 mysql 性能基线数据 以下为相同条件下 4 组测试数据的曲线图。每组测试 300 秒,tps 均值为:1500 tps,后续基线数据统一用 1500 tps 绘制对比。 标准迁移
以下为迁移网络和存储网络共享 10Gb 带宽 mysql 性能基线数据,相同条件下重复 3 次测试。每次测试 300 秒,热迁移平均时长为 134s,热迁移过程中剩余带宽:4.73 Gb/s、占用带宽:4.67 Gb/s: 以下为使用 1Gb 网卡作为迁移网络,mysql 性能基线数据, 1 次测试,热迁移时长 911 s: 以下为迁移网络和存储网络共享 10Gb 网络的 mysql 性能基线数据,以下作为迁移网络 mysql 性能基线数据, 2 次测试,热迁移平均时长 135 s,4 fd: 剩余带宽: 1.87 Gb/s、占用带宽:7.53 Gb/s:
结果分析: Multifd 在 10 Gb 网络基本测试中,在迁移时长和 mysql 性能影响方面,与标准迁移相当,但占用了更多的网络带宽;Multifd 设置为 8fd 占用 8.5 Gb 网络带宽,是其占用带宽的上限。而标准迁移最多使用 4.67 Gb 带宽。
Multi-FD 测试
10Gb 网络
仅开启 multifd 不设置压缩,1fd、2fd、4fd、6fd、8fd下的性能曲线: zstd compression level: 0 , 1fd、2fd、4fd、6fd、8fd 下的性能曲线: zstd compression level: 1 , 1fd、2fd、4fd、6fd、8fd 下的性能曲线: 结果分析: 不使能 zstd 压缩的 multifd 迁移,对 mysql 性能影响最大;zstd level 为 0 的 multifd 迁移,对 mysql 性能影响相对小。zstd level 为 1 multifd 迁移(level 越大压缩比例越高,但压缩速度越慢),时长比未开启压缩稍有增多,在带宽非瓶颈的情况下开启压缩不会有时间提升,符合我们的预期。 multifd 迁移随着 fd 的增加,迁移时间有减少趋势,但 mysql 性能也有下降的趋势,fd 线程越多越影响 mysql 性能(占用带宽增加)。观察发送线程 CPU 利用率:1fd 90%, 2fd 50% ~ 60%,4fd 30~40%, 6fd 20~30%,8fd 15 ~ 20 % 左右,1fd CPU 利用率最高,在 90% 以上的持续时间较长,其它 fd 配置的迁移线程利用率并不高,fd 增加虽然可以提升并发压缩数据的线程数,但压缩之前迁移线程的数据同步开销也随之增大,在数据同步期间,fd 线程处于睡眠状态,因此 CPU 不会被充分利用。因此 fd 并非越多越好,符合我们的分析。 总体来说,10Gb 网络及以上的网络环境,对于独立网络来说,默认使能 zstd level 0 且 fd 设置为 6fd 收益最大,使用的 CPU 资源约 150%;对于共享网络来说, multifd 迁移没有绝对收益,
1Gb 网络
zstd compression level: 1, 1fd、2fd、4fd、6fd、8fd 下的性能曲线: 结果分析: multifd 较标准迁移,迁移时长显著减少,1fd 迁移耗时最长,2fd、4fd、6fd、8fd 时长接近,第一次测试 2fd 耗时最短、第二次测试 4fd 耗时最短;第三次测试 2fd耗时最短。 观察发送线程 CPU 利用率:1fd 90%, 2fd 65% 左右,4fd 30% 左右, 6fd 20% 左右,8fd 15 % 左右,1fd CPU 利用率最高,2fd CPU 利用率。用于压缩的计算资源不足,2fd 下 CPU 利用率总计不到 200%,但大于 100%,说明分配的 fd 已满足压缩的算力。4fd、6fd、8fd虽然迁移时间有所减少,但 CPU 利用率低,算力换取带宽收益不明显。 总体来说,1Gb 网络,对于独立网络来说,开启 multi-fd + zstd 压缩收益显著,在主机 CPU 资源充足情况下,设置为 2fd 收益最大,在 CPU 资源紧张情况下,设置为 1fd 同样收益明显。对于共享网络来说,开启 multifd + zstd 自适应压缩同样有收益。