鏡像格式二十年:從Knoppix到OCI-Image-v2

衆所周知,Docker始於2013年的DotCloud,迄今剛剛七年,如果你剛好在圈中經歷了2013-2015年這段早期歲月,自然應該知道,最初的 Docker = LXC + AUFS ,前者就是所謂的 Linux 容器了,而後者則是我今天要聊的鏡像。

千禧年:驚豔的 Live CD

說到 Linux Distro,除了做差異化的界面主題之外,核心差異一般都在於:

  • 如何更方便地安裝
  • 如何更方便地升級

而在 Distro 界卻有一股清流,超脫於這兩件事情之外,它們就是 Live CD,它們裝在一張光盤裏,或者是一個 U盤上,不需要安裝,也不會改變。之前創業的時候,我司的運維大佬——彤哥曾經說過:

第一次見到 Live CD 時,我內心是震驚的。

這我當然是贊同的,那時我也是震驚的同學之一,要知道 Knoppix 在 2000 年(千禧年)就來到了世界,而它所基於的著名的 Debian,直到 2005 年 6 月,Sarge (3.1) 發佈的時候才正式在 Stable Release 裏帶上了圖形界面的安裝程序 debian-installer (簡稱 d-i),此前版本的安裝還在用文本菜單。就在這個年代,這樣一個開光盤即用、啓動起來就是圖形界面的系統,給我們這些玩家帶來的震撼是可想而知的。那時候的 Live CD 就是十三年後的 Docker,絕對配得上“驚豔”兩個字。

image

要知道,一張700MB左右的光盤裏塞下個完整的操作系統並不容易(當然有人開頭之後也不太難,後來我愛的DSL可以做到50MB)。Knoppix 有個很棒的想法 —— 把裝好的操作系統壓縮一下,放在光盤裏, 隨用隨解壓,這樣,一張700MB光盤裏可以放下大約2GB的根文件系統,這樣就跑KDE桌面也就沒啥問題了,當時,從 distrowatch.com 上可以看到,大多 distro 都是基於 Knoppix 魔改的,足見其影響力。

進化:可讀寫層與UnionFS

Knoppix在誕生之初的一個執念是“絕不碰本地存儲一根指頭”,而光盤,CD-ROM,所使用的 ISO9600 文件系統也是隻讀的,這無疑暗合了當今流行的“不可變基礎設施”的潮流,但是,即使在今天,沒有可寫文件系統對於很多 Linux 軟件仍然是非常困難的,畢竟隨隨便便一個程序也要寫一點配置文件、狀態信息、鎖、日誌之類的嘛。而誕生之初的 Knoppix 是不可寫的,那麼,要有什麼東西要羅盤,就得手工挖掘出本地硬盤來掛上,或者是掛個 NAS 到 /home 或其他掛載點上來,當你不想只是做個緊急啓動盤的時候,這就有點麻煩了。

如果我們從今天穿越回去,毫不費力就可以指出,用 OverlayFS 加上一個 Tmpfs 做可寫層。但是,OverlayFS 要到2010年才首次提交 Patchset,2014年才被合併到 3.18內核(據說當時的淘寶內核組也有不少貢獻和踩坑)。當然,比 Overlay 早的類似的 UnionFS 還是有的,Docker 最早採用的 AUFS 就是其中之一,它是 2006 年出現的,這裏 AUFS 的 A,可以理解成 Advanced,但它最早的意思實際是 Another ——是的,“另一個UFS”,而它的前身就是 UnionFS。

2005 年 5 月,也就是十五年前,Knoppix 創造性地引入了 UnionFS,而在一年半以後的 5.1 版本中,Knoppix 引入了當年誕生的更穩定的 AUFS,此後,包括大家熟悉的 Ubuntu Live CD、Gentoo Live CD 全都使用了 AUFS。可以說,正是 Live CD 們,提前了 8 年,爲 Docker 和 Docker Image 的誕生,做好了存儲上的準備。

