論文閱讀Slacker: Fast Distribution with Lazy Docker Containers

摘要

作者做了一個新的benchmark工具叫hello bench對各個容器進行分析,發現需要用76%的時間來pull鏡像,並且只讀取了6.4%的數據,於是作者設計了slacker,一個對快速啓動容器的最優化存儲引擎。用了一個集中的存儲區域可以分享給所有的docker workers和registries。worker提供容器存儲用來通過延遲獲取容器數據達到後臺clone並且最小化啓動的目的。Slacker將容器開發週期(?開發週期不清楚是什麼意思)中值提高了20倍,部署週期提高了5倍。

1. 介紹

隔離在雲計算和多租戶平臺很重要,沒有隔離的話,用戶(在系統中付費的用戶)會忍受不可預測的性能、碰撞和侵犯隱私。
虛擬機管理器已經習慣給程序提供隔離,每個程序部署在自己的虛擬設備上去,用設備自己的環境和資源。但是虛擬機管理器需要干預並且需要提供一些特權操作。並且用roundabout技術(?)去推斷資源使用情況。結果就是虛擬機管理器很龐大,並且啓動時間速度慢運行時開銷很龐大。
Docker最近作爲一個可供選擇的輕量級基於管理程序的虛擬化技術很受歡迎。在容器裏,一切進程相關的資源都通過操作系統來虛擬化,包括網絡端口和文件掛載系統。容器本質上是享受所有資源的虛擬化的進程,不僅僅只有CPU和內存。所以沒有本質的原因說明容器啓動比正常進程啓動要慢。
不幸的是實際上實驗顯示啓動容器的結果要慢很多,因爲文件系統的瓶頸。然而啓動網絡,計算和內存資源相對來說非常迅速並且簡單。一個容器程序運行需要初始化文件系統,程序二進制文件,完整的linux系統調度,和依賴的庫。在Docker或谷歌Borg集羣中部署容器通常需要大量的複製和安裝開銷。最近對bb0 Borg的研究表明:“[任務啓動延遲]是高度可變的,中位數通常在25秒左右。安裝包安裝大約佔總數的80%:一個已知的瓶頸是本地磁盤的爭用,在本地磁盤上編寫安裝包。”
如果可以提高啓動時間,就會有很多機會:應用程序可以立即擴展以處理flash-crowd事件(?),集羣調度程序可以快速地以低成本重新平衡節點,開發人員可以交互式地構建和測試分佈式應用程序。
我們使用兩個方法解決容器啓動的問題,首先,我們開發了一個新的開源的Dockerbenchmark,HelloBench,可以更加清楚的觀察容器啓動。HelloBench基於57種不同的容器工作負載,測量從部署開始到容器準備開始執行有用工作的時間。我們使用HelloBench和靜態分析來描述Docker圖像和I/O模式。根據我們的分析顯示,複製包數據佔容器啓動時間的76%,而容器開始有用的工作實際上只需要複製6.4%的數據,對於單個鏡像簡單的block-deduplication壓縮比gzip壓縮獲得更好的壓縮率。
然後我們根據我們的發現建立了Slacker,一種新的Docker存儲驅動程序,通過使用專門的存儲系統支持多個鏡像層來實現快速的容器分配。具體來說,Slacker使用我們的後端存儲服務器(Tintri VMstore[6])的快照和克隆功能來顯著降低常見Docker操作的成本。Slacker沒有預先傳輸整個容器鏡像,而是根據需要延遲地提取鏡像數據,從而大大減少了網絡I/O。Slacker還利用我們對Linux內核所做的修改來改進緩存共享。
使用這些技術的結果是對常見Docker操作的性能有了巨大的改進;鏡像推送變成153×更快,而拉出變成72×更快。涉及這些操作的對於常見Docker用例非常有用。例如,Slacker實現了容器部署週期的5倍中值加速和開發週期(?)的20倍加速。
我們還創建了MultiMake,一個新的基於容器的構建工具,展示了Slacker的快速啓動的好處。MultiMake使用不同的GCC版本從一個源碼中產生16個不同的二進制文件,使用slacker ,MultiMake過程速度提高了10倍。
剩下的內容由以下幾部分組成。首先,我們介紹了現存的Docker框架。然後我們介紹HelloBench,一個我們用來分析Docker加載特徵,我們從這個工具裏的發現來指導我們設計Slack。最後,我們模擬了Slacker,呈現了MultiMake,討論相關工作和總結。

2. Docker背景

現在我們介紹docker框架,存儲接口,和默認的存儲驅動。

2.1 容器版本控制

