直接进入主题,几个重点:
1、RAIDZ是和ZFS密切配合的一种RAID模型,RAIDZ在接收数据时是由ZFS指定一个可变长的数据流。根据这个数据流的大小不同,RAIDZ在存储时也会有不同。
2、RAIDZ相对于传统RAID,没有严格的blocksize概念,如果数据流小,甚至可以是1扇区的blocksize。
同时相对于传统RAID,也没有一个标准的校验模式,虽然比较像RAID5,但假如是1扇区的IO,就更像RAID1了。
3、RAIDZ也可以支持多重冗余,内部称之为RAIDZ_P(即通常提到的RAIDZ,支持1块硬盘掉线)、RAIDZ_Q(支持2块盘同时掉线,如同RAID6)、 RAIDZ_R(支持3块盘同时掉线)
4、RAIDZ的IO地址是带有校验的地址值,不同于传统RAID校验(传统RAID的校验区域对于文件系统而言是不可见的)
5、RAIDZ_P的校验位置在每次IO的位置相对一致,但为了负载均衡,约定,如果IO首地址是偶数1M内(即offset / 1M为偶数),校验在数据的最前面;如果IO首地址是奇数1M内,校验插入数据流,在第一个扇区(从0开始计数)。此规则仅适用于RAIDZ_P,不适用于RAIDZ_Q,RAIDZ_Q
6、RAIDZ约定,一次IO一定是校验数+1的整数倍,比如RAIDZ_P一次IO下来如果是3扇区,最后会有一个SKIP扇区(因此,才会有5中校验要交换的做法),zfs为了保证空间再分配时不至于出现孔洞,所以在申请空间时,就必须满足是(nparity + 1)的整数倍,就样的好处在于,任意申请的空间,重用时,至少都是够最小运算模式的。
比如:RAIDZ一段连续的空间中间,释放了6个扇区,如果再重用时只用了5个,那剩下的1个还是会浪费掉,无法分配。如果是RAIDZ2或RAIDZ3,这种问题就更突出了。反正无法避免浪费,为了运算简洁,干脆在每次申请时就按整块的处理,确保无论如何释放,都不会在下一次IO时出现浪费。
7、为了保证IO高效,zfs一次写入IO时,会优先以vdev为单位连续写入,所以,会很不像1扇区为条带大小的RAID5,具体见结构描述示例:
假设有5块硬盘组成RAIDZ,分别是DISK1,DISK2,DISK3,DISK4,DISK5顺序也按此排列: | ||||||||||
情况一: | 如果一次IO大小为1扇区,RAIDZ VDEV的offset地址为X,则(x/5)先计算出在哪个条带,再通过(x % 5)得到开始盘序,在同一条带上再向后挪一个磁盘(可能会返回disk1),这2个扇区一个是数据,一个是校验(此情况RAIDZ无需填充),就完成了此次IO的存储 | |||||||||
示例:如果x为10,位于偶数1M内,设数据为D,校验为P,则数据会存储在: | ||||||||||
disk1 | disk2 | disk3 | disk4 | disk5 | ||||||
sec#0 | 0 | 1 | 2 | 3 | 4 | |||||
sec#1 | 5 | 6 | 7 | 8 | 9 | |||||
sec#2 | 10 | 11 | P=D | D | ||||||
sec#3 | http://www.datahf.net 作者:张宇 | |||||||||
示例:如果x位于奇数1M内,设数据为D,校验为P,则数据会存储在: | ||||||||||
disk1 | disk2 | disk3 | disk4 | disk5 | ||||||
sec#512 | 0 | 1 | 2 | 3 | 4 | |||||
sec#513 | 5 | 6 | 7 | 8 | 9 | |||||
sec#514 | 10 | 11 | D | P=D | ||||||
sec#515 | http://www.datahf.net 作者:张宇 | |||||||||
情况二: | 如果一次IO大小为2扇区,RAIDZ VDEV的offset地址为X,设"+"表示异或 | |||||||||
示例:如果x为10,位于偶数1M内,设数据为D1,D2,校验为P,则数据会存储在: | ||||||||||
disk1 | disk2 | disk3 | disk4 | disk5 | ||||||
sec#0 | 0 | 1 | 2 | 3 | 4 | |||||
sec#1 | 5 | 6 | 7 | 8 | 9 | |||||
sec#2 | 10 | 11 | P=D1+D2 | D1 | D2 | |||||
sec#3 | SKIP | |||||||||
sec#4 | http://www.datahf.net 作者:张宇 | |||||||||
示例:如果x位于奇数1M内,设数据为D1,D2,校验为P,则数据会存储在: | ||||||||||
disk1 | disk2 | disk3 | disk4 | disk5 | ||||||
sec#512 | 0 | 1 | 2 | 3 | 4 | |||||
sec#513 | 5 | 6 | 7 | 8 | 9 | |||||
sec#514 | 10 | 11 | D1 | P=D1+D2 | D2 | |||||
sec#515 | SKIP | |||||||||
sec#516 | http://www.datahf.net 作者:张宇 | |||||||||
情况三: | 如果一次IO大小为5扇区,RAIDZ VDEV的offset地址为X,设"+"表示异或 | |||||||||
示例:如果x为10,位于偶数1M内,设数据为D1,D2,D3,D4,D5,校验为P,则数据会存储在: | ||||||||||
disk1 | disk2 | disk3 | disk4 | disk5 | ||||||
sec#0 | 0 | 1 | 2 | 3 | 4 | |||||
sec#1 | 5 | 6 | 7 | 8 | 9 | |||||
sec#2 | 10 | 11 | P=D1+D3+D4+D5 | D1 | D3 | |||||
sec#3 | D4 | D5 | P=D2 | D2 | SKIP | |||||
sec#4 | ||||||||||
sec#5 | http://www.datahf.net 作者:张宇 | |||||||||
示例:如果x位于奇数1M内,设数据为D1,D2,D3,D4,校验为P,则数据会存储在: | ||||||||||
disk1 | disk2 | disk3 | disk4 | disk5 | ||||||
sec#512 | 0 | 1 | 2 | 3 | 4 | |||||
sec#513 | 5 | 6 | 7 | 8 | 9 | |||||
sec#514 | 10 | 11 | D1 | P=D1+D3+D4+D5 | D3 | |||||
sec#515 | D4 | D5 | D2 | P=D2 | SKIP | |||||
sec#516 | ||||||||||
sec#517 | http://www.datahf.net 作者:张宇 | |||||||||
情况四: | 如果一次IO大小为6扇区,RAIDZ VDEV的offset地址为X,设"+"表示异或 | |||||||||
示例:如果x为10,位于偶数1M内,设数据为D1,D2,D3,D4,D5,校验为P,则数据会存储在: | ||||||||||
disk1 | disk2 | disk3 | disk4 | disk5 | ||||||
sec#0 | 0 | 1 | 2 | 3 | 4 | |||||
sec#1 | 5 | 6 | 7 | 8 | 9 | |||||
sec#2 | 10 | 11 | P=D1+D3+D5+D6 | D1 | D3 | |||||
sec#3 | D5 | D6 | P=D2+D4 | D2 | D4 | |||||
sec#4 | ||||||||||
sec#5 | http://www.datahf.net 作者:张宇 | |||||||||
示例:如果x位于奇数1M内,设数据为D1,D2,D3,D4,D5,校验为P,则数据会存储在: | ||||||||||
disk1 | disk2 | disk3 | disk4 | disk5 | ||||||
sec#512 | 0 | 1 | 2 | 3 | 4 | |||||
sec#513 | 5 | 6 | 7 | 8 | 9 | |||||
sec#514 | 10 | 11 | D1 | P=D1+D3+D5+D6 | D3 | |||||
sec#515 | D5 | D6 | D2 | P=D2+D4 | D4 | |||||
sec#516 | ||||||||||
sec#517 | http://www.datahf.net 作者:张宇 | |||||||||
源码主要位于module\zfs\vdev_raidz.c,涉及分配规则的函数为vdev_raidz_map_alloc(),仔细对源码解读、注释后的结果如下:
/*
* Divides the IO evenly across all child vdevs; usually, dcols is
* the number of children in the target vdev.
*
* Avoid inlining the function to keep vdev_raidz_io_start(), which
* is this functions only caller, as small as possible on the stack.
*/
/*
*分配原则是需要在所有子vdev之间平均分配IO,dcols是目标vdev中的子节点数。
*避免内联函数以保持vdev_raidz_io_start(),它是这个函数只有调用者,在堆栈上要尽可能小。
by:张宇
*/
noinline static raidz_map_t *
vdev_raidz_map_alloc(zio_t *zio, uint64_t unit_shift, uint64_t dcols,
uint64_t nparity)
{
raidz_map_t *rm;
/* The starting RAIDZ (parent) vdev sector of the block. */
/* 在父vdev上的扇区编号,其实就是RAIDZx这个vdev,DVA中标注的扇区号*/
uint64_t b = zio->io_offset >> unit_shift;
/* The zio's size in units of the vdev's minimum sector size. */
/*一次IO的字节大小,其实就是RAIDZx这个vdev,一次IO的有效数据大小(不包含校验,扇区数*每扇区字节数)*/
uint64_t s = zio->io_size >> unit_shift;
/* The first column for this stripe. */
/*条带的第一列,是用父vdev的扇区编号对vdev数(raid成员数)取余的结果*/
uint64_t f = b % dcols;
/* The starting byte offset on each child vdev. */
/*计算每个子vdev的起始字节位置,用父vdev的扇区号简单地除以"子vdev数量"*/
uint64_t o = (b / dcols) << unit_shift;
uint64_t q, r, c, bc, col, acols, scols, coff, devidx, asize, tot;
/*
* "Quotient": The number of data sectors for this stripe on all but
* the "big column" child vdevs that also contain "remainder" data.
*/
/*q表示共占用多少完整行(以每个扇区为行高)*/
q = s / (dcols - nparity);
/*
* "Remainder": The number of partial stripe data sectors in this I/O.
* This will add a sector to some, but not all, child vdevs.
*/
/*r表示除去整数行外,不足一行部分,还剩多少io扇区(仅计数据,不计校验)*/
r = s - q * (dcols - nparity);
/* The number of "big columns" - those which contain remainder data. */
/*尾部扇区数,加上可能的校验的大小---如果尾部扇区数为0,表示正好凑整N行,就不用另加校验扇区了。*/
bc = (r == 0 ? 0 : r + nparity);
/*
* The total number of data and parity sectors associated with
* this I/O.
*/
/*表示算上校验的完整扇区总数*/
tot = s + nparity * (q + (r == 0 ? 0 : 1));
/* acols: The columns that will be accessed. */
/* scols: The columns that will be accessed or skipped. */
/* acols:需要存取的io列数 */
/* scols:加上可能的skip后的io列数 */
/*如果io扇区数量不必要动用所有vdev,则没必要所有列都处理*/
if (q == 0) {
/* Our I/O request doesn't span all child vdevs. */
acols = bc;
scols = MIN(dcols, roundup(bc, nparity + 1));
} else {
acols = dcols;
scols = dcols;
}
ASSERT3U(acols, <=, scols);
rm = kmem_alloc(offsetof(raidz_map_t, rm_col[scols]), KM_SLEEP);
rm->rm_cols = acols;
rm->rm_scols = scols;
rm->rm_bigcols = bc;
rm->rm_skipstart = bc;//表示skip扇区默认位置,放在最后,这是RAIDZ列的位置顺序号,表示rm->rm_col[XXX].中的XXX
rm->rm_missingdata = 0;
rm->rm_missingparity = 0;
rm->rm_firstdatacol = nparity;//默认第一个数据块区在校验后(但后面为了均衡,会可能置换)
rm->rm_datacopy = NULL;
rm->rm_reports = 0;
rm->rm_freed = 0;
rm->rm_ecksuminjected = 0;
asize = 0;
for (c = 0; c < scols; c++) {
col = f + c;//f是io的第一列,再求从第一列开始,依次向后
coff = o; //io起始offset
if (col >= dcols) { //如果到了列尾,折到下一行
col -= dcols;
coff += 1ULL << unit_shift;
}
rm->rm_col[c].rc_devidx = col;
rm->rm_col[c].rc_offset = coff;
rm->rm_col[c].rc_data = NULL;
rm->rm_col[c].rc_gdata = NULL;
rm->rm_col[c].rc_error = 0;
rm->rm_col[c].rc_tried = 0;
rm->rm_col[c].rc_skipped = 0;
if (c >= acols) //如果不足一行,且skip部分的扇区
rm->rm_col[c].rc_size = 0;
else if (c < bc)//如果超过一行,计算当前列的"厚度"--如果折回来的最尾部所在的vdev要多一个io扇区
rm->rm_col[c].rc_size = (q + 1) << unit_shift;
else
rm->rm_col[c].rc_size = q << unit_shift;
asize += rm->rm_col[c].rc_size;//asize等于除去skip的IO字节数(包括校验)
}
ASSERT3U(asize, ==, tot << unit_shift);
rm->rm_asize = roundup(asize, (nparity + 1) << unit_shift);//加上skip的IO总字节数(含校验)
rm->rm_nskip = roundup(tot, nparity + 1) - tot;//skip扇区数
ASSERT3U(rm->rm_asize - asize, ==, rm->rm_nskip << unit_shift);
ASSERT3U(rm->rm_nskip, <=, nparity);
for (c = 0; c < rm->rm_firstdatacol; c++)//为校验分配内存
rm->rm_col[c].rc_data = zio_buf_alloc(rm->rm_col[c].rc_size);
rm->rm_col[c].rc_data = zio->io_data; //io的原始数据,指向rm_firstdatacol(等于校验数,即相当于先跳过几列校验,之后开始按列写入真实数据)
for (c = c + 1; c < acols; c++) //以列为单位,向vdev一次性分配io原始数据(此时还未涉及校验)
rm->rm_col[c].rc_data = (char *)rm->rm_col[c - 1].rc_data +
rm->rm_col[c - 1].rc_size;
/*
* If all data stored spans all columns, there's a danger that parity
* will always be on the same device and, since parity isn't read
* during normal operation, that that device's I/O bandwidth won't be
* used effectively. We therefore switch the parity every 1MB.
*
* ... at least that was, ostensibly, the theory. As a practical
* matter unless we juggle the parity between all devices evenly, we
* won't see any benefit. Further, occasional writes that aren't a
* multiple of the LCM of the number of children and the minimum
* stripe width are sufficient to avoid pessimal behavior.
* Unfortunately, this decision created an implicit on-disk format
* requirement that we need to support for all eternity, but only
* for single-parity RAID-Z.
*
* If we intend to skip a sector in the zeroth column for padding
* we must make sure to note this swap. We will never intend to
* skip the first column since at least one data and one parity
* column must appear in each row.
*/
/*
如果所有数据存储用到了每一列,则存在校验块始终在同一设备上的问题。而校验块不
参与正常的IO读取,所以,从负载角度看,该设备的I/O带宽无法被有效使用。因此,
我们每隔1MB切换奇偶校验(方法是仅针对RAID-Z,每隔1M,交换校验列与第一个数据列)。
疑问1:
校验列和第一个数据列交换,会不会因为厚度不同(IO行数),导致IO片断不连续
答:
不会,因为校验列是最厚列(必须保证每一行都有校验),第一个数据列,也是最厚列
疑问2:
为什么要有padding sector?
答:
zfs为了保证空间再分配时不至于出现孔洞,所以在申请空间时,就必须满足是(nparity + 1)
的整数倍,就样的好处在于,任意申请的空间,重用时,至少都是够最小运算模式的。
比如:RAIDZ一段连续的空间中间,释放了6个扇区,如果再重用时只用了5个,那剩下的1个还是会浪费掉,
无法分配。如果是RAIDZ2或RAIDZ3,这种问题就更突出了。反正无法避免浪费,为了运算简洁,干脆在每
次申请时就按整块的处理,确保无论如何释放,都不会在下一次IO时出现浪费。
疑问3:
为什么raidz2和raidz3无需每隔1M交换校验位置
答:
raidz2和raidz3都有超过1个的校验块,反正会横跨奇偶位置,交换的意义不大(虽然PQR的负载不完全对等)
*/
ASSERT(rm->rm_cols >= 2);
ASSERT(rm->rm_col[0].rc_size == rm->rm_col[1].rc_size);
/*if(raidZ && io位置是奇数个1M){
交换第一列(校验列),与第二列(第一个数据起始列)
}
*/
if (rm->rm_firstdatacol == 1 && (zio->io_offset & (1ULL << 20))) {
devidx = rm->rm_col[0].rc_devidx;
o = rm->rm_col[0].rc_offset;
rm->rm_col[0].rc_devidx = rm->rm_col[1].rc_devidx;
rm->rm_col[0].rc_offset = rm->rm_col[1].rc_offset;
rm->rm_col[1].rc_devidx = devidx;
rm->rm_col[1].rc_offset = o;
//rm->rm_skipstart = bc;
//bc=尾部扇区数,加上校验块的大小
//如果padding扇区正好位于第0列,被上面交换过后,就有错误了
if (rm->rm_skipstart == 0)
rm->rm_skipstart = 1;
}
zio->io_vsd = rm;
zio->io_vsd_ops = &vdev_raidz_vsd_ops;
return (rm);
}