Nydus 源碼解讀:nydus-image create

語言: CN / TW / HK

上一篇 Nydus 源碼解讀:nydusify convert 介紹了 nydusify convert 命令的大致流程,我們在那一片已經看到,每一層鏡像都是通過 nydus-image 命令來轉換的。

這一篇我們就來以 v2.0.0-rc.5 的代碼為基礎,分析一下 nydus-iamge create 命令的代碼。 nydus-image 的代碼主要在 src/bin/nydus-image 文件夾下。

測試環境

我們準備一個簡單的文件夾 fs ,然後將該文件夾轉換為 nydus 鏡像。

$ mkdir -p fs/dir1
$ mkdir -p fs/dir2
$ touch fs/foo.txt
$ echo abcde > fs/dir1/bar.txt

$ mkdir -p fs/dir1/dir1-1
$ touch fs/dir1/dir1-1/foo
$ echo abc > fs/dir1/dir1-1/hello

$ tree fs/
fs/
├── dir1
│   ├── bar.txt
│   └── dir1-1
│       ├── foo
│       └── hello
├── dir2
└── foo.txt

3 directories, 4 files

注意:這裏我們都是使用了常規的文件,沒有軟硬鏈接、設備文件等特殊類型的文件。

然後我們使用如下命令來創建 nydus 鏡像:

$ mkdir output
$ nydus-image create \
    --fs-version 5 \
    --bootstrap output/bootstrap \
    --blob-dir output \
    fs

上面參數的意思分別為:

--fs-version 5
--bootstrap output/bootstrap
--blob-dir output
fs

實際上 nydus 支持從 3 種類型的輸入源來創建 nydus 鏡像:

  • directory
  • diff
  • stargz_index

其中 stargz 也是一種鏡像延遲加載技術。在本例中,我們使用了默認的 directory 方式。這個值可以通過命令行參數 source-type 來控制。

代碼解析

下面我們就來看一下 nydus-image 的源代碼。

主函數

nydus-image create 命令的入口在 src/bin/nydus-image/main.rs 裏,這裏我們不貼代碼了,只簡單説一下里面幹了什麼。

這個函數主要創建了幾個數據結構:

  • build_ctx(類型:BuildContext):主要在各函數調用中傳遞一些變量
  • blob_mgr(類型:BlobManager):管理 blob
  • bootstrap_mgr(類型: BootstrapManager):管理 bootstrap
  • builder(類型:Box ):執行 build 過程

我們看到 builder 是一個接口,它會根據輸入源類型的不同而初始化為不同的具體實現。在這個例子中,我們基於文件夾構建 Nydus 鏡像,所以這裏 builder 的實際類型行為:

builder = Box::new(DirectoryBuilder::new())

最後,該函數會調用

builder.build(&mut build_ctx, &mut bootstrap_mgr, &mut blob_mgr)

來實現 build。

builder.build() 函數

builder.build() 函數 內容如下:

fn build(
    &mut self,
    ctx: &mut BuildContext,
    bootstrap_mgr: &mut BootstrapManager,
    blob_mgr: &mut BlobManager,
) -> Result<BuildOutput> {
    // 創建一個 BootstrapContext,在構建中將 bootstrap 數據保存在內存中
    let mut bootstrap_ctx = bootstrap_mgr.create_ctx(ctx.inline_bootstrap)?;

    // 基於輸入源文件夾在內存中構建文件樹結構
    let mut tree = self.build_tree_from_fs(ctx, &mut bootstrap_ctx, layer_idx)?;

    let mut bootstrap = Bootstrap::new()?;

    // 如果是多層構建,當層數據需要依賴父層進行計算
    if bootstrap_ctx.layered {
        // Merge with lower layer if there's one, do not prepare `prefetch` list during merging.
        bootstrap.build(ctx, &mut bootstrap_ctx, &mut tree)?;
        // 本例中不會走入這個分支,這裏我們就不介紹 apply 方法了。
        tree = bootstrap.apply(ctx, &mut bootstrap_ctx, bootstrap_mgr, blob_mgr, None)?;
    }

    // 將樹狀結構轉換為一個平鋪的數組
    // 保存到 `bootstrap_ctx.nodes` 中
    timing_tracer!(
        { bootstrap.build(ctx, &mut bootstrap_ctx, &mut tree) },
        "build_bootstrap"
    )?;

    // Dump blob file
    let mut blob_ctx = BlobContext::new()?;
    let blob_index = blob_mgr.alloc_index()?;
    let mut blob = Blob::new();

    // blob.dump 將內存的數據寫入磁盤 blob 文件
    let blob_exists = timing_tracer!(
        {
            blob.dump(
                ctx,
                &mut blob_ctx,
                blob_index,
                &mut bootstrap_ctx.nodes,
                &mut blob_mgr.chunk_dict_cache,
            )
        },
        "dump_blob"
    )?;

    let mut blob_writer = blob_ctx.writer.take().unwrap();
    let blob_id = blob_ctx.blob_id();

    // 這裏的 blob_exists 表示 compressed_blob_size  > 0,即 blob 文件大小大於 0 
    if blob_exists {
        blob_writer.finalize(blob_id.clone())?;
        // Add new blob to blob table.
        blob_mgr.add(blob_ctx);
    }

    // 將 bootstrap 寫入文件
    let blob_table = blob_mgr.to_blob_table(ctx)?;
    bootstrap.dump(ctx, &mut bootstrap_ctx, &blob_table)?;

    bootstrap_mgr.add(bootstrap_ctx);
}

