docker容器overlay2镜像辗平技术收益分析

0 背景

在今年的cncf上海大会上,某互联网公司介绍了在它们内部的实现场景下,将docker容器镜像进行压扁后,访问效率得到了10倍的提升。回来以后,马上开始做实验,发现只有在非常极端的场景:
50000个文件位于最底层tempA目录,50000个文件位于最上层tempB,中间有100层的镜像,tempA中有20%的文件在各个层都有分布时刻,通过ls -l 遍历容器目录tempA可以得到6倍的内核态访问时间提升,综合访问时间(用户态+内核态)有4倍的访问时间提升。
这种例子非常极端了,以至于我们在公司内部找到了一些典型的容器镜像(镜像很大、层数很多的镜像)进行测试;发现在公司现阶段而言没有什么容器镜像辗平后有明显的收益

1 镜像辗平的方法

目前来说docker提供了两种镜像辗平的方法:

  • docker build参数--squash
    在docker build命令中可以加入--squash参数,将待构建的镜像进行辗平。注意此处辗平只能对dockerfile 构建的新层进行辗平,对于dockerfile引用的基础镜像不能进行辗平
  • docker export & docker import 方式
    此种方式可对一个已存在的镜像进行辗平
#docker create mynginx:v1
0d8c45f9b9.....
#docker export 0d8c45 -o zxy.tar
#docker import zxy.tar newnginx:v2
sha256:1de4e991ed....
#docker history 1de4e99
IMAGE                  CREATE         CREATE BY         SIZE.        COMMENT
1de4e991edf5.      56 seconds ago.                          317MB.     Imported from -

2 原理

本文来以centos7.4内核(3.10.0-693)为例介绍一下镜像辗平的原理。本文中有大量的内核文件系统相关知识,如果读者对内核实现不感兴趣,可以直接跳到最后一章结论处。
下图展示了overlayfs的基本工作图


以docker为例的overlayfs的示意图

如图所示overlayfs分为:
1.lowerdir:可以有多层
2.upperdir:读写层,只有一层
3.mergedir:视图层,也就是overlayfs融合以后,用户可见的一层(对应docker上的rootfs)。
4.workdir:工作层,overlayfs做一些临时操作时刻的层。

overlayfs的使用方法

下面命令可以创建一个overlayfs:

root#cd /root
root#mkdir lower1 lower2 lower3 upperdir workdir mergedir
root#touch lower1/a
root#touch lower2/b
root#touch lower3/c
root#date>lower3/a
root#mount -t overlay overlay -o lowerdir=/root/lower1:/root/lower2:/root/lower3,upper=/root/upperdir,workdir=/root/workdir /root/mergedir
root#cd /root/mergedir
root#ls 
a b c 

通过上述命令,我们在/root/merge下就有一个overlayfs,里面的内容是lower1,lower2,lower3和upper的堆叠。
上述命令中lower3是最底层
当在/root/merge层发生写入后,写入的文件最终会出现在upperdir里

root#echo “zxy testing”>/root/merge/new
root#ls
a b c new
root#ls /root/upperdir
new

这里介绍完一个overlayfs的基本使用了。下面我们来看看在多层堆叠的情况下,overlayfs是如何打开一个普通文件;如何查找目录下的文件。

2.1 文件打开

2.1.1 文件系统基本结构

在linux内核里通用文件系统由4个重要的数据结构组成:superblock,inode,dentry和file,它们都位于内核空间内。

  • superblock每个文件系统一个,存储了文件系统的元信息。对于磁盘文件系统,superblock也存在磁盘上。假设一台主机上有两个设备/dev/sda1 /dev/sda2 分别格式化为两个ext4系统,它们挂在在系统上以后,存在两个superblock
  • inode保存文件的元信息,例如:时间,文件名,使用者和群组等。一个文件对应一个inode,每个inode都有一个inode号在内核中用inode->i_ino表示。它在一个文件系统内部是全局唯一的。对于磁盘文件系统,每个inode都有一个磁盘上的实体对应。
  • dentry 目录项,用来将inode和目录项(文件的路径)关联起来。一个文件可以有多个dentry,但是只有一个inode。通过dentry可以索引到文件的inode。dentry只存在于内存中。
  • file 文件,内核中用struct file表示。用于存储进程与打开文件的交互信息。由于一个文件可以被多个进程打开,所以同一个物理文件可以有多个file 实例。file结构中定义了dentry* 成员,用于指向dentry;定义了file_operations*成员,用于文件的操作函数。


    文件系统涉及