雖然Linux一直使用虛擬化來隔離內存,但是cgroups [37] (Linux的容器實現)通過爲文件系統掛載點、IPC隊列、網絡、主機名、進程id和用戶id[19]提供六個新的名稱空間來虛擬化更廣泛的資源。Linux cgroups是在2007年發佈的,但是容器的廣泛使用是最近纔出現的現象,與Docker(2013年發佈)等新的管理工具同時出現。使用Docker,像“Docker run -it ubuntu bash”這樣的單個命令將從Internet上拉出ubuntu包,用一個新的ubuntu安裝初始化一個文件系統,執行必要的cgroup設置,並在環境中返回一個交互式bash會話。這個示例命令有幾個部分。首先,“ubuntu”是一個鏡像的名字。鏡像是系統數據的只讀副本,通常包含應用程序所需的應用程序二進制文件、Linux發行版和其他包。將應用程序捆綁到Docker鏡像中非常方便,因爲分發服務器可以選擇一組特定的包(及其版本),這些包將在運行應用程序的任何地方使用。其次,“run”是對圖像執行的操作;run操作根據要用於新容器的映像創建初始化的根文件系統。其他操作包括“push”(用於發佈新圖像)和“pull”(用於從中心位置獲取已發佈的圖像);如果用戶試圖運行非本地鏡像,則會自動提取鏡像。第三,“bash”是要在容器內啓動的程序;用戶可以在給定的映像中指定任何可執行文件。Docker管理鏡像數據的方式與傳統版本控制系統管理代碼的方式非常相似。這個模型適用於兩個原因。首先,同一個鏡像可能有不同的分支(例如,“ubuntu:latest”或“ubuntu:12.04”)。第二,鏡像自然地建立在彼此之上。例如,Rails上的ruby映像構建在Rails映像之上,而Rails映像又構建在Debian映像之上。這些圖像代表在舊鏡像之上的一個新的提交。可能有其他未標記爲可運行鏡像的提交。當運行一個鏡像,可能從一個已經提交的鏡像開始,但是文件系統被修改了。在版本控制術語裏。這些修改被稱爲未提交的修改。Docker的“commit”命令將容器和對該容器的修改轉變爲一個新的只讀的鏡像。在Docker中,一個層要麼引用提交的數據,要麼引用容器的未分層更改(unstaged changes?)。
Docker工作的機器運行一個當地的守護進程。通過向某個特定的worker的本地守護進程發送命令,可以在該worker上創建新的容器和映像。映像共享是通過集中式的倉庫完成的,這些倉庫通常運行在與Docker worker位於同一集羣中的機器上。鏡像可以通過從守護進程向倉庫推送發佈,而鏡像可以通過在集羣中執行多個守護進程來部署。僅傳輸接收端沒有的層。層表示爲網絡上和倉庫機器上的gzip壓縮tar文件。守護程序計算機上的表示由可插拔存儲驅動程序決定。

2.2 存儲驅動程序接口

Table1:Docker Driver API
圖1
Docker容器以兩種方式訪問存儲,首先,用戶可以在容器內的主機上掛載目錄。例如,運行容器化編譯器的用戶可以將其宿主目錄裝入容器中,這樣編譯器就可以讀取代碼並在主機目錄中生成二進制文件。其次,容器需要訪問用於表示應用程序二進制文件和庫的Docker層。Docker通過容器用作根文件系統的掛載點顯示此應用程序數據的視圖。容器存儲和安裝由Docker存儲驅動程序管理;不同的驅動程序可以選擇以不同的方式表示層數據。驅動程序必須實現的方法如表1所示(沒有顯示一些無趣的函數和參數)。所有函數都使用一個字符串“id”參數來標識被操作的層。Get函數請求驅動程序掛載該層並返回到掛載點的路徑。返回的掛載點不僅應該包含“id”層的視圖,還應該包含它的所有祖先的視圖(例如,在掛載點的目錄遍歷期間應該看到“id”層的父層中的文件)。Create通過複製父層來創建一個新的鏡像層,如果父層是null,那麼新的層應該是空的。Docker使用Create命令來爲新的容器提供新的鏡像層,並且分配層來存儲拉取的數據。
Diff和ApplyDiff分別用於Docker push和pull操作,如圖1所示。當Docker pushing一個鏡像層,Diff將這個鏡像層從一個當地的表現形式轉變爲一個包含這個鏡像層的壓縮的tar文件。ApplyDiff恰恰相反,給定一個tar文件和一個本地層,它在現有層上解壓tar文件。
圖2顯示了第一次運行四層映像(例如ubuntu)時的驅動程序調用。在鏡像pull的的過程中創建了4層鏡像。鏡像本身多創建了兩層鏡像,A-D層呈現了鏡像。A的create在一個NULL的父層操作,所有A初始化的時候是空的。但是,隨後調用ApplyDiff操作告訴驅動程序從提取的tar文件添加到A中。B-D層各自都由兩步構成:從父文件拷貝副本(通過Create)和從tar文件添加(通過ApplyDiff)。在第8步之後,拉操作完成,Docker準備創建一個容器。它首先創建一個只讀層E-init,並向其添加一些小型初始化文件,然後創建容器將用作其根的文件系統E。

2.3 AUFS驅動程序實現

圖2
對於Docker發行版,AUFS存儲驅動程序是一個常見的默認設置。這個驅動程序基於AUFS文件系統(另一個聯合文件系統)。聯合文件系統不直接在磁盤上存儲數據,而是使用另一個文件系統(例如ext4)作爲底層存儲。
聯合掛載點提供基礎文件系統中多個目錄(multiple directories?)的視圖。使用底層文件系統中的目錄路徑列表掛載AUFS。在路徑解析期間,AUFS遍歷目錄列表;選擇包含要解析的路徑的第一個目錄,並使用該目錄中的inode。AUFS支持特殊的whiteout文件,使某些較低層的文件被刪除;這種技術類似於其他分層系統(例如LSM數據庫[29])中的刪除標記。AUFS還在文件粒度上支持COW(即寫時複製);寫時,在允許繼續寫之前,將底層的文件複製到頂層。
AUFS驅動程序利用了AUFS文件系統的分層和寫時複製功能,同時還可以直接訪問底層AUFS文件系統。AUFS驅動對自己存儲的每個鏡像層在底層AUFS文件系統上創建了一個新的文件夾。一個ApplyDiff操作的簡單的將歸檔的文件解壓到鏡像層的文件夾。當一個Get調用,AUFS驅動程序創建一個統一的鏡像視圖和它的歷史。當Create操作調用時AUFS驅動使用它的寫時複製的功能很高效的複製鏡像層的數據。不幸的是,我們發現寫時複製的功能在一個文件的力度有一些性能上的問題。

3 HelloBench