build_tree_from_fs 函數也比較簡單,就是構建文件夾結構,最後保存到 Tree 裏:

pub(crate) struct Tree {
    /// Filesystem node.
    pub node: Node,
    /// Children tree nodes.
    pub children: Vec<Tree>,
}

其中一個 node 表示一個文件/文件夾,而 children 則表示該文件夾下的子文件夾或者文件。

有了文件夾的樹狀結構,bootstrap 就可以開始 build 了。

Bootstrap build() 和 build_rafs 函數

build() 函數時構建 bootstrap 的入口:

pub fn build(
    &mut self,
    ctx: &mut BuildContext,
    bootstrap_ctx: &mut BootstrapContext,
    tree: &mut Tree,
) -> Result<()> {
    // 設置為 root 節點
    tree.node.index = RAFS_ROOT_INODE;
    tree.node.inode.set_ino(RAFS_ROOT_INODE);

    bootstrap_ctx.inode_map.insert(
        (tree.node.layer_idx, tree.node.src_ino, tree.node.src_dev),
        vec![tree.node.index],
    );

    let mut nodes = Vec::with_capacity(0x10000);
    // 將根節點 push 進去
    nodes.push(tree.node.clone());

    // 調用 build_rafs 構建子節點
    self.build_rafs(ctx, bootstrap_ctx, tree, &mut nodes)?;

    // 將結果保存到 bootstrap_ctx.nodes 中
    bootstrap_ctx.nodes = nodes;

    Ok(())
}

build_rafs 函數會遍歷前面生成的節點列表,為各節點設置 inode 屬性,包括 index,ino,子節點數量等。

這個函數會遞歸的進行構建,所以有一個參數 tree 表示當前節點,對於子節點,如果是文件夾的話,還會繼續調用 build_rafs 函數,構建子節點的數據結構。

nodes 參數是一個全局的 Node 列表,這是一個將樹狀結構轉換為一維數組的結構。

函數內容如下。

fn build_rafs(
    &mut self,
    ctx: &mut BuildContext,
    bootstrap_ctx: &mut BootstrapContext,
    tree: &mut Tree,
    nodes: &mut Vec<Node>,
) -> Result<()> {
    // 分配 index,新的值為 nodes 數組長度 + 1
    let index = nodes.len() as u32 + 1;
    // 每個 node 都會有這個 index 屬性,方便從 nodes 數組獲取該節點
    let parent = &mut nodes[tree.node.index as usize - 1];

    // Maybe the parent is not a directory in multi-layers build scenario, so we check here.
    if parent.is_dir() {
        parent.inode.set_child_index(index);
        parent.inode.set_child_count(tree.children.len() as u32);
    }

    let mut dirs: Vec<&mut Tree> = Vec::new();
    let parent_ino = parent.inode.ino();

    for child in tree.children.iter_mut() {
        let index = nodes.len() as u64 + 1;
        child.node.index = index;
        child.node.inode.set_parent(parent_ino);

        // 這裏刪除了 hardlink 處理的代碼
        if !hardlink {
            // 分配 ino,也就是數組索引
            child.node.inode.set_ino(index);
            child.node.inode.set_nlink(1);
        }

        // 這裏處理 whiteout 類型
        match (
            bootstrap_ctx.layered,
            child.node.whiteout_type(ctx.whiteout_spec),
        ) {
            (true, Some(whiteout_type)) => {
                // 這裏省略 layered 的處理
            }
            (false, Some(whiteout_type)) => {
                // Remove overlayfs opaque xattr for single layer build
                if whiteout_type == WhiteoutType::OverlayFsOpaque {
                    child
                        .node
                        .remove_xattr(&OsString::from(OVERLAYFS_WHITEOUT_OPAQUE));
                }
                nodes.push(child.node.clone());
            }
            _ => {
                // 這個例子中走默認分支,
                // 將 child.node 添加到 nodes 數組
                nodes.push(child.node.clone());
            }
        }

        // 如果當前 child 是文件夾,還需要再次遞歸處理
        // 所以將該文件夾保存到 dirs 數組
        if child.node.is_dir() {
            dirs.push(child);
        }
    }

    // 循環完當前節點的 children 之後
    // 還需要再次遞歸處理子文件夾
    for dir in dirs {
        self.build_rafs(ctx, bootstrap_ctx, dir, nodes)?;
    }

}

