揭祕!containerd 鏡像文件丟失問題,竟是鏡像生成惹得禍

導語

做者李志宇,騰訊雲後臺開發工程師,平常負責集羣節點和運行時相關的工做,熟悉 containerd、docker、runc 等運行時組件。近期在爲某位客戶提供技術支持過程當中,遇到了 containerd 鏡像丟失文件問題,通過一系列分析、推斷、復現、排查,最終成功找到根因並給出解決方案。現將整個詳細處理過程整理成文分享出來,但願可以爲你們提供一個有價值的問題處理思路以及幫助你們更好地理解相關原理。git

containerd 鏡像丟失文件問題說明

近期有客戶反映某些容器鏡像出現了文件丟失的奇怪現象,通過模擬復現彙總出丟失狀況以下:github

某些特定的鏡像會穩定丟失文件;docker

「丟失」在某些發行版穩定復現,但在 ubuntu 上不會出現;json

v1.2 版本的 containerd 會文件丟失,而 v1.3 不會。ubuntu

經過閱讀源碼和文檔,最終解決了這個 containerd 鏡像丟失問題,並寫下了這篇文章,但願和你們分享下解決問題的經歷和鏡像生成的原理。爲了方便某些心急的同窗,本文接下來將首先揭曉該問題的答案~數組

根因和解決方案

因爲內核 overlay 模塊 Bug,當 containerd 從鏡像倉庫下載鏡像的「壓縮包」生成鏡像的「層」時,overlay 錯誤地把trusted.overlay.opaque=y這個 xattrs 從下層傳遞到了上層。若是某個目錄設置了這個屬性,overlay 則會認爲這個目錄是不透明的,以致於在進行聯合掛載時該目錄將會把下面的目錄覆蓋掉,進而致使鏡像文件丟失的問題。bash

這個問題的解決方案能夠有兩種,一種簡單粗暴,直接升級內核中 overlay 模塊便可。網絡

另一種能夠考慮把 containerd 從 v1.2 版本升級到 v1.3,緣由在於 containerd v1.3 中會主動設置上述 opaque 屬性,該版本 containerd 不會觸發 overlayfs 的 bug。固然,這種方式是規避而非完全解決 Bug。app

snapshotter 生成鏡像原理分析

雖然根本緣由看起來比較簡單,但分析的過程仍是比較曲折的。在分享下這個問題的排查過程和收穫以前,爲了方便你們理解,本小節將集中講解問題排查過程涉及到的 containerd 和 overlayfs 的知識,比較瞭解或者不感興趣的同窗能夠直接跳過。函數

與 docker daemon 一開始的設計不一樣,爲了減小耦合性,containerd 經過插件的方式由多個模塊組成。結合下圖能夠看出,其中與鏡像相關的模塊包含如下幾種:

enter image description here

  • metadata 是 containerd 經過 bbolt 實現的 kv 存儲模塊,用來保存鏡像、容器或者層等元信息。好比命令行 ctr 列出全部 snapshot 或 kubelet 獲取全部 pod 都是經過 metadata 模塊查詢的數據。
  • content 是負責保存 blob 的模塊,其保存的關於鏡像的內容通常分爲三種:

    1. 鏡像的 manifest(一個普通的 json,其中指定了鏡像的 config 和鏡像的 layers 數組)
    2. 鏡像的 config(一樣是個 json,其中指定鏡像的元信息,好比啓動命令、環境變量等)
    3. 鏡像的 layer(tar 包,解壓、處理後會生成鏡像的層)
  • snapshots 是快照模塊總稱,能夠設置使用不一樣的快照模塊,常見的模塊有 overlayfs、aufs 或 native。在 unpack 時 snapshots 會把生成鏡像層並保存到文件系統;當運行容器時,能夠調用 snapshots 模塊給容器提供 rootfs 。

容器鏡像規範主要有 docker 和 oci v一、v2 三種,考慮到這三種規範在原理上大同小異,能夠參考如下示例,將 manifest 看成是每一個鏡像只有一份的元信息,用於指向鏡像的 config 和每層 layer。其中,config 即爲鏡像配置,把鏡像做爲容器運行時須要;layer 即爲鏡像的每一層。

type manifest struct {
  c config
  layers []layer
}

鏡像下載流程與圖 1 中數字標註出來的順序一致,每一個步驟做用總結以下:

首先在 metadata 模塊中添加一個 image,這樣咱們在執行 list image 時可看到這個 image。