我們實現了HelloBench,一個新的benchmark被設計於測試容器的啓動。HelloBench直接執行Docker命令,所以pushes,pull和run等操作可以被獨立測量。這個benchmark有兩部分組成:(1)一個容器鏡像的收集器和(2)用於在上述容器中執行簡單任務的測試工具。這些鏡像是截止至2015年6月1日Docker hub 庫中獲取的。HelloBench由當時72個鏡像中的57個組成。我們選擇的鏡像是使用最小配置運行並且不依賴於其他容器。比如說,不包括WokrdPress因爲WordPress容器依賴於分開的MySQL容器。
Table2
Table2列出了HelloBench使用的鏡像。我們將鏡像分成6個面板上的類別,有些分類有些主觀;比如說,Django鏡像包括一個web服務器,但是大多數認爲他是一個Web框架。HelloBench利用在容器中運行最簡單的任務或等待容器報告就緒來度量啓動時間。作爲語言類容器,任務就是的編譯或者解釋一個簡單的“hello world”語言程序。Linux發行版的鏡像就是運行一個非常簡單的shell命令,通常是“echo hello”。對於長時間運行的服務器(尤其是數據庫和web服務器),HelloBench測量到容器輸出“up and ready”的信息的時間。對於一些特別的服務器,將輪詢公開端口,直到有響應爲止。
每個HelloBench的鏡像由好幾個鏡像層組成,一些還在彼此的容器中共享。圖3顯示了這個鏡像層之間的關係。通過57個鏡像,由550個節點和19個根節點。在一些例子中,一個鏡像版本作爲一個其他的鏡像版本的基礎(比如說“ruby”是“rails”的基礎)。只有一個鏡像由單個鏡像層組成:”alpine“, 一個特別輕量級的Linux發行版鏡像(比如Debian的老版本),這就是爲什麼多個鏡像鏡像共享一個公共的基礎而不是一個solid black circle(?)。
爲了評估HelloBench的代表性,對於常用的鏡像,我們在2015年1月15日統計了每個Docker Hub library鏡像被拉取的次數(在原始HelloBench鏡像被拉取的7個月後)。在此期間,鏡像庫的鏡像從72個增加到94個。圖4顯示了94張鏡像,按HelloBench分類。HelloBench是代表了流行的鏡像,佔所有拉取鏡像的86%。大多數受歡迎的是Linux發行庫(例如BusyBox和Ubuntu)。數據庫(如Redis和MySQL)和web服務器(如nginx)也很受歡迎。
圖3
圖4

4 工作負載分析(Workload Analysis)

在本節中,我們將分析HelloBench工作負載的行爲和性能,提出四個問題:容器映像有多大,執行需要多少數據(§4.1)?推、拉和運行圖像需要多長時間(§4.2)?圖像數據是如何跨層分佈的,其性能影響是什麼(§4.3)?以及不同運行之間的訪問模式有多相似(§4.4)。
所有性能測量都是從運行在具有2ghz Xeon cpu (E5-2620)的PowerEdge R720主機上的虛擬機中獲得的。VM提供8 GB RAM、4個CPU內核和一個由Tintri T620[1]支持的虛擬磁盤。服務器和VMstore沒有其他負載。

4.1 容器數據

我們從研究Docker Hub中提取的HelloBench鏡像開始分析。對於每個圖像,我們採取三個度量:壓縮大小、未壓縮大小和執行HelloBench時從圖像中讀取的字節數。我們通過在blktrace (?)跟蹤的塊設備上運行工作負載來測量讀取。圖5顯示了這三個數字的CDF。我們觀察到讀取數據的中值爲20MB,但鏡像壓縮後的大小的中值爲117mb,未壓縮後爲329mb。
圖5
在這裏插入圖片描述
我們將讀取的數據和大小按類別分類,在圖6所示。相對浪費最大的是發行版工作負載(分別爲30×和85×用於壓縮和未壓縮),但是絕對浪費也最小。對於語言和web框架類別來說,絕對的浪費是最大的。在所有圖像中,平均只有27mb被讀取;平均未壓縮鏡像爲15×倍,說明容器啓動只需要6.4%的鏡像數據。
雖然Docker鏡像壓縮爲gzip歸檔文件時要小得多,但是這種格式不適合運行的容器修改數據。因此,容器通常存儲未壓縮的數據,這意味着壓縮減少了網絡I/O,而不是磁盤I/O。重複數據刪除是適用於更新的數據壓縮的簡單替代方法。爲了計算重複數據刪除的有效性,我們掃描HelloBench圖像來尋找文件塊之間的冗餘。圖7比較了文件和塊(4 KB)粒度上的gzip壓縮率和重複數據刪除率。條形圖表示單個鏡像的速率。gzip的速率在2.3到2.7之間,而重複數據刪除在每幅圖像的基礎上做得很差。然而,跨所有鏡像的重複數據刪除率分別爲2.6(文件粒度)和2.8(塊粒度)。
圖7
結論:執行過程中讀取的數據量遠遠小於總圖像大小,不管是壓縮或未壓縮。鏡像數據通過網絡被壓縮發送,然後到本地解壓存儲。因此,網絡和磁盤的開銷都很高。減少開銷的一種方法是用較少的安裝包構建更精簡的映像。另一種選擇是,當容器需要時,可以延遲下載鏡像數據。我們還發現,與gzip壓縮相比,基於全局塊的重複數據刪除是一種有效的鏡像數據表示方法。

4.2 操作性能

