1. 程式人生 > >【探索docker儲存之路】三、docker中的映象儲存與Overlayfs

【探索docker儲存之路】三、docker中的映象儲存與Overlayfs

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;
}