本文共 9589 字,大约阅读时间需要 31 分钟。
直接进入主题,几个重点:
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 | 作者:张宇 | |||||||||
示例:如果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 | 作者:张宇 | |||||||||
情况二: | 如果一次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 | 作者:张宇 | |||||||||
示例:如果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 | 作者:张宇 | |||||||||
情况三: | 如果一次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 | 作者:张宇 | |||||||||
示例:如果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 | 作者:张宇 | |||||||||
情况四: | 如果一次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 | 作者:张宇 | |||||||||
示例:如果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 | 作者:张宇 | |||||||||
源码主要位于module\zfs\vdev_raidz.c,涉及分配规则的函数为vdev_raidz_map_alloc(),仔细对源码解读、注释后的结果如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 | /* * 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); } |