其次是須要下載鏡像,由於鏡像是有 manifest、config、layers 等多個部分組成,因此先下載鏡像的 manifest 並保存到 content 模塊,再解析 manifest 獲取 config 的地址和 layers 的地址。接下來分別把 config 和每一個 layer 下載並保存到 content 模塊,這裏須要強調鏡像的 layer 原本應該是目錄,當建立容器時聯合掛載到 root 下,可是爲了方便網絡傳輸和存儲,這裏會用 tar + 壓縮的方式保存。這裏保存到 content 也是不解壓的。

③、④、⑤的做用關聯性比較強,此處放在一塊兒解釋。snapshot 模塊去 content 模塊讀取 manifest,找到鏡像的全部層,再去 content 模塊把這些層自「下」而「上」讀取出來,逐一解壓並加工,最後放到 snapshot 模塊的目錄下,像圖 1 中的 1001/fs、1002/fs 這些都是鏡像的層。(當建立容器時,須要把這些層聯合掛載生成容器的 rootfs,能夠理解成1001/fs + 1002/fs + ... => 1008/work)。

整個流程的函數調用關係以下圖 2,喜歡閱讀源碼的同窗能夠照着這個去看下。
enter image description here

爲了方便理解,接下來用 layer 表示 snapshot 中的層,把剛下載未通過加工的「層」稱之爲鏡像層的 tar 包或者是 tar 包。

下載鏡像保存入 content 的流程比較簡單,直接跳過就好。而經過鏡像的 tar 包生成 snapshot 中的 layer 這個過程比較巧妙,甚至 bug 也是出如今這裏,接下來進行重點描述。

首先經過 content 拿到了鏡像的 manifest,這樣咱們得知鏡像是有哪些層組成的。最下面一層鏡像比較簡單,直接解壓到 snapshot 提供的目錄就能夠了,好比 10/fs。假設接下來要在 11/fs 生成第二層(此時 11/fs 仍是空的),snapshot 會使用mount -t overlay overlay -o lowerdir=10/fs,upperdir=11/fs,workdir=11/work tmp把已經生成好的 layer 10 和還未生成的 layer 11 掛載到一個 tmp 目錄上,其中寫入層是 11/fs 也就是咱們想要生成的 layer。去 content 中拿到 layer 11 對應的 tar 包,遍歷這個 tar 包,根據 tar 包中不一樣的文件對掛載點 tmp 進行寫入或者刪除文件的操做(由於是聯合掛載,因此對於掛載點的操做都會變成對寫入層的操做)。把 tar 包轉化成 layer 的具體邏輯和下面通過簡化的源碼一致,能夠看到若是 tar 包中存在 whiteout 文件或者當前的層好比 11/fs 和以前的層有衝突好比 10/fs,會把底層目錄刪掉。在把 tar 包的文件寫入到目錄後,會根據 tar 包中記錄的 PAXRecords 給文件添加 xattr,PAXRecords 能夠看作是 tar 中每一個文件都帶有的 kv 數組,能夠用來映射文件系統中文件屬性。

// 這裏的tmp就是overlay的掛載點
applyNaive(tar, tmp) {
  for tar.hashNext() {
    tar_file := tar.Next()                                        // tar包中的文件
    real_file := path.Join(root, file.base)        // 現實世界的文件
    // 按照規則刪除文件
    if isWhiteout(info) {
      whiteRM(real_file)
    }
    if !(file.IsDir() && IsDir(real_file)) {
      rm(real_file)
    } 
    // 把tar包的文件寫入到layer中
    createFileOrDir(tar_file, real_file)
    for k, v := range tar_file.PAXRecords {
      setxattr(real_file, k, v)
    }
  }
}

須要刪除的這些狀況總結以下:

若是存在同名目錄,二者進行 merge

若是存在同名但不都是目錄,須要刪除掉下層目錄(上文件下目錄、上目錄下文件、上文件下文件)

若是存在 .wh. 文件,須要移除底層應該被覆蓋掉的目錄,好比目錄下存在 .wh..wh.opaque 文件,就須要刪除 lowerdir 中的對應目錄。

enter image description here

固然這裏的刪除也沒那麼簡單,還記得當前的操做都是經過掛載點來刪除底層的文件麼?在 overlay 中,若是經過掛載點刪除 lower 層的內容,不會把文件真的從 lower 的文件目錄中幹掉,而是會在 upper 層中添加 whiteout,添加 whiteout 的其中一種方式就是設置上層目錄的 xattr trusted.overlay.opaque=y。

當 tar 包遍歷結束之後,對 tmp 作個 umount,獲得的 11/fs 就是咱們想要的 layer,當咱們想要生成 12/fs 這個 layer 時,只須要把 10/fs,11/fs 做爲 lowerdir,把 12/fs 做爲 upperdir 聯合掛載就能夠。也就是說,以後鏡像的每個 layer 生成都是須要把以前的 layer 掛載,下面圖說明了整個流程。