經過上面的兩個函數的處理,就將所有節點的數據保存到了 bootstrap_ctx.nodes 中,接着就可以生成 bootstrap 和 blob 文件了。

blob.dump

blob 的 dump() 方法位於 src/bin/nydus-image/core/blob.rs 文件中。

其內容如下:

pub fn dump<'a, T: ChunkDict>(
    &mut self,
    ctx: &BuildContext,
    blob_ctx: &'a mut BlobContext,
    blob_index: u32,
    nodes: &mut Vec<Node>,
    chunk_dict: &mut T,
) -> Result<bool> {
    match ctx.source_type {
        SourceType::Directory | SourceType::Diff => {
            // layout_blob_simple 函數用於從 Vec<Node> 類型的 nodes
            // 返回 Vec<usize> 類型的 inodes
            // prefetch 相關的變量和預拉取有關,這裏我們暫時忽略
            let (inodes, prefetch_entries) = blob_ctx
                .blob_layout
                .layout_blob_simple(&ctx.prefetch, nodes)?;

            // inodes 類型為 Vec<usize> ,inode 即 usize,
            // 用這個 inode 作為數組索引,從 nodes 數組獲取 node 元素
            // 然後再調用 node 的 dump_blob 將 node 的信息寫到磁盤
            for (idx, inode) in inodes.iter().enumerate() {
                let node = &mut nodes[*inode];
                let size = node
                    .dump_blob(ctx, blob_ctx, blob_index, chunk_dict)
                    .context("failed to dump blob chunks")?;
            }
        }
        SourceType::StargzIndex => {
            // 省略 stargz 處理的 case
        }
    }

    // 如果沒有指定 blob_id,則自動從 blob_hash 創建 ID
    if blob_ctx.blob_id.is_empty() {
        blob_ctx.blob_id = format!("{:x}", blob_ctx.blob_hash.clone().finalize());
    }

    // compressed_blob_size > 0 才需要寫到磁盤,
    // 這裏返回的 blob_exists 即是該標誌
    let blob_exists = blob_ctx.compressed_blob_size > 0;

    Ok(blob_exists)
}

node.dump_blob

接着我們來看一下真正的寫 blob 的函數,該函數位於 src/bin/nydus-image/core/node.rs 文件中。

