内存热迁移

内存是整个虚拟机中重要的部分,也是迁移过程中迁移量最大的部分。

首先我们将从迁移框架的流程上看看整个过程都有哪些函数参与其中,了解内存迁移的流程结构。

接着我们要从数据结构层面看看我们在这些流程中究竟是要处理哪些数据。

内存迁移流程

发送流程

要说流程,还是要先回到总体架构那一节中的总体流程。

migration_thread()

  qemu_savevm_state_header
      qemu_put_be32(f, QEMU_VM_FILE_MAGIC);
      qemu_put_be32(f, QEMU_VM_FILE_VERSION);
      qemu_put_be32(f, QEMU_VM_CONFIGURATION);
      vmstate_save_state(f, &vmstate_configuration, &savevm_state, 0);
  qemu_savevm_send_open_return_path(s->to_dst_file);
  qemu_savevm_send_ping(s->to_dst_file, 1);
      qemu_savevm_command_send(f, MIG_CMD_PING, , (uint8_t *)&buf)

  ; iterate savevm_state and call save_setup
  qemu_savevm_state_setup(s->to_dst_file);
      save_section_header(f, se, QEMU_VM_SECTION_START)
      se->ops->save_setup(f, se->opaque)
      save_section_footer(f, se)
      precopy_notify(PRECOPY_NOTIFY_SETUP, &local_err)

  migrate_set_state(&s->state, MIGRATION_STATUS_SETUP, MIGRATION_STATUS_ACTIVE);
  migration_iteration_run

      ; iterate savevm_state and call save_live_pending
      qemu_savevm_state_pending(pend_pre/compat/post)
          se->ops->save_live_pending()

      ; iterate savevm_state and call save_live_iterate
      qemu_savevm_state_iterate()
          save_section_header(f, se, QEMU_VM_SECTION_PART)
          se->ops->save_live_iterate(f, se->opaque)
          save_section_footer(f, se)
      migration_completion()
          qemu_system_wakeup_request(QEMU_WAKEUP_REASON_OTHER, NULL);
          vm_stop_force_state(RUN_STATE_FINISH_MIGRATE)

          qemu_savevm_state_complete_precopy(s->to_dst_file, false, inactivate);
              ; iterate savevm_state and call save_live_complete_precopy
              cpu_synchronize_all_states();
              save_section_header(f, se, QEMU_VM_SECTION_END);
              se->ops->save_live_complete_precopy(f, se->opaque)
              save_section_footer(f, se);

              ; iterate savevm_state and call vmstate_save
              save_section_header(f, se, QEMU_VM_SECTION_FULL);
              vmstate_save(f, se, vmdesc)
              save_section_footer(f, se);

  migration_detect_error
  migration_update_counters
  migration_iteration_finish

在这个基础上我们进一步抽取和简化,能看到整个过程中主要是这么几个回调函数在起作用。

  • se->ops->save_setup

  • se->ops->save_live_pending

  • se->ops->save_live_iterate

  • se->ops->save_live_complete_precopy

那对应到内存,这个se就是savevm_ram_handlers,其对应的函数们就是

  • ram_save_setup

  • ram_save_pending

  • ram_save_iterate

  • ram_save_complete

虽然里面做了很多事儿,也有很多细节上的优化,不过总的流程可以总结成两句话:

  • 用bitmap跟踪脏页

  • 将脏页传送到对端

接收流程

看完了发送,还得来看看接收。同样,看具体内存之前,先看一看总体架构。

qemu_loadvm_state()
    qemu_get_be32, QEMU_VM_FILE_MAGIC
    qemu_get_be32, QEMU_VM_FILE_VERSION
    qemu_loadvm_state_setup
        se->ops->load_setup
    vmstate_load_state(f, &vmstate_configuration, &savevm_state, 0)
    cpu_synchronize_all_pre_loadvm
        cpu_synchronize_pre_loadvm(cpu)

    qemu_loadvm_state_main
        section_type = qemu_get_byte(f)
        QEMU_VM_SECTION_START | QEMU_VM_SECTION_FULL
        qemu_loadvm_section_start_full
           section_id = qemu_get_be32
           vmstate_load
        QEMU_VM_SECTION_PART | QEMU_VM_SECTION_END
        qemu_loadvm_section_part_end
            section_id = qemu_get_be32
            vmstate_load
        QEMU_VM_COMMAND
        loadvm_process_command
        QEMU_VM_EOF

    qemu_loadvm_state_cleanup
        se->ops->load_cleanup
    cpu_synchronize_all_post_init
        cpu_synchronize_post_init(cpu);

