寫(xiě)作本文的起因是我想讓修改后的分布式 PyTorch 程序能更快的在 Facebook 的集群上啟動(dòng)。探索過(guò)程很有趣,也展示了工業(yè)機(jī)器學(xué)習(xí)需要的知識(shí)體系。
2007 年我剛畢業(yè)后在 Google 工作過(guò)三年。當(dāng)時(shí)覺(jué)得分布式操作系統(tǒng) Borg 真好用。
從 2010 年離開(kāi) Google 之后就一直盼著它開(kāi)源,直到 Kubernetes 的出現(xiàn)。
Kubernetes 調(diào)度的計(jì)算單元是 containers(準(zhǔn)確的翻譯是“集裝箱”,而不是意思泛泛的“容器”,看看 Docker 公司的 Logo 上畫(huà)的是啥就知道作者的心意了)。
而一個(gè) container 執(zhí)行一個(gè) image,就像一個(gè) process 執(zhí)行一個(gè) program。
無(wú)論 Googlers 還是 ex-Googlers,恐怕在用 Borg 的時(shí)候都未曾接觸過(guò) container 和 image 這兩個(gè)概念。為啥 Borg 里沒(méi)有,而 Kubernetes 卻要引入了這樣兩個(gè)概念呢?
這個(gè)曾經(jīng)問(wèn)題在我腦海中一閃而過(guò)就被忽略了。畢竟后來(lái)我負(fù)責(zé)開(kāi)源項(xiàng)目比較多,比如百度 Paddle 以及螞蟻的 SQLFlow 和 ElasticDL,Docker 用起來(lái)很順手。于是也就沒(méi)有多想。
今年(2021 年)初,我加入 Facebook。恰逢 Facebook 發(fā)論文[1]介紹了其分布式集群管理系統(tǒng) Tupperware。
不過(guò) Tupperware 是一個(gè)注冊(cè)于 1946 年的品牌 https://en.wikipedia.org/wiki/Tupperware_Brands,所以在論文里只好起了另一個(gè)名字 Twine。
因?yàn)樾袠I(yè)里知道 Tupperware 這個(gè)名字的朋友很多,本文就不說(shuō) Twine 了。
總之,這篇論文的發(fā)表又引發(fā)了我對(duì)于之前問(wèn)題的回顧——Facebook 里也沒(méi)有 Docker!
和 Facebook Tuppware 團(tuán)隊(duì)以及 Google Borg 幾位新老同事仔細(xì)聊了聊之后,方才恍然。因?yàn)樾袠I(yè)里沒(méi)有看到相關(guān)梳理,本文是為記錄。
一言蔽之
簡(jiǎn)單的說(shuō),如果用 monolithic repository 來(lái)管理代碼,則不需要 Docker image(或者 ZIP、tarball、RPM、deb)之類的“包”。
所謂 monolithic repo 就是一家公司的所有項(xiàng)目的所有代碼都集中放在一個(gè)(或者極少數(shù))repo 里。
因?yàn)?monolithic repository 得有配套的統(tǒng)一構(gòu)建系統(tǒng)(build system)否則編譯不動(dòng)那么老大一坨代碼。
而既然有統(tǒng)一的 build system,一旦發(fā)現(xiàn)某個(gè)集群節(jié)點(diǎn)需要執(zhí)行的程序依賴的某個(gè)模塊變化了,同步這個(gè)模塊到此節(jié)點(diǎn)既可。完全不需要打包再同步。
反之,如果每個(gè)項(xiàng)目在一個(gè)獨(dú)立的 git/svn repo 里,各自用不同的 build system,比如各個(gè)開(kāi)源項(xiàng)目在不同的 GitHub repo 里,則需要把每個(gè)項(xiàng)目 build 的結(jié)果打包。
而 Docker image 這樣支持分層的包格式讓我們只需要傳輸那些容納被修改的項(xiàng)目的最上面幾層,而盡量復(fù)用被節(jié)點(diǎn) cache 了的下面的幾層。
Google 和 Facebook 都使用 monolithic repository,也都有自己的 build systems(我這篇老文尋找 Google Blaze[2] 解釋過(guò) Google 的 build system)所以不需要“包”,當(dāng)然也就不需要 Docker images。
不過(guò) Borg 和 Tupperware 都是有 container 的(使用 Linux kernel 提供的一些 system calls,比如 Google Borg 團(tuán)隊(duì)十多年前貢獻(xiàn)給 Linux kernel 的 cgroup)來(lái)實(shí)現(xiàn) jobs 之間的隔離。
只是因?yàn)槿绻恍枰蠹?build Docker image 了,那么 container 的存在就不容易被關(guān)注到了。
如果不想被上述蔽之,而要細(xì)究這個(gè)問(wèn)題,那就待我一層一層剝開(kāi) Google 和 Facebook 的研發(fā)技術(shù)體系和計(jì)算技術(shù)體系。
Packaging
當(dāng)我們提交一個(gè)分布式作業(yè)(job)到集群上去執(zhí)行,我們得把要執(zhí)行的程序(包括一個(gè)可執(zhí)行文件以及相關(guān)的文件,比如 *.so,*.py)傳送到調(diào)度系統(tǒng)分配給這個(gè) job 的一些機(jī)器(節(jié)點(diǎn)、nodes)上去。
這些待打包的文件是怎么來(lái)的呢?當(dāng)時(shí)是 build 出來(lái)的。在 Google 里有 Blaze,在 Facebook 里有 Buck。
感興趣的朋友們可以看看 Google Blaze 的“開(kāi)源版本”Bazel[3],以及 Facebook Buck 的開(kāi)源版本[4]。
不過(guò)提醒在先:Blaze 和 Facebook Buck 的內(nèi)部版都是用于 monolithic repo 的,而開(kāi)源版本都是方便大家使用非 mono repos 的,所以理念和實(shí)現(xiàn)上有不同,不過(guò)基本使用方法還是可以感受一下的。
假設(shè)我們有如下模塊依賴(module dependencies),用 Buck 或者 Bazel 語(yǔ)法描述(兩者語(yǔ)法幾乎一樣):
python_binary(name="A", srcs=["A.py"], deps=["B", "C"], ...)
python_library(name="B", srcs=["B.py"], deps=["D"], ...)
python_library(name="C", srcs=["C.py"], deps=["E"], ...)
cxx_library(name="D", srcs=["D.cxx", "D.hpp"], deps="F", ...)
cxx_library(name="E", srcs=["E.cxx", "E.hpp"], deps="F", ...)
那么模塊(build 結(jié)果)依賴關(guān)系如下:
A.py --> B.py --> D.so -\
\-> C.py --> E.so --> F.so
如果是開(kāi)源項(xiàng)目,請(qǐng)自行腦補(bǔ),把上述模塊(modules)替換成 GPT-3,PyTorch,cuDNN,libc++ 等項(xiàng)目(projects)。
當(dāng)然,每個(gè) projects 里包含多個(gè) modules 也依賴其他 projects,就像每個(gè) module 有多個(gè)子 modules 一樣。
Tarball
最簡(jiǎn)單的打包方式就是把上述文件 {A,B,C}.py, {D,E,F}.so 打包成一個(gè)文件 A.zip,或者 A.tar.gz。
更嚴(yán)謹(jǐn)?shù)恼f(shuō),文件名里應(yīng)該包括版本號(hào)。比如 A-953bc.zip,其中版本號(hào) 953bc 是 git/Mercurial commit ID。
引入版本號(hào),可以幫助在節(jié)點(diǎn)本地 cache,下次運(yùn)行同一個(gè) tarball 的時(shí)候,就不需要下載這個(gè)文件了。
請(qǐng)注意這里我引入了 package caching 的概念。為下文解釋 Docker 預(yù)備。
XAR
ZIP 或者 tarball 文件拷貝到集群節(jié)點(diǎn)上之后,需要解壓縮到本地文件系統(tǒng)的某個(gè)地方,比如:/var/packages/A-953bc/{A,B,C}.py,{D,E,F}.so。
一個(gè)稍顯酷炫的方式是不用 Tarball,而是把上述文件放在一個(gè) overlay filesystem 的 loopback device image 里。這樣“解壓”就變成了“mount”。
請(qǐng)注意這里我引入了 loopback device image 的概念。為下文解釋 Docker 預(yù)備。
什么叫 loopback device image 呢?在 Unix 里,一個(gè)目錄樹(shù)的文件們被稱為一個(gè)文件系統(tǒng)(filesystem)。
通常一個(gè) filesystem 存儲(chǔ)在一個(gè) block device 上。什么是 block device 呢?
簡(jiǎn)單的說(shuō),但凡一個(gè)存儲(chǔ)空間可以被看作一個(gè) byte array 的,就是一個(gè) block device。
比如一塊硬盤(pán)就是一個(gè) block device。在一個(gè)新買的硬盤(pán)里創(chuàng)建一個(gè)空的目錄樹(shù)結(jié)構(gòu)的過(guò)程,就叫做格式化(format)。
既然 block device 只是一個(gè) byte array,那么一個(gè)文件不也是一個(gè) byte array 嗎?
是的!在 Unix 的世界里,我們完全可以創(chuàng)建一個(gè)固定大小的空文件(用 truncate 命令),然后“格式化”這個(gè)文件,在里面創(chuàng)建一個(gè)空的文件系統(tǒng)。然后把上述文件 {A,B,C}.py,{D,E,F}.so 放進(jìn)去。
比如 Facebook 開(kāi)源的 XAR 文件[5]格式。這是和 Buck 一起使用的。
如果我們運(yùn)行 buck build A 就會(huì)得到 A.xar . 這個(gè)文件包括一個(gè) header,以及一個(gè) squashfs loopback device image,簡(jiǎn)稱 squanshfs image。
這里 squashfs 是一個(gè)開(kāi)源文件系統(tǒng)。感興趣的朋友們可以參考這個(gè)教程[6],創(chuàng)建一個(gè)空文件,把它格式化成 squashfs,然后 mount 到本地文件系統(tǒng)的某個(gè)目錄(mount point)里。
待到我們 umount 的時(shí)候,曾經(jīng)加入到 mount point 里的文件,就留在這個(gè)“空文件”里了。
我們可以把它拷貝分發(fā)給其他人,大家都可以 mount 之,看到我們加入其中的文件。
因?yàn)?XAR 是在 squashfs image 前面加上了一個(gè) header,所以沒(méi)法用 mount -t squashf 命令來(lái) mount,得用 mount -t xar 或者 xarexec -m 命令。
比如,一個(gè)節(jié)點(diǎn)上如果有了 /packages/A-953bc.xar,我們可以用如下命令看到它的內(nèi)容,而不需要耗費(fèi) CPU 資源來(lái)解壓縮:
這個(gè)命令會(huì)打印出一個(gè)臨時(shí)目錄,是 XAR 文件的 mount point。
分層
如果我們現(xiàn)在修改了 A.py,那么不管是 build 成 tarball 還是 XAR,整個(gè)包都需要重新更新。
當(dāng)然,只要 build system 支持 cache,我們是不需要重新生成各個(gè) *.so 文件的。
但是這個(gè)不解決我們需要重新分發(fā) .tar.gz 和 .xar 文件到集群的各個(gè)節(jié)點(diǎn)的麻煩。
之前節(jié)點(diǎn)上可能有老版本的 A-953bc87fe.{tar.gz,xar} 了,但是不能復(fù)用。為了復(fù)用 ,需要分層。
對(duì)于上面情況,我們可以根據(jù)模塊依賴關(guān)系圖,構(gòu)造多個(gè) XAR 文件。
A-953bc.xar --> B-953bc.xar --> D-953bc.xar -\
\-> C-953bc.xar --> E-953bc.xar --> F-953bc.xar
其中每個(gè) XAR 文件里只有對(duì)應(yīng)的 build rule 產(chǎn)生的文件。比如,F(xiàn)-953bc.xar 里只有 F.so。
這樣,如果我們只修改了 A.py,則只有 A.xar 需要重新 build 和傳送到集群節(jié)點(diǎn)上。這個(gè)節(jié)點(diǎn)可以復(fù)用之前已經(jīng) cache 了的 {B,C,D,E,F}-953bc.xar 文件。
假設(shè)一個(gè)節(jié)點(diǎn)上已經(jīng)有 /packages/{A,B,C,D,E,F}-953bc.xar,我們是不是可以按照模塊依賴順序,運(yùn)行 xarexec -m 命令,依次 mount 這些 XAR 文件到同一個(gè) mount point 目錄,既可得到其中所有的內(nèi)容了呢?
很遺憾,不行。因?yàn)楹笠粋€(gè) xarexec/mount 命令會(huì)報(bào)錯(cuò) —— 因?yàn)檫@個(gè) mount point 已經(jīng)被前一個(gè) xarexec/mount 命令占據(jù)了。
下面解釋為什么文件系統(tǒng) image 優(yōu)于 tarball。
那退一步,不用 XAR 了,用 ZIP 或者 tar.gz 不行嗎?可以,但是慢。我們可以把所有 .tar.gz 都解壓縮到同一個(gè)目錄里。
但是如果 A.py 更新了,我們沒(méi)法識(shí)別老的 A.py 并且替換為新的,而是得重新解壓所有 .tar.gz 文件,得到一個(gè)新的文件夾。而重新解壓所有的 {B,C,D,E,F}.tar.gz 很慢。
Overlay Filesystem
有一個(gè)申請(qǐng)的開(kāi)源工具 fuse-overlayfs。它可以把幾個(gè)目錄“疊加”(overlay)起來(lái)。
比如下面命令把 /tmp/{A,B,C,D,E,F}-953bc 這幾個(gè)目錄里的內(nèi)容都“疊加”到 /pacakges/A-953bc 這個(gè)目錄里。
fuse-overlayfs -o \
lowerdir="/tmp/A-953bc:/tmp/B-953bc:..." \
/packages/A-953bc
而 /tmp/{A,B,C,D,E,F}-953bc 這幾個(gè)目錄來(lái)自 xarcexec -m /packages/{A,B,C,D,E,F}-953bc.xar。
請(qǐng)注意這里我引入了 overlay filesystem 的概念。為下文解釋 Docker 預(yù)備。fuse-overlayfs 是怎么做到這一點(diǎn)的呢?
當(dāng)我們?cè)L問(wèn)任何一個(gè)文件系統(tǒng)目錄,比如 /packages/A 的時(shí)候,我們使用的命令行工具(比如 ls )調(diào)用 system calls(比如 open/close/read/write) 來(lái)訪問(wèn)其中的文件。
這些 system calls 和文件系統(tǒng)的 driver 打交道 —— 它們會(huì)問(wèn) driver:/packages/A 這個(gè)目錄里有沒(méi)有一個(gè)叫 A.py 的文件呀?
如果我們使用 Linux,一般來(lái)說(shuō),硬盤(pán)上的文件系統(tǒng)是 ext4 或者 btrfs。也就是說(shuō),Linux universal filesystem driver 會(huì)看看每個(gè)分區(qū)的文件系統(tǒng)是啥,然后把 system call 轉(zhuǎn)發(fā)給對(duì)應(yīng)的 ext4/btrfs driver 去處理。
一般的 filesystem drivers 和其他設(shè)備的 drivers 一樣運(yùn)行在 kernel mode 里。
這是為什么一般我們運(yùn)行 mount 和 umount 這類操作 filesystems 的命令的時(shí)候,都需要 sudo。而 FUSE 是一個(gè)在 userland 開(kāi)發(fā) filesystem driver 的庫(kù)。
fuse-overlayfs 這命令利用 FUSE 這個(gè)庫(kù),開(kāi)發(fā)了一個(gè)運(yùn)行在 userland 的 fuse-overlayfs driver。
當(dāng) ls 命令詢問(wèn)這個(gè) overlayfs driver /packages/A-953bc 目錄里有啥的時(shí)候,這個(gè) fuse-overlayfs driver 記得之前用戶運(yùn)行過(guò) fuse-overlayfs 命令把 /tmp/{A,B,C,D,E}-953bc 這幾個(gè)目錄給疊加上去過(guò),所以它返回這幾個(gè)目錄里的文件。
此時(shí),因?yàn)?nbsp;/tmp/{A,B,C,D,E}-953bc 這幾個(gè)目錄其實(shí)是 /packages/{A,B,C,D,E,F}-953bc.xar 的 mount points,所以每個(gè) XAR 就相當(dāng)于一個(gè) layer。
像 fuse-overlayfs driver 這樣實(shí)現(xiàn)把多個(gè)目錄“疊加”起來(lái)的 filesystem driver 被稱為 overlay filesystem driver,有時(shí)簡(jiǎn)稱為 overlay filesystems。
Docker Image and Layer
上面說(shuō)到用 overlay filesystem 實(shí)現(xiàn)分層。用過(guò) Docker 的人都會(huì)熟悉一個(gè) Docker image 由多層構(gòu)成。
當(dāng)我們運(yùn)行 docker pull <image-name> 命令的時(shí)候,如果本機(jī)已經(jīng) cache 了這個(gè) image 的一部分 layers,則省略下載這些 layers。這其實(shí)就是用 overlay filesystem 實(shí)現(xiàn)的。
Docker 團(tuán)隊(duì)開(kāi)發(fā)了一個(gè) filesystem(driver)叫做 overlayfs —— 這是一個(gè)特定的 filesystem 的名字。
顧名思義,Docker overlayfs 也實(shí)現(xiàn)了“疊加”(overlay)的能力,這就是我們看到每個(gè) Docker image 可以有多個(gè) layers 的原因。
Docker 的 overlayfs 以及它的后續(xù)版本 overlayfs2 都是運(yùn)行在 kernel mode 里的。
這也是 Docker 需要機(jī)器的 root 權(quán)限的原因之一,而這又是 Docker 被詬病容易導(dǎo)致安全漏斗的原因。
有一個(gè)叫 btrfs 的 filesystem,是 Linux 世界里最近幾年發(fā)展很迅速的,用于管理硬盤(pán)效果很好。
這個(gè) filesystem 的 driver 也支持 overlay。所以 Docker 也可以被配置為使用這個(gè) filesystem 而不是 overlayfs。
不過(guò)只有 Docker 用戶的電腦的 local filesystem 是 btrfs 的時(shí)候,Docker 才能用 btrfs 在上面疊加 layers。
所以說(shuō),如果你用的是 macOS 或者 Windows,那肯定沒(méi)法讓 Docker 使用 btrfs 了。
不過(guò)如果你用的是 fuse-overlayfs,那就是用了一副萬(wàn)靈藥了。只是通過(guò) FUSE 在 userland 運(yùn)行的 filesystem 的性能很一般,不過(guò)本文討論的情形對(duì)性能也沒(méi)啥需求。
其實(shí) Docker 也可以被配置使用 fuse-overlayfs。Docker 支持的分層 filesystem 列表在這里 Docker storage drivers[7]。
為什么需要 Docker Image
總結(jié)上文所述,從編程到可以在集群上跑起來(lái),我們要做幾個(gè)步驟:
- 編譯:把源碼編譯成可執(zhí)行的形式。
- 打包:把編譯結(jié)果納入一個(gè)“包”里,以便部署和分發(fā)
- 傳輸:通常是集群管理系統(tǒng)(Borg、Kubernetes、Tupperware 來(lái)做)。如果要在某個(gè)集群節(jié)點(diǎn)上啟動(dòng) container,則需要把“包”傳輸?shù)酱斯?jié)點(diǎn)上,除非這個(gè)節(jié)點(diǎn)曾經(jīng)運(yùn)行過(guò)這個(gè)程序,已經(jīng)有包的 cache。
- 解包:如果“包”是 tarball 或者 zip,到了集群節(jié)點(diǎn)上之后需要解壓縮;如果“包”是一個(gè) filesystem image,則需要 mount。
把源碼分成模塊,可以讓編譯這步充分利用每次修改只改動(dòng)一小部分代碼的特點(diǎn),只重新編譯被修改的模塊,從而節(jié)省時(shí)間。
為了節(jié)省 2,3 和 4 的時(shí)間,我們希望“包”是分層的。每一層最好只包含一個(gè)或者幾個(gè)代碼模塊。這樣,可以利用模塊之間的依賴關(guān)系,盡量復(fù)用容納底層模塊的“層”。
在開(kāi)源的世界里,我們用 Docker image 支持分層的特點(diǎn),一個(gè)基礎(chǔ)層可能只包括某個(gè) Linux distribution(比如 CentOS)的 userland programs,如 ls、cat、grep 等。
在其上,可以有一個(gè)層包括 CUDA。再其上安裝 Python 和 PyTorch。再再之上的一層里是 GPT-3 模型的訓(xùn)練程序。
這樣,如果我們只是修改了 GPT-3 訓(xùn)練程序,則不需要重新打包和傳輸下面三層。
這里的邏輯核心是:存在“項(xiàng)目”(project)的概念。每個(gè)項(xiàng)目可以有自己的 repo,自己的 building system(GNU make、CMake、Buck、Bazel 等),自己的發(fā)行版本(release)。
所以每個(gè)項(xiàng)目的 release 裝進(jìn) Docker image 的一層 layer。與其前置多層合稱為一個(gè) image。
為什么 Google 和 Facebook 不需要 Docker
經(jīng)過(guò)上述這么多知識(shí)準(zhǔn)備,請(qǐng)我們終于可以點(diǎn)題了。
因?yàn)?Google 和 Facebook 使用 monolithic repository,使用統(tǒng)一的 build system(Google Blaze 或者 Facebook Buck)。
雖然也可以利用“項(xiàng)目”的概念,把每個(gè)項(xiàng)目的 build result 裝入 Docker image 的一層。但是實(shí)際上并不需要。
利用 Blaze 和 Buck 的 build rules 定義的模塊,以及模塊之間依賴關(guān)系,我們可以完全去打包和解包的概念。
沒(méi)有了包,當(dāng)然就不需要 zip、tarball、以及 Docker image 和 layers 了。
直接把每個(gè)模塊當(dāng)做一個(gè) layer 既可。如果 D.so 因?yàn)槲覀冃薷牧?D.cpp 被重新編譯,那么只重新傳輸 D.so 既可,而不需要去傳輸一個(gè) layer 其中包括 D.so。
于是,在 Google 和 Facebook 里,受益于 monolithic repository 和統(tǒng)一的 build 工具。
我們把上述四個(gè)步驟省略成了兩個(gè):
- 編譯:把源碼編譯成可執(zhí)行的形式。
- 傳輸:如果某個(gè)模塊被重新編譯,則傳輸這個(gè)模塊。
Google 和 Facebook 沒(méi)在用 Docker
上一節(jié)說(shuō)了 monolithic repo 可以讓 Google 和 Facebook 不需要 Docker image。
現(xiàn)實(shí)是 Google 和 Facebook 沒(méi)有在使用 Docker。這兩個(gè)概念有區(qū)別。
我們先說(shuō)“沒(méi)在用”。歷史上,Google 和 Facebook 使用超大規(guī)模集群先于 Docker 和 Kubernetes 的出現(xiàn)。當(dāng)時(shí)為了打包方便,連 tarball 都沒(méi)有。
對(duì)于 C/C++ 程序,直接全靜態(tài)鏈接,根本沒(méi)有 *.so。于是一個(gè) executable binary file 就是“包”了。
直到今天,大家用開(kāi)源的 Bazel 和 Buck 的時(shí)候,仍然可以看到默認(rèn)鏈接方式就是全靜態(tài)鏈接。
Java 語(yǔ)言雖然是一種“全動(dòng)態(tài)鏈接”的語(yǔ)言,不過(guò)其誕生和演進(jìn)扣準(zhǔn)了互聯(lián)網(wǎng)歷史機(jī)遇,其開(kāi)發(fā)者發(fā)明 jar 文件格式,從而支持了全靜態(tài)鏈接。
Python 語(yǔ)言本身沒(méi)有 jar 包,所以 Blaze 和 Bazel 發(fā)明了 PAR 文件格式(英語(yǔ)叫 subpar),相當(dāng)于為 Python 設(shè)計(jì)了一個(gè) jar。開(kāi)源實(shí)現(xiàn)在這里[8]。
類似的,Buck 發(fā)明了 XAR 格式,也就是我上文所說(shuō)的 squashfs image 前面加了一個(gè) header。其開(kāi)源實(shí)現(xiàn)在這里[9]。
Go 語(yǔ)言默認(rèn)就是全靜態(tài)鏈接的。在 Rob Pike 早期的一些總結(jié)里提到,Go 的設(shè)計(jì),包括全靜態(tài)鏈接,基本就是繞坑而行,繞開(kāi) Google C/C++ 實(shí)踐中遇到過(guò)的各種坑。
熟悉 Google C++ style guide 的朋友們應(yīng)該感覺(jué)到了 Go 語(yǔ)法覆蓋了 guide 說(shuō)的“應(yīng)該用的 C++ 語(yǔ)法”,而不支持 guide 說(shuō)的 “不應(yīng)該用的 C++ 的部分”。
簡(jiǎn)單的說(shuō),歷史上 Google 和 Facebook 沒(méi)有在用 Docker image,很重要的一個(gè)原因是,其 build system 對(duì)各種常見(jiàn)語(yǔ)言的程序都可以全靜態(tài)鏈接,所以可執(zhí)行文件就是“包”。
但這并不是最好的解法,畢竟這樣就沒(méi)有分層了。哪怕我只是修改了 main 函數(shù)里的一行代碼,重新編譯和發(fā)布,都需要很長(zhǎng)時(shí)間,十分鐘甚至數(shù)十分鐘,要知道全靜態(tài)鏈接得到的可執(zhí)行文件往往大小以 GB 計(jì)。
所以全靜態(tài)鏈接雖然是 Google 和 Facebook 沒(méi)有在用 Docker 的原因之一,但是并不是一個(gè)好選擇。
所以也沒(méi)被其他公司效仿。大家還是更愿意用支持分層 cache 的 Docker image。
完美解法的技術(shù)挑戰(zhàn)
完美的解法應(yīng)該支持分層 cache(或者更精確的說(shuō)是分塊 cache)。所以還是應(yīng)該用上文介紹的 monolithic repo 和統(tǒng)一 build system 的特點(diǎn)。
但是這里有一個(gè)技術(shù)挑戰(zhàn),build system 描述的模塊,而模塊通常比“項(xiàng)目”細(xì)粒度太多了。
以 C/C++ 語(yǔ)言為例,如果每個(gè)模塊生成一個(gè) .so 文件,當(dāng)做一個(gè)“層”或者“塊”以便作為 cache 的單元,那么一個(gè)應(yīng)用程序可能需要的 .so 數(shù)量就太多了。
啟動(dòng)應(yīng)用的時(shí)候,恐怕要花幾十分鐘來(lái) resolve symbols 并且完成鏈接。
所以呢,雖然 monolithic repo 有很多好處,它也有一個(gè)缺點(diǎn),不像開(kāi)源世界里,大家人力的把代碼分解成“項(xiàng)目”。
每個(gè)項(xiàng)目通常是一個(gè) GitHub repo,其中可以有很多模塊,但是每個(gè)項(xiàng)目里所有模塊 build 成一個(gè) *.so 作為一個(gè) cache 的單元。
因?yàn)橐粋€(gè)應(yīng)用程序依賴的項(xiàng)目數(shù)量總不會(huì)太多,從而控制了 layer 的總數(shù)。
好在這個(gè)問(wèn)題并非無(wú)解。既然一個(gè)應(yīng)用程序?qū)Ω鱾€(gè)模塊的依賴關(guān)系是一個(gè) DAG,那么我們總可以想辦法做一個(gè) graph partitioning,把這個(gè) DAG 分解成不那么多的幾個(gè)子圖。
仍然以 C/C++ 程序?yàn)槔覀兛梢园衙總€(gè)子圖里的每個(gè)模塊編譯成一個(gè) .a,而每個(gè)子圖里的所有 .a 鏈接成一個(gè) *.so,作為一個(gè) cache 的單元。
于是,如何設(shè)計(jì)這個(gè) graph partitioning 算法就成了眼前最重要的問(wèn)題了。
相關(guān)鏈接:
https://engineering.fb.com/2019/06/06/data-center-engineering/twine/
https://zhuanlan.zhihu.com/p/55452964
https://bazel.build/
https://buck.build/
https://github.com/facebookincubator/xar
https://tldp.org/HOWTO/SquashFS-HOWTO/creatingandusing.html
https://docs.docker.com/storage/storagedriver/select-storage-driver/
https://github.com/google/subpar
https://github.com/facebookincubator/xar
到此這篇關(guān)于Google和Facebook不使用Docker的原理解析的文章就介紹到這了,更多相關(guān)Google和Facebook不使用Docker內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!