Borg、Omega、K8s:Google 十年三代容器管理系統(tǒng)的設(shè)計(jì)與思考
1 十年三代容器管理系統(tǒng)
業(yè)界這幾年對(duì)容器的興趣越來越大,但其實(shí)在 Google,我們十幾年前就已經(jīng)開始大規(guī)模容器實(shí)踐了, 這個(gè)過程中也先后設(shè)計(jì)了三套不同的容器管理系統(tǒng)。這三代系統(tǒng)雖然出于不同目的設(shè)計(jì),但每一代都受前一代的強(qiáng)烈影響。本文介紹我們開發(fā)和運(yùn)維這些系統(tǒng)所學(xué)習(xí)到的經(jīng)驗(yàn)與教訓(xùn)。
1.1 Borg
Google 第一代統(tǒng)一容器管理系統(tǒng),我們內(nèi)部稱為 Borg 7。
1.1.1 在線應(yīng)用和批處理任務(wù)混布
Borg 既可以管理 long-running service 也可以管理 batch job;在此之前,這兩種類型的任務(wù)是由兩個(gè)系統(tǒng)分別管理的,
- Babysitter
- Global Work Queue
Global Work Queue 主要面向 batch job,但它強(qiáng)烈影響了 Borg 的架構(gòu)設(shè)計(jì);
1.1.2 早于 Linux cgroups 的出現(xiàn)
需要說明的是,不論是我們?cè)O(shè)計(jì)和使用 Global Work Queue 還是后來的 Borg 時(shí), Linux cgroup 都還沒有出現(xiàn)。
1.1.3 好處:計(jì)算資源共享
Borg 實(shí)現(xiàn)了 long-running service 和 batch job 這兩種類型的任務(wù)共享計(jì)算資源, 提升了資源利用率,降低了成本。
在底層支撐這種共享的是Linux 內(nèi)核中新出現(xiàn)的容器技術(shù)(Google 給 Linux 容器技術(shù)貢獻(xiàn)了大量代碼),它能實(shí)現(xiàn)延遲敏感型應(yīng)用和 CPU 密集型批處理任務(wù)之間的更好隔離。
1.1.4 自發(fā)的 Borg 生態(tài)
隨著越來越多的應(yīng)用部署到 Borg 上,我們的應(yīng)用與基礎(chǔ)設(shè)施團(tuán)隊(duì)開發(fā)了大量圍繞 Borg 的管理工具和服務(wù),功能包括:
- 配置或更新 job;
- 資源需求量預(yù)測(cè);
- 動(dòng)態(tài)下發(fā)配置到線上 jobs;
- 服務(wù)發(fā)現(xiàn)和負(fù)載均衡;
- 自動(dòng)擴(kuò)縮容;
- Node 生命周期管理;
- Quota 管理
- …
也就是產(chǎn)生了一個(gè)圍繞 Borg 軟件生態(tài),但驅(qū)動(dòng)這一生態(tài)發(fā)展的是 Google 內(nèi)部的不同團(tuán)隊(duì), 因此從結(jié)果來看,這個(gè)生態(tài)是一堆異構(gòu)、自發(fā)的工具和系統(tǒng)(而非一個(gè)有設(shè)計(jì)的體系), 用戶必須通過幾種不同的配置語言和配置方式來和 Borg 交互。
雖然有這些問題,但由于其巨大的規(guī)模、出色的功能和極其的健壯性,Borg 當(dāng)前仍然是 Google 內(nèi)部主要的容器管理系統(tǒng)。
1.2 Omega
為了使 Borg 的生態(tài)系統(tǒng)更加符合軟件工程規(guī)范,我們又開發(fā)了 Omega6,
1.2.1 架構(gòu)更加整潔一致
Omega 繼承了許多已經(jīng)在 Borg 中經(jīng)過驗(yàn)證的成功設(shè)計(jì),但又是完全從頭開始開發(fā), 以便架構(gòu)更加整潔一致。
- Omega 將集群狀態(tài)存儲(chǔ)在一個(gè)基于 Paxos 的中心式面向事務(wù) store(數(shù)據(jù)存儲(chǔ))內(nèi);
- 控制平面組件(例如調(diào)度器)都可以直接訪問這個(gè) store;
- 用樂觀并發(fā)控制來處理偶發(fā)的訪問沖突。
1.2.2 相比 Borg 的改進(jìn)
這種解耦使得 Borgmaster 的功能拆分為了幾個(gè)彼此交互的組件, 而不再是一個(gè)單體的、中心式的 master,修改和迭代更加方便。Omega 的一些創(chuàng)新(包括多調(diào)度器)后來也反向引入到了 Borg。
1.3 Kubernetes
Google 開發(fā)的第三套容器管理系統(tǒng)叫 Kubernetes4。
1.3.1 為什么要設(shè)計(jì) K8s
開發(fā)這套系統(tǒng)的背景:
全球越來越多的開發(fā)者也開始對(duì) Linux 容器感興趣,Google 已經(jīng)把公有云基礎(chǔ)設(shè)施作為一門業(yè)務(wù)在賣,且在持續(xù)增長(zhǎng);因此與 Borg 和 Omega 不同的是:Kubernetes 是開源的,不是 Google 內(nèi)部系統(tǒng)。
1.3.2 相比 Omega 的改進(jìn)
- 與 Omega 類似,k8s 的核心也是一個(gè)共享持久數(shù)據(jù)倉(cāng)庫(kù)(store), 幾個(gè)組件會(huì)監(jiān)聽這個(gè) store 里的 object 變化;
- Omega 將自己的 store 直接暴露給了受信任的控制平面組件,但 k8s 中的狀態(tài) 只能通過一個(gè) domain-specific REST API 訪問,這個(gè) API 會(huì)執(zhí)行 higher-level versioning, validation, semantics, policy 等操作,支持多種不同類型的客戶端;
- 更重要的是,k8s 在設(shè)計(jì)時(shí)就非常注重應(yīng)用開發(fā)者的體驗(yàn):首要設(shè)計(jì)目標(biāo)就是在享受容器帶來的資源利用率提升的同時(shí),讓部署和管理復(fù)雜分布式系統(tǒng)更簡(jiǎn)單。
1.4 小結(jié)
接下來的內(nèi)容將介紹我們?cè)谠O(shè)計(jì)和使用以上三代容器管理系統(tǒng)時(shí)學(xué)到的經(jīng)驗(yàn)和教訓(xùn)。
2 底層 Linux 內(nèi)核容器技術(shù)
容器管理系統(tǒng)屬于上層管理和調(diào)度,在底層支撐整個(gè)系統(tǒng)的,是 Linux 內(nèi)核的容器技術(shù)。
2.1 發(fā)展歷史:從 chroot 到 cgroups
- 歷史上,最初的容器只是提供了 root file system 的隔離能力 (通過 chroot);
- 后來 FreeBSD jails 將這個(gè)理念擴(kuò)展到了對(duì)其他 namespaces(例如 PID)的隔離;Solaris 隨后又做了一些前沿和拓展性的工作;
- 最后,Linux control groups (cgroups) 吸收了這些理念,成為集大成者。內(nèi)核 cgroups 子系統(tǒng)今天仍然處于活躍開發(fā)中。
2.2 資源隔離
容器技術(shù)提供的資源隔離(resource isolation)能力,使 Google 的資源利用率遠(yuǎn)高于行業(yè)標(biāo)準(zhǔn)。例如,Borg 能利用容器實(shí)現(xiàn)延遲敏感型應(yīng)用和CPU 密集型批處理任務(wù)的混布(co-locate), 從而提升資源利用率,業(yè)務(wù)用戶為了應(yīng)對(duì)突發(fā)業(yè)務(wù)高峰和做好 failover, 通常申請(qǐng)的資源量要大于他們實(shí)際需要的資源量,這意味著大部分情況下都存在著資源浪費(fèi);通過混布就能把這些資源充分利用起來,給批處理任務(wù)使用。
容器提供的資源管理工具使以上需求成為可能,再加上強(qiáng)大的內(nèi)核資源隔離技術(shù), 就能避免這兩種類型任務(wù)的互相干擾。我們是開發(fā) Borg 的過程中,同步給 Linux 容器做這些技術(shù)增強(qiáng)的。
但這種隔離并未達(dá)到完美的程度:容器無法避免那些不受內(nèi)核管理的資源的干擾,例如三級(jí)緩存(L3 cache)、 內(nèi)存帶寬;此外,還需要對(duì)容器加一個(gè)安全層(例如虛擬機(jī))才能避免公有云上各種各樣的惡意攻擊。
2.3 容器鏡像
現(xiàn)代容器已經(jīng)不僅僅是一種隔離機(jī)制了:還包括鏡像 —— 將應(yīng)用運(yùn)行所需的所有文件打包成一個(gè)鏡像。
在 Google,我們用 MPM (Midas Package Manager) 來構(gòu)建和部署容器鏡像。隔離機(jī)制和 MPM packages 的關(guān)系,就像是 Docker daemon 和 Docker image registry 的關(guān)系。在本文接下來的內(nèi)容中,我們所說的“容器”將包括這兩方面, 即運(yùn)行時(shí)隔離和鏡像。
3 面向應(yīng)用的基礎(chǔ)設(shè)施
隨著時(shí)間推移,我們意識(shí)到容器化的好處不只局限于提升資源利用率。
3.1 從“面向機(jī)器”到“面向應(yīng)用”的轉(zhuǎn)變
容器化使數(shù)據(jù)中心的觀念從原來的面向機(jī)器(machine oriented) 轉(zhuǎn)向了面向應(yīng)用(application oriented),
容器封裝了應(yīng)用環(huán)境(application environment), 向應(yīng)用開發(fā)者和部署基礎(chǔ)設(shè)施屏蔽了大量的操作系統(tǒng)和機(jī)器細(xì)節(jié),每個(gè)設(shè)計(jì)良好的容器和容器鏡像都對(duì)應(yīng)的是單個(gè)應(yīng)用,因此 管理容器其實(shí)就是在管理應(yīng)用,而不再是管理機(jī)器。
Management API 的這種從面向機(jī)器到面向應(yīng)用的轉(zhuǎn)變,顯著提升了應(yīng)用的部署效率和問題排查能力。
3.2 應(yīng)用環(huán)境(application environment)
3.2.1 資源隔離 + 容器鏡像:解耦應(yīng)用和運(yùn)行環(huán)境
資源隔離能力與容器鏡像相結(jié)合,創(chuàng)造了一個(gè)全新的抽象:
內(nèi)核 cgroup、chroot、namespace 等基礎(chǔ)設(shè)施的最初目的是保護(hù)應(yīng)用免受 noisy、nosey、messy neighbors 的干擾。
而這些技術(shù)與容器鏡像相結(jié)合,創(chuàng)建了一個(gè)新的抽象, 將應(yīng)用與它所運(yùn)行的(異構(gòu))操作系統(tǒng)隔離開來。
這種鏡像和操作系統(tǒng)的解耦,使我們能在開發(fā)和生產(chǎn)環(huán)境提供相同的部署環(huán)境;這種環(huán)境的一致性提升了部署可靠性,加速了部署。這層抽象能成功的關(guān)鍵,是有一個(gè)hermetic(封閉的,不受外界影響的)容器鏡像,這個(gè)鏡像能封裝一個(gè)應(yīng)用的幾乎所有依賴(文件、函數(shù)庫(kù)等等);
那唯一剩下的外部依賴就是 Linux 系統(tǒng)調(diào)用接口了 —— 雖然這組有限的接口極大提升了鏡像的可移植性, 但它并非完美:應(yīng)用仍然可能通過 socket option、/proc、ioctl 參數(shù)等等產(chǎn)生很大的暴露面。我們希望 Open Container Initiative 等工作可以進(jìn)一步明確容器抽象的 surface area。
雖然存在不完美之處,但容器提供的資源隔離和依賴最小化特性,仍然使得它在 Google 內(nèi)部非常成功, 因此容器成為了 Google 基礎(chǔ)設(shè)施唯一支持的可運(yùn)行實(shí)體。這帶來的一個(gè)后果就是, Google 內(nèi)部只有很少幾個(gè)版本的操作系統(tǒng),也只需要很少的人來維護(hù)這些版本, 以及維護(hù)和升級(jí)服務(wù)器。
3.2.2 容器鏡像實(shí)現(xiàn)方式
實(shí)現(xiàn) hermetic image 有多種方式,在 Borg 中,程序可執(zhí)行文件在編譯時(shí)會(huì)靜態(tài)鏈接到公司托管的特定版本的庫(kù)5;
但實(shí)際上 Borg container image 并沒有做到完全獨(dú)立:所有應(yīng)用共享一個(gè)所謂的 base image,這個(gè)基礎(chǔ)鏡像是安裝在每個(gè) node 上的,而非打到每個(gè)鏡像里去;由于這個(gè)基礎(chǔ)鏡像里包含了 tar libc 等基礎(chǔ)工具和函數(shù)庫(kù), 因此升級(jí)基礎(chǔ)鏡像時(shí)會(huì)影響已經(jīng)在運(yùn)行的容器(應(yīng)用),偶爾會(huì)導(dǎo)致故障。
Docker 和 ACI 這樣的現(xiàn)代容器鏡像在這方面做的更好一些,它們地消除了隱藏的 host OS 依賴, 明確要求用戶在容器間共享鏡像時(shí),必須顯式指定這種依賴關(guān)系,這更接近我們理想中的 hermetic 鏡像。
3.3 容器作為基本管理單元
圍繞容器而非機(jī)器構(gòu)建 management API,將數(shù)據(jù)中心的核心從機(jī)器轉(zhuǎn)移到了應(yīng)用,這帶了了幾方面好處:
- 應(yīng)用開發(fā)者和應(yīng)用運(yùn)維團(tuán)隊(duì)無需再關(guān)心機(jī)器和操作系統(tǒng)等底層細(xì)節(jié);
- 基礎(chǔ)設(shè)施團(tuán)隊(duì)引入新硬件和升級(jí)操作系統(tǒng)更加靈活, 可以最大限度減少對(duì)線上應(yīng)用和應(yīng)用開發(fā)者的影響;
- 將收集到的 telemetry 數(shù)據(jù)(例如 CPU、memory usage 等 metrics)關(guān)聯(lián)到應(yīng)用而非機(jī)器, 顯著提升了應(yīng)用監(jiān)控和可觀測(cè)性,尤其是在垂直擴(kuò)容、 機(jī)器故障或主動(dòng)運(yùn)維等需要遷移應(yīng)用的場(chǎng)景。
3.3.1 通用 API 和自愈能力
容器能提供一些通用的 API 注冊(cè)機(jī)制,使管理系統(tǒng)和應(yīng)用之間無需知道彼此的實(shí)現(xiàn)細(xì)節(jié)就能交換有用信息。
在 Borg 中,這個(gè) API 是一系列 attach 到容器的 HTTP endpoints。例如,/healthz endpoint 向 orchestrator 匯報(bào)應(yīng)用狀態(tài),當(dāng)檢測(cè)到一個(gè)不健康的應(yīng)用時(shí), 就會(huì)自動(dòng)終止或重啟對(duì)應(yīng)的容器。這種自愈能力(self-healing)是構(gòu)建可靠分布式系統(tǒng)的最重要基石之一。
K8s 也提供了類似機(jī)制,health check 由用戶指定,可以是 HTTP endpoint 也可以一條 shell 命令(到容器內(nèi)執(zhí)行)。
3.3.2 用 annotation 描述應(yīng)用結(jié)構(gòu)信息
容器還能提供或展示其他一些信息。例如,Borg 應(yīng)用可以提供一個(gè)字符串類型的狀態(tài)消息,這個(gè)字段可以動(dòng)態(tài)更新;
K8s 提供了 key-value annotation, 存儲(chǔ)在每個(gè) object metadata 中,可以用來傳遞應(yīng)用結(jié)構(gòu)(application structure)信息。這些 annotations 可以由容器自己設(shè)置,也可以由管理系統(tǒng)中的其他組件設(shè)置(例如發(fā)布系統(tǒng)在更新完容器之后更新版本號(hào))。
容器管理系統(tǒng)還可以將 resource limits、container metadata 等信息傳給容器, 使容器能按特定格式輸出日志和監(jiān)控?cái)?shù)據(jù)(例如用戶名、job name、identity), 或在 node 維護(hù)之前打印一條優(yōu)雅終止的 warning 日志。
3.3.3 應(yīng)用維度 metrics 聚合:監(jiān)控和 auto-scaler 的基礎(chǔ)
容器還能用其他方式提供面向應(yīng)用的監(jiān)控:例如, cgroups 提供了應(yīng)用的 resource-utilization 數(shù)據(jù);前面已經(jīng)介紹過了, 還可以通過 export HTTP API 添加一些自定義 metrics 對(duì)這些進(jìn)行擴(kuò)展。
基于這些監(jiān)控?cái)?shù)據(jù)就能開發(fā)一些通用工具,例如 auto-scaler 和 cAdvisor3, 它們記錄和使用這些 metrics,但無需理解每個(gè)應(yīng)用的細(xì)節(jié)。由于應(yīng)用收斂到了容器內(nèi),因此就無需在宿主機(jī)上分發(fā)信號(hào)到不同應(yīng)用了;這更簡(jiǎn)單、更健壯, 也更容易實(shí)現(xiàn)細(xì)粒度的 metrics/logs 控制,不用再 ssh 登錄到機(jī)器執(zhí)行 top 排障了 —— 雖然開發(fā)者仍然能通過 ssh 登錄到他們的 容器,但實(shí)際中很少有人這樣做。
- 監(jiān)控只是一個(gè)例子。面向應(yīng)用的轉(zhuǎn)變?cè)诠芾砘A(chǔ)設(shè)施(management infrastructure)中產(chǎn)生漣漪效應(yīng):
- 我們的 load balancer 不再針對(duì) machine 轉(zhuǎn)發(fā)流量,而是針對(duì) application instance 轉(zhuǎn)發(fā)流量;
- Log 自帶應(yīng)用信息,因此很容易收集和按應(yīng)用維度(而不是機(jī)器維度)聚合;從而更容易看出應(yīng)用層面的故障,而不再是通過宿主機(jī)層的一些監(jiān)控指標(biāo)來判斷問題;
從根本上來說,實(shí)例在編排系統(tǒng)中的 identity 和用戶期望的應(yīng)用維度 identity 能夠?qū)?yīng)起來, 因此更容易構(gòu)建、管理和調(diào)試應(yīng)用。
3.3.4 單實(shí)例多容器(pod vs. container)
到目前為止我們關(guān)注的都是 application:container = 1:1 的情況, 但實(shí)際使用中不一定是這個(gè)比例。我們使用嵌套容器,對(duì)于一個(gè)應(yīng)用:
- 外層容器提供一個(gè)資源池;在 Borg 中成為 alloc,在 K8s 中成為 pod;
- 內(nèi)層容器們部署和隔離具體服務(wù)。
實(shí)際上 Borg 還允許不使用 allocs,直接創(chuàng)建應(yīng)用 container;但這導(dǎo)致了一些不必要的麻煩, 因此 K8s 就統(tǒng)一規(guī)定應(yīng)用容器必須運(yùn)行在 pod 內(nèi),即使一個(gè) pod 內(nèi)只有一個(gè)容器。常見方式是一個(gè) pod hold 一個(gè)復(fù)雜應(yīng)用的實(shí)例。
應(yīng)用的主體作為一個(gè) pod 內(nèi)的容器。其他輔助功能(例如 log rotate、click-log offloading)作為獨(dú)立容器。相比于把所有功能打到一個(gè)二進(jìn)制文件,這種方式能讓不同團(tuán)隊(duì)開發(fā)和管理不同功能,好處:
- 健壯:例如,應(yīng)用即使出了問題,log offloading 功能還能繼續(xù)工作;
- 可組合性:添加新的輔助服務(wù)很容易,因?yàn)椴僮鞫际窃谒约旱?container 內(nèi)完成的;
- 細(xì)粒度資源隔離:每個(gè)容器都有自己的資源限額,比如 logging 服務(wù)不會(huì)占用主應(yīng)用的資源。
3.4 編排是開始,不是結(jié)束
3.4.1 自發(fā)和野蠻生長(zhǎng)的 Borg 軟件生態(tài)
Borg 使得我們能在共享的機(jī)器上運(yùn)行不同類型的 workload 來提升資源利用率。但圍繞 Borg 衍生出的生態(tài)系統(tǒng)讓我們意識(shí)到,Borg 本身只是開發(fā)和管理可靠分布式系統(tǒng)的開始, 各團(tuán)隊(duì)根據(jù)自身需求開發(fā)出的圍繞 Borg 的不同系統(tǒng)與 Borg 本身一樣重要。下面列舉其中一些, 可以一窺其廣和雜:
- 服務(wù)命名(naming)和服務(wù)發(fā)現(xiàn)(Borg Name Service, BNS);
- 應(yīng)用選主:基于 Chubby2;
- 應(yīng)用感知的負(fù)載均衡(application-aware load balancing);
- 自動(dòng)擴(kuò)縮容:包括水平(實(shí)例數(shù)量)和垂直(實(shí)例配置/flavor)自動(dòng)擴(kuò)縮容;
- 發(fā)布工具:管理部署和配置數(shù)據(jù);
- Workflow 工具:例如允許多個(gè) job 按 pipeline 指定的依賴順序運(yùn)行;
- 監(jiān)控工具:收集容器信息,聚合、看板展示、觸發(fā)告警。
3.4.2 避免野蠻生長(zhǎng):K8s 統(tǒng)一 API(Object Metadata、Spec、Status)
開發(fā)以上提到的那些服務(wù)都是為了解決應(yīng)用團(tuán)隊(duì)面臨的真實(shí)問題,
- 其中成功的一些后來得到了大范圍的采用,使很多其他開發(fā)團(tuán)隊(duì)的工作更加輕松有效;
- 但另一方面,這些工具經(jīng)常使用非標(biāo)準(zhǔn) API、非標(biāo)準(zhǔn)約定(例如文件位置)以及深度利用了 Borg 內(nèi)部信息, 副作用是增加了在 Borg 中部署應(yīng)用的復(fù)雜度。
K8s 嘗試通過引入一致 API 的方式來降低這里的復(fù)雜度。例如,每個(gè) K8s 對(duì)象都有三個(gè)基本字段:
Object Metadata:所有 object 的 Object Metadata 字段都是一樣的,包括
- object name
- UID (unique ID)
- object version number(用于樂觀并發(fā)控制)
- labels
- Spec:用于描述這個(gè) object 的期望狀態(tài);Spec and Status 的內(nèi)容隨 object 類型而不同。
- Status:用于描述這個(gè) object 的當(dāng)前狀態(tài);
這種統(tǒng)一 API 提供了幾方面好處:
- 學(xué)習(xí)更加簡(jiǎn)單,因?yàn)樗?object 都遵循同一套規(guī)范和模板;
- 編寫適用于所有 object 的通用工具也更簡(jiǎn)單;
- 用戶體驗(yàn)更加一致。
3.4.3 K8s API 擴(kuò)展性和一致性
基于前輩 Borg 和 Omega 的經(jīng)驗(yàn),K8s 構(gòu)建在一些可組合的基本構(gòu)建模塊之上,用戶可以方便地進(jìn)行擴(kuò)展, 通用 API 和 object-metadata 設(shè)計(jì)使得這種擴(kuò)展更加方便。例如,pod API 可以被開發(fā)者、K8s 內(nèi)部組件和外部自動(dòng)化工具使用。
為了進(jìn)一步增強(qiáng)這種一致性,K8s 還進(jìn)行了擴(kuò)展,支持用戶動(dòng)態(tài)注冊(cè)他們自己的 API, 這些 API 和它內(nèi)置的核心 API 使用相同的方式工作。另外,我們還通過解耦 K8s API 實(shí)現(xiàn)了一致性(consistency)。API 組件的解耦考慮意味著上層服務(wù)可以共享相同的基礎(chǔ)構(gòu)建模塊。一個(gè)很好的例子:replica controller 和 horizontal auto-scaling (HPA) 的解耦。
Replication controller 確保給定角色(例如,”front end”)的 pod 副本數(shù)量符合預(yù)期;Autoscaler 這利用這種能力,簡(jiǎn)單地調(diào)整期望的 pod 數(shù)量,而無需關(guān)心這些 pod 是如何創(chuàng)建或刪除的。autoscaler 的實(shí)現(xiàn)可以將關(guān)注點(diǎn)放在需求和使用量預(yù)測(cè)上,而無需關(guān)心這些決策在底層的實(shí)現(xiàn)細(xì)節(jié)。
解耦確保了多個(gè)相關(guān)但不同的組件看起來和用起來是類似的體驗(yàn),例如,k8s 有三種不同類似的 replicated pods:
這三種 pod 的策略不同,但這三種 controller 都依賴相同的 pod object 來指定它們希望運(yùn)行的容器。
3.4.4 Reconcile 機(jī)制
我們還通過讓不同 k8s 組件使用同一套設(shè)計(jì)模式來實(shí)現(xiàn)一致性。Borg、Omega 和 k8s 都用到了 reconciliation controller loop 的概念,提高系統(tǒng)的容錯(cuò)性。
首先對(duì)觀測(cè)到的當(dāng)前狀態(tài)(“當(dāng)前能找到的這種 pod 的數(shù)量”)和期望狀態(tài)(“l(fā)abel-selector 應(yīng)該選中的 pod 數(shù)量”)進(jìn)行比較;如果當(dāng)前狀態(tài)和期望狀態(tài)不一致,則執(zhí)行相應(yīng)的行動(dòng) (例如擴(kuò)容 2 個(gè)新實(shí)例)來使當(dāng)前狀態(tài)與期望相符,這個(gè)過程稱為 reconcile(調(diào)諧)。
由于所有操作都是基于觀察(observation)而非狀態(tài)機(jī), 因此 reconcile 機(jī)制非常健壯:每次一個(gè) controller 掛掉之后再起來時(shí), 能夠接著之前的狀態(tài)繼續(xù)工作。
3.4.5 舞蹈編排(choreography)vs. 管弦樂編排(orchestration)
K8s 的設(shè)計(jì)綜合了 microservice 和 small control loop 的理念,這是 choreography(舞蹈編排)的一個(gè)例子 —— 通過多個(gè)獨(dú)立和自治的實(shí)體之間的協(xié)作(collaborate)實(shí)現(xiàn)最終希望達(dá)到的狀態(tài)。
- 舞蹈編排:場(chǎng)上沒有指揮老師,每個(gè)跳舞的人都是獨(dú)立個(gè)體,大家共同協(xié)作完成一次表演。代表分布式、非命令式。
我們特意這么設(shè)計(jì),以區(qū)別于管弦樂編排中心式編排系統(tǒng)(centralized orchestration system),后者在初期很容易設(shè)計(jì)和開發(fā), 但隨著時(shí)間推移會(huì)變得脆弱和死板,尤其在有狀態(tài)變化或發(fā)生預(yù)期外的錯(cuò)誤時(shí)。
- 管弦樂編排:場(chǎng)上有一個(gè)指揮家,每個(gè)演奏樂器的人都是根據(jù)指揮家的命令完成演奏。代表集中式、命令式。
4 避坑指南
這里列舉一些經(jīng)驗(yàn)教訓(xùn),希望大家要犯錯(cuò)也是去犯新錯(cuò),而不是重復(fù)踩我們已經(jīng)踩過的坑。
4.1 創(chuàng)建 Pod 時(shí)應(yīng)該分配唯一 IP,而不是唯一端口(port)
在 Borg 中,容器沒有獨(dú)立 IP,所有容器共享 node 的 IP。因此, Borg 只能在調(diào)度時(shí),給每個(gè)容器分配唯一的 port。當(dāng)一個(gè)容器漂移到另一臺(tái) node 時(shí),會(huì)獲得一個(gè)新的 port(容器原地重啟也可能會(huì)分到新 port)。這意味著,
- 類似 DNS(運(yùn)行在 53 端口)這樣的傳統(tǒng)服務(wù),只能用一些內(nèi)部魔改的版本;
- 客戶端無法提前知道一個(gè) service 的端口,只有在 service 創(chuàng)建好之后再告訴它們;
- URL 中不能包含 port(容器重啟 port 可能就變了,導(dǎo)致 URL 無效),必須引入一些 name-based redirection 機(jī)制;
- 依賴 IP 地址的工具都必須做一些修改,以便能處理 IP:port。
- 因此在設(shè)計(jì) k8s 時(shí),我們決定給每個(gè) pod 分配一個(gè) IP,這樣就實(shí)現(xiàn)了網(wǎng)絡(luò)身份(IP)與應(yīng)用身份(實(shí)例)的一一對(duì)應(yīng);
- 避免了前面提到的魔改 DNS 等服務(wù)的問題,應(yīng)用可以隨意使用 well-known ports(例如,HTTP 80);
- 現(xiàn)有的網(wǎng)絡(luò)工具(例如網(wǎng)絡(luò)隔離、帶寬控制)也無需做修改,直接可以用;
此外,所有公有云平臺(tái)都提供 IP-per-pod 的底層能力;在 bare metal 環(huán)境中,可以使用 SDN overlay 或 L3 routing 來實(shí)現(xiàn)每個(gè) node 上多個(gè) IP 地址。
4.2 容器索引不要用數(shù)字 index,用 labels
用戶一旦習(xí)慣了容器開發(fā)方式,馬上就會(huì)創(chuàng)建一大堆容器出來, 因此接下來的一個(gè)需求就是如何對(duì)這些容器進(jìn)行分組和管理。
4.2.1 Borg 基于 index 的容器索引設(shè)計(jì)
Borg 提供了 jobs 來對(duì)容器名字相同的 tasks 進(jìn)行分組。
每個(gè) job 由一個(gè)或多個(gè)完全相同的 task 組成,用向量(vector)方式組織,從 index=0 開始索引。
這種方式非常簡(jiǎn)單直接,很好用,但隨著時(shí)間推移,我們?cè)絹碓桨l(fā)覺它比較死板。例如,
- 一個(gè) task 掛掉后在另一臺(tái) node 上被創(chuàng)建出來時(shí),task vector 中對(duì)應(yīng)這個(gè) task 的 slot 必須做雙份的事情:識(shí)別出新的 task;在需要 debug 時(shí)也能指向老的 task;
- 當(dāng) task vector 中間某個(gè) task 正常退出之后,vector 會(huì)留下空洞;
- vector 也很難支持跨 Borg cluster 的 job;
- 應(yīng)用如何使用 task index(例如,在 tasks 之間做數(shù)據(jù)的 sharding/partitioning) 和 Borg 的 job-update 語義(例如,默認(rèn)是滾動(dòng)升級(jí)時(shí)按順序重啟 task)之間,也存在一些不明朗之處:如果用戶基于 task index 來設(shè)計(jì) sharding,那 Borg 的重啟策略就會(huì)導(dǎo)致數(shù)據(jù)不可用,因?yàn)樗际前?task 順序重啟的;
- Borg 還沒有很好的方式向 job 添加 application-relevant metadata, 例如 role (e.g. “frontend”)、rollout status (e.g. “canary”), 因此用戶會(huì)將這些信息編碼到 job name 中,然后自己再來解析 job name。
4.2.2 K8s 基于 label 的容器索引設(shè)計(jì)
作為對(duì)比,k8s 主要使用 labels 來識(shí)別一組容器(groups of containers)。
- Label 是 key/value pair,包含了可以用來鑒別這個(gè) object 的信息;例如,一個(gè) pod 可能有 role=frontend 和 stage=production 兩個(gè) label,表明這個(gè)容器運(yùn)行的是生產(chǎn)環(huán)境的前端應(yīng)用;
- Label 可以動(dòng)態(tài)添加、刪除和修改,可以通過工具或用戶手動(dòng)操作;
- 不同團(tuán)隊(duì)可以管理各自的 label,基本可以做到不重疊;
- 可以通過 label selector (例如,stage==production && role==frontend)來選中一組 objects;
- 組可以重合,也就是一個(gè) object 可能出現(xiàn)在多個(gè) label selector 篩選出的結(jié)果中,因此這種基于 label 的方式更加靈活;
- label selector 是動(dòng)態(tài)查詢語句,也就是說只要用戶有需要,他們隨時(shí)可以按自己的需求編寫新的查詢語句(selector);
- Label selector 是 k8s 的 grouping mechanism,也定義了所有管理操作的范圍(多個(gè) objects)。
在某些場(chǎng)景下,能精確(靜態(tài))知道每個(gè) task 的 identity 是很有用的(例如,靜態(tài)分配 role 和 sharding/partitioning), 在 k8s 中,通過 label 方式也能實(shí)現(xiàn)這個(gè)效果,只要給每個(gè) pod 打上唯一 label 就行了, 但打這種 label 就是用戶(或 k8s 之上的某些管理系統(tǒng))需要做的事情了。
Labels 和 label selectors 提供了一種通用機(jī)制, 既保留了 Borg 基于 index 索引的能力,又獲得了上面介紹的靈活性。
4.3 Ownership 設(shè)計(jì)要格外小心
- 在 Borg 中,task 并不是獨(dú)立于 job 的存在:
- 創(chuàng)建一個(gè) job 會(huì)創(chuàng)建相應(yīng)的 tasks,這些 tasks 永遠(yuǎn)與這個(gè) job 綁定;
- 刪除這個(gè) jobs 時(shí)會(huì)刪除它所有的 tasks。
這種方式很方便,但有一個(gè)嚴(yán)重不足:Borg 中只有一種 grouping 機(jī)制,也就是前面提到的 vector index 方式。例如,一個(gè) job 需要存儲(chǔ)某個(gè)配置參數(shù),但這個(gè)參數(shù)只對(duì) service 或 batch job 有用,并不是對(duì)兩者都有用。當(dāng)這個(gè) vector index 抽象無法滿足某些場(chǎng)景(例如, DaemonSet 需要將一個(gè) pod 在每個(gè) node 上都起一個(gè)實(shí)例)時(shí), 用戶必須開發(fā)一些 workaround。
Kubernetes 中,那些 pod-lifecycle 管理組件 —— 例如 replication controller —— 通過 label selector 來判斷哪些 pod 歸自己管;但這里也有個(gè)問題:多個(gè) controller 可能會(huì)選中同一個(gè) pod,認(rèn)為這個(gè) pod 都應(yīng)該歸自己管, 這種沖突理應(yīng)在配置層面解決。
label 的靈活性帶來的好處:例如, controller 和 pod 分離意味著可以 "orphan" 和 "adopt" container。考慮一個(gè) service, 如果其中一個(gè) pod 有問題了,那只需要把相應(yīng)的 label 從這個(gè) pod 上去掉, k8s service 就不會(huì)再將流量轉(zhuǎn)發(fā)給這個(gè) pod。這個(gè) pod 不再接生產(chǎn)流量,但仍然活在線上,因此就可以對(duì)它進(jìn)行 debug 之類的。而與此同時(shí),負(fù)責(zé)管理這些 pod 的 replication controller 就會(huì)立即再創(chuàng)建一個(gè)新的 pod 出來接流量。
4.4 不要暴露原始狀態(tài)
Borg、Omega 和 k8s 的一個(gè)核心區(qū)別是它們的 API 架構(gòu)。
- Borgmaster 是一個(gè)單體組件,理解每個(gè) API 操作的語義:
- 包含集群管理邏輯,例如 job/task/machine 的狀態(tài)機(jī);
運(yùn)行基于 Paxos 的 replicated storage system,用來存儲(chǔ) master 的狀態(tài)。Omega 除了 store 之外沒有中心式組件,
- store 存儲(chǔ)了 passive 狀態(tài)信息,執(zhí)行樂觀并發(fā)控制;
- 所有邏輯和語義都下放到了操作 store 的 client 上,后者會(huì)直接讀寫 store 內(nèi)容;
實(shí)際上,每個(gè) Omega 組件都使用了同一套 client-side library 來與 store 交互, 這個(gè) liabrary 做的事情包括 packing/unpacking of data structures、retries、 enforce semantic consistency。
k8s 在 Omega 的分布式架構(gòu)和 Borg 的中心式架構(gòu)之間做了一個(gè)折中,在繼承 Omega 分布式架構(gòu)的靈活性和擴(kuò)展性的同時(shí),對(duì)系統(tǒng)級(jí)別的規(guī)范、策略、數(shù)據(jù)轉(zhuǎn)換等方面還是集中式的;實(shí)現(xiàn)方式是在 store 前面加了一層集中式的 API server,屏蔽掉所有 store 實(shí)現(xiàn)細(xì)節(jié), 提供 object validation、defaulting 和 versioning 服務(wù)。與 Omega 類似,客戶端組件都彼此獨(dú)立,可以獨(dú)立開發(fā)、升級(jí)(在開源場(chǎng)景尤其重要), 但各組件都要經(jīng)過 apiserver 這個(gè)中心式服務(wù)的語義、規(guī)范和策略。
5 開放問題討論
雖然我們已經(jīng)了十幾年的大規(guī)模容器管理經(jīng)驗(yàn),但仍然有些問題還沒有很好的解決辦法。本節(jié)介紹幾個(gè)供討論,集思廣益。
5.1 應(yīng)用配置管理
在我們面臨的所有問題中,耗費(fèi)了最多腦力、頭發(fā)和代碼的是管理配置(configurations)相關(guān)的。這里的配置指的是應(yīng)用配置,即如何把應(yīng)用的參數(shù)在創(chuàng)建容器時(shí)傳給它, 而不是 hard-code。這個(gè)主題值得單獨(dú)一整篇文章來討論,這里僅 highlight 幾方面。
- 首先,Borg 仍然缺失的那些功能,最后都能與應(yīng)用配置(application configuration)扯上關(guān)系。這些功能包括:
- Boilerplate reduction:例如,根據(jù) workload 類型等信息為它設(shè)置默認(rèn)的 restart policy;
- 調(diào)整和驗(yàn)證應(yīng)用參數(shù)和命令行參數(shù);
- 實(shí)現(xiàn)一些 workaround,解決容器鏡像管理 API 缺失的問題;
- 給應(yīng)用用的函數(shù)庫(kù)和配置魔板;
- 發(fā)布管理工具;
- 容器鏡像版本規(guī)范;
為了滿足這些需求,配置管理系統(tǒng)傾向于發(fā)明一種 domain-specific 語言, 并希望最終成為一門圖靈完備的配置語言:解析配置文件,提取某些數(shù)據(jù),然后執(zhí)行一些計(jì)算。例如,根據(jù)一個(gè) service 的副本數(shù)量,利用一個(gè)函數(shù)自動(dòng)調(diào)整分給一個(gè)副本的內(nèi)存。用戶的需求是減少代碼中的 hardcode 配置,但最終的結(jié)果是一種難以理解和使用的“配置即代碼”產(chǎn)品,用戶避之不及。它沒有減少運(yùn)維復(fù)雜度,也沒有使配置的 debug 變得更簡(jiǎn)單;它只是將計(jì)算從一門真正的 編程語言轉(zhuǎn)移到了一個(gè) domain-specific 語言,而后者通常的配置開發(fā)工具更弱 (例如 debuggers, unit test frameworks 等)。
我們認(rèn)為最有效的方式是接受這個(gè)需求,承認(rèn) programmatic configuration 的不可避免性, 在計(jì)算和數(shù)據(jù)之間維護(hù)一條清晰邊界。表示數(shù)據(jù)的語言應(yīng)該是簡(jiǎn)單、data-only 的格式,例如 JSON or YAML, 而針對(duì)這些數(shù)據(jù)的計(jì)算和修改應(yīng)該在一門真正的編程語言中完成,后者有 完善的語義和配套工具。
有趣的是,這種計(jì)算與數(shù)據(jù)分離的思想已經(jīng)在其他領(lǐng)域開始應(yīng)用,例如一些前端框架的開發(fā), 比如 Angular 在 markup (data) 和 JavaScript (computation) 之間。
5.2 依賴管理
上線一個(gè)新服務(wù)通常也意味著需要上線一系列相關(guān)的服務(wù)(監(jiān)控、存儲(chǔ)、CI/CD 等等)。如果一個(gè)應(yīng)用依賴其他一些應(yīng)用,那由集群管理系統(tǒng)來自動(dòng)化初始化后者(以及它們的依賴)不是很好嗎?
但事情并沒有這么簡(jiǎn)單:自動(dòng)初始化依賴(dependencies)并不是僅僅啟動(dòng)一個(gè)新實(shí)例 —— 例如,可能還需要將其注冊(cè)為一個(gè)已有服務(wù)的消費(fèi)者, (e.g., Bigtable as a service),以及將認(rèn)證、鑒權(quán)和賬單信息傳遞給這些依賴系統(tǒng)。
但幾乎沒有哪個(gè)系統(tǒng)收集、維護(hù)或暴露這些依賴信息,因此即使是一些常見場(chǎng)景都很難在基礎(chǔ)設(shè)施層實(shí)現(xiàn)自動(dòng)化。上線一個(gè)新應(yīng)用對(duì)用戶來說仍然是一件復(fù)雜的事情,導(dǎo)致開發(fā)者構(gòu)建新服務(wù)更困難, 經(jīng)常導(dǎo)致最新的最佳實(shí)踐無法用上,從而影響新服務(wù)的可靠性。
一個(gè)常見問題是:如果依賴信息是手工提供的,那很難維持它的及時(shí)有效性, 與此同時(shí),自動(dòng)判斷(例如,通過 tracing accesses)通常會(huì)失敗,因?yàn)闊o法捕捉理解相應(yīng)結(jié)果的語義信息。(Did that access have to go to that instance, or would any instance have sufficed?)
一種可能的解決方式是:每個(gè)應(yīng)用顯式聲明它依賴的服務(wù),基礎(chǔ)設(shè)施層禁止它訪問除此之外的所有服務(wù) (我們的構(gòu)建系統(tǒng)中,編譯器依賴就是這么做的1)。好處就是基礎(chǔ)設(shè)施能做一些有益的工作,例如自動(dòng)化設(shè)置、認(rèn)證和連接性。
不幸的是,表達(dá)、分析和使用系統(tǒng)依賴會(huì)導(dǎo)致系統(tǒng)的復(fù)雜性升高, 因此并沒有任何一個(gè)主流的容器管理系統(tǒng)做了這個(gè)事情。我們?nèi)匀幌M?k8s 能成為一個(gè)構(gòu)建此類工具的平臺(tái),但這一工作目前仍困難重重。
6 總結(jié)
過去十多年開發(fā)容器管理系統(tǒng)的經(jīng)歷教會(huì)了我們很多東西,我們也將這些經(jīng)驗(yàn)用到了 k8s —— Google 最新的容器管理系統(tǒng) —— 的設(shè)計(jì)中, 它的目標(biāo)是基于容器提供的各項(xiàng)能力,顯著提升開發(fā)者效率,以及使系統(tǒng)管理(不管是手動(dòng)還是自動(dòng))更加方便。希望大家能與我們一道,繼續(xù)完善和優(yōu)化它。
參考資料
- Bazel: {fast, correct}—choose two; http://bazel.io.
- Burrows, M. 2006. The Chubby lock service for loosely coupled distributed systems. Symposium on Operating System Design and Implementation (OSDI), Seattle, WA.
- cAdvisor; https://github.com/google/cadvisor.
- Kubernetes; http://kubernetes.io/.
- Metz, C. 2015. Google is 2 billion lines of code—and it’s all in one place. Wired (September); http://www.wired.com/2015/09/google-2-billion-lines-codeand-one-place/.
- Schwarzkopf, M., Konwinski, A., Abd-el-Malek, M., Wilkes, J. 2013. Omega: flexible, scalable schedulers for large compute clusters. European Conference on Computer Systems (EuroSys), Prague, Czech Republic.
- Verma, A., Pedrosa, L., Korupolu, M. R., Oppenheimer, D., Tune, E., Wilkes, J. 2015. Large-scale cluster management at Google with Borg. European Conference on Computer Systems (EuroSys), Bordeaux, France.