這裏所謂 UnionFS,是指多個不同文件系統聯合(堆疊)在一起,呈現爲一個文件系統,它和一般的 FHS 規定的那種樹裝組織方式是不同的,如下圖,對於左邊的標準的目錄樹結構,任何一個文件系統,掛載在樹上的一個掛載點,根據路徑,就可以指到一個確定的文件系統,比如,下圖中,所有的 /usr/local/ 下面的路徑都在一個文件系統上,而 其他 /usr 就會在另一個文件系統上;而 UnionFS 是多層堆疊的,你寫的文件會停留在最上層,比如圖中,你修改過的 /etc/passwd 就會在最上的可寫層,其他的文件就要去下面的層中去找,也就是說,它允許同一個目錄中的不同文件在不同層中,這樣,Live CD 操作系統跑起來就像真正的本地操作系統一樣可以讀寫所有路徑了。

image

塊或文件:Cloop與SquashFS

讓我們把目光放在只讀層上,這一層是 Live CD 的基礎,在 Live CD 還沒有
UnionFS 來做分層的時候就已經存在這個只讀 Rootfs 了。對 Knoppix 來說,這一層是不能直接放完整、無壓縮的操作系統的,因爲在21世紀初,大家都還在用 24x 到 40x 速光驅的時代,Knoppix Live CD 面臨的一個大問題是 700MB 的光盤和龐大的桌面操作系統之間的矛盾。

開頭我們提到了,Knoppix 的想法就是“把裝好的操作系統給壓縮一下,放在光盤裏, 隨用隨解壓”,這樣精心選擇後的 2GB 的 Distro 就可以壓到一張光盤裏了,不過“隨用隨解壓“不是說有就有的,文件系統訪問塊設備,是要找到塊的偏移量的,壓縮了之後,這個偏移量就並不那麼好找了,全解壓到內存裏再找偏移並不那麼容易。

回到 2000 年,那時候還是 2.2 內核,Knoppix 的作者 Klaus Knopper 在創立 Knoppix 之初就引入了一種壓縮的 (Compressed) Loop 設備,稱爲
Cloop ,這種設備是一種特殊的格式,它包含了一個索引,從而讓解壓縮的過程對用戶來說是透明的,Knoppix 的 Cloop 設備看起來就是一個大約2GB大的塊設備,當應用讀寫一個偏移的數據的時候,它只需要根據索引解壓縮對應的數據塊,並返回數據給用戶就可以了,無需全盤解壓縮。

儘管 Knoppix 把衆多 Distro 帶上了 Live CD 的船,但是,衆多後繼者,諸如 arch, Debian, Fedora, Gentoo, Ubuntu 等等 Distro 的 LiveCD,以及大家熟悉的路由器上玩的 OpenWrt,都並沒有選擇 Cloop 文件,它們選擇了和應用語義更接近的文件系統級的解決方案——SquashFS。SquashFS 壓縮了文件、Inode 和目錄,並支持從 4K 到 1M 的壓縮單元尺寸。同樣,它也是根據索引按需解壓的,和 Cloop 的不同之處是,當用戶訪問一個文件的時候,它來根據索引解壓相應的文件所在的塊,而非再經過一層本地文件系統到壓縮塊的尋址,更加簡單直接。事實上,Knoppix 裏也一直有呼聲想要切換到 SquashFS,比如,2004 年就有開發者在轉換 Knoppix 到 SquashFS 上,而且,一些測試數據似乎也表明 SquashFS 的性能似乎要更好一些,尤其在元數據操作方面。

在 Wiki 上是這麼評價 Cloop 的缺點的

The design of the cloop driver requires that compressed blocks be read whole from disk. This makes cloop access inherently slower when there are many scattered reads, which can happen if the system is low on memory or when a large program with many shared libraries is starting. A big issue is the seek time for CD-ROM drives (~80 ms), which exceeds that of hard disks (~10 ms) by a large factor. On the other hand, because files are packed together, reading a compressed block may thus bring in more than one file into the cache. The effects of tail packing are known to improve seek times (cf. reiserfs, btrfs), especially for small files. Some performance tests related to cloop have been conducted.

我來畫蛇添足地翻譯一下:

