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,则数据会存储在:
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);
}
时间: 2024-10-26 12:27:44

zfs raidz结构详解的相关文章

PHP扩展代码结构详解

PHP扩展代码结构详解: 这个是继:使用ext_skel和phpize构建php5扩展  内容 (拆分出来) Zend_API:深入_PHP_内核:http://cn2.php.net/manual/zh/internals2.ze1.php 我们使用ext_skel创建扩展 hello_module,该模块包含一个方法:hello_world. 使用ext_skel 生成的代码都是PHP_开头的宏, 而不是ZEND_开头. 实际上这两者是一样的. 在源代码src/main/PHP.h 中发现:

logback教程(2) Logback结构详解

长话短说:LogBack的结构 为了适用不同的环境,logback的基础结构符合常规. logback分为三个模块: logback-core,logback-classic以及logback-access. 1 >核心模块(core)为其他两个模块提供基础. 2 >classic模块继承自core.classic模块很明相当于log4j的增强版. 3 >Logback-classic原生的继承自SLF4J API因此你可以很容易的在LogBack和其他像日志系统比如log4j或java

Windows GPT磁盘GUID结构详解

前一篇Windows磁盘MBR结构详解中我们介绍了Basic Disk中的Master Boot Record结构.GPT Disk作为Windows 2003以后引入的分区结构.使用了GUID分区表结构,它与MBR相比好处是支持更大和更多的分区,提高容错.本文介绍了GUID分区表的结构和各个字段的含义. GPT Disk 的Protective MBR: GPT Disk的结构中,第一个LBA位置(LBA 0)存放的是Protective MBR,随后LBA1的位置才是GPT的GUID分区表头

IPv4头部结构详解

IPv4头部结构详解 以下为书中原文摘录:

微赞微擎手动增加模块数据库表结构详解

微赞微擎手动增加模块数据库表结构详解 有时候微擎或微赞的模块没有安装模块的xml文件,那我们先想安装到自己的系统上,要怎么处理呢,下面我们详细的介绍下步骤,个人能力有限,如有不正确之处,敬请谅解~ 1.模块的代码复制 这个就不用多说了吧,当然需要把相应的addons文件夹里的模块复制到自己系统的目录里,不然不要做一下的事情了 2.数据库表结构修改 代码复制过来,如果有相应的xml安装包或者install.php文件,可以直接安装,但是我们这里讲的是没有,那只能把原来要复制的表结构记录复制过来,插

Linux下的文件目录结构详解

Linux下的文件目录结构详解 / Linux文件系统的上层根目录 /bin 存放用户可执行的程序 /boot 操作系统启动时所需要的文件 /dev 接口设备文件目录,例如:had表示硬盘 /etc 有关系统设置与管理的文件 /home 一般用户的主目录或者FTP站点管理目录 /mnt 装置的文件系统加载点,例如:光驱.软盘等... /proc 目前系统核心与程序执行的信息. /root 管理员的主目录 /sbin 此目录存放系统启动时所需要执行的程序 /tmp 用来存放暂存盘的目录 /usr

JS函数动作分层结构详解及Document.getElementById 释义 事件 函数 变量 script标签 var function

html +css 静态页面 js     动态 交互 原理: js就是修改样式, 比如弹出一个对话框. 弹出的过程就是这个框由disable 变成display:enable. 又或者当鼠标指向的时候换一个颜色,就是一个修改样式的工具. 编写JS的流程 布局:HTML+CSS 事件:确定用户做哪些操作(产品设计) 编写JS:在事件中,用JS来修改页面元素的样式(外加属性:确定要修改哪些属性) 什么是事件 一个完整的事件= <在某个作用域 事件声明='函数动作'> </> 作用域:

HP-lefthand底层结构详解及存储灾难数据恢复

一.HP-lefthand的特点 HP-lefhand是一款非常不错的SAN存储,使用iscsi协议为客户端分配空间.它支持RAID5.RAID6以及RAID10.并且还支持卷快照,卷动态扩容等.常见的型号有:P4500,P4300,P4000等,基于市场占有量和软件定义存储的弊端,有一定的数据恢复市场需求. HP-lefhand的存储系统是一款嵌入式LINUX系统,需要安装客户端软件才能配置lefthand. 服务端: 客服端: 二.HP-lefthand的存储结构 Lefthand存储一共分

Netty 核心容器之ByteBuf 结构详解

原文链接 Netty 核心容器之ByteBuf 结构详解 Java的NIO模块提供了ByteBuffer作为其字节存储容器,但是这个类的使用过于复杂,因此Netty实现了ByteBuf来替换NIO的ByteBuffer类,ByteBuf具有以下的特点: 自定义用户缓冲区域的类型 实现字节区域的深浅拷贝 容量可按需增长 在读写模式直接不需要像JDK的ByteBuffer那样调用flip()方法切换 读写使用不同的索引,即readIndex和writeIndex 支持方法链式调用 支持引用计数和池化