目录
一:简单介绍... 1
1.1 write hole问题... 1
1.2恢复原理... 1
1.3数据逻辑... 2
1.4 如何使用... 2
二:核心数据结构... 3
2.1 ppl_header和ppl_header_entry. 3
2.2 ppl_conf和ppl_log以及ppl_io_unit. 4
三:PPL写入逻辑以及数据分布... 7
3.1申请条带用来保存需要写入的数据... 7
3.2重构数据,计算ppl值... 7
3.3设置脏条带准备写入数据盘以及校验盘;... 7
3.4提交数据盘以及校验盘数据... 7
3.5返回... 7
3.6更新超级块信息... 8
四:PPL恢复策略... 9
部分奇偶校验日志(PPL)是RAID5阵列可用的特性,其他阵列没有该特性。PPL解决的问题是writehole问题,也就是在一次脏的关闭之后,如果只更新了该条带的部分数据磁盘的数据,校验位以及部分数据磁盘没有被更新到。特定条带的奇偶校验位会与其他数据成员磁盘上的数据不一致。如果RAID组同时也处于降级状态,那么就没有办法重新计算奇偶校验位,因为其中一个磁盘丢失了。这时如果使用旧的校验位重建挂掉的数据磁盘时,会导致该磁盘数据不正确。因此,在默认情况下,md不允许启动一个脏的降级数组。
写操作的部分奇偶性是由本次写操作未修改的条带数据块或者需要修改数据与校验块的XOR,计算时根据RCW或者RMW来决定,但结果是相同的,就看那种方式快捷。它只是从write hole中恢复挂掉的磁盘所需要的数据。Writehole发生后重启时使用部分奇偶校验数据与修改后的块的数据进行XOR操作生成该条带的新的奇偶校验位。如果这个条带的某一个未修改的数据磁盘丢失了,那么这个更新的奇偶校验就可以用来恢复它的内容,使用部分奇偶校验值也可以直接恢复该块数据,与保留的未修改数据进行异或操作就可以。在不干净的关闭和所有磁盘都可用的情况下启动一个阵列时,也会执行PPL恢复,只是用来更新奇偶校验数据位,从而消除了重新同步阵列的需要。因此,不支持同时使用bitmap和PPL。
在处理写请求时,PPL在新数据和奇偶校验被发送到磁盘之前写部分奇偶数据到该条带对应的校验磁盘上面。PPL是一个分布式日志——它存储在元数据区域的阵列成员驱动器上,也就是特定条带的奇偶校验数据的磁盘上。它不需要专门的日志记录驱动器。写性能降低了30%-40%但它性能随着数组中驱动器的数量变多而变快,而日志记录驱动器不会成为瓶颈或单点失败。
与raid5-cache不同,在md关闭write hole的另一个解决方案中,PPL不是一个真正的日志。它不能防止丢失飞行中的数据,只保护静默状态下的数据损坏恢复。如果一个条带的脏磁盘丢失了,那么就不会为这个条带执行PPL恢复(奇偶校验没有更新)。因此,如果磁盘丢失,则可能在条带的written部分中拥有任意数据。在这种情况下,行为和普通的raid5是一样的。
PPL可以用于md版本1,元数据和外部(特别是IMSM)元数据数组。它可以使用mdadm选项--consistency-policy=ppl来启用。
struct ppl_header {
__u8 reserved[PPL_HDR_RESERVED];/* reserved space, fill with 0xff */
__le32 signature; /* signature (family number of volume) */
__le32 padding; /* zero pad */
__le64 generation; /* generation number of the header */
__le32 entries_count; /* number of entries in entry array */
__le32 checksum; /* checksum of the header (~crc32c) */
struct ppl_header_entry entries[PPL_HDR_MAX_ENTRIES];
} __attribute__ ((__packed__));
struct ppl_header_entry {
__le64 data_sector; /* raid sector of the new data */
__le32 pp_size; /* length of partial parity */
__le32 data_size; /* length of data */
__le32 parity_disk; /* member disk containing parity */
__le32 checksum; /* checksum of partial parity data for this
* entry (~crc32c) */
} __attribute__ ((__packed__));
PPL由一个4 KB的头(struct ppl_header)组成。header 包含一个条目数组(struct ppl_header_entry)描述记录的写请求,每一个entry最多有128 KB部分奇偶校验数据。entries的部分奇偶数据是在header之后,以与entries相同的顺序写入:
Header
entry0
...
entryN
PP data
PP for entry0
...
PP for entryN
一个entry描述一个或多个连续的stripe_head,直到一个完整的条带。修改后的raid数据块形成一个m乘n矩阵,m是在每个entry中stripe_heads的数量和n是修改后的数据的磁盘数量。某个entry中的每个stripe_head必须写入相同的数据磁盘。也就是一个entry对应一个数据盘。有几个entry就有几块数据盘?每个entry又包含一个或多个stripe_head。
一个单一entry描述的有效案例的例子(写到第一个条目4个磁盘阵列的条带,16k trunk大小):
sh->sector dd0 dd1 dd2 ppl
+------+------+------+
0 | ----- | ----- | ---- | +----+
8 | -W- | -W- | ---- | | pp | data_sector = 8
16 | -W- | -W- | ---- | | pp | data_size = 3 * 2 * 4k
24 | -W- | -W- | ---- | | pp | pp_size = 3 * 4k
+------+------+------+ +----+
data_sector是修改后的数据的第一个raid扇区,data_size是修改数据总大小,pp_size是这个entry的部分奇偶校验的大小。
完整条带写的entries不包含部分奇偶校验数据(pp_size=0),它们只标记了条带的哪部分奇偶校验在unclean的关闭后重新计算。
每个entry都有一个部分奇偶校验的校验和,header本身也有一个校验和。
struct ppl_conf {
struct mddev *mddev;
/* array of child logs, one for each raid disk */
struct ppl_log *child_logs;
int count;
int block_size; /* the logical block size used for data_sector
* in ppl_header_entry */
u32 signature; /* raid array identifier */
atomic64_t seq; /* current log write sequence number */
struct kmem_cache *io_kc;
mempool_t *io_pool;
struct bio_set *bs;
struct bio_set *flush_bs;
/* used only for recovery */
int recovered_entries;
int mismatch_count;
/* stripes to retry if failed to allocate io_unit */
struct list_head no_mem_stripes;
spinlock_t no_mem_stripes_lock;
};
/* 每一个raid成员磁盘有一个独有的ppl_log */
struct ppl_log {
struct ppl_conf *ppl_conf; /* shared between all log instances */
struct md_rdev *rdev; /* array member disk associated with
* this log instance */
struct mutex io_mutex;
struct ppl_io_unit *current_io; /* current io_unit accepting new data
* always at the end of io_list */
spinlock_t io_list_lock;
struct list_head io_list; /* all io_units of this log */
sector_t next_io_sector;
unsigned int entry_space;
bool use_multippl;
bool wb_cache_on;
unsigned long disk_flush_bitmap;
};
一个写请求总是记录到该条带对应校验位所在的磁盘上面的PPL实例里面。
对于每个成员磁盘都有一个ppl_log用于处理此磁盘的日志记录,独立于其他磁盘的ppl_log。
他们是分组在struct ppl_conf中的child_logs数组中,它被分配给r5conf->log_private。
struct ppl_io_unit {
struct ppl_log *log;
struct page *header_page; /* for ppl_header */
unsigned int entries_count; /* number of entries in ppl_header */
unsigned int pp_size; /* total size current of partial parity */
u64 seq; /* sequence number of this log write */
struct list_head log_sibling; /* log->io_list */
struct list_head stripe_list; /* stripes added to the io_unit */
atomic_t pending_stripes; /* how many stripes not written to raid */
atomic_t pending_flushes; /* how many disk flushes are in progress */
bool submitted; /* true if write to log started */
/* inline bio and its biovec for submitting the iounit */
struct bio bio;
struct bio_vec biovec[PPL_IO_INLINE_BVECS];
};
ppl_io_unit代表一个完整的PPL写操作,结构体成员header_page指向ppl_header。
在ppl_log_stripe()中加入了PPL entries。
条目的校验和是递增的,因为包含了部分奇偶性的条带正在被添加。
ppl_submit_iounit()计算ppl_header的校验和,并提交一个包含ppl_header页和部分奇偶校验页(sh->ppl_page)的bio,包含这个ppl_io_unit的所有条带。当PPL写完成时,与这个io_unit相关的条带将被释放,raid5d开始写它们的数据和奇偶校验。
当所有的条带都被写入时,ppl_io_unit将被释放,下一个可以提交。
一个ppl_io_unit用于收集条带,直到它被提交或变得满(如果达到了最大的条目数或PPL的大小)。
另一个ppl_io_unit不能在上一个完成(PPL和条带数据+奇偶校验)之前提交。
log->io_list跟踪一个日志的所有io_units(对于单个成员磁盘)。
新的io_units被添加到列表的末尾,如果还没有提交,那么第一个iounit将被提交。
当前的io_units接受新条带总是在列表的末尾。
如果raid成员磁盘里面的任何一个的cache的回写模式使能情况下,在下一个io_unit提交前必须要把之前的链表成员刷写下去。
根据raid5状态机步骤进行数据拆分、重构、读写操作。将提交的BIO请求根据chunk大小以及磁盘个数进行拆分,并申请新的条带,使用条带指向拆分出来的BIO数据。
3.2.1:重构磁盘数据,如果需要读取就先读取上来数据,并进行校验盘数据p的计算,保存到该条带r5dev结构体对应的校验盘的page空间
3.2.2:使用未修改数据计算ppl值保存到该条带的ppl_page空间。
3.3.1申请ppl_io_unit单元以及entry单元,计算各个单元数据值,使用条带的ppl_page空间数据计算checksum保存到entry条目里面;
3.3.2提交包含ppl_header以及ppl_page数据的bio请求到该条带对应的校验盘;
总结:
该数据提交的起始扇区很小,并不是从该磁盘的data offset:266240 sectors开始,也不是super offset:8 sectors;而是通过算法计算出来的超级块后面一些的空间。
a:设置脏条带准备落盘,在下一次循环时将提交写数据盘和校验盘请求到通用块设备层。
b:申请ppl_io_unit进行填充,先把ppl_io_unit单元写入校验盘,之后再写data数据以及校验数据p。
c:ppl空间大小等于trunck_size*数据盘个数,空间复用,写完一个unit后就释放,新的unit写入同样地址,测试机器为【16-2048】参考文件《512ktrunk-1030K DATA》
按照条带中每个设备的4K大小的数据【data数据\校验盘数据】提交该条带到对应数据盘或者校验盘;
3.5.1:这个阶段ppl+data+p数据都已经落盘,就可以进行bio返回,通知用户数据处理完成。
3.5.2:根据disk_flush_bitmap获取要进行flush操作的磁盘量,如果磁盘没有出问题,就下发REQ_OP_WRITE | REQ_PREFLUSH;
创建raid5时,使用参数ppl,则会调用ppl_init_log,在这里面会调用到ppl_load(ppl_conf);这个函数会load各个磁盘的ppl数据,根据这个函数的返回值进行判断
如果成功恢复一个脏的阵列,则设置阵列为clean状态,如果在使能ppl情况下,这个函数返回mismatch_count的数据大于0,则返回错误参数;
之后进入ppl_load_distributed函数,读取校验盘数据,对比pplhdr->checksum与从数据盘读取出来的4kheader的校验数据,如果不一致则累加mismatch_count,signature不相同也不行,如果一致,则第一关通过,表明校验盘里面的ppl header数据没有问题,下面就需要继续校验header里面的每一个entry的checksum值与数据盘的重新计算的crc值是否相同。
之后判断是否需要进行ppl_recover操作,如果从一个dirty的阵列启动就尝试通过log进行恢复。通过前面从磁盘读取出来的header结构体信息,针对每一个entries进行判断
entry保留的checksum与该ppl size范围内的数据磁盘数据校验和是否相同,如果连校验和都不相同就累加mismatch_count计数,继续计算下一个entry。如果这个entry对应的数据盘得出的checksum值与entry结构体里面保存的checksum值相同,则可以用来进行数据恢复操作,否则就不能用来进行数据恢复。
之后就可以进入数据计算恢复流程,通过entry的信息计算出来总共的条带数目,遍历所有数据磁盘,读取被更新的数据,并使用该数据与ppl数据进行异或运算得出新的校验盘数据,使用新计算出来的校验盘数据更新校验盘,此时校验盘数据为正确的值,与数据盘中的数据相匹配。
参考流程如下: