(注:本文轉載自阿里巴巴雲原生課堂理解 Pod 和容器設計模式)
本次課程的分享主要圍繞以下三個部分:
- 為什么需要 Pod;
- Pod 的實現機制;
- 容器設計模式(另起一文)
1. 為什么需要 Pod
1.1 容器的基本概念
現在來看第一個問題:為什么需要 Pod?我們知道 Pod 是 Kubernetes 項目里面一個非常重要的概念,也是非常重要的一個原子調度單位,但是為什么我們會需要這樣一個概念呢?我們在使用容器 Docker 的時候,也沒有這個說法。其實如果要理解 Pod,我們首先要理解容器,所以首先來回顧一下容器的概念:
容器的本質實際上是一個進程,是一個視圖被隔離,資源受限的進程。Kubernetes 又是什么呢?很多人都說 Kubernetes 是雲時代的操作系統,以此類推,容器鏡像就是這個操作系統的軟件安裝包,它們之間是這樣的一個類比關系。
1.2 真實操作系統里的例子
如果說 Kubernetes 就是操作系統的話,那么我們不妨看一下真實的操作系統的例子。
例子里面有一個程序叫做 Helloworld,這個 Helloworld 程序實際上是由一組進程組成的,需要注意一下,這里說的進程實際上等同於 Linux 中的線程。
因為 Linux 中的線程是輕量級進程,所以如果從 Linux 系統中去查看 Helloworld 中的 pstree(注:pstree命令以樹狀圖的方式展現進程之間的派生關系),將會看到這個 Helloworld 實際上是由四個線程組成的,分別是 {api、main、log、compute}。也就是說,四個這樣的線程共同協作,共享 Helloworld 程序的資源,組成了 Helloworld 程序的真實工作情況。
這是操作系統里面進程組或者線程組中一個非常真實的例子,以上就是進程組的一個概念。
在真實的操作系統里面,一個程序往往是根據進程組來進行管理的。Kubernetes 類比為一個操作系統(比如Linux),容器則類比為進程(即Linux線程),那么 Pod 就是我們剛剛提到的進程組(即Linux 里的線程組)。
1.3 進程組概念
還是前面那個例子:Helloworld 程序由四個進程組成,這些進程之間會共享一些資源和文件。那么現在有一個問題:假如說現在把 Helloworld 程序用容器跑起來,你會怎么去做?
當然,最自然的一個解法就是,我現在就啟動一個 Docker 容器,里面運行四個進程。可是這樣會有一個問題,這種情況下容器里面 PID=1 的進程該是誰? 比如說,它應該是我的 main 進程,那么問題來了,“誰”又負責去管理剩余的 3 個進程呢?
這個核心問題在於,容器的設計本身是一種“單進程”模型,不是說容器里只能起一個進程,由於容器的應用等於進程,所以只能去管理 PID=1 的這個進程,其他再起來的進程其實是一個托管狀態。 所以說服務應用進程本身就具有“進程管理”的能力。(這段話不太理解)
比如說 Helloworld 的程序有 system 的能力,或者直接把容器里 PID=1 的進程直接改成 systemd,否則這個應用,或者是容器是沒有辦法去管理很多個進程的。因為 PID=1 進程是應用本身,如果現在把這個 PID=1 的進程給 kill 了,或者它自己運行過程中死掉了,那么剩下三個進程的資源就沒有人回收了,這個是非常非常嚴重的一個問題。
而反過來真的把這個應用本身改成了 systemd,或者在容器里面運行了一個 systemd,將會導致另外一個問題:使得管理容器,不再是管理應用本身了,而等於是管理 systemd(什么意思??),這里的問題就非常明顯了。比如說我這個容器里面 run 的程序或者進程是 systemd,那么接下來,這個應用是不是退出了?是不是 fail 了?是不是出現異常失敗了?實際上是沒辦法直接知道的,因為容器管理的是 systemd。這就是為什么在容器里面運行一個復雜程序往往比較困難的一個原因。
這里再幫大家梳理一下:由於容器實際上是一個“單進程”模型,所以如果你在容器里啟動多個進程,只有一個可以作為 PID=1 的進程,而這時候,如果這個 PID=1 的進程掛了,或者說失敗退出了,那么其他三個進程就會自然而然的成為孤兒,沒有人能夠管理它們,沒有人能夠回收它們的資源,這是一個非常不好的情況。
注意:Linux 容器的“單進程”模型,指的是容器的生命周期等同於 PID=1 的進程(容器應用進程)的生命周期,而不是說容器里不能創建多進程。當然,一般情況下,容器應用進程並不具備進程管理能力,所以你通過 exec 或者 ssh 在容器里創建的其他進程,一旦異常退出(比如 ssh 終止)是很容易變成孤兒進程的。
反過來,其實可以在容器里面 run 一個 systemd,用它來管理其他所有的進程。這樣會產生第二個問題:實際上沒辦法直接管理我的應用了,因為我的應用被 systemd 給接管了,那么這個時候應用狀態的生命周期就不等於容器生命周期,這個管理模型實際上是非常非常復雜的。
1.4 Pod = “進程組”
前面提到的,由四個進程共同組成的一個應用 Helloworld,在 Kubernetes 里面,實際上會被定義為一個擁有四個容器的 Pod,就是說現在有四個職責不同、相互協作的進程,需要放在容器里去運行,在 Kubernetes 里面並不會把它們放到一個容器里,因為這里會遇到兩個問題(哪兩個問題?)。那么在 Kubernetes 里會怎么去做呢?它會把四個獨立的進程分別用四個獨立的容器啟動起來,然后把它們定義在一個 Pod 里面。
所以當 Kubernetes 把 Helloworld 應用 拉起來的時候,實際上會看到四個容器,它們共享了某些資源,這些資源都屬於 Pod,所以我們說 Pod 在 Kubernetes 里面只有一個「邏輯單位」,並沒有一個真實的東西與之對應,真正在物理上存在的東西,就是四個容器。這四個容器,或者說是多個容器的組合就叫做 Pod。並且還有一個概念一定要非常明確,Pod 是 Kubernetes 分配資源的一個單位,因為里面的容器要共享某些資源,所以 Pod 也是 Kubernetes 的原子調度單位。
以上就是進程組的概念,也是 Pod 的用法。
1.5 為什么 Pod 必須是原子調度單位?
可能到這里大家會有一些問題:雖然了解這個東西是一個進程組,但是為什么要把 Pod 本身作為一個概念抽象出來呢?或者說能不能通過調度把 Pod 這個事情給解決掉呢?為什么 Pod 必須是 Kubernetes 里面的原子調度單位?
下面我們通過一個例子來解釋。
假如現在有兩個容器,它們是緊密協作的,所以它們應該被部署在一個 Pod 里面。具體來說,第一個容器叫做 App,就是業務容器,它會寫日志文件;第二個容器叫做 LogCollector,它會把剛剛 App 容器寫的日志文件轉發到后端的 ElasticSearch 中。
兩個容器的資源需求是這樣的:App 容器需要 1G 內存,LogCollector 需要 0.5G 內存,而當前集群環境的可用內存是這樣一個情況:Node_A:1.25G 內存,Node_B:2G 內存。
假如說現在沒有 Pod 概念,就只有兩個容器,這兩個容器要緊密協作、運行在一台機器上。可是,如果調度器先把 App 調度到了 Node_A 上面,接下來會怎么樣呢?這時你會發現:LogCollector 實際上是沒辦法調度到 Node_A 上的,因為資源不夠。其實此時整個應用本身就已經出問題了,調度已經失敗了,必須去重新調度。
以上就是一個非常典型的成組調度失敗的例子,英文叫做Task co-scheduling 問題,這個問題不是說不能解,在很多項目里面,這樣的問題都有解法。
比如說在 Mesos 里面,它會做一個事情,叫做資源囤積(resource hoarding):即當所有設置了 Affinity 約束的任務都達到時,才開始統一調度,這是一個非常典型的成組調度的解法。
所以上面提到的“App”和“LogCollector”這兩個容器,在 Mesos 里面,他們不會立刻調度,而是等兩個容器都提交完成,才開始統一調度。這樣也會帶來新的問題,首先調度效率會損失,因為需要等待。由於需要等還會有外一個情況會出現,就是產生死鎖,就是互相等待的一個情況。這些機制在 Mesos 里都是需要解決的,也帶來了額外的復雜度。
另一種解法是 Google 的解法。它在 Omega 系統(就是 Borg 下一代)里面,做了一個非常復雜且非常厲害的解法,叫做樂觀調度。比如說:不管這些沖突的異常情況,先調度,同時設置一個非常精妙的回滾機制,這樣經過沖突后,通過回滾來解決問題。這個方式相對來說要更加優雅,也更加高效,但是它的實現機制是非常復雜的。這個有很多人也能理解,就是悲觀鎖的設置一定比樂觀鎖要簡單。
而像這樣的一個 Task co-scheduling 問題,在 Kubernetes 里,就直接通過 Pod 這樣一個概念去解決了。因為在 Kubernetes 里,這樣的一個 App 容器和 LogCollector 容器一定是屬於一個 Pod 的,它們在調度時必然是以一個 Pod 為單位進行調度,所以這個問題是根本不存在的。
1.6 再次理解 Pod
在講了前面這些知識點之后,我們來再次理解一下 Pod,首先 Pod 里面的容器是“超親密關系”。
這里有個“超”字需要大家理解,正常來說,有一種關系叫做親密關系,這個親密關系是一定可以通過調度來解決的。
比如說現在有兩個 Pod,它們需要運行在同一台宿主機上,那這樣就屬於親密關系,調度器一定是可以幫助去做的。但是對於超親密關系來說,有一個問題,即它必須通過 Pod 來解決。因為如果超親密關系賦予不了,那么整個 Pod 或者說是整個應用都無法啟動。
什么叫做超親密關系呢?大概分為以下幾類:
- 比如說兩個進程之間會發生文件交換,前面提到的例子就是這樣,一個寫日志,一個讀日志;
- 兩個進程之間需要通過 localhost 或者說是本地的 Socket 去進行通信,這種本地通信也是超親密關系;
- 這兩個容器或者是微服務之間,需要發生非常頻繁的 RPC 調用,出於性能的考慮,也希望它們是超親密關系;
- 兩個容器或者是應用,它們需要共享某些 Linux Namespace。最簡單常見的一個例子,就是我有一個容器需要加入另一個容器的 Network Namespace。這樣我就能看到另一個容器的網絡設備,和它的網絡信息。
像以上幾種關系都屬於超親密關系,它們在 Kubernetes 中都是通過 Pod 的概念去解決的。
現在我們理解了 Pod 這樣的概念設計,理解了為什么需要 Pod。它解決了兩個問題:
- 我們怎么去描述超親密關系;
- 我們怎么去對超親密關系的容器或者說是業務去做統一調度,這是 Pod 最主要的一個訴求。
2. Pod 的實現機制
2.1 Pod 要解決的問題
像 Pod 這樣一個東西,本身是一個邏輯概念。那在機器上,它究竟是怎么實現的呢?這就是我們要解釋的第二個問題,即:Pod 是如何實現的?
既然說 Pod 要解決這個問題,核心就在於如何讓一個 Pod 里的多個容器之間最高效的共享某些資源和數據。因為容器之間原本是被 Linux Namespace 和 cgroups 隔開的,所以現在實際要解決的是怎么去打破這個隔離,然后共享某些事情和某些信息。這就是 Pod 的設計要解決的核心問題所在。
具體的解法分為兩個部分:「網絡」和「存儲」。
2.2 共享網絡
第一個問題是 Pod 里的多個容器怎么去共享網絡?下面是個例子:
比如說現在有一個 Pod,其中包含了一個容器 A 和一個容器 B,它們兩個就要共享 Network Namespace。在 Kubernetes 里的解法是這樣的:它會在每個 Pod 里,額外起一個 Infra container來共享整個 Pod 的 Network Namespace。
Infra container 是一個非常小的鏡像,大概 100~200KB 左右,是一個匯編語言寫的、永遠處於“暫停”狀態的容器。由於有了這樣一個 Infra container 之后,其他所有容器都會通過 Join Namespace 的方式加入到 Infra container 的 Network Namespace 中(回顧在Docker網絡實現提到的Docker網絡模型,這里提到的就是”container模式“)。
所以說一個 Pod 里面的所有容器,它們看到的網絡視圖是完全一樣的。即:它們看到的網絡設備、IP地址、Mac地址等等,都來自於 Pod 第一次創建的這個 Infra container。這就是 Pod 解決網絡共享的一個解法。
在 Pod 里面,必然是 Infra container 第一個啟動,並且整個 Pod 的生命周期是等同於 Infra container 的生命周期的,與容器 A 和 B 是無關的。這也是為什么在 Kubernetes 里面,它是允許去單獨更新 Pod 里的某一個鏡像的,即:做這個操作,整個 Pod 不會重建,也不會重啟,這是非常重要的一個設計。
2.3 共享存儲
第二問題:Pod 怎么去共享存儲?
比如說現在有兩個容器,一個是 Nginx,另外一個是非常普通的容器,在 Nginx 里放一些文件,讓我能通過 Nginx 訪問到。所以它需要去 share 這個目錄。我 share 文件或者是 share 目錄在 Pod 里面是非常簡單的,實際上就是把 volume 變成了 Pod level。然后所有同屬於一個 Pod 的容器,他們共享所有的 volume。
比如說上圖的例子,這個 volume 叫做 shared-data,它是屬於 Pod level 的,所以在每一個容器里可以直接聲明:要掛載 shared-data 這個 volume,只要你聲明了你掛載這個 volume,你在容器里去看這個目錄,實際上大家看到的就是同一份。這個就是 Kubernetes 通過 Pod 來給容器共享存儲的一個做法。
所以在之前的例子中,應用容器 App 寫了日志,只要這個日志是寫在一個 volume 中,只要聲明掛載了同樣的 volume,這個 volume 就可以立刻被另外一個 LogCollector 容器給看到。以上就是 Pod 實現存儲的方式。
3. 總結
首先需要明確,Pod 的是一個邏輯概念,而不是一個物理上真正存在的東西,真實存在的只有一個個容器。
本文首先介紹了為什么需要 Pod 這樣一個概念,在這里,我們把容器類比成操作系統中的進程,把Pod類比成進程組。Pod 就是一組超親密關系的容器的組合,通過 Pod 這樣一個概念,很好的解決了資源調度的問題。因此,Pod 也是Kubernetes世界里資源調度的最小單位。
其次,介紹了 Pod 的實現機制。Pod 的核心就是讓同個 Pod 內的多個容器共享資源和數據,而由於Linux namespace和cgroups,容器與容器之間本來是隔離的。因此,Pod 所要做的就是打破這種隔離,具體來說就是實現”網絡共享“和”存儲共享“。
摘自網友的總結:Pod 是一個小家庭,它把密不可分的家庭成員(container)聚在一起,Infra container則是家長,掌管家中共通資源,家庭成員通過 Sidecar 方式互幫互助。
還有另一種說法:Pod 的直譯是豆莢,一個豆莢內包含了一個或多個豆子,這些豆子共享一個物理豆莢,即可以共享調度、網絡、存儲,以及安全等。
(全文完)
【另外我的補充】:
在 Pod 中,”Pod IP + Container Port“ 組成了 Endpoint 的概念,由 Endpoint 表示Pod 里一個服務進程的對外通信地址。
每個 Pod 都可以對其能使用的服務器上的計算資源設置限額,當前可以設置限額的計算資源有CPU與Memory兩種,其中CPU的資源單位為CPU(Core)的數量,是一個絕對值而非相對值。
在 kubernetes中,以千分之一的CPU配額為最小單位,用m來表示。比如,一個容器的CPU配額通常被定義為100300m,表示的是占用0.10.3個CPU。注意這是一個絕對值,無論在擁有一個Core的機器上,還是在 擁有48個Core的機器上,100m這個配額所代表的CPU的使用量都是一樣 的。與CPU配額類似,Memory配額也是一個絕對值,它的單位是內存字節數。