进一步打开,我们可以看到有这么几个重要的函数。

  • se->ops->load_setup

  • se->ops->load_state

对应到内存,分别是

  • ram_load_setup

  • ram_load

发送接收对应关系

                 source                                destination

            +------------------------+             +-------------------------+
            |                        |             |                         |
  SETUP     | ram_save_setup         |             |  ram_load_setup         |
            |                        |             |                         |
            +------------------------+             +-------------------------+

            sync dirty bit to                      Setup RAMBlock->receivedmap
        RAMBlock->bmap


            +------------------------+             +-------------------------+
            |                        |             |                         |
  ITER      | ram_save_pending       |             |  ram_load               |
            | ram_save_iterate       |             |                         |
            |                        |             |                         |
            +------------------------+             +-------------------------+

            sync dirty bit                         Receive page
        and send page


            +------------------------+             +-------------------------+
            |                        |             |                         |
  COMP      | ram_save_pending       |             |  ram_load               |
            | ram_save_complete      |             |                         |
            |                        |             |                         |
            +------------------------+             +-------------------------+

            sync dirty bit                         Receive page
              and send page

脏页同步

在整个内存迁移的过程中脏页同步是重中之重了,这一小节我们来看看qemu是如何获得这一段时间中的虚拟机脏页的。

代码流程

qemu发起获得脏页的地方有几处,比如每次迭代开始,或者最后要结束的时候。这个动作都通过统一的函数migration_bitmap_sync()。

   migration_bitmap_sync
       memory_global_dirty_log_sync                          (1)
           memory_region_sync_dirty_bitmap(NULL);
               listener->log_sync(listener, &mrs) -> kvm_log_sync
                   kvm_physical_sync_dirty_bitmap
       migration_bitmap_sync_range; called on each RAMBlock
           cpu_physical_memory_sync_dirty_bitmap             (2)

其中主要工作分成两步:

  • 通过KVM_GET_DIRTY_LOG获得脏页到kvm_dirty_log.dirty_bitmap,并复制到ram_list.dirty_memory

  • 再将ram_list.dirty_memory的脏页拷贝到RAMBlock->bmap

至于KVM_GET_DIRTY_LOG是怎么得到的脏页这个要看kvm的代码,其中一部分的功劳在vmx_flush_pml_buffer()。

相关数据结构

在内存迁移过程中重要的数据结构就是跟踪脏页的bitmap了。

其中一共用到了两个bitmap:

  • RAMBlock->bmap

  • ram_list.dirty_memory[DIRTY_MEMORY_MIGRATION]

如果要画一个图来解释的画,那么这个可能会像一点。

    ram_list.dirty_memory[]
    +----------------------+---------------+--------------+----------------+
    |                      |               |              |                |
    +----------------------+---------------+--------------+----------------+
    ^                      ^               ^              ^
    |                      |               |              |
    RAMBlock.bmap                          RAMBlock.bmap
    +----------------------+               +--------------+
    |                      |               |              |
    +----------------------+               +--------------+

ram_list.dirty_memroy[]是一整个虚拟机地址空间的bitmap。虽然虚拟机的地址空间可能有空洞,但是这个bitmap是连续的。 RAMBlock.bmap是每个RAMBlock表示的地址空间的bitmap。

所以同步的时候分成两步:

  • 虚拟机对内存发生读写时会更新ram_list.dirty_memroy[]

  • 每次内存迁移迭代开始,将ram_list.dirty_memory[]更新到RAMBlock.bmap

这样两个bitmap各司其职可以同步进行。

进化

理解了总体的流程和重要的数据结构,我们来看看内存迁移部分是如何进化的。如何从最开始的找到脏页迁移脏页,变成现在这么复杂的逻辑的。

零页

迁移的时候,如果知道这个页面的内容都是0,那么从信息的角度看,其实信息量很小。那是不是有更好的方法去传送这些信息呢?

函数save_zero_page_to_file(),就做了这件事。如果这个页面是零页,我们只传送一些标示符号。

压缩

