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

2022-10-10    分類: 網(wǎng)站建設(shè)

導(dǎo)語(yǔ)

作者李志宇,騰訊云后臺(tái)開發(fā)工程師,日常負(fù)責(zé)集群節(jié)點(diǎn)和運(yùn)行時(shí)相關(guān)的工作,熟悉 containerd、docker、runc 等運(yùn)行時(shí)組件。近期在為某位客戶提供技術(shù)支持過程中,遇到了 containerd 鏡像丟失文件問題,經(jīng)過一系列分析、推斷、復(fù)現(xiàn)、排查,最終成功找到根因并給出解決方案。現(xiàn)將整個(gè)詳細(xì)處理過程整理成文分享出來(lái),希望能夠?yàn)榇蠹姨峁┮粋€(gè)有價(jià)值的問題處理思路以及幫助大家更好地理解相關(guān)原理。

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

containerd 鏡像丟失文件問題說明

近期有客戶反映某些容器鏡像出現(xiàn)了文件丟失的奇怪現(xiàn)象,經(jīng)過模擬復(fù)現(xiàn)匯總出丟失情況如下:

某些特定的鏡像會(huì)穩(wěn)定丟失文件;

“丟失”在某些發(fā)行版穩(wěn)定復(fù)現(xiàn),但在 ubuntu 上不會(huì)出現(xiàn);

v1.2 版本的 containerd 會(huì)文件丟失,而 v1.3 不會(huì)。

通過閱讀源碼和文檔,最終解決了這個(gè) containerd 鏡像丟失問題,并寫下了這篇文章,希望和大家分享下解決問題的經(jīng)歷和鏡像生成的原理。為了方便某些心急的同學(xué),本文接下來(lái)將首先揭曉該問題的答案~

根因和解決方案

由于內(nèi)核 overlay 模塊 Bug,當(dāng) containerd 從鏡像倉(cāng)庫(kù)下載鏡像的“壓縮包”生成鏡像的“層”時(shí),overlay 錯(cuò)誤地把trusted.overlay.opaque=y這個(gè) xattrs 從下層傳遞到了上層。如果某個(gè)目錄設(shè)置了這個(gè)屬性,overlay 則會(huì)認(rèn)為這個(gè)目錄是不透明的,以至于在進(jìn)行聯(lián)合掛載時(shí)該目錄將會(huì)把下面的目錄覆蓋掉,進(jìn)而導(dǎo)致鏡像文件丟失的問題。

這個(gè)問題的解決方案可以有兩種,一種簡(jiǎn)單粗暴,直接升級(jí)內(nèi)核中 overlay 模塊即可。

另外一種可以考慮把 containerd 從 v1.2 版本升級(jí)到 v1.3,原因在于 containerd v1.3 中會(huì)主動(dòng)設(shè)置上述 opaque 屬性,該版本 containerd 不會(huì)觸發(fā) overlayfs 的 bug。當(dāng)然,這種方式是規(guī)避而非徹底解決 Bug。

snapshotter 生成鏡像原理分析

雖然根本原因看起來(lái)比較簡(jiǎn)單,但分析的過程還是比較曲折的。在分享下這個(gè)問題的排查過程和收獲之前,為了方便大家理解,本小節(jié)將集中講解問題排查過程涉及到的 containerd 和 overlayfs 的知識(shí),比較了解或者不感興趣的同學(xué)可以直接跳過。

與 docker daemon 一開始的設(shè)計(jì)不同,為了減少耦合性,containerd 通過插件的方式由多個(gè)模塊組成。結(jié)合下圖可以看出,其中與鏡像相關(guān)的模塊包含以下幾種:

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

metadata 是 containerd 通過 bbolt 實(shí)現(xiàn)的 kv 存儲(chǔ)模塊,用來(lái)保存鏡像、容器或者層等元信息。比如命令行 ctr 列出所有 snapshot 或 kubelet 獲取所有 pod 都是通過 metadata 模塊查詢的數(shù)據(jù)。 content 是負(fù)責(zé)保存 blob 的模塊,其保存的關(guān)于鏡像的內(nèi)容一般分為三種: 鏡像的 manifest(一個(gè)普通的 json,其中指定了鏡像的 config 和鏡像的 layers 數(shù)組) 鏡像的 config(同樣是個(gè) json,其中指定鏡像的元信息,比如啟動(dòng)命令、環(huán)境變量等) 鏡像的 layer(tar 包,解壓、處理后會(huì)生成鏡像的層) snapshots 是快照模塊總稱,可以設(shè)置使用不同的快照模塊,常見的模塊有 overlayfs、aufs 或 native。在 unpack 時(shí) snapshots 會(huì)把生成鏡像層并保存到文件系統(tǒng);當(dāng)運(yùn)行容器時(shí),可以調(diào)用 snapshots 模塊給容器提供 rootfs 。