圖8
圖9
一旦構建好,容器化的應用程序通常按以下方式部署:開發人員將應用程序映像一次推入中央倉庫,許多用戶拉出映像並且運行應用程序。我們使用HelloBench測量這些操作的延遲,在圖8中報告CDFs。推、拉和運行的平均時間分別爲61秒、16秒和0.97秒。 圖9按工作負載類別劃分了操作時間。這一模式大體上是成立的:運行快,推拉鏡像慢。運行速度最快的是發行版和語言類別(分別爲0.36和1.9秒)。推、拉和運行的平均時間分別爲72秒、20秒和6.1秒。因此,在遠程倉庫上啓動一個新映像時,76%的啓動時間將花在pull上。
因爲推和拉是最慢的,所以我們想知道這些操作是否只是高延遲,或者它們是否在某種程度上也很需要時間,從而限制了吞吐量,即使多個操作同時運行。爲了研究可伸縮性,我們同時推拉不同大小的不同數量的人造的鏡像。每個鏡像包含一個隨機生成的文件。我們使用人造的鏡像而不是HelloBench鏡像來創建不同大小的相同大小的圖像。圖10顯示了總時間大致與鏡像數量和鏡像大小成線性關係。因此,推和拉不僅是高延遲,而且消耗網絡和磁盤資源,限制了可伸縮性。
圖10
**結論:**容器啓動時間以pull爲主,新部署中76%的時間將花在pull上。對於迭代開發應用程序的程序員來說,使用push發佈圖像將非常緩慢,儘管這種情況可能沒有已發佈圖像的多部署常見。大部分推操作由存儲驅動程序的Diff函數完成,而大部分拉操作由ApplyDiff函數完成(§2.2)。優化這些驅動程序功能將提高分發性能。

4.3 鏡像層

圖11
鏡像數據通常跨多個層進行分割。AUFS驅動程序在運行時合成圖像的各個層,以提供文件系統的完整視圖。在本節中,我們將研究分層的性能和數據跨層分佈。我們首先看看分層文件系統容易出現的兩個性能問題(圖11):查找深層文件和對非頂層文件進行小的寫操作。首先,我們創建(並使用AUFS組合)16個層,每個層包含1K個空文件。然後,使用冷緩存(cold cache?),我們從每層隨機打開10個文件,測量打開延遲。圖11a顯示了結果(平均運行超過100次):層深度和延遲之間有很強的相關性。其次,我們創建兩個層,其底部包含大小不同的大型文件。我們測量將一個字節附加到存儲在底層的文件的延遲。如圖11b所示,小寫入的延遲對應於文件大小(而不是寫大小),正如AUFS在文件粒度上所做的COW操作。在一個文件被修改之前,它被複制到最上層,所以寫一個字節可能需要20秒。幸運的是,對較低層的小寫操作會導致每個容器的一次性開銷;後續的寫操作將更快,因爲大文件將被複制到頂層。
圖12
圖13
在考慮了層深度與性能之間的關係之後,我們現在要問的是,HelloBench圖像通常存儲的數據有多深?圖12顯示了每個深度級別的總數據百分比(以文件數量、目錄數量和字節爲單位的大小表示)。這三個指標大致對應。一些數據深達28層,但質量更集中在左邊。超過一半的字節深度至少爲9層。現在我們考慮數據如何跨層分佈的差異,爲每個圖像測量存儲在最上層、最下層和最大層中的部分(以字節爲單位)。圖13顯示了分佈情況:對於79%的圖像,最上層包含0%的圖像數據。相反,在中位數情況下,27%的數據位於最底層。大多數數據通常駐留在一個單層中。
**含義:**對於分層文件系統,存儲在更深層的數據訪問速度較慢。不幸的是,Docker鏡像往往比較深,至少一半的數據深度在第九層或更大。扁平層是避免這些性能問題的一種技術;然而,扁平化可能需要額外的複製,從而使分層文件系統提供的其他COW優勢失效。

4.4 緩存

圖14
我們現在考慮的情況是,同一個worker運行相同的鏡像不止一次。特別是,我們想知道是否可以使用第一次執行的I/O預填充緩存,以避免在後續運行時執行I/O。爲此,我們連續兩次運行每個HelloBench工作負載,每次收集塊跟蹤。我們計算第二次運行期間讀取的部分,這些部分可能受益於第一次運行期間由讀取填充的緩存狀態。
圖14顯示了第二次運行的讀和寫。讀取分爲命中和未命中,對於給定的塊,只計算第一次讀取(我們希望研究工作負載本身,而不是收集跟蹤的特定緩存的特徵)。在所有工作負載中,讀/寫比率是88/12。對於發行版、數據庫和語言工作負載,工作負載幾乎完全由讀取組成。在讀取操作中,99%的操作可能由以前運行時緩存的數據來完成。
**結論:**相同的數據經常在同一映像的不同運行期間讀取,這表明當同一映像在同一臺機器上多次執行時,緩存共享將非常有用。在具有許多容器化應用程序的大型集羣中,除非容器放置的機器受到很強的限制,否則不太可能重複執行。此外,其他目標(例如負載平衡和故障隔離)可能會使託管變得不常見。然而,對於容器化的實用程序(如python或gc1c)和在小集羣中運行的應用程序,重複執行可能很常見。我們的結果表明,後一種場景將受益於緩存共享。

5 Slacker

圖15
在本節中,我們將介紹一種新的Docker存儲驅動程序Slacker。我們的設計基於對容器工作負載的分析和五個目標:(1)非常快速地進行推拉操作;(2)對長時間運行的容器不造成任何損害;(3)儘可能重用現有的存儲系統;(4)利用現代存儲服務器提供的強大原語,並且(5)除了在存儲驅動程序插件(§2.2)中,不對Docker註冊表或守護進程進行任何更改。
圖15說明了運行Slacker的Docker集羣的體系結構。該設計基於集中式NFS存儲,在所有Docker守護進程和註冊中心之間共享。容器中的大多數數據都不需要執行容器,因此Docker worker只根據需要從共享存儲延遲地獲取數據。對於NFS存儲,我們使用Tintri VMstore服務器[6]。Docker鏡像由VMstore的只讀快照表示。鏡像倉庫不再用作層數據的主機,而是僅用作將圖像元數據與相應快照關聯起來的名稱服務器。推拉不再涉及大型網絡傳輸;相反,這些操作只是共享快照id。Slacker使用VMstore快照將容器轉換爲可共享的映像,並根據從註冊中心提取的快照ID克隆到提供容器存儲。在內部,VMstore使用塊級COW來高效地實現快照和克隆。
Slacker的設計基於我們對容器工作負載的分析;特別是,以下四個設計小節(§5.1至§5.4)對應於前面四個分析小節(§4.1至§4.4)。最後,我們討論了Docker框架本身可能的修改,以便更好地支持非傳統的存儲驅動程序,如Slacker(§5.5)。

