首页 > 代码库 > zfs raidz结构详解

zfs raidz结构详解

直接进入主题,几个重点:

    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,则数据会存储在:


disk1disk2disk3disk4disk5

sec#001234

sec#156789

sec#21011P=DD


sec#3http://www.datahf.net 作者:张宇

示例:如果x位于奇数1M内,设数据为D,校验为P,则数据会存储在:


disk1disk2disk3disk4disk5

sec#51201234

sec#51356789

sec#5141011DP=D


sec#515http://www.datahf.net 作者:张宇

情况二:如果一次IO大小为2扇区,RAIDZ VDEV的offset地址为X,设"+"表示异或

示例:如果x为10,位于偶数1M内,设数据为D1,D2,校验为P,则数据会存储在:


disk1disk2disk3disk4disk5

sec#001234

sec#156789

sec#21011P=D1+D2D1D2

sec#3SKIP





sec#4http://www.datahf.net 作者:张宇

示例:如果x位于奇数1M内,设数据为D1,D2,校验为P,则数据会存储在:


disk1disk2disk3disk4disk5

sec#51201234

sec#51356789

sec#5141011D1P=D1+D2D2

sec#515SKIP





sec#516http://www.datahf.net 作者:张宇

情况三:如果一次IO大小为5扇区,RAIDZ VDEV的offset地址为X,设"+"表示异或

示例:如果x为10,位于偶数1M内,设数据为D1,D2,D3,D4,D5,校验为P,则数据会存储在:


disk1disk2disk3disk4disk5

sec#001234

sec#156789

sec#21011P=D1+D3+D4+D5D1D3

sec#3D4D5P=D2D2SKIP

sec#4






sec#5http://www.datahf.net 作者:张宇

示例:如果x位于奇数1M内,设数据为D1,D2,D3,D4,校验为P,则数据会存储在:


disk1disk2disk3disk4disk5

sec#51201234

sec#51356789

sec#5141011D1P=D1+D3+D4+D5D3

sec#515D4D5D2P=D2SKIP

sec#516






sec#517http://www.datahf.net 作者:张宇

情况四:如果一次IO大小为6扇区,RAIDZ VDEV的offset地址为X,设"+"表示异或

示例:如果x为10,位于偶数1M内,设数据为D1,D2,D3,D4,D5,校验为P,则数据会存储在:


disk1disk2disk3disk4disk5

sec#001234

sec#156789

sec#21011P=D1+D3+D5+D6D1D3

sec#3D5D6P=D2+D4D2D4

sec#4






sec#5http://www.datahf.net 作者:张宇

示例:如果x位于奇数1M内,设数据为D1,D2,D3,D4,D5,校验为P,则数据会存储在:


disk1disk2disk3disk4disk5

sec#51201234

sec#51356789

sec#5141011D1P=D1+D3+D5+D6D3

sec#515D5D6D2P=D2+D4D4

sec#516






sec#517http://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);
}


本文出自 “张宇(数据恢复)” 博客,转载请与作者联系!

zfs raidz结构详解