pub fn dump_blob<T: ChunkDict>(
    self: &mut Node,
    ctx: &BuildContext,
    blob_ctx: &mut BlobContext,
    blob_index: u32,
    chunk_dict: &mut T,
) -> Result<u64> {
    if self.is_dir() {
        return Ok(0);
    } else if self.is_symlink() {
      // 針對 symlink 的特殊處理
        return Ok(0);
    } else if self.is_special() {
      // 針對 special 文件的特殊處理
        return Ok(0);
    }

    let mut file = File::open(&self.path)
        .with_context(|| format!("failed to open node file {:?}", self.path))?;
    let mut inode_hasher = RafsDigest::hasher(ctx.digester);
    let mut blob_size = 0u64;

    // `child_count` of regular file is reused as `chunk_count`.
    // 一個文件過大的話,大於 chunk size,就要被拆分為多個 chunk 存儲。
    for i in 0..self.inode.child_count() {
        let chunk_size = blob_ctx.chunk_size;
        let file_offset = i as u64 * chunk_size as u64;
        let chunk_size = if i == self.inode.child_count() - 1 {
          // 最後一個 chunk,可能實際的 chunk size 小於 blob_ctx.chunk_size
          // 這裏檢查下是否 chunk size 不足
            (self.inode.size() as u64)
                .checked_sub((chunk_size * i) as u64)
                .ok_or_else(|| {
                    anyhow!("the rest chunk size of inode is bigger than chunk_size")
                })? as u32
        } else {
            chunk_size
        };

        // 使用 read_exact 讀取一個 chunk 的數據到 chunk_data
        let mut chunk_data = &mut blob_ctx.chunk_data_buf[0..chunk_size as usize];
        file.read_exact(&mut chunk_data)
            .with_context(|| format!("failed to read node file {:?}", self.path))?;

        // 生成 chunk_id
        let chunk_id = RafsDigest::from_buf(chunk_data, ctx.digester);
        inode_hasher.digest_update(chunk_id.as_ref());

        // 生成 chunk 對象,對本例來説這是一個 RafsV5ChunkInfo 類型的數據結構
        let mut chunk = self.inode.create_chunk();
        chunk.set_id(chunk_id);

        // 通過對比 chunk digest 來判斷是否該 chunk 已經存在
        // 注意這裏從兩個緩存的地方獲取,
        // 一個是 blob_ctx.chunk_dict,
        // 一個是輸入參數的 chunk_dict
        let exist_chunk = match blob_ctx.chunk_dict.get_chunk(&chunk_id) {
            Some(v) => Some((v, true)),
            None => chunk_dict.get_chunk(&chunk_id).map(|v| (v, false)),
        };

        if let Some((cached_chunk, from_dict)) = exist_chunk {
            if cached_chunk.uncompressed_size() == 0
                || cached_chunk.uncompressed_size() == chunk_size
            {
              // 從 cached_chunk 拷貝數據
                chunk.copy_from(cached_chunk);
                chunk.set_file_offset(file_offset);
                if from_dict {
                  // 如果是 blob_ctx.chunk_dict 裏已存在該 chunk,
                  // 則設置 blob index 為該 chunk 的 blob id
                    let idx = blob_ctx.chunk_dict.get_real_blob_idx(chunk.blob_index());
                    chunk.set_blob_index(idx);
                }

                let source = if from_dict {
                    ChunkSource::Dict
                } else {
                    ChunkSource::Build
                };
                self.chunks.push(NodeChunk {
                    source,
                    inner: chunk,
                });

                continue;
            }
        }

        // 沒有從任何緩存中獲得該 chunk,則創建該 chunk
        // 首先先對數據進行壓縮處理
        let (compressed, is_compressed) = compress::compress(&chunk_data, ctx.compressor)
            .with_context(|| format!("failed to compress node file {:?}", self.path))?;
        let compressed_size = compressed.len();

        // 4k 對齊
        let aligned_chunk_size = if ctx.aligned_chunk {
            try_round_up_4k(chunk_size).unwrap()
        } else {
            chunk_size
        };

        // 更新 blob_ctx 中的一些信息
        let pre_decompress_offset = blob_ctx.decompress_offset;
        let pre_compress_offset = blob_ctx.compress_offset;

        blob_ctx.compress_offset += compressed_size as u64;
        blob_ctx.decompressed_blob_size =
            blob_ctx.decompress_offset + aligned_chunk_size as u64;
        blob_ctx.compressed_blob_size += compressed_size as u64;
        blob_ctx.decompress_offset += aligned_chunk_size as u64;
        blob_ctx.blob_hash.update(&compressed);

        // 將壓縮後的 chunk 數據寫入到 blob 文件
        if let Some(writer) = &mut blob_ctx.writer {
            writer
                .write_all(&compressed)
                .context("failed to write blob")?;
        }

        // 更新 chunk 對象的一些屬性
        let chunk_index = blob_ctx.alloc_index()?;
        chunk.set_chunk_info(
            blob_index,
            chunk_index,
            file_offset,
            pre_decompress_offset,
            pre_compress_offset,
            compressed_size,
            chunk_size,
            is_compressed,
        )?;

        blob_ctx.add_chunk_meta_info(&chunk)?;

        // 將 chunk 對象加入到 chunk_dict,即該方法的輸入參數
        chunk_dict.add_chunk(chunk.clone());

      // 將 chunk 對象添加到 self.chunks
        self.chunks.push(NodeChunk {
            source: ChunkSource::Build,
            inner: chunk,
        });
        blob_size += compressed_size as u64;
    }

    Ok(blob_size)
}