容器鏡像規(guī)范主要有 docker 和 oci v1、v2 三種,考慮到這三種規(guī)范在原理上大同小異,可以參考以下示例,將 manifest 當(dāng)作是每個(gè)鏡像只有一份的元信息,用于指向鏡像的 config 和每層 layer。其中,config 即為鏡像配置,把鏡像作為容器運(yùn)行時(shí)需要;layer 即為鏡像的每一層。

type manifest struct { c config layers []layer }

鏡像下載流程與圖 1 中數(shù)字標(biāo)注出來(lái)的順序一致,每個(gè)步驟作用總結(jié)如下:

首先在 metadata 模塊中添加一個(gè) image,這樣我們?cè)趫?zhí)行 list image 時(shí)可看到這個(gè) image。

其次是需要下載鏡像,因?yàn)殓R像是有 manifest、config、layers 等多個(gè)部分組成,所以先下載鏡像的 manifest 并保存到 content 模塊,再解析 manifest 獲取 config 的地址和 layers 的地址。接下來(lái)分別把 config 和每個(gè) layer 下載并保存到 content 模塊,這里需要強(qiáng)調(diào)鏡像的 layer 本來(lái)應(yīng)該是目錄,當(dāng)創(chuàng)建容器時(shí)聯(lián)合掛載到 root 下,但是為了方便網(wǎng)絡(luò)傳輸和存儲(chǔ),這里會(huì)用 tar + 壓縮的方式保存。這里保存到 content 也是不解壓的。

③、④、⑤的作用關(guān)聯(lián)性比較強(qiáng),此處放在一起解釋。snapshot 模塊去 content 模塊讀取 manifest,找到鏡像的所有層,再去 content 模塊把這些層自“下”而“上”讀取出來(lái),逐一解壓并加工,最后放到 snapshot 模塊的目錄下,像圖 1 中的 1001/fs、1002/fs 這些都是鏡像的層。(當(dāng)創(chuàng)建容器時(shí),需要把這些層聯(lián)合掛載生成容器的 rootfs,可以理解成1001/fs + 1002/fs + ... => 1008/work)。

整個(gè)流程的函數(shù)調(diào)用關(guān)系如下圖 2,喜歡閱讀源碼的同學(xué)可以照著這個(gè)去看下。

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

為了方便理解,接下來(lái)用 layer 表示 snapshot 中的層,把剛下載未經(jīng)過加工的“層”稱之為鏡像層的 tar 包或者是 tar 包。

下載鏡像保存入 content 的流程比較簡(jiǎn)單,直接跳過就好。而通過鏡像的 tar 包生成 snapshot 中的 layer 這個(gè)過程比較巧妙,甚至 bug 也是出現(xiàn)在這里,接下來(lái)進(jìn)行重點(diǎn)描述。

首先通過 content 拿到了鏡像的 manifest,這樣我們得知鏡像是有哪些層組成的。最下面一層鏡像比較簡(jiǎn)單,直接解壓到 snapshot 提供的目錄就可以了,比如 10/fs。假設(shè)接下來(lái)要在 11/fs 生成第二層(此時(shí) 11/fs 還是空的),snapshot 會(huì)使用mount -t overlay overlay -o lowerdir=10/fs,upperdir=11/fs,workdir=11/work tmp把已經(jīng)生成好的 layer 10 和還未生成的 layer 11 掛載到一個(gè) tmp 目錄上,其中寫入層是 11/fs 也就是我們想要生成的 layer。去 content 中拿到 layer 11 對(duì)應(yīng)的 tar 包,遍歷這個(gè) tar 包,根據(jù) tar 包中不同的文件對(duì)掛載點(diǎn) tmp 進(jìn)行寫入或者刪除文件的操作(因?yàn)槭锹?lián)合掛載,所以對(duì)于掛載點(diǎn)的操作都會(huì)變成對(duì)寫入層的操作)。把 tar 包轉(zhuǎn)化成 layer 的具體邏輯和下面經(jīng)過簡(jiǎn)化的源碼一致,可以看到如果 tar 包中存在 whiteout 文件或者當(dāng)前的層比如 11/fs 和之前的層有沖突比如 10/fs,會(huì)把底層目錄刪掉。在把 tar 包的文件寫入到目錄后,會(huì)根據(jù) tar 包中記錄的 PAXRecords 給文件添加 xattr,PAXRecords 可以看做是 tar 中每個(gè)文件都帶有的 kv 數(shù)組,可以用來(lái)映射文件系統(tǒng)中文件屬性。