5.1 存儲層

我們的分析顯示,在容器開始有用的工作之前,通過拉傳輸的數據中實際上只需要6.4%(§4.1)。爲了避免在未使用的數據上浪費I/O, Slacker將所有容器數據存儲在NFS服務器(Tintri VMstore)上,由所有worker共享。圖16a說明了這種設計:每個容器的存儲都表示爲一個NFS文件。Linux loopbacks(§5.4)用於將每個NFS文件視爲一個虛擬塊設備,可以將其作爲運行容器的根文件系統進行掛載和卸載。Slacker將每個NFS文件格式化爲ext4文件系統。
圖16b比較了Slacker堆棧和AUFS堆棧。儘管兩者都使用ext4(或其他一些本地文件系統)作爲關鍵層,但有三個重要的不同之處。首先,ext4由Slacker中的一個網絡磁盤支持,但是由一個帶有AUFS的本地磁盤支持。因此,Slacker可以通過網絡延遲加載數據,而AUFS必須在容器啓動之前將所有數據複製到本地磁盤。
其次,AUFS在文件級別上執行的COW高於ext4,因此很容易受到分層文件系統所面臨的性能問題的影響(§4.3)。相反,Slacker層在文件級得到了有效的擴展。然而,Slacker仍然受益於COW,它利用了VMstore中實現的塊級COW(§5.2)。此外,VMstore還可以在不同的Docker worker上運行包含程序,從而進一步節省空間。
第三,AUFS使用單個ext4實例的不同目錄作爲容器的存儲,而Slacker使用不同的ext4實例來備份每個容器。這種差異帶來了一個有趣的權衡,因爲每個ext4實例都有自己的日誌。使用AUFS,所有容器將共享相同的日誌,從而提供更高的效率。然而,衆所周知,日誌共享會導致版本內的優先級降低,從而破壞QoS保證[48],這是多租戶平臺(如Docker)的一個重要特性。當NFS存儲被劃分爲許多小的、非完整的ext4實例時,內部碎片[10,Ch. 17]是另一個潛在的問題。幸運的是,VMstore文件是稀疏的,所以Slacker不會遇到這個問題。

5.2 VMstore集成

早些時候,我們發現與運行相比,Docker的推拉速度相當慢(§4.2)。運行速度很快,因爲新容器的存儲是使用AUFS提供的COW功能從圖像初始化的。相比之下,推和拉在傳統驅動程序中速度較慢,因爲它們需要在不同的機器之間複製大的層,所以AUFS的COW功能是不可用的。與其他Docker驅動程序不同,Slacker構建在共享存儲之上,因此在概念上可以在守護進程和鏡像倉庫之間進行COW共享。
圖17
幸運的是,VMstore使用一個輔助的基於rest的API擴展了它的基本NFS接口,其中包括兩個相關的COW函數,snapshot和clone。snapshot函數調用會創建NFS文件的只讀快照,clone函數從快照創建NFS文件。快照不會出現在NFS名稱空間中,但是有惟一的id。文件級快照和克隆是功能強大的原語,用於構建更高效的日誌記錄、重複數據刪除和其他常見存儲操作[46]。在Slacker中,我們分別使用snapshot和clone來實現Diff和ApplyDiff。這些驅動程序函數由Docker push和pull操作(§2.2)分別調用。
圖17a顯示了一個運行Slacker的守護進程如何在推送時與VMstore和Docker倉庫進行交互。Slacker要求VMstore創建表示該層的NFS文件的快照。VMstore獲取快照,並返回快照ID(大約50個字節),在本例中爲“212”。Slacker將ID打包到壓縮的tar文件中。Slacker將ID打包進tar文件中,以實現向後兼容性:當就收到一個tar文件後不需要修改鏡像倉庫。如圖17b所示,A pull實際上是相反的操作。Slacker從註冊表接收快照ID,它可以從該ID克隆NFS文件用於容器存儲。Slacker的實現速度很快,因爲(a)layer數據從不壓縮或未壓縮,(b)layer數據從不離開VM存儲,所以只有元數據通過網絡發送。
考慮到Slacker的實現,“Diff”和“ApplyDiff”這兩個操作有些不恰當。特別是,Diff(A, B)應該返回一個delta,另一個守護進程(已經有A)可以從這個delta重構B。因此,Diff(A, B)沒有返回delta,而是返回一個引用,另一個工作者可以從這個引用獲得B的克隆,不管有沒有A。
Slacker與運行非Slacker驅動程序的其他守護進程部分兼容。Slacker提取tar時,它會在處理前查看流tar的第一個字節。如果tar包含層文件(而不是嵌入快照),Slacker將返回到簡單的解壓縮而不是克隆。因此,Slacker可以抓取其他驅動推送的鏡像,儘管速度很慢。但是,其他驅動程序將無法獲取Slacker圖像,因爲它們不知道如何處理打包到到tar文件中的快照ID。

5.3 優化Snapshot和Clone操作