以ext2文件系统为例说明文件内核结构关系图

本文不对这套vfs文件结构做过多说明,我们只需知道一个文件打开的主要流程是:

  1. 创建一个struct file结构
  2. 根据给定的路径找到文件的dentry,如果没有在内核维护的dentry缓存中找到,那么就新创建一个dentry。在新创建dentry时刻就会涉及到根据dentry查找/创建inode的流程。
  3. 将inode的file_operation指针地址复制给inode的对应成员。在centos7.4上struct file里有成员f_inode,可以直接指向inode,在文件打开时刻,此成员被赋值*

2.1.2关键流程

在整个过程中,第二步是整个文件打开的核心步骤。在centos7.4上overlayfs上整个dentry查找调用链为:
open()系统调用的入口是do_sys_open()函数。do_sys_open()->do_filep_open()->path_openat()->do_lask()

2.1.2.1 do_last()函数

do_last()函数的功能是打开文件路径查找文件的最后一个分量。
这个根据文件路径查找文件分量的过程,我们举一个例子展示一下:假设我们需要打开/root/test/lower1/a 。
step1、内核先打开root找到其dentry,读取其中子文件/目录信息;查找root下有test。
step2、查找test的dentry,读取其子文件或目录信息,找到test下有lower1.
stepn、这么一路查找,直到a文件,后此时就调用do_last()

do_last()做的事情比较简单,但却非常重要:
1)尝试在dentry cache中查找待打开文件的dentry和inode。如果这是第一次打开此文件,当然找不到此inode。这次查找使用lookup_fast()方式快速查找。
2)调用lookup_open()尝试查找、分配dentry和inode。

  1. 在lookup_open()完成后,使用vfs_open()实际完成文件打开动作
2.1.2.1.1 lookup_open()函数
  1. lookup_open()此函数先调用lookup_dcache()再次尝试查找dentry,如果找不到则分配一个新的dentry。
  2. lookup_open()在分配新的dentry后,调用lookup_real()。在此函数中调用其父目录的dir->i_op->lookup()查找(overlayfs文件的inode)。在overlayfs中,这个lookup()函数指针指向ovl_lookup()
2.1.2.1.1.1 ovl_lookup()函数真正执行overlayfs查找dentry和inode

ovl_lookup()是这次打开文件过程查找dentry和inode真正执行者,对于理解其整个流程非常重要所以我们贴上部分代码

struct dentry *ovl_lookup(struct inode *dir,struct dentry* dentry,unsigned int flags){
   ...
    if(upperopage && poe->numberlower){
        stack = kcalloc(poe->numlower, sizeof(struct path),GFP_KERNEL);
       ...
    }
    ...
    for(i=0;!upperopage&&i<poe->numlower;i++){
       struct path lowerpath= poe->lowerstack[i];
       this = ovl_lookup_real(lowerpath.dentry,&dentry->d_name);

       if(!this)
          continue;
       if(ovl_is_whiteout(this)){
           dput(this);
           break;
       }
       ...
      if(!S_ISDIR(this->d_inode->i_mode))
          opague= true;
     stack[ctr].dentry = this;
     ...
     ctr++;
     ...
   }
  oe = ovl_alloc_entry(ctr);
  ...
  if(upperdentry||ctr){
      ...
      realdentry= upperdentry ? upperdentry :stack[0].dentry;
      realinode = d_inode(realdentry);
      if(upperdentry && !d_is_dir(upperdentry)){
           inode= ovl_get_inode(dentry->d_sb,realinode);
     } else{
          inode = ovl_new_inode(dentry->d_sb,realinode->i_mode)
          If(inode)
             ovl_inode_init(inode,realinode,!!upperdentry);
    
    }
    ...
    ovl_copyattr(realdentry->d_inode,inode);
       
  }
  ...
  memcpy(oe->lowerstack,stack,sizeof(struct path) * ctr);
  dentry->d_fsdata= oe;
  d_add(dentry,inode);
  ...
}