// 這里的tmp就是overlay的掛載點(diǎn)   applyNaive(tar, tmp) {     for tar.hashNext() {       tar_file := tar.Next()                                        // tar包中的文件       real_file := path.Join(root, file.base)        // 現(xiàn)實(shí)世界的文件       // 按照規(guī)則刪除文件       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)       }     }   }  

需要?jiǎng)h除的這些情況總結(jié)如下:

如果存在同名目錄,兩者進(jìn)行 merge

如果存在同名但不都是目錄,需要?jiǎng)h除掉下層目錄(上文件下目錄、上目錄下文件、上文件下文件)

如果存在 .wh. 文件,需要移除底層應(yīng)該被覆蓋掉的目錄,比如目錄下存在 .wh..wh.opaque 文件,就需要?jiǎng)h除 lowerdir 中的對(duì)應(yīng)目錄。

當(dāng)然這里的刪除也沒那么簡(jiǎn)單,還記得當(dāng)前的操作都是通過掛載點(diǎn)來(lái)刪除底層的文件么?在 overlay 中,如果通過掛載點(diǎn)刪除 lower 層的內(nèi)容,不會(huì)把文件真的從 lower 的文件目錄中干掉,而是會(huì)在 upper 層中添加 whiteout,添加 whiteout 的其中一種方式就是設(shè)置上層目錄的 xattr trusted.overlay.opaque=y。

當(dāng) tar 包遍歷結(jié)束以后,對(duì) tmp 做個(gè) umount,得到的 11/fs 就是我們想要的 layer,當(dāng)我們想要生成 12/fs 這個(gè) layer 時(shí),只需要把 10/fs,11/fs 作為 lowerdir,把 12/fs 作為 upperdir 聯(lián)合掛載就可以。也就是說,之后鏡像的每一個(gè) layer 生成都是需要把之前的 layer 掛載,下面圖說明了整個(gè)流程。

可以考慮下為什么要這么大費(fèi)周章?關(guān)鍵有兩點(diǎn)。

一是鏡像中的刪除下層文件是要遵循 image-spec 中對(duì)于 whiteout 文件的定義(image-spec),這個(gè)文件只會(huì)在 tar 包中作為標(biāo)識(shí),并不會(huì)產(chǎn)生真正的影響。而起到真正作用的是在 applyNaive 碰到了 whiteout 文件,會(huì)調(diào)用聯(lián)合文件系統(tǒng)對(duì)底層目錄進(jìn)行刪除,當(dāng)然這個(gè)刪除對(duì)于 overlay 就是標(biāo)記 opaque。

二是因?yàn)榇嬖谖募湍夸浵嗷ジ采w的現(xiàn)象,每一個(gè) tar 包中的文件都需要和之前所有 tar包 中的內(nèi)容進(jìn)行比對(duì),如果不借用聯(lián)合文件系統(tǒng)的“超能力”,我們就只能拿著 tar 中的每一個(gè)文件對(duì)之前的層遍歷。

問題排查過程

了解了鏡像相關(guān)的知識(shí),我們來(lái)看看這個(gè)問題的排查過程。首先我們觀察用戶的容器,經(jīng)過簡(jiǎn)化和打碼目錄結(jié)構(gòu)如下,其中目錄 modules 就是事故多發(fā)地。

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

再觀察下用戶的鏡像的各個(gè)層。我們把鏡像的層按照從下往上用遞增的 ID 來(lái)標(biāo)注,對(duì)這個(gè)目錄有修改的有 5099、5101、5102、5103、5104 這幾層。把容器運(yùn)行起來(lái)后,看到的 modules 目錄和 5104 提供的一樣。并沒有把 5103 等“下面”的鏡像合并起來(lái),相當(dāng)于 5104 把下面的目錄都覆蓋掉了(當(dāng)然,51045103 文件是有區(qū)別的)。

