您好,登錄后才能下訂單哦!
這期內容當中小編將會給大家帶來有關 面向K8s設計誤區是怎樣的,文章內容豐富且以專業的角度為大家分析和敘述,閱讀完這篇文章希望大家可以有所收獲。
Kubernetes 是一個具有普遍意義的容器編排工具,它提供了一套基于容器構建分布式系統的基礎依賴,其意義等同于 Linux 在操作系統中的地位,可以認為是分布式的操作系統。
K8s 提供了 Pod、Service、Volume 等一系列基礎資源定義,為了更好提供擴展性,CRD 功能是在 1.7 版本被引入。
用戶可以根據自己的需求添加自定義的 Kubernetes 對象資源(CRD)。值得注意的是,這里用戶自己添加的 Kubernetes 對象資源都是 native 的都是一等公民,和 Kubernetes 中自帶的、原生的那些 Pod、Deployment 是同樣的對象資源。在 Kubernetes 的 API Server 看來,它們都是存在于 etcd 中的一等資源。
同時,自定義資源和原生內置的資源一樣,都可以用 kubectl 來去創建、查看,也享有 RBAC、安全功能。用戶可以開發自定義控制器來感知或者操作自定義資源的變化。
在自定義資源基礎上,如何實現自定義資源創建或更新時的邏輯行為,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 設計的標準。
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。
K8s 的 API 是 YAML 格式,Operator 設計流程也是讓大家首先定義 CRD,所以團隊開始設計時直接采用了 YAML 格式。
根據標準化流程,團隊面向 YAML 設計流程大體如下:
1、先根據已知的數據初步整理一個大而全的 YAML,做一下初步的分類,例如應用大概包含基礎信息,依賴服務,運維邏輯,監控采集等,每個分類做一個子部分。
2、開會討論具體的內容是否能滿足要求,結果每次開會都難以形成共識。
因為總是有新的需求滿足不了,在討論A時,就有人提到 B、C、D,不斷有新的需求;
每個部分的屬性非常難統一,因為不同的實現屬性差異較大;
理解不一致,相同名字但使用時每個人的理解也不同;
3、由于工期很緊,只能臨時妥協,做一個中間態,后面再進一步優化。
4、后續優化升級,相同的流程再來一遍,還是很難形成共識。
這是第 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;
為了保證一個應用的終態,或者為了使用 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">
創建一個應用,就不能先創建 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 里面。
2、不適用場景
多個對象要一次性創建,并且需要按照順序創建,存在依賴關系,需要通過編排層實現。
體驗了 K8s 的終態化之后,大家在設計時言必稱終態,仿佛不能用上終態設計,不下發一個 YAML 聲明對象的終態就是落伍,就是上一代的設計。
案例
案例1:應用編排
還是以 WordPress 為例,將 WordPressDB 和 WordPress 放在一起進行部署,先部署 DB,再創建應用。示例 YAML 同上。
案例2:應用發布
應用第一次部署及后續的升級直接下發一個完整的應用 YAML,系統會自動幫你到達終態。但為了能夠細粒度控制發布的流程,努力在 Deployment 或 StatefulSet 上下功夫,進行 partition 的控制,試圖在終態里增加一點點的交互性。
說到終態,必然要提到命令式、聲明式編程,終態其實就是聲明式最終的執行結果。我們先回顧一下命令式、終態式編程。
1、命令式編程
命令式編程的主要思想是關注計算機執行的步驟,即一步一步告訴計算機先做什么再做什么。
比如:如果你想在一個數字集合 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 種互補的編程模式,就像有了面向對象之后,有人就鄙視面向過程的編程,現在有了聲明式,就開始鄙視命令式編程,那一屋!
因為 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 直接調用。
K8s 對 YAML 操作命令有 create、apply、patch、delete、get 等,但一個應用的生命周期狀態機不只是這幾個命令可以涵蓋,我們比較一下應用狀態機(上)和 YAML 狀態機(下):
不同的有狀態應用,在收到不同的指令,需要觸發不同的邏輯,例如:MQ 在收到 stop 指令時,需要先停寫,檢查數據是否消費完成。如果只是通過 YAML 狀態機是無法涵蓋應用狀態機相關的 event,所以我們必須打破下發 cr 的模式。對于應用來說,理想的交互方式是通過 event driven 應用狀態機的變化,狀態發生變換時觸發對應的邏輯。
需要持久化,保持終態的數據。
高頻的服務調用,無需持久化的數據。
復雜狀態機的驅動。
上述就是小編為大家分享的 面向K8s設計誤區是怎樣的了,如果剛好有類似的疑惑,不妨參照上述分析進行理解。如果想知道更多相關知識,歡迎關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。