【探索docker存储之路】三、docker中的镜像存储与Overlayfs

来源:互联网 发布:外资企业数据 编辑:程序博客网 时间:2024/05/17 04:40

docker中的镜像存储

docker中镜像的概念其实就是一组只读目录。每一个目录是一个layer,多个layer按照一定的顺序组成一个stack。在容器创建时,docker增加在stack之上一个thin和writable layer,如下图
docker image的组成 (图片来自docker官网)

基于内容寻址

docker1.10推翻了之前的镜像管理方式,重新开发了基于内容寻址的策略。该策略至少有3个好处:①提高了安全性。②避免了ID冲突。③确保数据完整性。

基于内容寻址的实现,使用了两个目录:/var/lib/docker/image和/var/lib/docker/overlay, 后面的这个根据存储驱动的名称不同,而目录名不同。image目录保存了image的内容(sha256)数据。overlay目录保持了image的真实数据。基于内容寻址的镜像管理逻辑,比较复杂,如下图简述各个目录的作用,docker使用该目录的文件,进行镜像管理。
这里写图片描述

写时复制策略
每个container都有自己的读写layer,对镜像文件的修改和删除操作都会先执行镜像文件拷贝到读写layer的操作,然后对读写layer的文件进行修改和删除。如下图,多个容器共享一个镜像,每个容器拥有自身独立的读写layer。
这里写图片描述

镜像共享
多个镜像可以共享低层layer,如本机有一个ubuntu:15.04的镜像,用户基于该镜像做了修改,如下图,新的镜像的低层会直接引用ubuntu15.04的镜像。通过镜像共享的方式,可以减少本机存储空间,加快pull和push的速度。
这里写图片描述

overlayfs概述

简介

这里写图片描述

操作

① 加载
确保内核版本大于3.18,检查是否已经加载内核模块: lsmod | grep overlay
输出: overlay 45056 0
如果没有输出任何内容,加载overlay内核模块: modprobe overlayfs

② 挂载
准备目录和文件:
mkdir lower upper work merged
echo “lower.aaaa” > lower/aaaa
echo “lower.bbbb” > lower/bbbb
echo “upper.bbbb” > upper/bbbb
echo “upper.cccc” > upper/cccc

挂载lower和upper目录到merged目录:
mount -t overlay overlay -olowerdir=lower,upperdir=upper,workdir=work merged

这里写图片描述

③ Upper and Lower
merged目录有3个文件,有lower目录的aaaa,upper目录的bbbb和cccc。可以看到upper目录的bbbb把lower目录的bbbb覆盖了。
这里写图片描述

④ Directories
从上面的例子看,upper目录和lower目录有相同的文件,lower目录的同名文件将会隐藏。如果是upper目录和lower目录有相同名称的目录呢?
mkdir lower/same
mkdir upper/same
echo “lower/same.dddd” > lower/same/dddd
echo “upper/same.dddd” > upper/same/dddd
echo “lower/same.eeee” > lower/same/eeee
创建两个same目录,在两个same目录下创建dddd文件,lower/same目录下创建eeee文件。查看merged目录。

这里写图片描述
upper目录和lower目录有相同名称的目录, 两个同名目录(same目录)会合并,同名目录(same/dddd)中有同名文件,upper目录仍然会覆盖lower目录的文件。

⑤ whiteouts and opaque directories
继续上面的例子,merged目录有aaaa,bbbb,cccc 3个文件,其中aaaa是lower目录提供的。如果在merged目录执行rm aaaa,是否为影响lower/aaaa文件呢?overlay是如何确保lower目录是只读的呢?
echo “lower.ffff” > lower/ffff
mkdir lower/ldir
echo “lower/ldir/gggg” > lower/ldir/gggg
rm merged/ffff
rm merged/ldir -rf
查看upper、lower和merged目录有什么变化:
这里写图片描述

删除之后merged目录已经没有ffff文件和ldir目录了;upper目录多了ffff和ldir字符设备文件;lower目录的文件和目录保持原样。overlayfs正是删除lower目录提供的文件或目录时,在upper目录创建主次设备号都为0的字符设备文件,用来表示文件、目录已被删除,这就是whiteout。
如果upper目录有一个目录设置了xattr属性trusted.overlay.opaque=y,这就是opaque directory。如果upper目录中有一个opaque directory,则所有lower目录的同名目录都将被忽略。

