数据结构组成

QCOW2格式磁盘镜像的主要组成部分如下:

格式头(Header)

typedef struct QCowHeader {
    uint32_t magic;
    uint32_t version;
    uint64_t backing_file_offset;
    uint32_t backing_file_size;
    uint32_t cluster_bits;
    uint64_t size; /* in bytes */
    uint32_t crypt_method;
    uint32_t l1_size; /* XXX: save number of clusters instead ? */
    uint64_t l1_table_offset;
    uint64_t refcount_table_offset;
    uint32_t refcount_table_clusters;
    uint32_t nb_snapshots;
    uint64_t snapshots_offset;

    /* The following fields are only valid for version >= 3 */
    uint64_t incompatible_features;
    uint64_t compatible_features;
    uint64_t autoclear_features;

    uint32_t refcount_order;
    uint32_t header_length;
} QEMU_PACKED QCowHeader;

跟随这个字段后面的还有一些可选的TLV格式扩展数据,如外部快照的格式。(对于QCOW3,后面还有新特性字段)

L1表

L1,由Header中的“l1_table_offset”和“l1_size”定位。每个表项描述一个L2表项的相关信息,大小为64比特。

[00-08] 保留为0;
[09-55] 为L2表的偏移地址,如果为0则表明L2表未被分配;
[56-62] 保留为0;
[63]    为0则表明未被使用或者是COW,1则表明L2表被正确分配。

L2表

L2表,每个L2表占用1个簇。每个表项描述一个簇的属性,大小为64比特。

[00-61] 簇描述符,根据是否加密而不同;
[62]    未加密则为0,加密则为1;
[63]    为0则表明未被使用或者是COW,1则表明簇被正确分配。

refcount表

用于保存cluster的一级分配表,由Header中的“refcount_table_offset”和“refcount_table_clusters”定位。每个表项描述一个“refcount块”的起始地址,为0则表明未被使用,表项大小为64比特。

一个或多个“refcount块”

用于保存cluster的二级级分配表,每个refcount块占用一个cluster。每个表项的大小为refcount_bit(qcow2必须为16),0表明簇未被使用,1表明被使用,大于等于2则表明会被COW。

快照表

用于保存快照头, 由Header中的“nb_snapshots”和“snapshots_offset”定位,每个表项描述一个快照相关的信息,长度可变。

数据簇(cluster)

整个镜像文件都以cluster为单位进行管理,包括qcow2格式头,refcount表等元数据全部都被纳入cluster。

主要算法

偏移地址计算(从客户机的磁盘设备虚拟偏移地址转换为宿主机的镜像文件中的真实偏移地址)

// 每个cluster包含的L2表个数
l2_entries = (cluster_size / sizeof(uint64_t));

// offset在L2中的索引
l2_index = (offset / cluster_size) % l2_entries;

// offset所属的L2在L1中的索引
l1_index = (offset / cluster_size) / l2_entries;

// 从L1中获取L2的起始地址”并加载到内存
l2_table = load_cluster(l1_table[l1_index]);

// 从offset所在的L2中获取所在簇的起始地址
cluster_offset = l2_table[l2_index];

// offset在镜像中的真实地址 = 簇起始地址 + 簇内偏移
return cluster_offset + (offset % cluster_size);

偏移地址所在簇的refcount获取

// 每个refcount块容纳的refcount表项个数
refcount_block_entries = (cluster_size * 8 / refcount_bits)

// offset所在的refcount块索引
refcount_block_index = (offset / cluster_size) % refcount_block_entries

// offset所在refcount块在refcount表中索引
refcount_table_index = (offset / cluster_size) / refcount_block_entries

// 加载offset所在的refcount块到内存
refcount_block = load_cluster(refcount_table[refcount_table_index]);

// 获得offset所在簇的refcount值
return refcount_block[refcount_block_index];