以上就是 dump blob 的大致流程。

bootstrap.dump

Bootstrap 的 dump 會根據 rafs 版本來調用不同的實現函數,這裏我們用的是 v5 的版本,所以實現函數在 dump_rafsv5 中。

fn dump_rafsv5(
    &mut self,
    ctx: &mut BuildContext,
    bootstrap_ctx: &mut BootstrapContext,
    blob_table: &RafsV5BlobTable,
) -> Result<()> {
    // 計算文件夾 inode 的 digest
    for idx in (0..bootstrap_ctx.nodes.len()).rev() {
        self.digest_node(ctx, bootstrap_ctx, idx);
    }

    // Set inode table
    let super_block_size = size_of::<RafsV5SuperBlock>();
    let inode_table_entries = bootstrap_ctx.nodes.len() as u32;
    // 創建 inode table
    let mut inode_table = RafsV5InodeTable::new(inode_table_entries as usize);
    let inode_table_size = inode_table.size();

    // Set blob table, use sha256 string (length 64) as blob id if not specified
    let prefetch_table_offset = super_block_size + inode_table_size;
    let blob_table_offset = prefetch_table_offset + prefetch_table_size;
    let blob_table_size = blob_table.size();
    let extended_blob_table_offset = blob_table_offset + blob_table_size;
    let extended_blob_table_size = blob_table.extended.size();
    let extended_blob_table_entries = blob_table.extended.entries();

    // 創建 super block
    let mut super_block = RafsV5SuperBlock::new();
    let inodes_count = bootstrap_ctx.inode_map.len() as u64;
    super_block.set_inodes_count(inodes_count);
    super_block.set_inode_table_offset(super_block_size as u64);
    super_block.set_inode_table_entries(inode_table_entries);
    super_block.set_blob_table_offset(blob_table_offset as u64);
    super_block.set_blob_table_size(blob_table_size as u32);
    super_block.set_extended_blob_table_offset(extended_blob_table_offset as u64);
    super_block.set_extended_blob_table_entries(u32::try_from(extended_blob_table_entries)?);
    super_block.set_prefetch_table_offset(prefetch_table_offset as u64);
    super_block.set_prefetch_table_entries(prefetch_table_entries);
    super_block.set_compressor(ctx.compressor);
    super_block.set_digester(ctx.digester);
    super_block.set_chunk_size(ctx.chunk_size);

    // Set inodes and chunks
    let mut inode_offset = (super_block_size
        + inode_table_size
        + prefetch_table_size
        + blob_table_size
        + extended_blob_table_size) as u32;

    for node in &mut bootstrap_ctx.nodes {
        inode_table.set(node.index, inode_offset)?;
        // Add inode size
        inode_offset += node.inode.inode_size() as u32;
        // Add chunks size
        if node.is_reg() {
            inode_offset += node.inode.child_count() * size_of::<RafsV5ChunkInfo>() as u32;
        }
    }

    // Dump super block
    super_block
        .store(bootstrap_ctx.writer.as_mut())
        .context("failed to store superblock")?;

    // Dump inode table
    inode_table
        .store(bootstrap_ctx.writer.as_mut())
        .context("failed to store inode table")?;

    // Dump blob table
    blob_table
        .store(bootstrap_ctx.writer.as_mut())
        .context("failed to store blob table")?;

    // Dump extended blob table
    blob_table
        .store_extended(bootstrap_ctx.writer.as_mut())
        .context("failed to store extended blob table")?;

    // Dump inodes and chunks
    timing_tracer!(
        {
            for node in &bootstrap_ctx.nodes {
                node.dump_bootstrap_v5(&ctx, bootstrap_ctx.writer.as_mut())
                    .context("failed to dump bootstrap")?;
            }

            Ok(())
        },
        "dump_bootstrap",
        Result<()>
    )?;

    bootstrap_ctx
        .writer
        .finalize(Some(bootstrap_ctx.name.to_string()))?;

    Ok(())
}

關於 bootstrap 數據中存儲的內容,可以參考前幾天的這篇文章。

結束

Nydus 的代碼量還是挺大,上面只是分析了大致的流程,至於具體到其中的每一個函數調用,可能還涉及到很多不同的執行路徑,不同的標誌值,肯定要比上面的複雜的多,到時候還得根據實際情況,進一步的細讀。