⑥ readdir
在merged目录读取一个upper目录和lower目录都存在的一个目录的内容,在前面的例子,可以看到内容是会合并的。合并的逻辑是:先读取upper目录的内容添加到name lists中,再读取lower目录的内容添加到name lists中,如果name lists已经存在同名文件,则不会添加到name lists中,如果是同名目录会产生递归合并。name lists会一直缓存在struct file结构中,直到文件被关闭。如果多个进程打开同一个文件,name lists将在多个struct file缓存多份,如果其中一个进程修改了merged目录的内容,将会导致所有name list失效和重建。

⑦ Non-directories
当一个lower目录下的文件、符号链接、设备文件等称为非目录对象,以写访问方式打开时,非目录对象需要从lower目录拷贝到upper目录(copy_up)。copy_up在不需要拷贝的时候,如以读写的模式打开文件却没有修改,此时将不会执行拷贝操作。
copy_up首先确认包含修改非目录对象的目录是否存在upper目录中,不存在则创建。新建的非目录对象与就对象拥有相同的metadata。

⑧ Multiple lower layers
多个lower目录,用 “:” 分割:
mount -t overlay overlay -olowerdir=/lower1:/lower2:/lower3 /merged
这些指定的lower目录,构成一个stack,如上例lower1是栈顶,lower3是栈底。 3.19.0-25-generic版本内核并不支持该功能

⑨ Changes to underlying filesystems
即使一个修改低层目录的操作是overlay未定义的,也不会引起crash或deadlock,修改一个已挂载的overlay文件系统的低层目录是不允许的。

overlayfs原理

overlayfs原理的核心就是:把对一个文件的操作直接转为对另一个文件的操作。下面的文章都会假设已经掌握vfs,并大概指导如何实现一个文件系统。overlayfs的源码在fs/overlayfs/

① Operations
这里写图片描述

  • ovl_entry:overlayfs的dentry的私有结构类型,记录upper和lower的相关信息。保存struct entry的d_fsdata字段中。
  • ovl_dir_file:保存在struct file的private_data字段。进程可以通过这个字段找到ovl_dir_cache,找到所有的目录项。
  • ovl_dir_cache:管理ovl_cache_entry,以链表的形式串联起所有ovl_cache_entry。
  • ovl_cache_entry:代表overlay文件系统中,每一个目录项。
  • ovl_readdir_data: 保存merged目录的所有的目录项,通过红黑树增加目录项的查找性能。

② 打开正确的文件
overlayfs中存在一个upper目录,一个或多个lower目录,挂载后都呈现在merged目录中。当我们使用merged目录的文件时,该文件有可能是upper目录,也有可能是任何一层lower目录的,如何找到正确的文件呢?
找到overlayfs的open函数:ovl_dir_open (fs/overlayfs/readdir.c)。

