QEMU Multifd迁移原理

Multifd迁移特点

  • 我们知道QEMU的内存迁移分为pre-copy,post-copy,这通过内存拷贝的时间段来区分,当内存拷贝在目的虚机启动之前完成,称为pre-copy,反之称为post-copy。这两种迁移拷贝内存数据都在迁移线程中完成。multifd则不同,它发送内存数据在专门的线程中完成,每个线程都通过socket连接到目的端,建立一个发送通道,所有线程可以并行发送内存数据。
  • multifd迁移与普通迁移的主要区别在于以下两点:
  1. 连接建立方式
    普通迁移可以有两种连接建立方式,一是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线程,同时初始化一些数据结构,这也是由于连接建立方式不同导致的。
  2. 内存拷贝方式
    普通迁移的内存拷贝只发生在主迁移通道中,所有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中的偏移。iovoffset域设计的目的在此。
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 绘制对比。
    在这里插入图片描述
  • 标准迁移
  1. 以下为迁移网络和存储网络共享 10Gb 带宽 mysql 性能基线数据,相同条件下重复 3 次测试。每次测试 300 秒,热迁移平均时长为 134s,热迁移过程中剩余带宽:4.73 Gb/s、占用带宽:4.67 Gb/s:
    在这里插入图片描述
  2. 以下为使用 1Gb 网卡作为迁移网络,mysql 性能基线数据, 1 次测试,热迁移时长 911 s:
    在这里插入图片描述
  3. 以下为迁移网络和存储网络共享 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 自适应压缩同样有收益。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

享乐主

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值