要說明什麼是「新的抽象與原語」,最快的方式是拿大家熟悉的物件導向程式設計(object-oriented programming,OOP)——特別是 Java——來對照。在 OOP 的世界裡,我們有類別、物件、套件、繼承、封裝、多型這些概念;Java 執行期則提供了特定的功能與保證,用來管理物件與整個應用的生命週期。

Java 語言與 Java 虛擬機(JVM)提供的是本地、行程內的建構單元。Kubernetes 為這套熟悉的心智模型加上了全新的維度:它提供一組分散式原語與執行期,用來建構橫跨多個節點與行程的分散式系統。有了 Kubernetes,我們不必再只靠本地原語來實現整個應用行為。

我們仍然需要物件導向的建構單元來打造分散式應用的元件,但部分應用行為可以改用 Kubernetes 原語來實現。

本地原語與分散式原語的對照#

概念本地原語分散式原語
行為封裝Class容器映像檔
行為實例Object容器
重用單元.jar容器映像檔
組合Class A 內含 Class BSidecar 模式
繼承Class A extends Class B容器的 FROM 父映像檔
部署單元.jar / .war / .earPod
建置期/執行期隔離Module、Package、ClassNamespace、Pod、容器
初始化前置條件ConstructorInit container
初始化後觸發Init-methodpostStart
銷毀前觸發Destroy-methodpreStop
清理程序finalize()、shutdown hookDefer 容器
非同步與平行執行ThreadPoolExecutorForkJoinPoolJob
週期性任務TimerScheduledExecutorServiceCronJob
背景任務Daemon threadDaemonSet
組態管理System.getenv()PropertiesConfigMap、Secret

Defer(或稱 de-init)容器尚未實作,但已有提案打算在未來版本的 Kubernetes 中納入此功能。生命週期掛鉤(lifecycle hook)在「受管生命週期」一章討論。

行程內原語與分散式原語有共通處,但不能直接類比、也不能互相取代。它們運作在不同的抽象層次,有不同的前置條件與保證。有些原語本來就該搭配使用——例如我們仍得用類別建立物件、再把它們放進容器映像檔;但另一些原語(如 Kubernetes 的 CronJob)則可以完全取代 Java 的 ExecutorService 行為。

以下看幾個對應用開發者特別有意思的 Kubernetes 分散式抽象與原語。

容器(Containers)#

容器是 Kubernetes 雲原生應用的建構基石。若與 OOP 和 Java 對照:容器映像檔像類別,容器像物件。就像我們可以繼承類別來重用與改變行為,容器映像檔也可以擴充其他容器映像檔;就像我們可以做物件組合,我們也可以把多個容器放進一個 Pod、讓它們協作,達成容器組合。

延續這個類比:Kubernetes 就像一個散布在多台主機上的 JVM,負責執行與管理容器;Init container 類似物件的建構子;DaemonSet 類似在背景執行的守護執行緒(例如 Java 的垃圾回收器);Pod 則類似一個控制反轉(Inversion of Control,IoC)的容器脈絡(例如 Spring Framework),多個執行中的物件在其中共享受管的生命週期,並可直接互相存取。

類比到這裡就差不多了,重點是:容器在 Kubernetes 中扮演根本角色,而打造模組化、可重用、單一目的的容器映像檔,是任何專案(甚至整個容器生態系)長期成功的關鍵。

除了封裝與隔離這些技術特性之外,容器在分散式應用的脈絡中代表什麼、目的又是什麼?以下是幾個看待容器的角度:

  • 容器映像檔是處理單一關注點的功能單元。
  • 容器映像檔由單一團隊擁有,並有自己的發布週期。
  • 容器映像檔是自足的,它定義並攜帶自己的執行期依賴。
  • 容器映像檔是不可變的:一旦建置完成就不再改變,只能被組態。
  • 容器映像檔有明確定義的執行期依賴與資源需求。
  • 容器映像檔有定義良好的 API 來暴露其功能。
  • 容器通常以單一 Unix 行程執行
  • 容器是可拋棄的,任何時刻都能安全地擴大或縮小規模。

除了上述特性,一個合格的容器映像檔還必須是模組化的:它被參數化,以便在各種將要執行的環境中重用,也為各種使用案例而參數化。長期來看,小而模組化、可重用的容器映像檔,會催生出更專門也更穩定的映像檔——就像程式語言世界裡優秀的可重用函式庫。

Pod#

看過容器的特性後可以發現,它們與微服務原則完美契合:一個容器映像檔提供單一功能單元、屬於單一團隊、有獨立的發布週期、並提供部署與執行期隔離。多數情況下,一個微服務對應一個容器映像檔。