enter image description here

能夠考慮下爲何要這麼大費周章?關鍵有兩點。

一是鏡像中的刪除下層文件是要遵循 image-spec 中對於 whiteout 文件的定義(image-spec),這個文件只會在 tar 包中做爲標識,並不會產生真正的影響。而起到真正做用的是在 applyNaive 碰到了 whiteout 文件,會調用聯合文件系統對底層目錄進行刪除,固然這個刪除對於 overlay 就是標記 opaque。

二是由於存在文件和目錄相互覆蓋的現象,每個 tar 包中的文件都須要和以前全部 tar包 中的內容進行比對,若是不借用聯合文件系統的「超能力」,咱們就只能拿着 tar 中的每個文件對以前的層遍歷。

問題排查過程

瞭解了鏡像相關的知識,咱們來看看這個問題的排查過程。首先咱們觀察用戶的容器,通過簡化和打碼目錄結構以下,其中目錄 modules 就是事故多發地。

/data
└── prom
    ├── bin
    └── modules
        ├── file
        └── lib/

再觀察下用戶的鏡像的各個層。咱們把鏡像的層按照從下往上用遞增的 ID 來標註,對這個目錄有修改的有 509九、510一、510二、510三、5104 這幾層。把容器運行起來後,看到的 modules 目錄和 5104 提供的同樣。並無把 5103 等「下面」的鏡像合併起來,至關於 5104 把下面的目錄都覆蓋掉了(固然,51045103 文件是有區別的)。

5104 下層目錄爲什麼被覆蓋?

看到這裏,首先想到是否是建立容器的 rootfs 時參數出現了問題,致使少 mount 了一些層?因而模擬手動掛載mount -t overlay overlay -o lowerdir=5104:5103 point把最上兩層掛載,結果 5104 依然把 5103 覆蓋了。這裏推斷多是存在 overlay 的 .wh. 文件,因而嘗試在這兩層中搜 .wh. 文件,無果。因而去查 overlayfs 的文檔:

A directory is made opaque by setting the xattr "trusted.overlay.opaque"
to "y". Where the upper filesystem contains an opaque directory, any
directory in the lower filesystem with the same name is ignored.

設置了屬性 trusted.overlay.opaque=y 的目錄會變成「不透明」的,當上層文件系統被設置爲「不透明」時,下層中同名的目錄會被忽略。overlay 若是想要在上層把下層覆蓋掉,就須要設置這個屬性。

經過命令getfattr -n "trusted.overlay.opaque" dir查看發現,5104 下面的 /data/asr_offline/modules 果真帶有這個屬性,這一現象也進而致使了下層目錄被「覆蓋」。

[root@]$ getfattr -n "trusted.overlay.opaque" 5104/fs/data/asr_offline/modules
# file: 5102/fs/data/asr_offline/modules
trusted.overlay.opaque="y"

一波多折,層層追究
那麼問題來了,爲何只有特定的發行版會出現這個現象?咱們嘗試在 ubuntu 拉下鏡像,發現「同源」目錄竟然沒有設置 opaque!因爲鏡像的層經過把源文件解壓和解包生成的,咱們決定在確保不一樣操做系統中的「鏡像源文件」的 md5 相同以後,在各個操做系統上把鏡像源文件經過tar -zxf進行解包並從新手動掛載,發現 5104 均不會把 5103 覆蓋。

根據以上現象推斷,多是某些發行版下的 containerd 從 content 讀取 tar 包並解壓制做 snapshot 的 layer 時出現問題,錯誤地把 snapshot 的目錄設置上了這個屬性。

爲驗證該推斷,決定進行源代碼梳理,由此發現了其中的疑點(相關代碼以下)——生成 layers 時遍歷 tar 包會讀取每一個文件的 PAXRecords 而且把這個設置在文件的 xattr 上( tar 包給每一個文件都準備了 PAXRecords,和 Pod 的 labels 等價)。

func applyNaive() {
  // ...
  for k, v := range tar_file.PAXRecords {
        setxattr(real_file, k, v)
  }
}

func setxattr(path, key, value string) error {
    return unix.Lsetxattr(path, key, []byte(value), 0)
}

由於以前實驗過 v1.3 的 containerd 不會出現這個問題,因此對照了下二者的代碼,發現二者從 tar 包中抽取 PAXRecords 設置 xattr 的邏輯二者是不同的。v1.3 的代碼以下:

func setxattr(path, key, value string) error {
    // Do not set trusted attributes
    if strings.HasPrefix(key, "trusted.") {
        return errors.Wrap(unix.ENOTSUP, "admin attributes from archive not supported")
    }
    return unix.Lsetxattr(path, key, []byte(value), 0)
}

