直接进入主题,几个重点:
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); }