然而大多數雲原生平台還提供另一個原語,用來管理一組容器的生命週期——在 Kubernetes 中,它叫 Pod。Pod 是一組容器在排程、部署與執行期隔離上的原子單元:

  • Pod 內所有容器永遠被排程到同一台主機
  • 無論是為了擴縮還是主機遷移,它們一起被部署
  • 它們可以共享檔案系統、網路與行程命名空間。

這種共同生命週期,讓 Pod 內的容器可以透過檔案系統互動,或透過 localhost 網路、乃至主機的行程間通訊機制(例如基於效能考量)來溝通。

圖 1-2:Pod 作為部署與管理的單元

在開發與建置期,一個微服務對應一個由某團隊開發與發布的容器映像檔;但在執行期,微服務是由 Pod 來代表,Pod 才是部署、配置與擴縮的單元。執行容器的唯一途徑——無論是擴縮還是遷移——都要透過 Pod 抽象。 有時一個 Pod 會包含多個容器,例如容器化的微服務在執行期使用了輔助容器(helper container),這正是「邊車」一章後續示範的情境。

Pod 的幾項特性:

  • Pod 是排程的原子單元。 排程器會試著找出一台能滿足 Pod 內所有容器需求的主機(init container 有一些細節上的差異,見「初始化容器」一章)。若你建立一個含多個容器的 Pod,排程器就得找到資源足以滿足所有容器需求總和的主機。排程流程在「自動化配置」一章詳述。
  • Pod 確保容器共置(colocation)。 拜共置之賜,同一 Pod 內的容器有額外的互動手段:共享本地檔案系統交換資料、使用 localhost 網路介面,或使用主機的 IPC 機制做高效能互動。
  • Pod 擁有一組共享的 IP 位址、名稱與連接埠範圍,由其中所有容器共用。這表示同一 Pod 內的容器必須小心設定以避免連接埠衝突——就像平行執行的 Unix 行程共享主機網路空間時必須留意一樣。

Pod 是應用棲身的 Kubernetes 原子單位,但你不會直接存取 Pod——這時 Service 就登場了。

服務(Services)#

Pod 是短暫的(ephemeral):它們可能因為擴縮、容器健康檢查失敗、節點遷移等各種原因隨時來去。Pod 的 IP 位址要等它被排程並在節點上啟動之後才會知道;若所在節點不再健康,Pod 可能被重新排程到另一個節點。這一切意味著 Pod 的網路位址可能在應用生命週期中改變,因此需要另一個原語來做發現與負載平衡。

這就是 Kubernetes Service 的角色。Service 是另一個簡單卻強大的抽象,它把服務名稱永久地綁定到一個 IP 位址與連接埠號,因而代表一個具名的應用存取入口。最常見的情境是 Service 作為一組 Pod 的入口,但不必然如此——Service 是通用原語,也可以指向 Kubernetes 叢集之外提供的功能。因此 Service 原語可用於服務發現與負載平衡,並讓實作的變更與擴縮不影響服務的使用者。細節見「服務發現」一章。

標籤(Labels)#

我們已經看到:微服務在建置期是容器,在執行期是 Pod。那麼由多個微服務組成的「應用」又是什麼?Kubernetes 提供了兩個原語幫你定義應用的概念:標籤命名空間

在微服務之前,一個應用對應單一部署單元、單一版本方案與發布週期——就是一個 .war.ear 或其他封裝格式的檔案。但應用被切分成微服務之後,各服務可獨立開發、發布、執行、重啟與擴縮,「應用」這個概念隨之淡化,不再有必須在應用層級處理的關鍵成品或活動。

儘管如此,若你仍需要標示「某些獨立服務同屬一個應用」,就可以使用標籤。想像我們把一個單體應用拆成三個微服務,另一個應用拆成兩個微服務:現在有五份 Pod 定義(實例可能更多),從開發與執行期的角度看它們彼此獨立,但我們仍需要標示前三個屬於一個應用、後兩個屬於另一個。這些 Pod 雖然獨立,卻可能為了交付商業價值而彼此依賴——例如一個 Pod 裝的是前端容器,另兩個負責提供後端功能,任何一個掛掉,從商業角度看整個應用就沒用了。使用標籤選擇器(label selector),我們就能查詢並識別一組 Pod,把它當成一個邏輯單元來管理。

圖 1-3:標籤作為 Pod 的應用身分,把分散式應用的各部分歸入特定子系統

標籤有用的幾個場景:

  • ReplicaSet 用標籤來維持特定 Pod 的若干實例處於執行狀態。這表示每份 Pod 定義都需要一組獨特的標籤組合供排程使用。
  • 排程器大量使用標籤,用來共置或分散 Pod,把 Pod 放到滿足其需求的節點上。
  • 標籤可以表示一組 Pod 的邏輯分組,賦予它們應用身分。
  • 標籤也可以用來儲存中繼資料。 很難預測某個標籤未來會派上什麼用場,因此最好有足夠的標籤來描述 Pod 的所有重要面向——例如應用的邏輯分組、商業特性與關鍵程度、特定執行期平台依賴(如硬體架構)、位置偏好等。日後排程器可以用這些標籤做更細緻的排程,或從命令列用同一批標籤大規模管理相符的 Pod。

