qcow2文件的snapshot管理

qcow2是qemu虚拟机中特别常用的镜像文件格式,QCOW即Qemu Copy-On-Write写时拷贝,后面的2即为版本,因为在qcow2出现之前还有qcow格式的镜像文件。从字面上理解,qcow/qcow2的文件组织形式应该是建立在Copy-On-Write这个基本的机制上,即当某个数据块被引用多次(两次或两次以上)时,若某个实例尝试写该数据块,为了不让其他实例看到该数据块的变化,就会将该数据块拷贝一份,然后将修改生效到拷贝的数据块上。这个机制和内存的Copy-On-Write的机制是基本一致的。

qcow2镜像文件组织结构

qcow2镜像文件以固定大小的单元组成,每个单元称作cluster,每个cluster在物理磁盘中都是连续分布的,便于数据的读取和写入。cluster的大小即为虚拟机看到的虚拟磁盘基本单元大小,类似于机械硬盘的sector大小,一个cluster可能对应一个sector或者连续多个sector,取决与cluster size和sector size的比例。

虚拟磁盘的数据被拆分为一个个cluster,然后这些cluster用一个二级索引表组织起来,第一级称作L1 Table,第二级称作L2 Table,其组织形式如下所示:

qcow2文件的snapshot管理

其中L1 Table的大小可变,可能只占用一个cluster,也可能占用多个连续的cluster,L1 Table的大小在虚拟磁盘创建的那一刻就已经决定,并且在cluster大小相同的情况下,虚拟磁盘的大小越大,L1 Table的size也越大。每个L2 Table则固定占用一个cluster,不同的L2 Table不需要放在相邻的cluster上。L1 Table和L2 Table中每个entry的大小为64bit,因为以目前的磁盘容量来看,64bit的地址已经能够完全满足寻址需求了。

另外,由于qcow2文件支持在文件内部建立snapshot(在文件外建立snapshot即为差分文件),故需要对每个cluster的使用进行记录,即需要记录下每个cluster被引用的次数,这样当某个snapshot被删除的时候,才能够知道哪些cluster可以释放出来继续使用,哪些cluster还在被使用,主机不能主动修改。为了维持对cluster引用次数的记录,qcow2文件中同样使用一个二级的页表来记录所有cluster的引用计数,如下图所示:

qcow2文件的snapshot管理

recfound table为第一级表,refcount block为第二级表,refcount table的大小是可变的,并且需要占用连续的cluster。在cluster size大小一样的情况下,虚拟机磁盘的容量越大,refcount table的大小也越大,就像L1 Table一样。每个refcount block占用一个cluster,并且不同的refcount block不需要占用连续的cluster。refcount block中,每个entry的大小是可以设置的,不一定是类似L2 Table的64bit,默认是16bit,因为每个entry只是一个简单的计数,没必要使用64bit,浪费空间。

L1 Table和refcount table的索引入口都在qcow2文件的头一个cluster中。qcow2镜像文件的第一个cluster即为qcow2镜像文件的头部,该头部包含了许多重要的信息,其定义位于qemu源码的block/qcow2.h头文件中。

qcow2文件的snapshot管理

主要包括:

  • magic: QCOW magic字符串("QFI\xfb")
  • backing_file_offset,该qcow2镜像文件的base文件名字在该qcow2文件中存放的位置。如果该qcow2文件没有base文件,则该offset为0。
  • cluster_bits,即用于定位一个cluster内某个字节数据所需要用到的地址位数,如cluster的大小为512B,则cluster_bits为9,若cluster的大小为4KB,则cluster_bits为12。
  • size,即为虚拟磁盘的大小
  • l1_size,表示L1 table中entry的个数
  • l1_table_offset,表示存放L1 table的cluster在qcow2文件中的位置。
  • refcount_table_offset,在该qcow2文件中refcount table所存放的位置。
  • refcount_table_clusters,用于表示refcount table的大小,即占用的cluster个数。
  • nb_snapshot,即该qcow2文件中,存在的snapshot的个数。
  • snapshot_offset,即存放snapshot的cluster在qcow2文件中的位置。

qcow2镜像文件snapshot管理

qcow2文件第一个cluster中定义的l1_table_offset、l1_size、refcount_table_offset/clusters定义了L1 Table和refcount table的入口地址和大小,qcow2程序就可以通过该入口找到虚拟磁盘中的所有数据了。另外头部中还提供了nb_snapshot和snapshot_offset,这两个区域定义了该qcow2文件中存在的snapshot个数和存放snapshot所在的cluster位置。

snapshot本质上其实是qcow2文件对应的虚拟磁盘的不同入口,即不同的L1 Table,不同的snapshot对应不同的L1 Table,根据Copy-On-Write的基本管理机制,当不同的L1 Table引用相同的L2 Table时,它们将共用一份物理磁盘上的L2 Table,但是若通过L1 Table对对应的L2 Table进行修改时,将会触发L2 Table进行拷贝和修改,使得不同的L1 Table使用不同的L2 Table,所以不同的snapshot只需要维护不同的L1 Table,不需要单独对L2 Table进行维护。另外,一个qcow2镜像文件中,所有的snapshot将会共用同一个refcount table和refcount block页表。

