通往Cloud Native的道路
我們以Kubernetes這種微服務為主的架構,簡單的介紹企業如何走向這種Application development的模式。
這種微服務(Kubernetes,以下稱K8S)架構主要是將開發技術通過將業務需求的各項功能模組化以及將開發複雜性轉變成維運面的複雜性來解決軟體在開發時的複雜性。簡單來說以前開發人員在寫程式時除了要考量業務功能面時還要加上其他非功能面的問題一併寫入代碼裡面。但變成微服務後,功能模組化了而其發非功能面的問題也下放微服務架構(維運面)來負責了。
現今有關於的理論與其他補充與加強微服務的技術也是一大堆。但大部分的實踐都來自Domain Driven design 這本書,其中兩個重要的概念是 bounded contexts and aggregates。bounded contexts概念處裡的是把大的models切分成不同的components。而aggregates做的則是相反的事,將bounded context group起來成為一個modules,但這個modules是帶有transaction boundaries特性的。不過上述的都是理論,每一個公司組織的分散式系統(不論是微服務或不是)都是基於它自己本身的組織架構與組織運作時員工的行為(處理方式)都有關。也就是理論僅供參考,實際的運作就有可能需要作一些變動。
像K8S這種分散式系統同時也是containers orchestrators提供很多新的primitives 與abstractions來解決我們將Application運行在分散式系統會有的顧慮,K8S提供多種的選項來解決這些運行上的顧慮。這篇文章中我們將container視為是一個black boxes來看整個K8S platform與container的交互。 但是放在containers裡面的東西(code)也是很重要的。Containers and cloud-native platforms帶來了很大的效益,但如果你放垃圾在container裡,哪你得到的也會一堆垃圾的containers。
下圖為一個在High level角度來看我們真的要運行Cloud native Application有哪些面向需要考量
最底層code level(Clean code)
每一個我們定義的變數,每一個我們建立的method,以及每一個我們決定的class在我們將代碼跑起來後都會在這一個程式的長期維護中起到重要的角色。開發人員或團隊如何寫出好的Application運行在K8S是對整個平台會有至關影響的衝擊。寫出clean code,有足夠時間做自動化測試,可能需要不斷的refactor來增進代碼的品質,這些都是需要謹記在心的。
Domain driven design
這是一種從業務的角度進行software design方法,目的是使整個軟體架構盡量接近真實世界。這種方式最適合物件導向程式的語言,但是還有其他一些很好的方法可以為實際問題model和software design。具有正確的業務和transaction boundaries,易於使用的interface以及豐富的API的模型是以後成功進行容器化和自動化的基礎。
Microservice architectural style
Microservice architectural style現在成為一種規範,Microservice architectural style它是為設計不斷變化的分佈式應用程序提供了寶貴的原理和practices。應用這些原理,可以創建針對規模,彈性和變化速度進行了優化的實現,這是現在任何現代化的軟體的常見要求。
Containers
Container現在已是被採用為打包和運行分佈式應用程序的標準方法。創建modular,可重複使用的,具有良好cloud-native containers是另一個基本先決條件。隨著每個企業組織中容器的數量不斷增加,需要使用更有效的方法和工具來管理它們。Cloud native已經是一個流行的術語,用於描述原理,模式和工具以大規模自動化容器化微服務。K8S是當今最流行的cloud-native platform.
但是我們這一篇只聚焦在container orchestration的pattern and practices.
Distributed Primitives
為了解是甚麼是新的abstractions and primitives,我們會來比較大家熟知的物件導向程式(OOP)與JAVA。在OOP的世界裡一般我們會有的概念有class/object/package/inheritance/encapsulation/polymorphism等。而JAVA runtime提供特別的功能並且保證它如何管理object和整個application的lifecycle.
The Java language and the Java Virtual Machine (JVM)在create application提供了一個local, in-process building blocks。K8S通過提供一組新的分佈式primitives和runtime來為跨多個modes和process分佈的分佈式系統提供新的維度,從而為這一眾所周知的思維方式增加了新的維度。在K8S,我們不僅僅依賴於local primitives來實現整個應用程序行為。我們仍然需要使用object-oriented building blocks來創建分佈式應用程序的components,但是對於某些應用程序行為,我們也可以使用K8S primitives。
下圖顯示瞭如何使用local和分佈式primitives以不同方式實現各種開發概念。
上面兩種primitive有些有共通性,但不是可以直接拿來比較或是取代的。它們是運作在不同levle的abstracion的。有些primitive是應該被放在一起的,例如我們是必須使用class來create object並將它們放入contain image中。但還是有一些是可以被取代的,例如kuberbetes 的CronJob可以完全取代JAVA的ExecutorService.接下來我們介紹K8S比較基本的部分
Containers
conatiner是基於K8S的cloud native application的blocks。如果我們拿OOP與JAVA來比較,conatiner image類似classes,container類似objects。相同的我們可以extent classes來達到reuse/alter behavior, container images也可以extent到其他的container images來達到reuse/alter behavior。同樣的我們可以使用object composition與使用它的功能性,把containers放到Pod 裡面並使這個Pod裡的conatiner能夠協同作業來達成container composition。
如果我們進一步來比較的話,K8S會很像是JVM但是這個JVM是橫跨多台機器而且會負責運作與管理裡面的containers。Init containers像是object constructors, DaemonSetsy則像是在background運行的daemon threads(例如像是Java Garbage collection)。Pod類似一個 Inversion of control(IoC) context(例如 Spring Framework),這個context中多個運行的objects share一個managed lifecycle並且可以直接互相access.
Container在K8S裡是最小的單位,它create 了 modularized, reusable, single-purpose 的container image. 哪container在整個分散式程式裡代表了甚麼呢?以下有幾個觀點可以提供參考
- Container image 是解決單個問題的功能單元
- Container image 是由一個team來own的,並且具有發佈的週期
- Container image是self-contained的,並定義帶有其運行時相關性
- Container image是immutable,而且它build之後是不會更改:它是可以被configure的
- Container image已定義了運行時相關性和資源要求
- Container image具有定義明確的API並expose 它的相關功能
- Container image通常作為單一個Unix process運行
- Container image是一次性的,可以隨時放大或縮小
除了以上這些特徵,適當的container image是modular的。它已被參數化並create以在不同運行的環境中重複使用。但是,它也因其各種use case而被參數化。從長遠來看,具有小型,modular和可重複使用的container image 可導致create 更專業,更穩定的create image ,這與寫程式的世界中的大型可重複使用的library相似。
Pods
我們檢視container的特性,我們可以發現它們是實現微服務原則的完美匹配。container image提供single unit功能並是一個團隊擁有的,具有獨立的發布週期,並提供部署和運行時隔離。在大多數情況下,一種微服務對應於一個container image。
但是,大多數cloud native platform為管理一組container的生命週期提供了另一個primative,在K8S中稱為Pod。Pod是一組(群)container的scheduling,deployment和運行時隔離的基本單位(你可以把它視為是K8S的第二個最小單位)。Pod中的所有conatiner總是安排在同一host(vm or physical machine)上,為了scaling或host migration目的而一起部署,並且還可以共享filesystem,networking和process namespace。這個joint lifecycle允許Pod中的container在filesystem上進行interact,或者在需要時通過local host或local host inter-communication機制通過networking進行interact。
如下圖所示,在開發和build時,微服務對應於一個團隊開發和發布的container image。但是在運行時,微服務由Pod呈現,這一個Pod是deployment,和placement的單位。運行container的唯一方法(無論是用於scaling還是migration)都是通過Pod abstraction。有時,一個Pod可能包含多個container。一個這樣的例子是當容器化的微服務在運行時使用helper container(Sidecar概念我們會在其他文章提到)時。
Container和Pod及其獨特特性為設計基於微服務的應用程序提供了一組新的模式和原理。我們來看一下設計良好的container的一些特徵
- Pod是scheduling的基本單位。這意味著scheduling程序試圖找到一個滿足這個Pod裡面所有container要求的host。如果我們create具有多個container的Pod,則scheduling程序需要找尋具有足夠資源以滿足所有容器需求組合的host(若沒有就會啟動一個新的host)。
- Pod可確保container的colocation。由於colocation,同一Pod中的container具有其他container互相溝通的方式。最常見的溝通方式包括使用共享的local filesystem交換資料或使用localhost network interface,或使用某些host 的inter-process communication(IPC)機制進行高效能的溝通
- Pod具有IP address,name和 port range,該IP address, name 和port range由屬於該Pod的所有container共享。這意味著必須仔細配置同一Pod中的container,以免發生port發生衝突,就像在主機上共享networking space時必須平行處理Unix process時要ㄧ樣小心。
Pod是K8S的atom,你的Application跑在裡面,但你不能直接訪問Pod,這是Services負責的部分。
Services
Pod不是永久在run的它是浮動的,它們可以出於各種原因(例如,scaling up and down,container運行狀況health check失敗以及node migration)隨時隨地來來去去的。Pod IP address只有在node 上被schedule並且啟動後才知道。如果Pod運行於現有node有問題,則可以將其重新安排到其他node。這意味著Pod的network address可能會在Application的整個生命週期內發生變化,並且需要用於discovery和load balancing的另一個primitive。
這就是K8S服務發揮作用的地方。Services是另一個簡單但功能強大的K8S abstraction,它將Service name永久綁定到IP address和 port number。因此,Services代表用於aaccess Application的命名的入口點。在最常見的情況下,Services充當一組Pod的入口點,但並非總是如此。該服務是一個通用primitive,它也可能指向K8S cluster外部提供的功能。這樣,Services primitive可用於Service Discovery和Load Balancing,並允許在不影響服務使用者的情況下進行更版/修改/scaling等動作。
Labels
我們已經看到,微服務在build時是一個container,但在運行時由Pod來呈現。那麼,由多個微服務組成的Application又是什麼呢?在這裡,K8S提供了兩個可以幫助你定義Application概念的primitive:labels和namespace。這一段我們先介紹labels.
在微服務之前的時代,Application對應於具有single version scheme和發布週期的single deployment unit。Application只有一個file,格式可能是.war,.ear或其他打包格式。但是隨後,Application被分成了微服務,這些微服務是獨立開發,發布,運行,重新啟動或sacled的。使用微服務時,Application的概念將減少,並且不再需要在Application程級執行的關鍵的artifacts或actives。但是,如果仍然需要一種方法來指示某些獨立services屬於某個Application,則可以使用labels。假設我們已經將一個整體Application劃分為三個微服務,而另一個Application劃分為兩個微服務。
現在,我們有五個Pod definitions(可能還有更多Pod instance),它們與開發時與運行時的觀點(point of view)是不相關的。但是,我們可能仍需要指出前三個Pod代表一個Application,而其他兩個Pod代表另一個Application。甚至Pod可能是獨立的,以提供business value,但它們之間可能是有dependence。例如,一個Pod可能包含負責frontend的container,而另外兩個Pod則負責提供backend的功能。如果這兩個Pod中的任何一個發生故障,那麼從業務角度來看,該Application是沒有business value的。使用label selectors使我們能夠查詢和識別一組Pod,並將其作為一個logical unit進行管理。下圖顯示瞭如何使用labels將distributed application的各個部分分組到特定子系統中.
以下有一些使用label有用的例子
- Lables由ReplicaSets用於保持特定Pod的某些instance運行。這意味著每個Pod definition都必須具有用於scheduling的唯一labels組合
- Scheduling程序是重度的使用labels這個功能。scheduling程序使用labels將Pod co-locating或spreading,以將Pod放在能滿足Pods運行需求的nodes上。
- Label可以指示Pod sets的logical grouping,並為其賦予Application label
- 除了前面的典型use case之外,labels還可用於存儲metadata。標籤的用途非常多,但是最好有足夠的標籤來描述Pod的所有重要性。例如,使lables具有指示Application的logical group,業務特性和關鍵性,特定的運行時platform dependencies(例如你可以讓重要的Pod跑在比較新的機機器上)。
之後,scheduling程序可以將這些labels用於fine-grained的scheduling,或者可以從command line使用相同的label來大規模管理匹配的Pod。但是,我們不應該過分提前添加太多lable。可以根據需求隨時加上新的lable. 但刪除lable的風險會更大,因為沒有簡單明瞭的方法來找出label的用途以及這個行動可能導致的意外後果。
Annotations
與labels非常相似的另一個primitive稱為Annotations。像labels一樣,Annotation被組織為map,但是它們主要在用於指定不可搜索的metadata以及供機器使用,而不是人用的。
Annotations上的資訊不適用於查詢和matching objects。相反,它主要是要將其他metadata附加到我們要使用的各種工具和libraries中的目標。使用Annotations的一些範例包括 build IDs, release IDs, image information, timestamps, Git branch names, pull request numbers, image hashes, registry addresses, author names, tooling information。因此,雖然lables主要用於query match和對匹配資源執行操作,但是annotations用於附加可以由機器使用的metadata.
Namespace
K8S namespace是另一個可以幫助管理一組資源的primitive。正如我們已經描述的那樣,namespace似乎類似於labels,但是實際上,它是一個非常不同的primitive,具有不同的特性和用途。
K8S namespace允許將K8S cluster(通常分佈在多個主機上)劃分為邏輯上resource pool。namespace提供了K8S資源的scope,並提供了一種將授權和其他策略應用於集群的subsection機制。namespace最常見的use case是代表不同的software 環境,例如開發,測試,UAT或Production。namespace還可以用於實現多租戶,並為每個團隊建立獨立的workspace,project甚至特定的Application而這些都是隔離起來的環境。但是最終,為了更好地隔離某些環境,namespace其實是不夠的,如果真的是重要的環境(例如Production)擁有單獨的K8S cluster是很常見的。
以下是namespace的一些特性,以及它們如何在不同情況下為我們提供幫助:
- namespace作為K8S資源進行管理
- namespace為諸如containers, Pods, Services, ReplicaSet之類的resource劃定了一個scope。resource name在namespace中必須唯一,但在不同的名稱namespace可以有相同名字的resource name,不過這並不是指resource name可以橫跨不同的namespace。
- 預設情況下,namespace作為一群resource的scope,但是沒有任何東西可以隔離那些resource並阻止從一個namespace的resource access 另一個namespace的resource。例如,只要知道Pod IP address, development namespace中的Pod便可以access production namespace中的另一個Pod。但是,有一些K8S plugins可提供network isolation,以便在需要跨namespace實現真正的多租戶。
- 其他一些resource(例如namespace本身,node和PersistentVolumes)不屬於namespace,並且應具有在整個kubetnetes cluster具有唯一的名稱。
- 每個K8S services都屬於一個namespace,並獲取一個相應的DNS address,該address具有該namespace的格式為<service-name>。<namespace-name> .svc.cluster.local。因此,namespace的名稱位於屬於 given namespace的每個Service的URI中。這就是聰明的namespace至關重要的原因之一。
- ResourceQuotas提供了限制每個namespace的總和資源消耗的限制。使用ResourceQuotas,cluster admin可以控制namespace中允許的每種objetc的數量。例如,開發人員namespace可能僅允許3個ConfigMap,3個Secret,3個services,3個replicaSet,3個PersistentVolumeClaims和20個Pod。
- ResourceQuotas還可以限制我們可以在特定namespace中請求的computer資源總數。例如,在容量為128 GB RAM和32個核心的cluster中,可以為production namespace分配一半的資源-64 GB RAM和16個核心-為stageing 環境分配16 GB RAM和2個核心.通過使用名稱空間和ResourceQuotas在一組目標上施加資源約束的能力是非常powerful的。
總結
我們上面介紹了一些K8S的重要概念。但是,開發人員每天都會使用更多的primitive。例如,如果create 容器化服務,則可以使用K8S objects的collection來獲得K8S的所有效益。這些只是Application開發人員用於將容器化服務整合到K8S中的objetcs。K8S admin還使用其他概念來使開發人員能夠有效地管理平台。下圖概述了對開發人員有用的大量K8S資源。