圖像通常由許多層組成,HelloBench中超過一半的數據深度至少爲9(§4.3)。對於這類數據,塊級COW相對於文件級COW具有固有的性能優勢,例如遍歷塊映射索引(可能是衰減的)比遍歷底層文件系統的目錄更簡單。
然而,深度分層的圖像仍然對Slacker構成了挑戰。正如前面所討論的(§5.2),Slacker層是填充的,所以安裝任何一層都可以提供一個容器可以使用的文件系統的完整視圖。不幸的是,Docker框架沒有增加層的概念。當Docker獲取一個圖像時,它獲取所有的層,並通過ApplyDiff將每個層傳遞給驅動程序。對於Slacker來說,最上層就足夠了。對於28層的圖像(如jetty),額外的克隆代價很大。
我們的目標之一是在現有的Docker框架內工作,因此我們沒有修改框架以消除不必要的驅動程序調用,而是使用延遲克隆(lazy cloning)對它們進行優化。我們發現,一次pull的主要成本不是快照tar文件的網絡傳輸,而是VMstore克隆。因此,Slacker(如果可能的話)沒有將每個層表示爲NFS文件,而是用一個記錄快照ID的本地元數據表示它們。ApplyDiff只是設置這個元數據,而不是立即克隆。如果Docker調用到達該層,Slacker將在掛載之前執行真正的克隆。我們還使用快照id元數據進行快照緩存。特別地,Slacker實現了Create,它創建了一個層的邏輯副本(§2.2),其中快照緊隨其後的是一個克隆(§5.2)。如果許多容器是由相同的映像創建的,Create將在同一層上被多次調用。Slacker不爲每個創建創建快照,而是隻在第一次創建快照,然後重用快照ID。如果掛載某個層,則該層的快照緩存將失效(掛載後,該層可能發生更改,使快照過期)。
快照緩存和延遲克隆的結合可以使創建非常高效。特別是,從A層複製到B層可能只涉及從A的快照緩存條目複製到B的快照緩存條目,而不需要對VMstore進行特殊調用。在背景部分(§2.2)的圖2中,我們展示了10個Create和ApplyDiff調用,它們用於拉出和運行一個簡單的四層鏡像。如果沒有延遲緩存和快照緩存,Slacker將需要執行6個快照(每個創建一個)和10個克隆(每個創建或ApplyDiff一個)。使用我們的優化,Slacker只需要做一個快照和兩個克隆。在步驟9中,Create執行一個延遲克隆,但是Docker在E-init層調用Get操作,因此必須執行一個真正的克隆。對於步驟10,Create必須同時執行快照和克隆,以生成和掛載層E作爲新容器的根。

5.4 Linux內核修改

我們的分析表明,從同一鏡像開始的多個容器傾向於讀取相同的數據,這表明緩存共享可能是有用的(§4.4)。AUFS驅動程序的一個優點是COW是在底層文件系統之上完成的。這意味着不同的容器可以在底層系統中加載並利用相同的緩存狀態。lacker在VMstore中做COW,低於本地系統的級別。這意味着兩個NFS文件可能是相同快照的克隆(經過一些修改),但是不會共享緩存狀態,因爲NFS協議不是圍繞COW共享概念構建的。緩存重複數據刪除可以幫助節省緩存空間,但這不會阻止初始I/O。在從VMstore通過網絡傳輸兩個塊之前,重複數據刪除不可能實現兩個塊是相同的。在本節中,我們將描述在NFS文件級別的Linux頁面緩存中實現共享的技術。
爲了實現NFS文件之間的客戶端緩存共享,我們修改了NFS客戶端(比如loopback模塊),以添加對VMstore快照和克隆的操作。特別是,我們使用位圖來跟蹤類似NFS文件之間的差異。所有對NFS文件的寫入都是通過環回模塊完成的,因此環回模塊可以自動更新位圖以記錄新的更改。快照和克隆是由Slacker驅動程序發起的,因此我們擴展了回循環API,以便Slacker可以通知模塊文件之間的COW關係。特別是,我們使用位圖來跟蹤類似的NFS文件之間的差異。所有對NFS文件的寫入都是通過loopback模塊完成的,因此loopback模塊可以自動更新位圖以記錄新的更改。快照和克隆是由Slacker驅動程序發起的,因此我們擴展了loopback API,以便Slacker可以通知模塊文件之間的COW關係。
圖18用一個簡單的例子說明了這種技術:兩個容器,B和C,從同一個圖像開始,A.當啓動容器時,Docker首先從基礎鏡像(A)創建兩個init層(B-init和C-init)。注意,在init層中,“m”被修改爲“x”和“y”,而第0位被修改爲“1”來標記更改。Docker從B-init和C-init創建最頂層的容器層B和C。Slacker使用新的loopback API將B-init和C-init位圖分別複製到B和C。如圖所示,隨着容器的運行和寫入數據,B和C位圖積累了更多的變化。作爲API的一部分,Docker並沒有顯式地將init層與其他層區分開來,但是Slacker可以推斷出層類型,因爲Docker恰好爲init層的名稱使用了“-init”後綴。
現在假設容器B讀取block 3.loopback模塊在3的位置看到一個未修改的“0”位,表示文件B和文件A中的block 3是相同的。因此,loopback模塊將read發送給A而不是B,從而填充A的緩存狀態。現在假設C讀取block 3。C的第3塊也是未修改的,因此讀取再次被重定向到A。當然,對於B和C與A不同的塊,重要的是讀取沒有重定向。假設B讀取block 1,然後C讀取block 1。在這種情況下,B的讀取不會填充緩存,因爲B的數據與A不同。在這種情況下,C的讀取不會使用緩存,因爲C的數據與A不同。

5.5 Docker框架討論