static int ovl_dir_open(struct inode *inode, struct file *file){    struct path realpath;    struct file *realfile;    struct ovl_dir_file *od;    enum ovl_path_type type;    od = kzalloc(sizeof(struct ovl_dir_file), GFP_KERNEL);    if (!od)        return -ENOMEM;    //struct dentry的d_fsdata存放了对应文件的upper和lower信息,从中可以得到文件的真实路径。    type = ovl_path_real(file->f_path.dentry, &realpath);    //把文件真实路径传给ovl_path_open,最终调用vfs_open,被打开的文件就是文件的真实路径了。    realfile = ovl_path_open(&realpath, file->f_flags);    if (IS_ERR(realfile)) {        kfree(od);        return PTR_ERR(realfile);    }    od->realfile = realfile;    od->is_real = !OVL_TYPE_MERGE(type);    od->is_upper = OVL_TYPE_UPPER(type);    file->private_data = od; //ovl_dir_file结构可以通过struct file的private_data找到。    return 0;}

ovl_path_real函数对应存在于lower目录的文件,通过file->f_path.dentry的d_fsdata字段类型为struct ovl_entry,真实路径在ovl_entry.lowerstack中,这个是在路径名查找lookup时填进去的,ovl_lookup函数(fs/overlayfs/super.c)。

③ upper、lower上下合并,同名覆盖
上面的操作结果可以看到
在linux-4.4.1版本readdir已经替换成iterate,在fs/overlayfs/readdir.c中的ovl_dir_operations,iterate设置为ovl_iterate。

static int ovl_iterate(struct file *file, struct dir_context *ctx){    struct ovl_dir_file *od = file->private_data;    struct dentry *dentry = file->f_path.dentry;    struct ovl_cache_entry *p;    if (!ctx->pos)        ovl_dir_reset(file);    if (od->is_real)        return iterate_dir(od->realfile, ctx);    if (!od->cache) {  //构建cache        struct ovl_dir_cache *cache;        cache = ovl_cache_get(dentry); //调用ovl_dir_read_merged,将dentry下的所有dentry缓存在cache中。        if (IS_ERR(cache))            return PTR_ERR(cache);        od->cache = cache;        ovl_seek_cursor(od, ctx->pos); //od->cursor指向od->cache->entries链表的pos位置    }    while (od->cursor != &od->cache->entries) { //遍历cache中的所有dentry        p = list_entry(od->cursor, struct ovl_cache_entry, l_node);        if (!p->is_whiteout)            //回调ovl_fill_merge把entry加入ovl_readdir_data的红黑树中,用于展示            if (!dir_emit(ctx, p->name, p->len, p->ino, p->type))                break;        od->cursor = p->l_node.next;        ctx->pos++;    }    return 0;}

④ 写时复制
对lower目录的文件进行修改,删除时,会将lower目录的文件拷贝到upper目录。
创建时拷贝:普通文件、子目录、块/字符设备文件、符号链接,硬链接。
删除时拷贝:对父目录进行拷贝,并创建设备号为0 0的字符设备文件。
修改文件属性时拷贝:

/** * vfs_open - open the file at the given path * @path: path to open * @file: newly allocated file with f_flag initialized * @cred: credentials to use */int vfs_open(const struct path *path, struct file *file,         const struct cred *cred){    struct dentry *dentry = path->dentry;    struct inode *inode = dentry->d_inode;    file->f_path = *path;    if (dentry->d_flags & DCACHE_OP_SELECT_INODE) {        inode = dentry->d_op->d_select_inode(dentry, file->f_flags);        if (IS_ERR(inode))            return PTR_ERR(inode);    }    return do_dentry_open(file, inode, NULL, cred);}
struct inode *ovl_d_select_inode(struct dentry *dentry, unsigned file_flags){    int err;    struct path realpath;    enum ovl_path_type type;    if (d_is_dir(dentry))        return d_backing_inode(dentry);    type = ovl_path_real(dentry, &realpath);    if (ovl_open_need_copy_up(file_flags, type, realpath.dentry)) {        err = ovl_want_write(dentry);        if (err)            return ERR_PTR(err);        if (file_flags & O_TRUNC)            err = ovl_copy_up_truncate(dentry);        else            err = ovl_copy_up(dentry);        ovl_drop_write(dentry);        if (err)            return ERR_PTR(err);        ovl_path_upper(dentry, &realpath);    }    if (realpath.dentry->d_flags & DCACHE_OP_SELECT_INODE)        return realpath.dentry->d_op->d_select_inode(realpath.dentry, file_flags);    return d_backing_inode(realpath.dentry);}
static bool ovl_open_need_copy_up(int flags, enum ovl_path_type type,                  struct dentry *realdentry){    if (OVL_TYPE_UPPER(type))  //目标路径是upper无需copy        return false;    if (special_file(realdentry->d_inode->i_mode)) //块、字符、管道、套接字文件无需copy        return false;    //不以write模式打开,或者不截断文件,无需copy    if (!(OPEN_FMODE(flags) & FMODE_WRITE) && !(flags & O_TRUNC))        return false;    return true;}

附: rc25482.pdf

1 0