5104 下層目錄為何被覆蓋?

看到這里,首先想到是不是創(chuàng)建容器的 rootfs 時(shí)參數(shù)出現(xiàn)了問題,導(dǎo)致少 mount 了一些層?于是模擬手動(dòng)掛載mount -t overlay overlay -o lowerdir=5104:5103 point把最上兩層掛載,結(jié)果 5104 依然把 5103 覆蓋了。這里推斷可能是存在 overlay 的 .wh. 文件,于是嘗試在這兩層中搜 .wh. 文件,無(wú)果。于是去查 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.

設(shè)置了屬性 trusted.overlay.opaque=y 的目錄會(huì)變成“不透明”的,當(dāng)上層文件系統(tǒng)被設(shè)置為“不透明”時(shí),下層中同名的目錄會(huì)被忽略。overlay 如果想要在上層把下層覆蓋掉,就需要設(shè)置這個(gè)屬性。

通過命令getfattr -n "trusted.overlay.opaque" dir查看發(fā)現(xiàn),5104 下面的 /data/asr_offline/modules 果然帶有這個(gè)屬性,這一現(xiàn)象也進(jìn)而導(dǎo)致了下層目錄被“覆蓋”。

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

一波多折,層層追究那么問題來(lái)了,為什么只有特定的發(fā)行版會(huì)出現(xiàn)這個(gè)現(xiàn)象?我們嘗試在 ubuntu 拉下鏡像,發(fā)現(xiàn)“同源”目錄居然沒有設(shè)置 opaque!由于鏡像的層通過把源文件解壓和解包生成的,我們決定在確保不同操作系統(tǒng)中的“鏡像源文件”的 md5 相同之后,在各個(gè)操作系統(tǒng)上把鏡像源文件通過tar -zxf進(jìn)行解包并重新手動(dòng)掛載,發(fā)現(xiàn) 5104 均不會(huì)把 5103 覆蓋。

根據(jù)以上現(xiàn)象推斷,可能是某些發(fā)行版下的 containerd 從 content 讀取 tar 包并解壓制作 snapshot 的 layer 時(shí)出現(xiàn)問題,錯(cuò)誤地把 snapshot 的目錄設(shè)置上了這個(gè)屬性。

為驗(yàn)證該推斷,決定進(jìn)行源代碼梳理,由此發(fā)現(xiàn)了其中的疑點(diǎn)(相關(guān)代碼如下)——生成 layers 時(shí)遍歷 tar 包會(huì)讀取每個(gè)文件的 PAXRecords 并且把這個(gè)設(shè)置在文件的 xattr 上( tar 包給每個(gè)文件都準(zhǔn)備了 PAXRecords,和 Pod 的 labels 等價(jià))。

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

因?yàn)橹皩?shí)驗(yàn)過 v1.3 的 containerd 不會(huì)出現(xiàn)這個(gè)問題,所以對(duì)照了下兩者的代碼,發(fā)現(xiàn)兩者從 tar 包中抽取 PAXRecords 設(shè)置 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 中不會(huì)設(shè)置以trusted.開頭的 xattr!如果 tar 包中某目錄帶有trusted.overlay.opaque=y這個(gè) PAX,低版本的 containerd 可能就會(huì)把這些屬性設(shè)置到 snapshot 的目錄上,而高版本的卻不會(huì)。那么,當(dāng)用戶在打包時(shí),如果把 opaque 也打到 tar 包中,解壓得到的 layer 對(duì)應(yīng)目錄也就會(huì)帶有這個(gè)屬性。5104 這個(gè)目錄可能就是這個(gè)原因才變成 opaque 的。

為了驗(yàn)證這個(gè)觀點(diǎn),我寫了一段簡(jiǎn)單的程序來(lái)掃描與 layer 對(duì)應(yīng)的 content 來(lái)尋找這個(gè)屬性,結(jié)果發(fā)現(xiàn) 5102、51035104 幾個(gè)層都沒有這個(gè)屬性。這時(shí)我也開始懷疑這個(gè)觀點(diǎn)了,畢竟如果只是 tar 包中有特別的標(biāo)識(shí),應(yīng)該不會(huì)在不同的操作系統(tǒng)表現(xiàn)不同。