内部快照生成(一个新快照的初始大小主要就是创建时的L1表大小)

  1. 复制一份L1表,将所有已被分配的L1和L2表项的最高位置位0,以及对应的refcount计数加1;
  2. 在快照表中新分配个表项,把L1属性指向新创建的L1表,Header中的nb_snapshots加1;
  3. 当向镜像写入数据时,对应的L1或者L2表项为0,对应的refcount大于或等于2,则需要重新分配对应的L2表项和簇,并让L1表项指向新的L2表,L2表项执行新分配的簇。
  4. 簇分配(不预先分配时的算法,写入时进行分配,读取时不分配,直接填充0)
  5. 当要向偏移地址offset处写入数据时,从Header的l1_table_offset中获得L1表的起始偏移地址;
  6. 用前偏移地址offset的前“64 - l2_bits - cluster_bits“位作为索引从L1表中获取对应L2表的描述符(l2_bits为每个簇中保存的L2表项个数,cluster_bits为每个簇大小的比特数);
  7. 如果L2表的表述符的最高位为0(未被分配或者是COW),则需要新分配L2表;
  8. 从refcount表和refcount块中查找2块未被使用的簇,标记为1;
  9. 初始化第一个簇为L2表,并使用它的起始地址初始化对应的L1表项;
  10. 初始化第二个簇,并使用它的地址初始化对应的L2表项;
  11. 使用第二个簇的首地址加上偏移地址的簇内偏移得出其真实地址;
  12. 在此地址进行数据写入。

镜像的大小计算

  • cluster_size -- 簇大小,默认为65536字节(64K)
  • total_size -- 要分配的镜像大小
  • refcount_bits -- refcount占用bit数,qcow2必须为16
  1. 按字节对齐的镜像大小
aligned_total_size = align_offset(total_size, cluster_size);
  1. Header 大小
header_size = cluster_size;
  1. L2表项个数
l2_num = aligned_total_size / cluster_size;
l2_num = align_offset(l2_num, cluster_size / sizeof(uint64_t));
  1. L2表大小
l2_size = l2_num * sizeof(uint64_t);
  1. L1表项个数
l1_num = l2_num * sizeof(uint64_t) / cluster_size;
l1_num = align_offset(l1_num, cluster_size / sizeof(uint64_t));
  1. L1表大小
l1_size = l1_num * sizeof(uint64_t);
  1. 每个refcount的大小,以及一个refcount块包含的refcount个数
refcount_size = refcount_bits / 8;
refcount_num = cluster_size / refcount_size;
	
    /* total size of refcount blocks
*
* note: every host cluster is reference-counted, including metadata
* (even refcount blocks are recursively included).
* Let:
*   a = total_size (this is the guest disk size)
*   m = meta size not including refcount blocks and refcount tables
*   c = cluster size
*   y1 = number of refcount blocks entries
*   y2 = meta size including everything
*   rces = refcount entry size in bytes
* then,
*   y1 = (y2 + a)/c
*   y2 = y1 * rces + y1 * rces * sizeof(u64) / c + m
* we can get y1:
*   y1 = (a + m) / (c - rces - rces * sizeof(u64) / c)
*/
  1. refcount块个数
refcount_block_num = (aligned_total_size + header_size + l1_size + l2_size) / 
(cluster_size – refcount_size - refcount_size * sizeof(uint64_t) / cluster_size)
  1. refcount块大小
refcount_block_size = DIV_ROUND_UP(refcount_block_num, refcount_num) * cluster_size;
  1. refcount表个数
refcount_table_num = refcount_block_num / refcount_num;
refcount_table_num = align_offset(refcount_block_num, cluster_size / sizeof(uint64_t));
  1. refcount表大小
refcount_table_size = refcount_table_num * sizeof(uint64_t);
  1. 总大小

使用默认参数(簇大小为65526字节,8字节地址,refcount为16位,2字节),不考虑字节对齐,不考虑快照的情况下,镜像为10G的近似计算如下:

t = total_size;
c = cluster_size;
header_size = c;
l2_size = t/c * 8;
l1_size = t/c / (c/8) * 8;
rb_size = t/c * 2;
rt_size = t/c / (c/2) * 8;
image_size ≈ t + c + t/c * 8 + t/c / (c/8) * 8 + t/c * 2 + t/c / (c/2) * 8
		= t + c + t/c*10 + t/(c*c)*80
		≈ 10G + 1.63M

因此使用默认参数时,元数据的大小大概只占磁盘数据大小的0.02%不到。新分配快照时,初始需要的空间为当时的L1表大小,约为160字节,几乎可以忽略不计(在修改后,所需空间会大幅增长,具体跟写入的量有关)。