Cloop 的設計要求從磁盤上以壓縮塊爲單位讀取數據。這樣,當有很多隨機的讀操作時,Cloop 就會顯著地變慢,當系統內存不足或者一個有很多共享庫的大型程序啓動的時候都很可能發生。Cloop 面臨的一個很大的問題是CD-ROM 的尋道時間(約80毫秒),這在很大程度上超過了硬盤的查找時間(約10毫秒)。另一方面,由於文件可以被打包在一起,因此讀取壓縮塊實際可能可以將多個文件帶入緩存。這樣,那些支持 Tail Packing 的文件系統(比如 ReiserFS,Btrfs)可能可以顯著改善 Seek 操作的時間,尤其是對於小文件更是如此。已經有一些與 Cloop 相關的性能測試也證明了這些觀點。

當然,儘管有這些爭論,Cloop 也仍然在 Knoppix 上存在,不過,這個爭論最終隨着 2009 年 SquashFS 被併入 2.6.29 主線內核,應該算是分出勝負了,進入 Kernel 帶來的開箱即用換來的是壓倒性的佔有率和更好的支持,SquashFS 的優勢不僅在上述的 Distro 用戶之多,也在於支持了了各種的壓縮算法,只用於不同的場景。

Docker: Make Unionfs Great Again

斗轉星移,不再年輕的 Live CD 也因爲如此普及,而讓人覺得並不新奇了。但是,技術圈也有輪迴一般,當年被 Live CD 帶紅的 Union FS 們再一次被 Docker 捧紅,煥發了第二春。一般地說,雖然 AUFS 們支持多個只讀層,但普通的 Live CD 只要一個只讀鏡像,和一個可寫層留給用戶就可以了,然而, 以 Solomon 爲首的 DotCloud 的朋友們充分發揮了他們卓越的想象力,把整個文件系統變成了“軟件包”的基本單位,從而做到了 #MUGA (Make Unionfs Great Again)。

回想一下,從1993年的 Slackware 到今天的 RHEL,(服務端的)Distro 的所作所爲,不外乎我開頭提到的兩件事情——安裝和升級。從 rpm 到 APT/deb 再到 Snappy,初始化好系統之後的工作的精髓就在於怎麼更平滑地安裝和升級、確保沒有依賴問題,又不額外佔據太多的空間。解決這個問題的基礎就是 rpm/deb 這樣的包以及包之間的依賴關係,然而,類似“A 依賴 B 和 C,但 B 卻和 C 衝突” 這樣的事情仍然層出不窮,讓人們不停地嘗試解決了二十年。

但 Docker 跳出了軟件包這個思路,他們是這麼看的:

  • 完整的操作系統就是一個包,它必然是自包含的,而且如果在開發、測試、部署時都保持同樣完整、不變的操作系統環境,那麼也就沒有那麼多依賴性導致的問題了;
  • 這個完整的操作系統都是不可變的,就像 Live CD 一樣,我們叫它鏡像,可以用 AUFS 這樣的 UnionFS在上面放一個可寫層,應用可以在運行時寫東西到可寫層,一些動態生成的配置也可以放在可寫層;
  • 如果一些應用軟件鏡像,它們共用同一部分基礎系統,那麼,把這些公共部分,放在 UnionFS 的下層作爲只讀層,這樣他們可以給不同的應用使用;當然,如果兩個應用依賴的東西不一樣,那它們就用不同的基礎層,也不需要互相遷就了,自然沒有上面的依賴性矛盾存在了;
  • 一個鏡像可以包含多個層,這樣,更方便應用可以共享一些數據,節省存儲和傳輸開銷。

大概的示意圖是這樣的:

image

這樣,如果在同一臺機器上跑這三個應用(容器),那麼這些共享的只讀層都不需要重複下載。

