docker中的镜像存储
docker中镜像的概念其实就是一组只读目录。每一个目录是一个layer,多个layer按照一定的顺序组成一个stack。在容器创建时,docker增加在stack之上一个thin和writable layer,如下图
基于内容寻址
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