ovl_lookup()函数的入参:dir为待查找的文件父目录的inode,dentry为lookup_real()函数中为本次查找文件分配的
dentry。
上面的大段代码分为四个部分:
1、创建一个struct path数组stack,用于保存此待查找overlayfs文件的所有需要参考的底层文件的dentry信息。
2、遍历poe->lowerstack[]每一层,在其中查找待查找的文件是否存在。存在且待查找文件不是目录,那么查找结束;如果待查找文件为目录,那么会接着将所有层遍历完毕。
每次有效遍历都会向stack数组记录查找到低层文件的dentry。那么对于一般文件而言,完成遍历后stack[]只有一个元素;但是对于目录文件来说stack[]有多个元素,每个元素对应着此目录在overlay诸多低层中出现一次。
3、realdentry 设置为真正有效的文件的dentry,也就是最上层文件的dentry。为待查找的overlay文件分配一个inode,将realdentry’s inode的属性拷贝到刚刚分配的overlay文件的inode上。
4、将stack数组放置到overlay文件dentry的d_fsdata字段上,并且将新分配的inode与此dentry关联起来。

综上可以看到:

  1. 对于常规文件而言,当overlayfs低层文件位于愈低层,那么其查找时间愈长。且查找时长和层数成正比。
  2. 对于目录文件而言,因为overlayfs查找需要遍历所有层(直到whiteout或者opage),那么层数的增加查找时间会增加。

whiteout:在overlayfs上一个低层存在的文件,在merge上被删除以后,会在upper上创建一个同名的whiteout文件,且文件类型为c(char),主次设备号都是0
opague:在overlayfs上,如果一个目录在低层存在,但是在后面某一层被删除了,现在在merge上又创建了。那么在upper目录上会出现一个同名opaque文件,其属性“trusted.overlay.opaque”扩展属性值为y

2.1.2.1.2 vfs_open()实际打开文件

vfs_open()对于非目录文件分为两步操作

  1. 调用d_real()获取overlayfs文件真正起效果的文件(也就是overlay文件最上层的文件)的dentry,这一dentry在ovl_lookup()中被关联到overlayfs文件dentry内部数据结构里了(dentry对应的inode的i_private字段中了)。
    d_real()真正使用的ovl_real()查找到真实的dentry和inode。在ovl_real()中会根据vfs_open()的打开属性执行overlayfs的copyup动作(只要以写属性打开就会执行copyup动作,执行copyup动作后,生效的dentry和inode指向这个新创建出现了copyup destination的文件)。

2.通过这个真正起效文件的dentry和其inode,打开真正起效的文件。

2.2 文件读写

在文件打开章节2.1中,我们介绍了对于overlayfs的文件打开动作,真实打开的文件是overlayfs最上层的文件。那么后续所有的读写动作都直接定位到终极目标文件了,不用再经过overlayfs干预了。

2.3 目录文件读

目录文件是可以读写的,目录文件里记录了当前这个目录下有哪些子文件/目录。目录文件读取动作之前首先也需要open此目录文件,open过程就伴随着2.1过程中介绍的dentry、inode查找/创建。对于overlayfs在open的lookup阶段,就已经在将待读取目录的所有低层信息记录在dentry->fs_data->lowerstack[]数组中了。那么overlayfs读取目录文件,就是对lowerstack[]数组内部所有的低层dentry进行遍历读。

综上,如果一个目录在overlayfs的低层中多个层存在,那么此目录的读取肯定比只是单层存在更耗时。

3 结论

本文通过分析发现:
1.overlayfs 越低层的文件,比越上层的文件在首次dentry查找时更耗时。
2.一个目录在多层都存在,那么其读取时间也更耗时
3.在文件正常读写阶段,overlayfs没有起任何干扰作用。

所以层数并不影响overlayfs 文件read和write的速度,只影响首次open文件/目录或者目录读取的速度。在现实使用中,通过辗平overlay2 镜像并不能获取太多收益。
只有下面极端场景中才有意义:
1.层数特别多
2.低层文件很多 或者同一目录文件在很多层都存在。
3.业务启动后首次读取文件或遍历目录时可以感觉到有收益。
但是镜像辗平后带来的副作用时,多层镜像的基层本来在多个镜像之间是共用的(共享),辗平后无法共用。这带来的镜像存储空间变大,网络传输镜像数据变多。
所以你的业务是否真正需要镜像辗平,请通过测试来决定

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,324评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,356评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,328评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,147评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,160评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,115评论 1 296
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,025评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,867评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,307评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,528评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,688评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,409评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,001评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,657评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,811评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,685评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,573评论 2 353