更進一步說,Docker 這種分層結構的另一個優點在於,它本身是非常開發者友好的,可以看到,下面這個是一個 Dockerfile 的示意,FROM 代表最底下的基礎層,之後的 RUN, ADD 這樣改變 Rootfs 的操作,都會將結果存成一個新的中間層,最終形成一個鏡像。這樣,開發者對於軟件依賴性的組織可以非常清晰地展現在鏡像的分層關係中,比如下面這個 Dockerfile 是一個 Packaging 用的 Image,它先裝了軟件的依賴包、語言環境,然後初始化了打包操作的用戶環境,然後拉源代碼,最後把製作軟件包的腳本放到鏡像裏。這個組織方式是從通用到特定任務的組織方式,鏡像製作者希望讓這些層可以儘量通用一些,底層的內容可以在其他鏡像中也用得上,而上層則是和本鏡像的工作最直接相關的內容,其他開發者在看到這個 Dockerfile 的時候已經可以基本知道這個鏡像裏有什麼、要幹什麼,以及自己是否可以借鑑了。這個鏡像的設計是 Docker 設計裏最巧妙的地方之一,也是爲什麼大家都願意認同,Solomon 要做的就是開發者體驗(DX, Developer Experiences)。

FROM       debian:jessie
MAINTAINER Hyper Developers <[email protected]>

