docker中的镜像存储

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

docker 镜像存放路径 docker镜像存储_存储

基于内容寻址

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

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

docker 镜像存放路径 docker镜像存储_docker_02

写时复制策略

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

docker 镜像存放路径 docker镜像存储_docker 镜像存放路径_03

镜像共享

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

docker 镜像存放路径 docker镜像存储_字符设备_04

overlayfs概述

简介

docker 镜像存放路径 docker镜像存储_字符设备_05

操作

① 加载
确保内核版本大于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

docker 镜像存放路径 docker镜像存储_字符设备_06

③ Upper and Lower

merged目录有3个文件,有lower目录的aaaa,upper目录的bbbb和cccc。可以看到upper目录的bbbb把lower目录的bbbb覆盖了。

docker 镜像存放路径 docker镜像存储_存储_07

④ 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目录。

docker 镜像存放路径 docker镜像存储_存储_08


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目录有什么变化:

docker 镜像存放路径 docker镜像存储_寻址_09

删除之后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

docker 镜像存放路径 docker镜像存储_存储_10

  • 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