我們的目標之一是不更改Docker鏡像倉庫或守護進程,除非在可插入存儲驅動程序中。儘管存儲驅動程序接口非常簡單,但它已經足夠滿足我們的需求。但是,對Docker框架進行了一些更改,可以啓用更優雅的Slacker實現。首先,如果鏡像倉庫可以表示不同的層格式(§5.2),這將有助於驅動程序之間的兼容性。目前,如果一個非Slacker層拉一個Slacker推的層,它將以一種不友好的方式失敗。格式跟蹤可以提供友好的錯誤消息,或者,理想情況下,爲自動格式轉換啓用掛鉤(hook)。其次,添加扁平層的概念將非常有用。特別是,如果驅動程序可以通知框架某個層是平的。Docker就不需要在拉動時獲取祖先層。這將消除我們對延遲克隆和快照緩存的需求(§5.3)。第三,如果框架顯式地標識了init層,那麼Slacker就不需要依賴層名作爲提示(§5.4),這將非常方便。

6 評價

我們使用與分析相同的硬件進行評估(§4)。爲了進行公平的比較,我們還對Slacker存儲使用了與運行AUFS實驗的VM的虛擬磁盤相同的VMstore。

6.1 HelloBench 鏡像加載

圖19
圖20
早些時候,我們看到在HelloBench中,推拉時間占主導地位,而運行時間非常短(圖9)。我們用Slacker重複這個實驗,在圖19中顯示了新的結果和AUFS結果。平均而言,推階段是153×更快,拉階段是72×更快,但是運行階段慢17% (AUFS拉階段爲運行階段預熱緩存)。
在不同的場景中使用了不同的Docker操作。一個用例是開發週期:在每次更改代碼之後,開發人員將應用程序推入鏡像倉庫,將其拉到多個工作節點,然後在節點上運行它。另一個是部署週期:一個不經常修改的應用程序由鏡像倉庫託管,但是偶爾的負載爆發或負載平衡需要在woker上拉出並運行。圖20顯示了這兩種情況下Slacker相對於AUFS的加速速度。對於中值鏡像加載,Slacker在部署和開發週期中分別將啓動提高5.3×和20×。速度提升是高度可變的:幾乎所有工作負載都至少有適度的改進,但是10%的工作負載至少在部署和開發方面分別提高了16×和64×。

6.2 長時間運行表現

圖21
在圖19中,我們看到使用Slacker推和拉的速度要快得多,而運行的速度要慢得多。這是預期的,因爲運行在任何數據傳輸之前就開始了,二進制數據只在需要時才延遲傳輸。我們現在運行幾個長時間運行的容器實驗;我們的目標是證明,一旦AUFS完成了對所有圖像數據的提取,而Slacker完成了對熱圖像數據的延遲加載,那麼AUFS和Slacker具有相同的性能。
爲了進行評估,我們選擇了兩個數據庫和兩個web服務器。對於所有的實驗,我們執行5分鐘,測量每秒的操作。每一個實驗都從一個pull開始。我們使用“loosely based on TPC-B”的pgbench來評估PostgreSQL數據庫。我們使用具有相同頻率的獲取、設置和更新鍵的自定義基準來評估內存數據庫Redis。我們評估Apache web服務器,使用wrk[4]基準反覆獲取靜態頁面。最後,我們評估io。類似node的基於javascript的web服務器。使用wrk基準反覆獲取動態頁面。
圖21A顯示了結果。AUFS和Slacker通常提供大致相同的性能,不過對於Apache來說,Slacker要快一些。雖然驅動程序在長期性能方面類似,但是圖21B顯示Slacker容器開始處理任務的速度比AUFS快3-19倍。

6.3 緩存

圖22
我們已經證明Slacker提供了相對於AUFS(當需要pull時)更快的啓動時間和等效的長期性能。Slacker處於劣勢的一種情況是,同一臺機器上多次運行相同的短集羣工作負載。對於AUFS,第一次運行將比較慢(因爲需要拉操作),但是後續的運行將比較快,因爲圖像數據將存儲在本地。此外,COW是在本地完成的,因此從同一個啓動映像運行的多個容器將受益於共享RAM緩存。
而Slacker則依賴Tintri VM-store在服務器端做COW。這種設計支持快速分發,但缺點是,如果不更改內核,NFS客戶端不會自然地意識到文件之間的冗餘。我們將修改後的 loopback 驅動程序(§5.4)與AUFS作爲共享緩存狀態的一種方法進行比較。爲此,我們將每個HelloBench工作負載運行兩次,測量第二次運行的延遲(在第一次運行預熱了緩存之後)。我們將AUFS與Slacker進行比較,其中包含和不包含內核修改。
圖22顯示了所有工作的運行時CDF—三個系統的負載(注意:這些數字是使用運行在ProLiant DL360p Gen8上的VM收集的)。雖然AUFS仍然是最快的(平均運行時間爲0.67秒),但是內核修改顯著加快了Slacker的速度。Slacker單獨運行的平均時間爲1.71秒;對loopback模塊進行內核修改後,時間是0.97秒。雖然Slacker避免了不必要的網絡I/O,但是AUFS驅動程序可以直接緩存ext4文件數據,而Slacker緩存ext4下的塊,這可能會帶來一些開銷。

6.4 可伸縮性

圖23
早些時候(§4.2),我們看到,就同時處理的圖像的大小和數量而言,推拉時AUFS的伸縮性很差。我們使用Slacker重複前面的實驗(圖10),再次創建合成圖像,並同時推拉不同數量的圖像。
圖23顯示了結果:圖像大小不再像對AUFS那樣重要。總時間仍然與同時處理的圖像數量相關,但絕對時間要好得多;即使有32張圖片,推和拉的時間最多也只有兩秒鐘。同樣值得注意的是,對於slaker來說,推的時間與拉的時間相似,而對於AUFS來說,推的時間則要長得多。這是因爲AUFS對其大型數據傳輸使用壓縮,而壓縮通常比解壓縮更昂貴。