qcow2文件的snapshot管理

每个snapshot table entry的格式如下所示:

qcow2文件的snapshot管理

对qcow2文件中snapshot的管理对应到qemu-img snapshot命令,snapshot命令支持创建(-c)、删除(-d)、罗列(-l)和应用(-a)的操作。

  • 创建,其底层操作就是将当前使用的L1 Table拷贝一份到新分配的一个或多个连续的cluster中,然后将该新拷贝的L1 Table的地址更新到qcow2头部snapshot_offset指向的snapshot table中。
  • 删除,其底层操作就是将指定snapshot对应的L1 Table删除掉,并且更新snapshot table对应的entry。
  • 罗列,即将snapshot中存放的所有snapshot列出来。
  • 应用,即将指定的snapshot对应的L1 Table更新到qcow2文件头中,若不先将当前qcow2文件header中的L1 Table进行保存,就会造成部分的数据丢失。

另外,还需要特别注意的是,在对snapshot进行管理的同时,一个特别重要的工作就是对qcow2文件中各个cluster的引用计数进行更新,也就是对refcount table和refcount blocks进行更新,snapshot创建时,需要对应L1 Table所能索引到的cluster的引用计数都加1,而删除snapshot,则将相应的引用计数减1,当引用计数为0时,则表示该cluster没有被使用。

更新引用计数时,由于L1 Table和L2 Table中,并非所有的entry都是有效的(当虚拟机使用磁盘时,未使用到某个cluster对应的磁盘位置时,qcow2并不会对该cluster分配相应的物理磁盘空间,这也就为什么qcow2文件该开始创建的时候很小(1MB以下),而使用的时候慢慢增加),所以qemu需要遍历整个L1 Table和所有的L2 Table,找出所有有效的entry,然后将相应entry对应的cluster引用计数更新到refcount table和refcount block中,相当于进行snapshot管理(除罗列操作)的时候,将需要遍历L1 Table、所有有效的L2 Table、refcount table和所有有效的refcount block。极端情况下,对snapshot进行管理的时候,cluster的大小将会对snapshot操作的耗时产生比较大的影响,甚至相差两个数量级。

例如:同样针对40GB大小的虚拟磁盘

  • 若cluster的大小为默认的64KB,则一个L2 Table将包含64KB/8B=8K个entry,每个entry可以记录一个cluster,所以一个L2 Table可以覆盖64KBx8K=512MB的磁盘空间,虚拟磁盘大小为40GB,则需要40GB/512MB=80个L2 Table,即L1 Table需要80个entry,一个cluster即可覆盖。所以需要64KB的L1 Table和80x64K=5120KB,即大概5MB的L2 Table。
  • 若cluster的大小为4KB,则一个L2 Table将包含4KB/8B=512个entry,每个entry可以记录一个cluster,所以一个L2 Table可以覆盖4KBx512=2MB的磁盘空间,虚拟磁盘大小为40G,则需要40GB/2MB=20K个entry,即20K个L2 Table,20K个L1 Table entry将需要占用20K/512=40个cluster,即单纯L1 Table就需要占用40个cluster,即40x4KB=160KB的磁盘空间,而若所有L2 Table都有效,则需要20Kx4KB=80MB的磁盘空间。

所以64KB cluster size和4KB cluster size对应的L1 Table、L2 Table综合大概是5MB和80MB,更要命的是,这5MB是以64KB大小为单位散落在磁盘中,而80MB则是以4KB大小为单位散落在磁盘中,在一个普通的SSD上,4KB大小的随机读写速度就只有25MB/s和7MB/s,而64KB大小的随机读写则是89MB/s和30MB/s。综合起来,两者snapshot操作的耗时差一两个数量级就能解释得通了。

所以,若对snapshot操作的性能有要求的话,需要特别注意qcow2文件的cluster大小,该大小可以在qcow2镜像文件时通过-o选项设置,如

qemu-img create -f qcow2 -o cluster_size=16K test.img 40G

最后,qcow2程序中,为了加快程序对镜像文件的访问,其实会将L1 Table和refcount table一直存放在内存中,因为这两个表比较小,而且会频繁访问,而针对L2 Table和refcount block,则会建立一个cache机制,该cache会将最近访问到的L2 Table和refcount block放到内存中,从而加快对镜像文件的访问。L2 Table和refcount block的cache大小并不是在创建镜像文件时,通过qemu-img的参数来设定,而是通过qemu的-drive参数来确定,即可以根据虚拟机的运行环境进行设置,如下所示(具体可以参考说明文档docs/qcow2-cache.txt):

qcow2文件的snapshot管理