这个想法也比较直接,就是在传送前先压缩一下。这个工作交给了do_compress_ram_page()。

多线程

这也是一个比较直观的想法,一个人干活慢,那就多叫几个人。多线程的设置在compress_threads_save_setup()。

Free Page

这个想法相对来说就比较隐晦了。意思是系统中总有部分的内存没有使用是free的,那么这些内存在最开始的时候就没有必要传送。(默认第一遍是都传送的。)

当然要做到这点,需要在虚拟机中有一个内应。当前这个内应是virtio-balloon。在启动虚拟机时需要加上参数。

-object iothread,id=iothread1 --device virtio-balloon,free-page-hint=true,iothread=iothread1

multifd

接下来这个东西略微有点复杂。也不知道为什么名字叫multifd,好像很有来头的样子。那就先来看看其中重要的数据结构。

第一个叫multifd_send_state。这个结构在函数multifd_save_setup中初始化。样子大概长这样。

    multifd_send_state
    +-------------------------------+
    |packet_num                     |  global
    |    (uint64_t)                 |
    |sem_sync                       |
    |channels_ready                 |
    |    (QemuSemaphore)            |
    |                               |
    |pages                          |
    |    (MultiFDPages_t*)          |
    |                               |
    |params                         |  [migrate_multifd_channels()]  each channel has one MultiFDSendParams
    |    (MultiFDSendParams*)       |
    |                               |      MultiFDSendParams           MultiFDSendParams           MultiFDSendParams  
    +-------------------------------+      +-------------------+       +-------------------+       +-------------------+
                                           |name               |       |name               |       |name               |
                                           |  multifdsend_0    |       |  multifdsend_1    |       |  multifdsend_2    |
                                           |pages              |       |pages              |       |pages              |
                                           | (MultiFDPages_t*) |       | (MultiFDPages_t*) |       | (MultiFDPages_t*) |
                                           |packet             |       |packet             |       |packet             |
                                           | (MultiFDPacket_t*)|       | (MultiFDPacket_t*)|       | (MultiFDPacket_t*)|
                                           |                   |       |                   |       |                   |
                                           |                   |       |                   |       |                   |
                                           +-------------------+       +-------------------+       +-------------------+

几点说明:

  • multifd是一个多线程的结构,一共有migrate_multifd_channels个线程,这个可以通过参数设置

  • 所以也很好理解的是对应有migrate_multifd_channels()个params,每个线程人手一个不要打架

  • 每个params中重要的两个成员是pages/packet,packet相当于控制信息,pages就是数据

接着我们来看看page和packet的样子。

pages
   (MultiFDPages_t*)
   +----------------------+
   |packet_num            |  global
   |   (uint64_t)         |
   |block                 |
   |   (RAMBlock*)        |
   |allocated             |
   |used                  |
   |   (uint32_t)         |
   |offset                |  [allocated]
   |   (ram_addr_t)       |
   |iov                   |  [allocated]
   |   (struct iovec*)    |
   |   +------------------+
   |   |iov_base          |  = block->host + offset
   |   |iov_len           |  = TARGET_PAGE_SIZE
   |   +------------------+
   |                      |
   +----------------------+

看到这个大家或许能够明白一点,page其实是一个iov的集合。一共分配有allocated个iov的结构,用于发送数据。不过要注意的一点是,发送的所有数据都属于同一个RAMBlock。

packet
   (MultiFDPacket_t*)
   +----------------------+
   |magic                 |  = MULTIFD_MAGIC
   |version               |  = MULTIFD_VERSION
   |flags                 |  = params->flags
   |                      |
   |pages_alloc           |  = page_count
   |pages_used            |  = pages->used
   |next_packet_size      |  = params->next_packet_size
   |packet_num            |  = params->packet_num
   |                      |
   |offset[]              |  [page_count]
   |    (ram_addr_t)      |
   +----------------------+

这个相当于每次发送时的元数据了。而且因为iov的传送只有base/len,所以这里还要传送在RAMBlock中的offset。

xbzrle

这个东西也很高端,全称叫 XOR Based Zero run lenght encoding. 其中使用到的编码是LEB128.

这个东西很有意思,说起来其实也简单。

不直接传送每个页的内容,而是传送每个页的差值

怎么样,是不是很有意思?好了,这个思想的所有内容就这么写,其他的就剩下具体的实现细节了。

Last updated