7 案例研究:MultiMake

圖24
在創建Dropbox時,Drew Houston(聯合創始人兼首席執行官)發現,構建一個廣泛部署的客戶端需要進行大量“粗糙的操作系統工作”,以使代碼兼容各種平臺[18]的特性。例如,一些bug只會出現在瑞典版本的Windows XP Service Pack 3中,而其他非常類似的部署(包括挪威版本)則不會受到影響。避免這些錯誤的一種方法是在許多不同的環境中廣泛地測試軟件。有幾家公司提供了容器化集成測試服務[33,39],包括針對幾十個版本的Chrome、Firefox、Internet Explorer和其他瀏覽器[36]快速測試web應用程序。當然,這種測試的範圍受到不同測試環境供應速度的限制。
我們演示了快速容器準備在使用新工具(數百萬)進行測試時的有用性。在源目錄上運行multiake使用最後16個GCC重新租約構建目標二進制文件的16個不同版本。每個編譯器都由一個鏡像倉庫託管的Docker映像表示。比較二進制文件有很多用途。例如,某些安全檢查已知會被某些編譯器重新租約[44]優化掉。數百萬使開發人員能夠評估跨GCC版本的此類檢查的健壯性。
multimake的另一個用途是針對不同的GCC版本評估代碼片段的性能,這些版本使用不同的優化。例如,我們在一個簡單的C程序上使用了multiplake,做了20M的向量算術運算,如下:

for (int i=0; i<256; i++) { 
	a[i] = b[i] + c[i] * 3;
}

圖24a顯示了結果:最新的GCC發行版很好地優化了向量操作,但是4.6和4.7系列編譯器生成的代碼執行起來要花費大約50%的時間。GCC 4.8.0生成了快速的代碼,儘管它是在一些較慢的4.6和4.7版本之前發佈的,所以一些優化顯然沒有進行反向移植。圖24b顯示,Slacker(68秒)收集該數據的速度比AUFS驅動程序(646秒)快9.5倍,因爲大部分時間都是用AUFS拉動的。儘管所有GCC映像都有一個公共的Debian基礎(只能提取一次),但是GCC安裝代表了大部分數據,AUFS每次都提取這些數據。清理是AUFS比Slacker更昂貴的另一個操作。在AUFS中刪除一個層需要刪除數千個小的ext4文件,而在Slacker中刪除一個層則需要刪除一個大型NFS文件。
快速運行不同版本代碼的能力可能會使除數百萬之外的其他工具受益。例如,git bisect通過在提交[23]的範圍內進行二叉搜索來發現引入錯誤的提交。除了基於容器的自動構建系統[35],一個集成了Slacker的平分工具可以非常快速地搜索大量提交。

8 相關工作

優化磁盤映像的多部署與我們的工作類似,Slacker使用的ext4格式的NFS文件類似於虛擬磁盤映像。Hibler等人構建了Frisbee系統,該系統使用基於文件系統感知的技術(例如,Frisbee不考慮未被系統使用的塊)來優化差分圖像更新。Wartel等人比較了從中央存儲庫(很像Docker鏡像倉庫)延遲分發虛擬機映像的多種方法。Nicolae等人研究了映像部署,發現“預傳播是一個昂貴的步驟,特別是因爲初始VM只有一小部分是實際訪問的。他們進一步建立了一個分佈式的系統來託管支持VM數據延遲傳播的虛擬機映像。Zhe等人構建了一個基於雲的web應用程序平臺Twinkle,旨在處理“快速擁擠的事件處理”。“不幸的是,虛擬機往往是重量級的,正如他們所指出的:“虛擬設備的創建可能需要幾秒鐘。”
各種集羣管理工具提供了容器調度,包括Kubernetes[2]、谷歌的Borg[41]、Facebook的Tupperware[26]、Twitter的Aurora[21]和Apache Mesos[17]。Slacker是這些系統的補充;快速部署爲集羣管理器提供了更多的可用性,支持廉價的遷移和經過調優的負載平衡。
許多技術與我們共享緩存狀態和減少冗餘I/ O.VMware ESX server[43]和Linux KSM9掃描和重複刪除內存的策略相似。雖然這種技術節省了緩存空間,但它沒有預先釋放初始I/O。Xingbo等人[47]也發現了這樣一個問題,即讀取多個幾乎相同的文件會導致可避免的I/O。他們將btrfs修改爲根據磁盤位置索引緩存頁面,從而使用頁面緩存服務於btrfs發出的一些塊讀取。Sapuntzakis等人對VM映像使用髒位圖來標識遷移過程中必須傳輸的虛擬磁盤映像塊的子集。Lagar-Cavilla等人構建了一個“VM fork”函數,該函數可以快速創建正在運行的VM的許多克隆。一個克隆需要的數據被多播給所有克隆,作爲預取的一種方式。Slacker可能會從類似的預取中獲益。

9 結論

Fast startup擁有用於可伸縮web服務、集成測試和分佈式應用程序交互開發的應用程序。Slacker填補了兩者之間的空白。容器本身很輕,但是目前的管理系統,如Docker和Borg,在分發圖像方面非常慢。相比之下,虛擬機本身就是重量級的,但是對虛擬機映像的多部署已經進行了深入的研究和優化。Slacker爲容器提供了高效的部署,借鑑了VM映像管理的思想,比如延遲傳播,還引入了新的特定於docker的優化,比如延遲克隆。使用這些技術,Slacker將典型的部署週期提高了5倍,開發週期提高了20倍。HelloBench和我們在本文實驗中使用的圖片的快照[15]可以在網上找到:https://github.com/Tintri/hello-bench。

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