抱著最后一絲希望掃描了 50995101,果然也并沒有這個(gè)屬性。但在掃描的過程中,注意到 5101 的 tar 包里存在 /data/asr_offline/modules/.wh..wh.opq 這個(gè)文件。記得當(dāng)時(shí)看代碼 applyNaive 時(shí)如果遇到了 .wh..wh.opq 對(duì)應(yīng)的操作應(yīng)該是在掛載點(diǎn)刪除 /data/asr_offline/modules,而在 overlay 中刪除 lower 目錄會(huì)給 upper 同名目錄加上trusted.overlay.opaque=y。也就是說,在生成 layer 5101 時(shí)(需要提前掛載好 51005099),遍歷 tar 包遇到了這個(gè) wh 文件,應(yīng)該先在掛載點(diǎn)刪除 modules,也就是會(huì)在 5101 對(duì)應(yīng)目錄加上 opaque=y。

再次以驗(yàn)證源代碼成果的心態(tài),去 snapshot 的 5101/fs 下查看目錄 modules 的 opaque,果然和想象的一樣。這些文件應(yīng)該都是在 lower層,所以對(duì)應(yīng)的 overlayfs 的操作應(yīng)該是在 upper 也就是 5101 層的 /data/asr_offline/modules 目錄設(shè)置trusted.overlay.opaque=y。去查看 5101 的這個(gè)目錄,果然帶有這個(gè)屬性,好奇心驅(qū)使著我繼續(xù)查看了 5102、5103、5104 這幾層的目錄,發(fā)現(xiàn)居然都有這個(gè)屬性。

也就是這些 layer 每個(gè)都會(huì)把下面的覆蓋掉?這好像不符合常理。于是,去表現(xiàn)正常的 ubuntu 中查看,發(fā)現(xiàn)只有 5101 有這個(gè)屬性。經(jīng)過反復(fù)確認(rèn) 5102、5103、5104 的 tar 包中的確沒有目錄 modules 的 whiteout 文件,也就是說鏡像原本的意圖就是讓 5101 把下面的層覆蓋掉,再把 5101、5102、5103、5104 這幾層的 modules 目錄 merge 起來(lái)。整個(gè)生成鏡像的流程里,只有“借用”overlay 生成 snapshot 的 layer 會(huì)涉及到操作系統(tǒng)。

云開霧散,大膽猜探

我們不妨大膽猜測(cè)一下,會(huì)不會(huì)像下圖這樣,在生成 layer 5102 時(shí),因?yàn)閮?nèi)核或 overlay 的 bug 把 modules 也添加了不透明的屬性?

為了對(duì)這個(gè)特性做單獨(dú)的測(cè)試,寫了個(gè)簡(jiǎn)單的腳本。運(yùn)行腳本之后,果然發(fā)現(xiàn)在這個(gè)發(fā)行版中,如果 overlay 的低層目錄有這個(gè)屬性并且在 upper 層中創(chuàng)建了同樣的目錄,會(huì)把這個(gè) opaque“傳播”到 upper 層的目錄中。如果像 containerd 那樣遞推生成鏡像,肯定從有 whiteout 層開始上面的每一層都會(huì)具有這個(gè)屬性,也就導(dǎo)致了最終容器在某些特定的目錄只能看到最上面一層。

`#!/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`  

最終總結(jié)

在幾個(gè)內(nèi)核大佬的幫助下,確認(rèn)了是內(nèi)核 overlayfs 模塊的 bug。在 lower 層調(diào)用 copy_up 時(shí)并沒有檢測(cè) xattr,從而導(dǎo)致 opaque 這個(gè) xattr 傳播到了 upper 層。做聯(lián)合掛載時(shí),如果上層的文件得到了這個(gè)屬性,自然會(huì)把下層文件覆蓋掉,也就出現(xiàn)了鏡像中丟失文件的現(xiàn)象。反思整個(gè)排查過程,其實(shí)很難在一開始就把問題定位到內(nèi)核的某個(gè)模塊上,好在可以另辟蹊徑通過測(cè)試和閱讀源碼逐步逼近“真相”,成功尋得解決方案。

分享文章:揭秘!Containerd鏡像文件丟失問題,竟是鏡像生成惹得禍
本文鏈接:http://muchs.cn/news/204333.html

成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供標(biāo)簽優(yōu)化網(wǎng)站排名、品牌網(wǎng)站建設(shè)網(wǎng)站營(yíng)銷、網(wǎng)站策劃ChatGPT

廣告

聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來(lái)源: 創(chuàng)新互聯(lián)

外貿(mào)網(wǎng)站建設(shè)