f2fs系列之十:f2fs到底如何避免wandering tree的?

名词解释

  • f2fs
    flash friend file system,最早由三星开发的一个对flash/SSD 友好的文件系统。

  • block
    f2fs中数据读写最小的切片单位,通常4K。没个block 有一个编号,每个编号对应某个偏移的物理切片。包括:
  1. data block 专门存放数据
  2. node block 专门存放Block 编号,这个编号可以视为NodeID。
  • f2fs的索引层次

包括四层:

  1. indoe
    文件或目录的索引节点,含义同通常的inode节点,可以理解成文件或目录的最顶层索引。

  2. direct pointer:
    包含在inode中,地址直接指向数据块。

  3. indirect node
    indirect node是一个node block,里面存放的是Node ID。这个NodeID需要差NAT才能找到对应的数据Node。

  4. doubel indirect node:
    indirect node是一个node block,里面存放的是Node ID。这个NodeID需要差NAT才能找到对应的indirect node。
  • NAT
    顾名思义,NAT是node address table,是file/directory内逻辑地址到物理地址转换的查找表。

背景

基于LFS的文件系统如果使用平常的多级索引数据结构,会存在wandering tree问题。比如,如果使用下面的多级索
引:

f2fs系列之十:f2fs到底如何避免wandering tree的?

当我们对某个indirect指向的数据更新的时候,基于LFS实现,就需要更新对应的索引树,如下图所示:

f2fs系列之十:f2fs到底如何避免wandering tree的?

显然这样一次更新数据,需要更新三级索引结构,写放大太大。这个问题就是LFS的wandring tree 问题。f2fs是如何解决这个问题的呢?

实现原理

分析上面过程,造成wandring tree的关键是上级索引直接指向了下级索引,基于LFS索引也不能原地更新,这样一旦下级索引有改动,上级索引也需要随之更新。为此,可以在上级索引和下级索引直接引入一层防火墙,来隔离这两者的相互影响,避免更新向上层传播。

f2fs里这里利用的就是NAT(Node Address Table)这个统一的表,用以实现node id和node block的地址映射。
如下图所示:

f2fs系列之十:f2fs到底如何避免wandering tree的?

比如,我们要访问某个文件第11个block的数据块,根据pointer11,找到对应的indirect block,基于inode的索引层次,算出对应物理块地址在这个indirect block内由哪个entry索引,读取对应entry记录的NodeID,查找NAT,得到最终的物理地址。最后基于这个物理地址,访问数据。

相关数据结构

主要的数据结构包括:

  • inode

    struct inode {
        ......
        __le32 i_flags;                 /* file attributes */
        __le32 i_pino;                  /* parent inode number */
        __le32 i_namelen;               /* file name length */
        __u8 i_name[F2FS_NAME_LEN];     /* file name for SPOR */
        __u8 i_dir_level;               /* dentry_level for large dir */
        ......
        union {
                struct { // for what usage?
                        __le16 i_extra_isize;   /* extra inode attribute size */
                        __le32 i_inode_checksum;/* inode meta checksum */
                        __le64 i_crtime;        /* creation time */
                        __le32 i_crtime_nsec;   /* creation time in nano scale */
                        __le32 i_extra_end[0];  /* for attribute size calculation */
                } __packed;
                __le32 i_addr[DEF_ADDRS_PER_INODE];     /* Pointers to data blocks */
        };
        __le32 i_nid[DEF_NIDS_PER_INODE];       /* direct(2), indirect(2),
                                                double_indirect(1) node id */
    } __packed;
  • node
struct direct_node {
        __le32 addr[ADDRS_PER_BLOCK];   /* array of data block address */
} __packed;

struct indirect_node {
        __le32 nid[NIDS_PER_BLOCK];     /* array of data block address */
} __packed;
struct f2fs_node {
        /* can be one of three types: inode, direct, and indirect types */
        union {
                struct f2fs_inode i;
                struct direct_node dn;
                struct indirect_node in;
        };
        struct node_footer footer;
} __packed;
  • nat
/*
 * For NAT entries
 */
#define NAT_ENTRY_PER_BLOCK (PAGE_SIZE / sizeof(struct f2fs_nat_entry))

struct f2fs_nat_entry {
        __u8 version;           /* latest version of cached nat entry */
        __le32 ino;             /* inode number */
        __le32 block_addr;      /* block address */
} __packed;

struct f2fs_nat_block {
        struct f2fs_nat_entry entries[NAT_ENTRY_PER_BLOCK];
} __packed;

NAT的更新

根据上面流程可以看到,写文件的过程中实际也会更新对应的NAT。那么,NAT 是每一次IO都需要更新吗?如果这样,也会有一次写放大,为此实际工程中可以把这些更新攥在一起落盘,减少整体写的次数。

那么如果不及时更新,掉电怎么办?可以把NAT的更新也以log strucuture 的形式追加落盘,在f2fs 做checkpoint的时候,把这些更新统一写入NAT,然后回收先前的NAT 更新日志空间,流程参考:http://xiaqichao.cn/wordpress/?p=211

//首发于http://xiaqichao.cn 欢迎光临

猜你喜欢

转载自blog.51cto.com/xiamachao/2495889