也就是說 v1.3.0 中不會設置以trusted.開頭的 xattr!若是 tar 包中某目錄帶有trusted.overlay.opaque=y這個 PAX,低版本的 containerd 可能就會把這些屬性設置到 snapshot 的目錄上,而高版本的卻不會。那麼,當用戶在打包時,若是把 opaque 也打到 tar 包中,解壓獲得的 layer 對應目錄也就會帶有這個屬性。5104 這個目錄可能就是這個緣由才變成 opaque 的。

爲了驗證這個觀點,我寫了一段簡單的程序來掃描與 layer 對應的 content 來尋找這個屬性,結果發現 510251035104 幾個層都沒有這個屬性。這時我也開始懷疑這個觀點了,畢竟若是隻是 tar 包中有特別的標識,應該不會在不一樣的操做系統表現不一樣。

抱着最後一絲但願掃描了 50995101,果真也並無這個屬性。但在掃描的過程當中,注意到 5101 的 tar 包裏存在 /data/asr_offline/modules/.wh..wh.opq 這個文件。記得當時看代碼 applyNaive 時若是遇到了 .wh..wh.opq 對應的操做應該是在掛載點刪除 /data/asr_offline/modules,而在 overlay 中刪除 lower 目錄會給 upper 同名目錄加上trusted.overlay.opaque=y。也就是說,在生成 layer 5101 時(須要提早掛載好 51005099),遍歷 tar 包遇到了這個 wh 文件,應該先在掛載點刪除 modules,也就是會在 5101 對應目錄加上 opaque=y。

再次以驗證源代碼成果的心態,去 snapshot 的 5101/fs 下查看目錄 modules 的 opaque,果真和想象的同樣。這些文件應該都是在 lower層,因此對應的 overlayfs 的操做應該是在 upper 也就是 5101 層的 /data/asr_offline/modules 目錄設置trusted.overlay.opaque=y。去查看 5101 的這個目錄,果真帶有這個屬性,好奇心驅使着我繼續查看了 510251035104 這幾層的目錄,發現竟然都有這個屬性。

也就是這些 layer 每一個都會把下面的覆蓋掉?這好像不符合常理。因而,去表現正常的 ubuntu 中查看,發現只有 5101 有這個屬性。通過反覆確認 510251035104 的 tar 包中的確沒有目錄 modules 的 whiteout 文件,也就是說鏡像本來的意圖就是讓 5101 把下面的層覆蓋掉,再把 5101510251035104 這幾層的 modules 目錄 merge 起來。整個生成鏡像的流程裏,只有「借用」overlay 生成 snapshot 的 layer 會涉及到操做系統。

雲開霧散,大膽猜探

咱們不妨大膽猜想一下,會不會像下圖這樣,在生成 layer 5102 時,由於內核或 overlay 的 bug 把 modules 也添加了不透明的屬性?

enter image description here

爲了對這個特性作單獨的測試,寫了個簡單的腳本。運行腳本以後,果真發如今這個發行版中,若是 overlay 的低層目錄有這個屬性而且在 upper 層中建立了一樣的目錄,會把這個 opaque「傳播」到 upper 層的目錄中。若是像 containerd 那樣遞推生成鏡像,確定從有 whiteout 層開始上面的每一層都會具備這個屬性,也就致使了最終容器在某些特定的目錄只能看到最上面一層。

`#!/bin/bash

mkdir 1 2 work p
mkdir 1/func
touch 1/func/min

mount -t overlay overlay p -o lowerdir=1,upperdir=2,workdir=work
rm -rf p/func
mkdir -p p/func
touch p/func/max
umount p
getfattr -n "trusted.overlay.opaque" 2/func

mkdir 3
mount -t overlay overlay p -o lowerdir=2:1,upperdir=3,workdir=work
touch p/func/sqrt
umount p
getfattr -n "trusted.overlay.opaque" 3/func`

最終總結

在幾個內核大佬的幫助下,確認了是內核 overlayfs 模塊的 bug。在 lower 層調用 copy_up 時並無檢測 xattr,從而致使 opaque 這個 xattr 傳播到了 upper 層。作聯合掛載時,若是上層的文件獲得了這個屬性,天然會把下層文件覆蓋掉,也就出現了鏡像中丟失文件的現象。反思整個排查過程,其實很難在一開始就把問題定位到內核的某個模塊上,好在能夠另闢蹊徑經過測試和閱讀源碼逐步逼近「真相」,成功尋得解決方案。

相關文章
相關標籤/搜索