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