RUN apt-get update &&\
    apt-get install -y autoconf automake pkg-config dh-make cpio git \
        libdevmapper-dev libsqlite3-dev libvirt-dev python-pip && \
    pip install awscli && \
    apt-get clean && rm -fr /var/lib/apt/lists/* /tmp/* /var/tmp/*
RUN curl -sL https://storage.googleapis.com/golang/go1.8.linux-amd64.tar.gz | tar -C /usr/local -zxf -

RUN useradd makedeb && mkdir -p ~makedeb/.aws && chown -R makedeb.makedeb ~makedeb && chmod og-rw ~makedeb/.aws
RUN mkdir -p /hypersrc/hyperd/../hyperstart &&\
    cd /hypersrc/hyperd && git init && git remote add origin https://github.com/hyperhq/hyperd.git && \
    cd /hypersrc/hyperstart && git init && git remote add origin https://github.com/hyperhq/hyperstart.git && \
    chown makedeb.makedeb -R /hypersrc

ENV PATH /usr/local/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
ENV USER makedeb
ADD entrypoint.sh /

USER makedeb
WORKDIR /hypersrc
ENTRYPOINT ["/entrypoint.sh"]

一個規範的 Docker Image 或者脫胎於其中的 OCI Image,實際上就是一組元數據和一些層數據,這些層數據每個都是一個文件系統內容的打包,從某種意義上說,典型的 Live CD 的 OS,基本上可以理解成一個只讀層加上一個可寫層的 Docker Container 的 rootfs。在 Docker 這裏,Union FS 可以說是 Great Again 了。

未來:現代化的鏡像系統

然而,儘管 Docker Image (或者說 OCI Image)的設計蘊含了“完整的操作系統就是一個包”的優秀思想,又利用 Union FS 實現了“分層”這種既實現漂亮的開發者體驗,又能節約時間空間的精巧設計,但隨着時間的推移,還是暴露出來了一些問題。從去年(2019年)年初,OCI 社區中開始有人討論下一代鏡像格式的問題,這個熱烈的討論中,集中討論了 OCIv1(實際也是Docker的)鏡像格式的一些問題,Aleksa Sarai 也專門寫了一篇博客來討論這個話題,具體說,除了 Tar 格式本身的標準化問題外,大家對當前的鏡像的主要不滿集中在:

  • 內容冗餘:不同層之間相同信息在傳輸和存儲時都是冗餘內容,在不讀取內容的時候無法判斷到這些冗餘的存在;
  • 無法並行:單一層是一個整體,對同一個層既無法並行傳輸,也不能並行提取;
  • 無法進行小塊數據的校驗,只有完整的層下載完成之後,才能對整個層的數據做完整性校驗;
  • 其他一些問題:比如,跨層數據刪除難以完美處理。

上述這些問題用一句話來總結就是“層是鏡像的基本單位”,然而,鏡像的數據的實際使用率是很低的,比如 Cern 的這篇論文中就提到,一般鏡像只有 6% 的內容會被實際用到,這就產生了實質性的升級鏡像數據結構,不再以層爲基本單位的動力。

可見,下一代鏡像的一個趨勢就是打破層的結構來對這些只讀進行更進一步的優化,是的,反應快的同學可能已經回想起了前面提到的Live CD 裏常用的 SquashFS,它可以根據讀取的需要來解壓相應的文件塊來放入內存、供應用使用,這裏是不是可以擴展一下,變成在需要的時候再去遠端拉回鏡像的內容來供應用使用呢——從文件的 Lazy decompress 到 Lazy Load,一步之遙,水到渠城。

是的,螞蟻的鏡像加速實踐裏就採取了這樣的架構。在過去,龐大的鏡像不僅讓拉取過程變慢,而且如果這一過程同樣風險重重,貢獻了大半的 Pod 啓動失敗率,而今天,當我們把延遲加載的 Rootfs 引入進來的時候,這些失敗幾乎被完全消滅掉了。在去年年末的第10屆中國開源黑客松裏,我們也演示了通過 virtio-fs 把這套系統對接到 Kata Containers 安全容器裏的實現

image

如圖,類似 SquashFS,這種新的 Image 格式中,壓縮數據塊是基本單位,一個文件可以對應0到多個數據塊,在數據塊之外,引入了一些附加的元數據來做目錄樹到數據塊的映射關係,從而可以在沒有下載完整鏡像數據的時候也給應用呈現完整的文件系統結構,並在發生具體讀取的時候,根據索引,去獲取相應的數據來提供給應用使用。這個鏡像系統可以帶來這些好處:

  • 按需加載,啓動時無需完全下載鏡像,同時對加載的不完全鏡像內容可以進行完整性校驗,作爲全鏈路可信的一個部分;
  • 對於 runC 容器,通過 FUSE可以提供用戶態解決方案,不依賴宿主機內核變動;
  • 對於 Kata 這樣的虛擬化容器,鏡像數據直接送給Pod沙箱內部使用,不加載在宿主機上;
  • 使用體驗上和之前的 Docker Image 並沒有顯著不同,開發者體驗不會變差;

而且,這套系統在設計之初,我們就發現,因爲我們可以獲取到應用文件數據訪問模式的,而基於文件的一個好處是,即使鏡像升級了,它的數據訪問模式也是傾向於不太變化的,所以,我們可以利用應用文件數據的訪問模式做一些文件預讀之類的針對性操作。

可以看到,系統存儲這個領域二十年來發生了一個螺旋式的進化,發生在 Live CD 上的進化,在容器這裏也又來了一次,恍如隔世。目前,我們正在積極地參與 OCI Image v2 的標準推動,也正在把我們的參考實現和 DragonFly P2P分發結合在一起,並成爲 CNCF 的開源項目 Dragonfly 的一部分,希望在未來可以和 OCI 社區進一步互動,讓我們的需求和優勢成爲社區規範的一部分,也讓我們可以和社區保持一致、可平滑過渡,未來可以統一在 OCI-Image-v2 鏡像之下。

作者介紹:
王旭,螞蟻金服資深技術專家,也是開源項目 Kata Containers 的架構委員會創始成員,在過去幾年中活躍在國內的開源開發社區與標準化工作中。在加入螞蟻金服之前,他是音速神童的聯合創始人和 CTO,他們在 2015 年開源了基於虛擬化技術的容器引擎 runV,在 2017 年 12 月,他們和 Intel 一起宣佈 runV與Clear Containers 項目合併,成爲 Kata Containers 項目,該項目於 2019 年 4 月被董事會通過成爲了 OpenStack 基金會 2012 年以來的首個新開放基礎設施頂級項目。在創立音速神童之前,王旭曾工作於盛大雲計算和中國移動研究院的雲計算團隊。2011 年王旭曾經主持過杭州 QCon 的雲計算主題,同時,也曾經是一位活躍的技術作者、譯者和老 blogger。另外,他也是QCon上海2019“雲原生的新思考”專題的優秀出品人。

活動推薦:

基礎設施技術可以爲上層業務提供更強大的功能支持,提升服務的可靠性。基礎設施如果出了問題,往往帶來的都是全局性的大事故。QCon北京2020設置了“下一代基礎設施技術”專題,重點關注下一代基礎設施的變化及演進實踐案例。點擊瞭解更多。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章