做者:姬望mysql
K8s 設計模式
Kubernetes 是一個具備廣泛意義的容器編排工具,它提供了一套基於容器構建分佈式系統的基礎依賴,其意義等同於 Linux 在操做系統中的地位,能夠認爲是分佈式的操做系統。git
自定義資源
K8s 提供了 Pod、Service、Volume 等一系列基礎資源定義,爲了更好提供擴展性,CRD 功能是在 1.7 版本被引入。程序員
用戶能夠根據本身的需求添加自定義的 Kubernetes 對象資源(CRD)。值得注意的是,這裏用戶本身添加的 Kubernetes 對象資源都是 native 的都是一等公民,和 Kubernetes 中自帶的、原生的那些 Pod、Deployment 是一樣的對象資源。在 Kubernetes 的 API Server 看來,它們都是存在於 etcd 中的一等資源。redis
同時,自定義資源和原生內置的資源同樣,均可以用 kubectl 來去建立、查看,也享有 RBAC、安全功能。用戶能夠開發自定義控制器來感知或者操做自定義資源的變化。sql
Operator
在自定義資源基礎上,如何實現自定義資源建立或更新時的邏輯行爲,K8s Operator 提供了相應的開發框架。Operator 經過擴展 Kubernetes 定義 Custom Controller,list/watch 對應的自定義資源,在對應資源發生變化時,觸發自定義的邏輯。數據庫
Operator 開發者能夠像使用原生 API 進行應用管理同樣,經過聲明式的方式定義一組業務應用的指望終態,而且根據業務應用的自身特色進行相應控制器邏輯編寫,以此完成對應用運行時刻生命週期的管理並持續維護與指望終態的一致性。編程
通俗的理解
CRD 是 K8s 標準化的資源擴展能力,以 Java 爲例,int、long、Map、Object 是 Java 內置的類,用戶能夠自定義 Class 實現類的擴展,CRD 就是 K8s 中的自定義類,CR 就是對應類的一個 instance。設計模式
Operator 模式 = 自定義類 + 觀察者模式,Operator 模式讓你們編寫 K8s 的擴展變得很是簡單快捷,逐漸成爲面向 K8s 設計的標準。
api
Operator 提供了標準化的設計流程:安全
- 使用 SDK 建立一個新的 Operator 項目;
- 經過添加自定義資源(CRD)定義新的資源 API;
- 指定使用 SDK API 來 watch 的資源;
- 自定義 Controller 實現 K8s 協調(reconcile)邏輯;
有了錘子,看到的只有釘子
咱們團隊(KubeOne 團隊)一直在致力於解決複雜中間件應用如何部署到 K8s,天然也是 Operator 模式的踐行者。經歷了近 2 年的開發,初步解決了中間件在各個環境 K8s 的部署,當前中間也走了不少彎路,踩了不少坑。
KubeOne 內核也經歷 3 個大版本的迭代,前 2 次開發過程基本都是 follow Operator 標準開發流程進行開發設計。遵循一個標準的、典型的 Operator 的設計過程,看上去一切都是這麼的完美,可是每次設計都很是痛苦,踐行 Operator 模式以後,最值得反思和借鑑的就是」有了錘子,看到的只有釘子,簡單總結一下就是 4 個一切:
- 一切設計皆 YAML;
- 一切皆合一;
- 一切皆終態;
- 一切交互皆 cr。
誤區1:一切設計皆 YAML
K8s 的 API 是 YAML 格式,Operator 設計流程也是讓你們首先定義 CRD,因此團隊開始設計時直接採用了 YAML 格式。
案例
根據標準化流程,團隊面向 YAML 設計流程大致以下:
一、先根據已知的數據初步整理一個大而全的 YAML,作一下初步的分類,例如應用大概包含基礎信息,依賴服務,運維邏輯,監控採集等,每一個分類作一個子部分。
二、開會討論具體的內容是否能知足要求,結果每次開會都難以造成共識。
- 由於老是有新的需求知足不了,在討論A時,就有人提到 B、C、D,不斷有新的需求;
- 每一個部分的屬性很是難統一,由於不一樣的實現屬性差別較大;
- 理解不一致,相同名字但使用時每一個人的理解也不一樣;
三、因爲工期很緊,只能臨時妥協,作一箇中間態,後面再進一步優化。
四、後續優化升級,相同的流程再來一遍,仍是很難造成共識。
這是第 2 個版本的設計:
apiVersion: apps.mwops.alibaba-inc.com/v1alpha1 kind: AppDefinition metadata: labels: app: "A" name: A-1.0 //chart-name+chart-version namespace: kubeone spec: appName: A //chart-name version: 1.0 //chart-version type: apps.mwops.alibaba-inc.com/v1alpha1.argo-helm workloadSettings: //注 workloadSettings 標識type應該使用的屬性 - name: "deployToK8SName" value: "" - name: "deployToNamespace" value: ${resources:namespace-resource.name} parameterValues: //注 parameterValues標識業務屬性 - name: "enableTenant" value: "1" - name: "CPU" value: "1" - name: "MEM" value: "2Gi" - name: "jvm" value: "flag;gc" - name: vip.fileserver-edas.ip value: ${resources:fileserver_edas.ip} - name: DB_NAME valueFromConfigMap: name: ${resources:rds-resource.cm-name} expr: ${database} - name: DB_PASSWORD valueFromSecret: name: ${instancename}-rds-secret expr: ${password} - name: object-storage-endpoint value: ${resources:object-storage.endpoint} - name: object-storage-username valueFromSecret: name: ${resources:object-storage.secret-name} expr: ${username} - name: object-storage-password valueFromSecret: name: ${resources:object-storage.secret-name} expr: ${password} - name: redis-endpoint value: ${resources:redis.endpoint} - name: redis-password value: ${resources:redis.password} resources: - name: tolerations type: apps.mwops.alibaba-inc.com/tolerations parameterValues: - name: key value: "sigma.ali/is-ecs" - name: key value: "sigma.ali/resource-pool" - name: namespace-resource type: apps.mwops.alibaba-inc.com/v1alpha1.namespace parameterValues: - name: name value: edas - name: fileserver-edas type: apps.mwops.alibaba-inc.com/v1alpha1.database.vip parameterValues: - name: port value: 21,80,8080,5000 - name: src_port value: 21,80,8080,5000 - name: type value: ClusterIP - name: check_type value: "" - name: uri value: "" - name: ip value: "" - name: test-db type: apps.mwops.alibaba-inc.com/v1alpha1.database.mysqlha parameterValues: - name: name value: test-db - name: user value: test-user - name: password value: test-passwd - name: secret value: test-db-mysqlha-secret - name: service-slb type: apps.mwops.alibaba-inc.com/v1alpha1.slb mode: post-create parameterValues: - name: service value: "serviceA" - name: annotations value: "app:a,version:1.0" - name: external-ip value: - name: service-resource2 type: apps.mwops.alibaba-inc.com/v1alpha1.service parameterValues: - name: second-domain value: edas.console - name: ports value: "80:80" - name: selectors value: "app:a,version:1.0" - name: type value: "loadbalance" - name: service-dns type: apps.mwops.alibaba-inc.com/v1alpha1.dns parameterValues: - name: domain value: edas.server.${global:domain} - name: vip value: ${resources:service-resource2.EXTERNAL-IP} - name: dns-resource type: apps.mwops.alibaba-inc.com/v1alpha1.dns parameterValues: - name: domain value: edas.console.${global:domain} - name: vip value: 「127.0.0.1」 - name: cni-resource type: apps.mwops.alibaba-inc.com/v1alpha1.cni parameterValues: - name: count value: 4 - name: ip_list value: - name: object-storage type: apps.mwops.alibaba-inc.com/v1alpha1.objectStorage.minio parameterValues: - name: namespace value: test-ns - name: username value: test-user - name: password value: test-password - name: storage-capacity value: 20Gi - name: secret-name value: minio-my-store-access-keys - name: endpoint value: minio-instance-external-service - name: redis type: apps.mwops.alibaba-inc.com/v1alpha1.database.redis parameterValues: - name: cpu value: 500m - name: memory value: 128Mi - name: password value: i_am_a_password - name: storage-capacity value: 20Gi - name: endpoint value: redis-redis-cluster - name: accesskey type: apps.mwops.alibaba-inc.com/v1alpha1.accesskey parameterValues: - name: name value: default - name: userName value: ecs_test@aliyun.com exposes: - name: dns value: ${resources:dns-resource.domain} - name: db-endpoint valueFromConfigmap: name: ${resources:rds-resource.cm-name} expr: ${endpoint}:3306/${database} - name: ip_list value: ${resources:cni-resource.ip_list} - name: object-storage-endpoint value: ${resources:object-storage.endpoint}.${resource:namespace-resource.name} - name: object-storage-username valueFromSecret: name: ${resources:object-storage.secret-name} expr: ${username} - name: object-storage-password valueFromSecret: name: ${resources:object-storage.secret-name} expr: ${password} - name: redis-endpoint value: ${resources:redis.endpoint}.${resource:namespace-resource.name} - name: redis-password value: ${resources:redis.password}
反思
這樣的痛苦難以用語言表達,感受一切都脫離了掌控,沒有統一的判斷標準,設計標準,公說公有理婆說婆有理,內容一直加,字段一直改。事不過三,第三次設計時,咱們集體討論反思爲何這麼難造成共識?爲何每一個人理解不一樣?爲何老是在改?
結論很一致,沒有面向 YAML 的設計,只有面向對象的設計,設計語言也只有 UML,只有這些歷經考驗、成熟的設計方法論,纔是最簡單也是最高效的。
從上面那個一個巨大無比的 YAML 你們能夠體會咱們設計的複雜,可是這仍是不是最痛苦的。最痛苦的是你們拋棄了原有的設計流程及設計語言,試圖使用一個開放的 Map 來描述一切。當設計沒有對象,也沒有關係,只剩下 Map 裏一個個屬性,也就無所謂對錯,也無所謂優劣。最後爭來爭去,最後不過是再加一個字段,爭了一個寂寞。
適用範圍
那 Operator 先設計 CRD,再開發 controller 的方式不正確嗎? 答案:部分正確。
一、適用場景
與 Java Class 相同,簡單對象不須要通過複雜的設計流程,直接設計 YAML 簡單高效。
二、不適用場景
在設計一個複雜的體系時,例如:應用管理,包含多個對象且對象之間有複雜的關係,有複雜的用戶故事,UML 和麪向對象的設計就顯得很是重要。
設計時只考慮 UML 和領域語言,設計完成後,CRD 能夠認爲是 Java 的 Class,或者是數據庫的表結構,只是最終要實現時的一種選擇。並且有不少對象不須要持久化,也不須要經過 Operator 機制觸發對應的邏輯,就不須要設計 CRD,而是直接實現一個 controller 便可。
YAML 是接口或 Class 聲明的一種格式化表達,常規 YAML 要儘量小,儘量職責單一,儘量抽象。複雜的 YAML 是對簡單 CRD 資源的一種編排結果,提供相似一站式資源配套方案。
在第 3 個版本及 PaaS-Core 設計時,咱們就採起了以下的流程:
- UML 用例圖;
- 梳理用戶故事;
- 基於用戶故事對齊 Domain Object,肯定關鍵的業務對象以及對象間關係;
- 須要 Operator 化的對象,每一個對象描述爲一個 CRD,固然 CRD 缺少接口、繼承等面向對象的能力,能夠經過其餘方式曲線表達;
- 不須要 Operator 化的對象,直接編寫 Controller;
誤區2:一切皆合一
爲了保證一個應用的終態,或者爲了使用 gitops 管理一個應用,是否應該把應用相關的內容都放入一個 CRD 或一個 IAC 文件?根據 gitops 設計,每次變動時須要下發整個文件?
案例
案例1: 應用 WordPress,須要依賴一個 MySQL,終態如何定義?
apiVersion: apps.mwops.alibaba-inc.com/v1alpha1kind: AppDefinitionmetadata: labels: app: "WordPress" name: WordPress-1.0 //chart-name+chart-version namespace: kubeonespec: appName: WordPress //chart-name version: 1.0 //chart-version type: apps.mwops.alibaba-inc.com/v1alpha1.argo-helm parameterValues: //注 parameterValues標識業務屬性 - name: "enableTenant" value: "1" - name: "CPU" value: "1" - name: "MEM" value: "2Gi" - name: "jvm" value: "flag;gc" - name: replicas value: 3 - name: connectstring valueFromConfigMap: name: ${resources:test-db.exposes.connectstring} expr: ${connectstring} - name: db_user_name valueFromSecret: .... resources: - name: test-db //建立一個新的DB type: apps.mwops.alibaba-inc.com/v1alpha1.database.mysqlha parameterValues: - name: cpu value: 2 - name: memory value: 4G - name: storage value: 20Gi - name: username value: myusername - name: password value: i_am_a_password - name: dbname value: wordPress exposes: - name: connectstring - name: username - name: password exposes: - name: dns value: ...
上方的代碼是 wordPress 應用的終態嗎?這個文件包含了應用所須要的 DB 的定義和應用的定義,只要一次下發就能夠先建立對應的數據庫,再把應用拉起。
案例2:每次變動時,直接修改整個 yaml 的部份內容,修改後直接下發到 K8s,引發沒必要要的變動。例如:要從 3 個節點擴容到 5 個節點,修改上面 YAML 文件的 replicas 以後,須要下發整個 YAML。整個下發的 YAML 通過二次解析成底層的 StatefulSet 或 Deployment,解析邏輯升級後,可能會產生不符合預期的變化,致使全部 Pod 重建。
反思
先回答第一個問題,上方 YAML 文件不是應用的終態,而是一個編排,此編排包含了 DB 的定義和應用的定義。應用的終態只應該包含本身必須的依賴引用,而不包含依賴是如何建立的。由於這個依賴引用能夠是新建立的,也能夠是一個已有的,也能夠是手工填寫的,依賴如何建立與應用終態無關。
apiVersion: apps.mwops.alibaba-inc.com/v1alpha1 kind: AppDefinition metadata: labels: app: "WordPress" name: WordPress-1.0 //chart-name+chart-version namespace: kubeone spec: appName: WordPress //chart-name version: 1.0 //chart-version name: WordPress-test type: apps.mwops.alibaba-inc.com/v1alpha1.argo-helm parameterValues: //注 parameterValues標識業務屬性 - .... resources: - name: test-db-secret value: "wordPress1Secret" //引用已有的secret exposes: - name: dns value: ...
建立一個應用,就不能先建立 db,再建立應用嗎?
能夠的,多個對象之間依賴是經過編排實現的。編排有單個應用建立的編排,也有一個複雜站點建立的編排。以 Argo 爲例:
apiVersion: argoproj.io/v1alpha1 kind: Workflow metadata: generateName: wordPress- spec: templates: - name: wordPress steps: # 建立db - - name: wordpress-db template: wordpress-db arguments: parameters: [{name: wordpress-db1}] # 建立應用 - - name: template: wordpress arguments: parameters: [{db-sercet: wordpress-db1}]
針對第 2 個案例,是否每次交互都須要下發所有完整的 YAML?
答案:
- 編排是一次性的配置,編排文件下發一次以後,後續操做都是操做單個對象,例如:變動時,只會單獨變動 wordPress,或單獨變動 wordPressDB,而不會一次性同時變動 2 個對象。
- 單獨變動應用時,是否須要下發整個終態 YAML,這個要根據實際狀況進行設計,值得你們思考。後面會提出針對整個應用生命週期狀態機的設計,裏面有詳細的解釋。
適用範圍
一、適用場景
CRD 或 IAC 定義時,單個對象的終態只應該包含自身及對依賴的引用。與面向對象的設計相同,咱們不該該把全部類的定義都放到一個 Class 裏面。
二、不適用場景
多個對象要一次性建立,而且須要按照順序建立,存在依賴關係,須要經過編排層實現。
誤區3:一切皆終態
體驗了 K8s 的終態化以後,你們在設計時言必稱終態,彷彿不能用上終態設計,不下發一個 YAML 聲明對象的終態就是落伍,就是上一代的設計。
案例
案例1:應用編排
仍是以 WordPress 爲例,將 WordPressDB 和 WordPress 放在一塊兒進行部署,先部署 DB,再建立應用。示例 YAML 同上。
案例2:應用發佈
應用第一次部署及後續的升級直接下發一個完整的應用 YAML,系統會自動幫你到達終態。但爲了可以細粒度控制發佈的流程,努力在 Deployment 或 StatefulSet 上下功夫,進行 partition 的控制,試圖在終態裏增長一點點的交互性。
反思
說到終態,必然要提到命令式、聲明式編程,終態其實就是聲明式最終的執行結果。咱們先回顧一下命令式、終態式編程。
一、命令式編程
命令式編程的主要思想是關注計算機執行的步驟,即一步一步告訴計算機先作什麼再作什麼。
好比:若是你想在一個數字集合 collection(變量名) 中篩選大於 5 的數字,你須要這樣告訴計算機:
-
第一步,建立一個存儲結果的集合變量 results;
-
第二步,遍歷這個數字集合 collection;
-
第三步,一個一個地判斷每一個數字是否是大於 5,若是是就將這個數字添加到結果集合變量 results 中。
代碼實現以下:
List results = new List(); foreach(var num in collection) { if (num > 5) results.Add(num); }
很明顯,這個樣子的代碼是很常見的一種,無論你用的是 C、C++ 仍是 C#、Java、Javascript、BASIC、Python、Ruby 等,你均可以以這個方式寫。
二、聲明式編程
聲明式編程是以數據結構的形式來表達程序執行的邏輯。它的主要思想是告訴計算機應該作什麼,但不指定具體要怎麼作。
SQL 語句就是最明顯的一種聲明式編程的例子,例如:
SELECT * FROM collection WHERE num > 5
除了 SQL,網頁編程中用到的 HTML 和 CSS 也都屬於聲明式編程。
經過觀察聲明式編程的代碼咱們能夠發現它有一個特色是它不須要建立變量用來存儲數據。
另外一個特色是它不包含循環控制的代碼如 for, while。
換言之:
-
命令式編程:命令「機器」如何去作事情(how),這樣無論你想要的是什麼(what),它都會按照你的命令實現。
-
聲明式編程:告訴「機器」你想要的是什麼(what),讓機器想出如何去作(how)。
當接口越是在表達「要什麼」,就是越聲明式;越是在表達「要怎樣」,就是越命令式。SQL就是在表達要什麼(數據),而不是表達怎麼弄出我要的數據,因此它就很「聲明式」。
簡單的說,接口的表述方式越接近人類語言——詞彙的串行鏈接(一個詞彙其實是一個概念)——就越「聲明式」;越接近計算機語言——「順序+分支+循環」的操做流程——就越「命令式」。
越是聲明式,意味着下層要作更多的東西,或者說能力越強,也意味着效率的損失。越是命令式,意味着上層對下層有更多的操做空間,能夠按照本身特定的需求要求下層按照某種方式來處理。
簡單的講,Imperative Programming Language (命令式語言)通常都有 control flow, 而且具備能夠和其餘設備進行交互的能力。而 Declarative Programming language (聲明式語言) 通常作不到這些。
基於以上的分析,編排或工做流本質是一個流程
性控制的過程,通常是一次性的過程,無需強行終態化,並且建站編排執行結束後,不能保持終態,由於後續會根據單個應用進行發佈和升級。案例1是一個典型的編排,只是一次性的建立了 2 個對象 DB 和應用的終態。
應用發佈實際上是經過一個發佈單或工做流,控制 2 個不一樣版本的應用節點和流量的終態化的過程,不該該是應用終態的一部分,而是一個獨立的控制流程。
適用範圍
聲明式或終態設計。
一、適用場景
無過多交互,無需關注底層實現的場景,即把聲明提供給系統後,系統會自動化達到聲明所要求的狀態,而不須要人爲干預。
二、不適用場景
一次性的流程編排,有頻繁交互的控制流程。
命令式和聲明式本就是 2 種互補的編程模式,就像有了面向對象以後,有人就鄙視面向過程的編程,如今有了聲明式,就開始鄙視命令式編程,那一屋!
誤區4:一切交互皆 cr
由於 K8s 的 API 交互只能經過 YAML,致使你們的設計都以 cr 爲中心,全部的交互都設計爲下發一個 cr,經過 watch cr 觸發對應的邏輯。
案例
- 調用一個 http 接口或 function,須要下發一個 cr;
-
應用 crud 都下發完整 cr;
反思
案例1:是否全部的邏輯都須要下發一個 cr?
下發 cr 其實作了比較多的事情,流程很長,效率並不高,流程以下:
-
經過 API 傳入 cr,cr 保存到 etcd;
-
觸發 informer;
-
controller 接收到對應的事件,觸發邏輯;
-
更新 cr 狀態;
-
清理 cr,不然會佔用 etcd 存儲;
若是須要頻繁的調用對應的接口,儘可能經過 sdk 直接調用。
案例2:
K8s 對 YAML 操做命令有 create、apply、patch、delete、get 等,但一個應用的生命週期狀態機不僅是這幾個命令能夠涵蓋,咱們比較一下應用狀態機(上)和 YAML 狀態機(下):
不一樣的有狀態應用,在收到不一樣的指令,須要觸發不一樣的邏輯,例如:MQ 在收到 stop 指令時,須要先停寫,檢查數據是否消費完成。若是隻是經過 YAML 狀態機是沒法涵蓋應用狀態機相關的 event,因此咱們必須打破下發 cr 的模式。對於應用來講,理想的交互方式是經過 event driven 應用狀態機的變化,狀態發生變換時觸發對應的邏輯。
適用範圍
一、適用場景
須要持久化,保持終態的數據。
二、不適用場景
-
高頻的服務調用,無需持久化的數據。
-
複雜狀態機的驅動。
總結
K8s 給咱們打開了一扇門,帶給了咱們不少優秀的設計,優秀的理念,可是這些設計和理念也是有本身的適用的場景,並非放之四海而皆準。咱們不該該盲從,試圖一切都要 follow K8s 的設計和規則,而拋棄以前的優秀設計理念。
軟件設計經歷了 10 多年的發展,造成了一套行之有效的設計方法論,K8s 也是在這些設計方法論的支持下設計出來的。取其精華去其糟粕,是咱們程序員應該作的事情。
參考文章:
- 揭祕 Kubernetes Operator: http://www.dockone.io/article/8769
- 聲明式編程和命令式編程有什麼區別 : https://www.zhihu.com/question/22285830
- 如何在 Kubernetes 中編寫自定義控制器: https://www.sohu.com/a/363619791_198222