Blog

  • inode

    来源:理解inode-阮一峰的网络日志

    文件inode

    每一个文件都有对应的inode,里面包含了与该文件有关的一些信息。

    ➜  ~ stat /tmp/hui/x
      File: ‘/tmp/hui/x’
      Size: 0           Blocks: 0          IO Block: 4096   regular empty file
    Device: fd01h/64769d    Inode: 395197      Links: 1
    Access: (0644/-rw-r--r--)  Uid: (    0/    root)   Gid: (    0/    root)
    Access: 2021-04-12 13:56:18.677116921 +0800
    Modify: 2021-04-12 13:56:18.677116921 +0800
    Change: 2021-04-12 13:56:18.677116921 +0800
     Birth: -
    
    ➜  ~ ls -i /tmp/hui/x
    395197 /tmp/hui/x
    

    由于inode号码与文件名分离,这种机制导致了一些Unix/Linux系统特有的现象。
      1. 有时,文件名包含特殊字符,无法正常删除。这时,直接删除inode节点,就能起到删除文件的作用。
      2. 移动文件或重命名文件,只是改变文件名,不影响inode号码。
      3. 打开一个文件以后,系统就以inode号码来识别这个文件,不再考虑文件名。因此,通常来说,系统无法从inode号码得知文件名。
    第3点使得软件更新变得简单,可以在不关闭软件的情况下进行更新,不需要重启。因为系统通过inode号码,识别运行中的文件,不通过文件名。更新的时候,新版文件以同样的文件名,生成一个新的inode,不会影响到运行中的文件。等到下一次运行这个软件的时候,文件名就自动指向新版文件,旧版文件的inode则被回收。

    ➜  ~ lsblk
    NAME   MAJ:MIN RM  SIZE RO TYPE MOUNTPOINT
    sr0     11:0    1 41.2M  0 rom
    vda    253:0    0   50G  0 disk
    └─vda1 253:1    0   50G  0 part /
    ➜  ~ df -i /dev/vda1
    Filesystem      Inodes  IUsed   IFree IUse% Mounted on
    /dev/vda1      3276800 197965 3078835    7% /
    
    ➜  ~ dumpe2fs -h /dev/vda1|grep "Inode size"
    dumpe2fs 1.42.9 (28-Dec-2013)
    Inode size:           256
    

    硬连接

    • inode 相同
    • 源文件与目标文件的inode号码相同,都指向同一个inode
    • inode信息中有一项叫做”链接数”,记录指向该inode的文件名总数,这时就会增加1。
    • 反过来,删除一个文件名,就会使得inode节点中的”链接数”减1。
    • 当这个值减到0,表明没有文件名指向这个inode,系统就会回收这个inode号码,以及其所对应block区域
    ➜  ~ ln /tmp/hui/x /tmp/hui/x1
    ➜  ~ ls -li /tmp/hui
    total 0
    395197 -rw-r--r-- 2 root root 0 Apr 12 13:56 x
    395197 -rw-r--r-- 2 root root 0 Apr 12 13:56 x1
    
    ➜  ~ stat /tmp/hui/x1
      File: ‘/tmp/hui/x1’
      Size: 0           Blocks: 0          IO Block: 4096   regular empty file
    Device: fd01h/64769d    Inode: 395197      Links: 2
    Access: (0644/-rw-r--r--)  Uid: (    0/    root)   Gid: (    0/    root)
    Access: 2021-04-12 13:56:18.677116921 +0800
    Modify: 2021-04-12 13:56:18.677116921 +0800
    Change: 2021-04-12 13:57:57.015423754 +0800
     Birth: -
    

    软连接

    软链接与硬链接最大的不同:指向文件名,而不是文件的inode.因此inode不同

    ➜  ~ ln -s /tmp/hui/x /tmp/hui/x2
    ➜  ~ ls -li /tmp/hui
    total 0
    395197 -rw-r--r-- 2 root root  0 Apr 12 13:56 x
    395197 -rw-r--r-- 2 root root  0 Apr 12 13:56 x1
    395212 lrwxrwxrwx 1 root root 10 Apr 12 13:58 x2 -> /tmp/hui/x
    
    

    目录

    任何一个目录的”硬链接”总数=2+子目录总数(含隐藏目录)

    创建目录时,默认会生成两个目录项:”.”和”..”。
    * 前者的inode号码就是当前目录的inode号码,等同于当前目录的”硬链接”;
    * 后者的inode号码就是当前目录的父目录的inode号码,等同于父目录的”硬链接”。

    ➜  ~ stat /tmp/hui
      File: ‘/tmp/hui’
      Size: 4096        Blocks: 8          IO Block: 4096   directory
    Device: fd01h/64769d    Inode: 394833      Links: 2
    Access: (0755/drwxr-xr-x)  Uid: (    0/    root)   Gid: (    0/    root)
    Access: 2021-04-12 13:58:36.718546254 +0800
    Modify: 2021-04-12 13:58:31.312529621 +0800
    Change: 2021-04-12 13:58:31.312529621 +0800
     Birth: -
    
    ➜  ~ mkdir /tmp/hui/huichild
    
    ➜  ~ stat /tmp/hui
      File: ‘/tmp/hui’
      Size: 4096        Blocks: 8          IO Block: 4096   directory
    Device: fd01h/64769d    Inode: 394833      Links: 3
    Access: (0755/drwxr-xr-x)  Uid: (    0/    root)   Gid: (    0/    root)
    Access: 2021-04-12 13:58:36.718546254 +0800
    Modify: 2021-04-12 14:02:40.264280838 +0800
    Change: 2021-04-12 14:02:40.264280838 +0800
     Birth: -
    
  • centos7 nfs

    来源

    http://cn.linux.vbird.org/linux_server/0330nfs.php#rpcinfo

    安装与配置

     # install on centos7
     yum install nfs-utils 
    
     # nfs config
     /etc/sysconfig/nfs
    
    # nfs share path config
    /etc/exports 
    
    cat /etc/exports
    
    #设置要分享的目录,分享给谁,定义相关属性权限
    /home/sharefiles 192.168.8.60 (rw,sync,all_squash,anonuid=45,anongid=45)
    /home/sharefiles * (rw,sync,all_squash,anonuid=45,anongid=45)
    
    • 分享目标可以是完整IP或IP域192.168.100.0/24表示一个网段主机,支持通配符* 表示所有主机
    • 权限:rw读写权限、ro只读
    • sync数据同步写入内存硬盘生产环境使用、async异步先写入内存后再写入硬盘
    • 客户端可以使用的身份权限
      • no_root_squash :加上这个选项后,root用户就会对共享的目录拥有至高的权限控制,就像是对本机的目录操作一样。不安全,不建议使用
      • root_squash :和上面的选项对应,root用户对共享目录的权限不高,只有普通用户的权限,即限制了root;
      • all_squash :不管使用NFS的用户是谁,他的身份都会被限定成为一个指定的普通用户身份;
      • anonuid/anongid :要和root_squash 以及 all_squash一同使用,用于指定使用NFS的用户限定后的uid和gid,前提是本机的/etc/passwd中存在这个uid和gid。

    rpcinfo

    显示出指定主机的 RPC 状态

    rpcinfo -p 192.168.8.61
       program vers proto   port  service
        100000    4   tcp    111  portmapper
        100000    3   tcp    111  portmapper
        100000    2   tcp    111  portmapper
        100000    4   udp    111  portmapper
        100000    3   udp    111  portmapper
        100000    2   udp    111  portmapper
        100024    1   udp  30004  status
        100024    1   tcp  30004  status
        100005    1   udp  30003  mountd
        100005    1   tcp  30003  mountd
        100005    2   udp  30003  mountd
        100005    2   tcp  30003  mountd
        100005    3   udp  30003  mountd
        100005    3   tcp  30003  mountd
        100003    3   tcp   2049  nfs
        100003    4   tcp   2049  nfs
        100227    3   tcp   2049  nfs_acl
        100003    3   udp   2049  nfs
        100003    4   udp   2049  nfs
        100227    3   udp   2049  nfs_acl
        100021    1   udp  30002  nlockmgr
        100021    3   udp  30002  nlockmgr
        100021    4   udp  30002  nlockmgr
        100021    1   tcp  30002  nlockmgr
        100021    3   tcp  30002  nlockmgr
        100021    4   tcp  30002  nlockmgr
    

    exportfs

    查看指定nfs server的共享情况

    选项与参数:
    -a :全部挂载(或卸除) /etc/exports 档案内的设定
    -r :重新挂载 /etc/exports 里面的设定,此外,亦同步更新 /etc/exports
         及 /var/lib/nfs/xtab 的内容!
    -u :卸除某一目录
    -v :在 export 的时候,将分享的目录显示到屏幕上!
    
    exportfs -arv
    

    showmount

    showmount [-ae] [hostname|IP]
    选项与参数:
    -a :显示目前主机与客户端的 NFS 联机分享的状态;
    -e :显示某部主机的 /etc/exports 所分享的目录数据。
    
    showmount -e 192.168.8.61
    Export list for 192.168.8.61:
    /data/share            *
    /media/pool0/snapshots *
    
  • innodb存储引擎

    innodb存储引擎关键点

    [TOC]

    缓存池

    • 索引页

    • 数据页

    • undo页

    • 插入缓存

    • 自适应哈希索引

    • innodb引擎的锁信息

    • 数据字典信息

    innodb 内存数据对象

    mysql> use information_schema;                                                   Database changed
    mysql> select pool_id,pool_size, free_buffers,database_pages from innodb_buffer_pool_stats \G;
    *************************** 1. row ***************************
           pool_id: 0
         pool_size: 8192
      free_buffers: 1024
    database_pages: 7160
    1 row in set (0.00 sec)
    

    缓存池管理

    innodb_buffer_pool_list

    • LRU(默认)
      • 频繁使用的在LRU列表前端
      • 优化:新访问的页不是放在列表前端,而是放在Midpoint。避免扫描全表的sql,将热点数据挤压刷出
      • midpoint之前称为新表(new sublist),表示热点数据
      • midpoint之前称为旧表(old sublist)
    • unzip_LRU
      • 管理非16KB的页
      • LRU列表中包含unzip_LRU列表中的页

      在LRU列表中的页被修改后,成改页为脏页(dirty page)。

      数据块通过checkpoint机制将脏页刷新回磁盘

      Flush列表中的页为脏页列表。脏页同时存在于LRU列表和Flush列表

    重做日志缓存(redo log buffer)

    • 先将redo信息放入redo log buffer
    • 然后按照一定频率刷新到redo log file(一般1秒一刷)
    • flush redo log
      • master thread 一秒刷redo log buffer to redo log
      • 每个事务提交时
      • 当redo log buffer 剩余空间少于1/2时

    额外的内存池

    • 一些数据结构本身的内存优先从(额外的内存池)中分配
    • 当(额外的内存池)内存不足,会从缓存池中分配

    checkpoint技术

    面对的难题

    • 缓存池不可能缓存数据库所有数据
    • redo log不可能无限大

    用于解决

    • 缩短数据库恢复时间
    • 缓冲池不够用时,将脏页刷新到磁盘
    • 重做日志不可用时,刷新脏页

    通过LSN(log sequence Number)来标记版本的。

    • LSN是8字节的数字
    • 每个页有LSN
    • redo log有LSN
    • checkpoint 有LSN

    有两种checkpoint

    • Sharp checkpoint,数据库关闭时,将所有脏页刷新回磁盘
    • Fuzzy checkpoint,数据库运行中使用,进行页的刷新,只刷新一部分脏页回到磁盘
      • master Fuzzy checkpoint。异步定时从缓冲池刷脏页到磁盘
      • FLUSH_LRU_LIST Checkpoint 为了保证LRU LIST有差不多100个空闲页可供使用,Page Cleaner线程中执行
      • Async/Sync flush Checkpoint.
      • dirty page too much Checkpoint

    MASTER THREAD

    • 具有最高的线程优先级
    • 由多个loop循环组成
      • 主循环(loop):每秒操作和每十秒操作
      • 后台循环(backgroup loop)
      • flush loop
      • 暂停循环(suspend loop)

    loop

    每秒操作

    • 刷redo log buffer到磁盘,即使这个事务还没有提交(总是)

      解析了为什么再大的事务commit都很快

    • 合并插入缓冲(可能)

      只有io压力很低,前一秒的io总数<5,才会执行。

    • 至多刷100个Innodb 缓冲池中的脏页到磁盘(可能)

      当前缓存池中的脏页比例buffer_get_modified_ratio_pct大于配置值

    • 当前没有用户,切换到backupgroup loop状态(可能)

    每十秒操作

    • 刷100个Innodb 缓冲池中的脏页到磁盘(可能)
    • 合并至多5个插入缓冲(总是)
    • 刷redo log buffer到磁盘(总是)
    • 删除无用的undo页(总是)
    • 刷100或者10个Innodb 缓冲池中的脏页到磁盘(总是)

    backupgroup loop

    • 删除无用的undo页(总是)
    • 合并20个插入缓冲(总是)
    • 跳回主循环(总是)
    • 不断刷100个Innodb 缓冲池中的脏页到磁盘(总是)(可能跳转到flush loop中完成)

    InnoDB1.0.x之后

    1.0.x版本之前,Master Thread 有了一定的hard coding。随着硬件性能日新月异,这对编码方式不能很好的匹配不同性能的硬件。

    • 使用配置代替hard coding
    • 增加了一些参数自适应

    InnoDB1.2.x之后

    刷新脏页的操作从Master Thread分离到一个独立的Page Cleaner Thread。减轻Master Thread 的工作,同时进一步提高了系统的并发性。

    Innode关键特征

    • insert buffer
    • double write
    • adaptive hash index
    • async IO
    • Flush Neighbor Page

    insert buffer

    insert buffer 和数据页一样,也是物理页的一个组成部分,用于非唯一辅助索引的插入操作。

    每一张表上除了有一个聚集索引(primary key),有可能还有多个非聚集的辅助索引。

    在进行插入操作的时候,数据页还是按照primary key进行顺序存放的。但是对于非聚集索引叶子节点的插入不再是顺序的。这时需要离散的访问非聚集索引页,由于随机读取的存在,进而影响了插入性能。

    mysql引入insert buffer,对非聚集索引的插入或更新操作,不是每一次直接插入到索引页中,而是先判断插入的非聚集索引页是否在缓冲池中,若是在,则直接插入;若不在,则先放入到一个insert buffer对象中。

    然后再以一定的频率和情况进行insert buffer和辅助叶子节点的merge操作。

    辅助索引不能是唯一的,因为在插入缓冲时,数据库为了避免离散读取的情况发生,并不去查找索引页来判断插入的记录的唯一性。

    Change Buffer

    Innodb 1.0.x版本开始引入Change Buffer,可将其视为Insert BUFFER的升级。Innodb可以对DML操作–INSERT、DELETE、UPDATE都进行缓冲,它们分别是:

    • Insert Buffer
    • Delete Buffer
    • Purge Buffer

    Innodb 提供参数Innodb_change_bufferiing,用于开启各种Buffer的选项

    对一条记录进行UPDATE操作可能分为连个过程:

    • Delete buffer复制将记录标记为删除

    • Purge buffer负责将记录真正的删除

    Insert Buffer的内部实现

    • 使用b+树

      mysql 4.1之前每张表有一颗insert buffer B+树。现在全局只有一颗Insert Buffer B+树

    • 它保存在共享空间中(ibdata1),因此,试图通过独立表空间ibd文件恢复表中数据时,往往会导致CREATE TABLE失败。这是因为表的辅助索引中的数据可能还在Insert Buffer中。所以通过idb文件进行恢复后,还需要进行REPAIR TABLE操作来重建表上所有的辅助索引。

    • 由叶子节点和非叶子节点组成

    • 非叶子节点存放的是查询的search key(键值),结构如下

    space marker offset
    表示待插入记录所以在表的表空间id 用于兼容向后兼容 页所在的偏移量
    • 当一个辅助索引要插入到页(space,offset)时,如果这个页不再缓冲池中,那么Innodb首先根据上述规则构造一个search key,接下来查询insert buffer,然后将记录插入到insert buffer B+树的叶子节点中。
    space :marker offset metadata
    • metadata中IBUF_REC_OFFSET_COUNT(2字节的整数)用来排序每个记录进入insert buffer的顺序

    • insert buffer bitmap:一个特殊的页,用来标记每个辅助索引页的可用空间,保证每次merge insert buffer页必须成功。

    • 每个insert buffer bitmap用来追踪16384个辅助索引页,也就是256个区(Extent)

    • 每个辅助索引页在insert buffer bitmap中存储结构

    merge insert buffer

    • 辅助索引页被读取到缓冲池中
      • 检查insert buffer bitmap页,确认该辅助索引页是否存放在insert buffer B+树中
      • 存在,则将B+树中该页的记录插入到该辅助索引页中。对该页多次的记录操作通过一次合并到原有的辅助索引页
    • insert buffer bitmap页追踪到该辅助索引页已无可用空间时。(小于1/32页,会强制进行一次合并操作)
    • Master Thread中每秒或者每十秒的合并操作。

    两次写

    避免数据库宕机时,出现部分写失效(partial page write)。例如某个页16KB,只写了4KB就发生了宕机。

    通过redo log进行数据恢复,由于redo日志记录的是对页的物理操作(如偏移量800,写‘aaa’记录),需要基于页是好的。如果这个页本身已经发生了损坏,无法通过redo 日志进行重放。

    doublewrrite:当写入失效发生时,先通过页的副本还原该页,再进行重做。

    doublewrrite由两部分组成:

    • 内存中的doublewrrite buffer,2MB
    • 物理磁盘上共享表空间中连续的128个页,即2个区(extent),2MB

    在对缓冲池进行flush时,并不是通过写磁盘,而是通过memcpy函数将脏页先复制到内存中的doublewrite buffer,之后通过doublewrite buffer再分两次,每次写1MB顺序写入共享表空间的物理磁盘上,然后马上调用fsync函数,同步磁盘避免缓冲写带来的问题(顺序写)。doublewrite buffer中的页写入到各个表空间文件中。(离散写)

    自适应哈希索引(AHI)

    自适应哈希索引:Innodb存储引擎会监控对表上各索引的查询,如果观察到建立哈希索引可以带来速度提升,则建立哈希索引。

    AHI是通过缓冲池的B+树页构造,不需要对整张表构建哈希索引。

    条件:

    • 访问模式一样,即查询的条件一样。
    • 以该模式访问了100次
    • 页通过该模式访问了N次,其中(N=页中记录*1/16)。

    异步IO

    Innodb支持AIO。

    • read ahead 方式的读取(借助aio,将所有io发送完毕后,然后等待所有IO操作的完成)
    • 脏页的刷新(可以将多个连续io进行IO Merge)

    刷新邻近页

    当刷新一个脏页时,Innodb会检测改页所在区(extent)的所有页,如果是脏页,那么一起进行刷新。然后通过AIO将多个io写入操作合并成一个IO操作。

    缺点:

    • 可能会将不怎么脏的页进行了写入,而该页之后很快又会变成脏页。
    • 固态硬盘有着较高的IOPS,不需要这个特性。
  • 如何进行k/v存储选型

    下面是bitcask的论文里,作者考虑到的目标

    • low latency per item read or written
    • high throughput, especially when writing an incoming stream of random items
    • ability to handle datasets much larger than RAM w/o degradation
    • crash friendliness, both in terms of fast recovery and not losing data
    • ease of backup and restore
    • a relatively simple, understandable (and thus supportable) code structure and data format
    • predictable behavior under heavy access load or large volume
    • a license that allowed for easy default use in Riak

    论文地址:https://riak.com/assets/bitcask-intro.pdf

  • 使用位运算进行取模

    一般取模 $A mod B$

    12%8=4

    如果 $B = 2^n$ 以使用 $&(B-1)$ 代替 $%B$ 来取模.

    12&(8 -1) = 4
    12&(2^3 – 1) = 4

    glibc strlen 就用到了

    #include <string.h>
    #include <stdlib.h>
    
    #undef strlen
    
    #ifndef STRLEN
    # define STRLEN strlen
    #endif
    
    /* Return the length of the null-terminated string STR.  Scan for
       the null terminator quickly by testing four bytes at a time.  */
    size_t
    STRLEN (const char *str)
    {
      const char *char_ptr;
      const unsigned long int *longword_ptr;
      unsigned long int longword, himagic, lomagic;
    
        /*利用取模运算,控制这里最大为 str + 8*/
      /* Handle the first few characters by reading one character at a time.
         Do this until CHAR_PTR is aligned on a longword boundary.  */
      for (char_ptr = str; ((unsigned long int) char_ptr
                & (sizeof (longword) - 1)) != 0;
           ++char_ptr)
        if (*char_ptr == '\0')
          return char_ptr - str;
    
      /* All these elucidatory comments refer to 4-byte longwords,
         but the theory applies equally well to 8-byte longwords.  */
    
      longword_ptr = (unsigned long int *) char_ptr;
    
      /* Bits 31, 24, 16, and 8 of this number are zero.  Call these bits
         the "holes."  Note that there is a hole just to the left of
         each byte, with an extra at the end:
         bits:  01111110 11111110 11111110 11111111
         bytes: AAAAAAAA BBBBBBBB CCCCCCCC DDDDDDDD
         The 1-bits make sure that carries propagate to the next 0-bit.
         The 0-bits provide holes for carries to fall into.  */
      himagic = 0x80808080L;
      lomagic = 0x01010101L;
      if (sizeof (longword) > 4)
        {
          /* 64-bit version of the magic.  */
          /* Do the shift in two steps to avoid a warning if long has 32 bits.  */
          himagic = ((himagic << 16) << 16) | himagic;
          lomagic = ((lomagic << 16) << 16) | lomagic;
        }
      if (sizeof (longword) > 8)
        abort ();
    
        /*代替一次比较一个character,每次读取多个 byte,然后一起比较,提高了读取的效率。就是批处理的思想*/
      /* Instead of the traditional loop which tests each character,
         we will test a longword at a time.  The tricky part is testing
         if *any of the four* bytes in the longword in question are zero.  */
      for (;;)
        {
          longword = *longword_ptr++;
    
          if (((longword - lomagic) & ~longword & himagic) != 0)
        {
          /* Which of the bytes was the zero?  If none of them were, it was
             a misfire; continue the search.  */
    
          const char *cp = (const char *) (longword_ptr - 1);
    
          if (cp[0] == 0)
            return cp - str;
          if (cp[1] == 0)
            return cp - str + 1;
          if (cp[2] == 0)
            return cp - str + 2;
          if (cp[3] == 0)
            return cp - str + 3;
          if (sizeof (longword) > 4)
            {
              if (cp[4] == 0)
            return cp - str + 4;
              if (cp[5] == 0)
            return cp - str + 5;
              if (cp[6] == 0)
            return cp - str + 6;
              if (cp[7] == 0)
            return cp - str + 7;
            }
        }
        }
    }
    libc_hidden_builtin_def (strlen)