不要過頭、預先加上太多標籤。需要時隨時可以補加;移除標籤的風險則高得多——沒有簡單的方法能查出某個標籤正被誰使用,也難以預料移除會造成什麼非預期後果。

註解(Annotations)#

另一個與標籤非常相似的原語是註解。它同樣以映射(map)形式組織,但用途是指定不可搜尋的中繼資料,且面向機器而非人類

註解上的資訊不是用來查詢與比對物件的,而是用來讓各種工具與函式庫把額外中繼資料附加到物件上。常見用途包括:建置 ID、發布 ID、映像檔資訊、時間戳記、Git 分支名稱、pull request 編號、映像檔雜湊、registry 位址、作者名稱、工具資訊等。

一句話區分:標籤主要用於查詢比對,並對相符的資源執行動作;註解用於附加可被機器消費的中繼資料。

命名空間(Namespaces)#

另一個有助於管理一組資源的原語是 Kubernetes 命名空間。從描述上看它可能像標籤,但實際上是特性與目的都截然不同的原語。

命名空間讓你把一個(通常橫跨多台主機的)Kubernetes 叢集,切分成邏輯上的資源池。它為 Kubernetes 資源提供作用域(scope),也提供一套機制,讓你能對叢集的某個子區段套用授權與其他政策。最常見的用途是代表不同的軟體環境,例如開發、測試、整合測試或正式環境;也可以用來達成多租戶(multitenancy),為團隊工作區、專案甚至特定應用提供隔離。

若要更強的環境隔離,命名空間並不足夠,改用獨立叢集才是常見做法。典型配置是:一個非正式叢集供開發、測試與整合測試使用,另一個正式叢集承載效能測試與正式環境。

命名空間的特性與適用場景:

  • 命名空間本身就是一種 Kubernetes 資源
  • 它為容器、Pod、Service、ReplicaSet 等資源提供作用域。資源名稱在同一命名空間內必須唯一,跨命名空間則不必
  • 預設情況下,命名空間只提供作用域,並不隔離資源、也不阻止跨命名空間存取。例如只要知道 Pod IP,開發命名空間的 Pod 就能存取正式命名空間的 Pod。若需要跨命名空間的真正多租戶,可透過提供網路隔離的 Kubernetes 外掛達成。
  • 有些資源(命名空間本身、節點、PersistentVolume)不屬於任何命名空間,必須有叢集層級唯一的名稱。
  • 每個 Service 都屬於某個命名空間,並取得對應的 DNS 位址,格式為 <service-name>.<namespace-name>.svc.cluster.local命名空間名稱會出現在每個 Service 的 URI 裡,這正是為它取好名字至關重要的原因之一。
  • ResourceQuota 提供限制,用來約束每個命名空間的資源消耗總量。叢集管理員可藉此控制某命名空間內各類物件的數量上限——例如開發命名空間只允許五個 ConfigMap、五個 Secret、五個 Service、五個 ReplicaSet、五個 PersistentVolumeClaim 與十個 Pod。
  • ResourceQuota 也能限制某命名空間內可請求的運算資源總和。例如在一個容量為 32 GB RAM、16 核心的叢集中,可以把一半資源(16 GB RAM、8 核心)配給正式命名空間,8 GB RAM 與 4 核心給預備(staging)環境,4 GB RAM 與 2 核心給開發,測試命名空間同量。

討論#

以上只是簡要涵蓋本書會用到的幾個主要 Kubernetes 概念,開發者日常還會用到更多原語。舉例來說,當你建立一個容器化服務時,可以搭配一整組 Kubernetes 物件來取得平台的全部好處。要留意的是,這些只是應用開發者用來把容器化服務整合進 Kubernetes 的物件;另外還有一批概念是管理員用來讓開發者能有效管理平台的。

圖 1-4:對開發者有用的各類 Kubernetes 資源總覽

隨著時間推移,這些新原語催生出解決問題的新方法,其中一些重複出現的解法就成了模式。本書接下來的重點,不在詳述某個 Kubernetes 資源,而在於那些已被證明為模式的 Kubernetes 面向。

更多資訊#

  • Principles of Container-Based Application Design
  • The Twelve-Factor App
  • 《Domain-Driven Design: Tackling Complexity in the Heart of Software》
  • Container Best Practices
  • Best Practices for Writing Dockerfiles
  • Container Patterns
  • General Container Image Guidelines
  • Pods