本文為攜程技術分享演講内容整理而來。
早在 2011 年,阿裡内部便開始了應用容器化,當時最開始是基于 LXC 技術建構容器,然後逐漸切換到 Docker,自研了大規模編排排程系統。到了 2018 年,我們團隊依托 K8s 體系開始推進“輕量級容器化”,同時投入了工程力量跟開源社群一起解決了諸多規模與性能問題,進而逐漸将過去“類虛拟機”的運維鍊路和阿裡巴巴整體應用基礎設施架構更新到了雲原生技術棧。
到了 2019 年,Kubernetes 基礎設施底盤在阿裡巴巴經濟體中已經覆寫了阿裡巴巴方方面面的業務,規模化的接入了包括核心電商、物流、金融、外賣、搜尋、計算、AI 等諸多頭部網際網路場景。這套技術底盤,也逐漸成為了阿裡巴巴支撐 618、雙 11 等網際網路級大促的主力軍之一。
目前,阿裡巴巴與螞蟻金服内部運作了數十個超大規模的 K8s 叢集,其中最大的叢集約 1 萬個機器節點,而其實這還不是能力上限。每個叢集都會服務上萬個應用。在阿裡雲 Kubernetes 服務(ACK)上,我們還維護了上萬個使用者的 K8s 叢集,這個規模和其中的技術挑戰在全世界也是首屈一指的。
我們的 Kubernetes 面臨的新挑戰
在規模和性能等基礎設施領域的問題逐漸解決的同時,在規模化鋪開 Kubernetes 的過程中,我們逐漸發現這套體系裡面其實還有很多意想不到的挑戰。這也是今天分享的主題。
第一個是 K8s 的 API 裡其實并沒有“應用”的概念。而且,Kubernetes API 的設計把研發、運維還有基礎設施關心的事情全都糅雜在一起了。這導緻研發覺得 K8s 太複雜,運維覺得 K8s 的能力非常淩亂、零散,不好管理,隻有基礎設施團隊(也就是我們團隊)覺得 Kubernetes 比較好用。但是基礎設施團隊也很難跟研發和運維解釋清楚 Kubernetes 的價值到底是什麼。
我們來看個實際的例子。
kind: Deployment
apiVersion: apps/v1
metadata:
name: nginx-deployment
spec:
replicas: 3
selector:
matchLabels:
deploy: example
template:
metadata:
labels:
deploy: example
spec:
containers:
- name: nginx
image: nginx:1.7.9
shareProcessNamespace: false
就拿上圖中的 replica 為 3 來說,開發人員怎麼知道執行個體數應該配幾個呢?如果運維想要改replica,敢不敢改?能不能改?如果 replica 還能了解的話,那像 shareProcessNamespace 這種字段真是靈魂拷問了。 開發人員僅從字面意思知道這個可能跟容器程序共享有關,那麼配置了這個應用會有什麼影響呢?會不會有安全問題?
在阿裡巴巴内部,很多 Paas 平台隻允許開發填 Deployment 的極個别字段。為什麼允許填的字段這麼少?是平台能力不夠強嗎?其實不是的,本質原因在于業務開發根本不想了解這衆多的字段。
是以這個 PaaS 平台隻允許使用者填個别字段,其實反倒是幫助業務開發人員避免了這些靈魂拷問。但是反過來想,屏蔽掉大量字段真的就解決問題了嗎?這種情況下,整個組織的基礎設施能力還如何演進?應用開發和應用運維人員的訴求又該怎麼傳遞給基礎設施呢?
實際上,歸根到底,Kubernetes 是一個 Platform for Platform 項目,它的設計是給基礎設施工程師用來建構其他平台(比如 PaaS 或者 Serverless)用的,而不是直面研發和運維同學的。從這個角度來看,Kubernetes 的 API,其實可以類比于 Linux Kernel 的 System Call,這跟研發和運維真正要用的東西(Userspace 工具)完全不是一個層次上的。你總不能讓本來寫Java Web 的同學每天直接調用着 Linux Kernel System Call ,還給你點贊吧?
第二, K8s 實在是太靈活了,插件太多了,各種人員開發的 Controller 和 Operator 也非常多。這種靈活性,讓我們團隊開發各種能力很容易,但也使得對應用運維來說, K8s 的這些能力管理變得非常困難。比如,一個環境裡的不同運維能力,實際上有可能是沖突的。
我們來看一個例子,基礎設施團隊最近開發上線了一個新的插件,叫做 CronHPA,一個具體的 Spec 如下所示。
apiVersion: "app.alibaba.com/v1"
kind: CronHPA
metadata:
name: cron-scaler
spec:
timezone: Asia/Shanghai
schedule:
- cron: '0 0 6 * * ?'
minReplicas: 20
maxReplicas: 25
- cron: '0 0 19 * * ?'
minReplicas: 1
maxReplicas: 9
template:
spec:
scaleTargetRef:
apiVersion: apps/v1
name: nginx-deployment
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 50
作為基礎設施團隊,我們覺得這種 K8s 插件很簡單, CRD 也很容易了解。就像這個的 CronHPA 的功能,從早上六點開始到下午七點鐘這個執行個體最少有 20 個、最多有 25 個,到第二天早上六點鐘最少 1 個、最多有 9 個,在每個階段會根據 CPU 這個名額衡量調整執行個體數。
然而,就在我們美滋滋的上線這個插件不久,應用運維同學就開始跟我們抱怨了:
- “這個能力到底該怎麼使用呢?它的使用手冊在哪裡?是看 CRD 呢還是看文檔呢?”
- “我怎麼知道這個插件在某個叢集裡有沒有裝好呢?”
- “我們運維不小心把 CronHPA 和 HPA 綁定給同一個應用,結果發現這個應用是會抽風的。為什麼你們 K8s 非要等到這種沖突發生的時候才報錯呢?你們就不能設計個機制自動檢查一下這些插件的使用過程有沒有發生沖突嗎?”這個我們後來确實做了,解決方法是給我們的 K8s 加了 20 多個 Admission Hook。
第三,也是阿裡巴巴上雲之後我們團隊特别痛的一個點。我們需要處理的應用的傳遞場景,除了公有雲以外,還會有專有雲、混合雲、IoT 等各種複雜的環境。各種各樣的雲服務在這種複雜場景下,連 API 都是不統一的,這個時候我們就需要專門的傳遞團隊來進行彌補,一個一個的去對接、去傳遞應用。對他們來說這是一個非常痛苦的事情:“不是說好的 Docker 化了之後就能‘一次打包、随處運作’了嗎?”說白了,K8s 現在并沒有一個統一的、平台無關的應用描述能力。
阿裡巴巴的解決辦法
是以在 2019 年,我們團隊開始思考如何通過技術手段解決上述應用管理與傳遞相關的問題,到現在已經取得了一定得成果。
不過,在講解阿裡巴巴如何解決上述問題的方案之前,有必要先介紹一下我們推進所有這些方案的理論基礎。在這裡,我們主要遵循的是 CNCF 倡導的“
應用傳遞分層模型”,如下圖所示:

這個模型的基礎假設是:Kubernetes 本身并不提供完整的應用管理體系。換句話說,基于 K8s 的應用管理體系,不是一個開箱即用的功能,而是需要基礎設施團隊基于雲原生社群和生态自己建構出來的。這裡面就需要引入很多開源項目或者能力。而上面這個模型的一個重要作用,就是能夠把這些項目和能力,以及它們的協作關系,非常清晰的分類和和表達出來。
- 比如,Helm 就是位于整個應用管理體系的最上面,也就是第 1 層;還有 Kustomize 等各種 YAML 管理工具,CNAB 等打包工具,它們都對應在第 1.5 層。
- 然後有 Tekton、Flagger 、Kepton 等應用傳遞項目,包括釋出部署的流程、配置管理等,目前比較流行的是基于 GitOps 的管理,通過 git 作為“the source of truth”,一切都面向終态、透明化的管理,也友善對接,對應在第 2 層。
- 而 Operator ,以及 K8s 的各種工作負載元件(Deployment、StatefulSet 等),具體來說就像某個執行個體挂了這些元件自動拉起來一個彌補上原來所需要三個的執行個體數,包括一些自愈、擴縮容等能力,對應在第 3 層。
- 最後一層則是平台層,包括了所有底層的核心功能,負責對工作負載的容器進行管理,封裝基礎設施能力,對各種不同的工作負載對接底層基礎設施提供 API 等。
這些層次之間,通過互相之間的緊密協作,共同建構出一套高效、簡潔的應用管理與傳遞體系。在這些層次當中,目前阿裡巴巴在今年 KubeCon 時已經宣布開源了第三層的
OpenKruise項目。最近,我們則正在聯合微軟等更廣泛的生态,和整個社群一起推進第一層“應用定義”相關的工作。
應用定義到底該怎麼做?
其實,關于應用定義,無論是開源社群還是在阿裡巴巴内部,都已經做了不少嘗試,比如一開始我提到 Docker 解決了單機應用傳遞,它就通過 Docker 鏡像把單機應用定義的很好。
圍繞 Kubernetes 我們也試過使用
Helm以及
Application CRD來試着定義應用。但是現在的雲原生應用,它往往會依賴雲上的資源,像資料庫會依賴 RDS、通路會依賴 SLB,Helm 和 Application CRD 隻是單純地将 K8s 的 API 組合在一起,無法描述我們對雲上面資源的依賴,當我們用 CRD 來描述雲上資源依賴的時候,它其實是 freestyle 的,沒有一個很好的規範和限制,無論是使用者、開發、運維還是平台資源提供方都沒有一個共識,自然也就無法協作和複用。
另一方面,他們既然是簡單的對 K8s API 的組合,那麼 K8s API 本身“不面向應用研發和運維設計”的問題就依然存在,這并不符合我們所希望的“應用定義”應該走的方向。此外,像
這種,它雖然是 K8s 社群的項目,但是卻明顯缺乏社群活躍度,大多數修改都停留在一年前。
試了一圈,我們發現“應用定義”這個東西,在整個雲原生社群裡其實是缺失的。這也是為什麼阿裡巴巴内部有很多團隊開始嘗試設計了自己的“定義應用”。簡單地說,這個設計其實就是把應用本身的鏡像、啟動參數、依賴的雲資源等等全部描述起來,分門别類的進行放置,并通過一個模闆,最終渲染出一個配置檔案,檔案裡有上千個字段,完整描述了一個應用定義的所有内容。這個配置檔案大概長下面這個樣子:
apiVersion: v1
kind: Application
spec:
commands:
stop: ''
start: 'exec java -Xms1G -Xmx1G -jar server.jar nogui'
package:
label: v1
image: itzg/minecraft-server
rds:
engineVersion: '1.0.0'
dbInstanceClass: ''
databaseName: minecraft
account: minecraft
rdsId: 'vvks123s123scdh34flsd4'
engine: 'MySQL'
enable: false
password: ''
...
slb.internet:
Spec: slb.s1.small
slbId: '2ze7clg78xsx1g879a5yo'
protocol: http
backendPort: 80
enable: false
listenerPort: 80
...
platform:
os: linux
buildpack: Java Tomcat
category: java
network:
vpcOption:
vpcId: vpc-2zed9pncds1131savnry0zm1x8
vSwitches:
- vsw-2zeb48r2w7cdjxd4jx62x
healthCheck:
path: /
port: '8080'
retryCount: 3
timeoutSeconds: 3
type: http
intervalSeconds: 3
...
autoScaling:
scalingPolicy: release
instanceChargeType: PostPaid
userData: ''
instanceNum: 1
instanceName: craft
instanceType: []
internetMaxBandwidthIn: 100
passwordInherit: false
systemDiskSize: 100
internetChargeType: PayByTraffic
dataDiskInfo: ''
securityGroupIds: []
enableInternet: true
systemDiskCategory: ''
...
hooks:
preStart: ''
postPrepareApp: ''
preInstallStack: ''
postinit: ''
postPrepareEnv: ''
postInstallStack: ''
postStart: ''
postStop: ''
prePrepareApp: ''
...
除了基本的 Deployment 這種描述字段,這種 in-house 應用定義往往還會包含雲上資源的聲明,比如使用哪種 ECS 套餐,如何續費,使用的是哪種磁盤和規格等等一系列額外的描述。這些資源的定義是一大塊,并且上面的例子裡我們已經盡量精簡了;另一大塊就是運維能力的描述,比如自動擴縮容,流量切換、灰階、監控等,涉及到一系列的規則。
然而,你也不難看到,這種定義方式最終所有的配置還是會全部堆疊到一個檔案裡,這跟 K8s API all-in-one 的問題其實是一樣的,甚至還更嚴重了。而且,這些應用定義最終也都成為了黑盒,除了對應項目本身可以使用,其他系統基本無法複用。自然就更無法使得多方協作複用了。
吸取了這些教訓以後,我們團隊決定從另外一個方向開始設計一個新的應用定義。
具體來說,相比于其他“應用定義”給 K8s 做加法、做整合的思路,我們認為,真正良好的應用定義,應該給 K8s API 做“減法”。更準确的說,是我們應該通過“做減法”,把開發者真正關心的 API 給暴露出來,把運維、平台關心的 API 給封裝起來。
也就是說,既然 k8s API 為了友善基礎設施工程師,已經選擇把各方的關注點混在了一起。那麼,當基礎設施工程師想要基于 K8s 來服務更上層應用開發和運維人員時,其實應該考慮把這些關注點重新梳理出來,讓應用管理的各個參與方重新拿到屬于自己的 API 子集。
是以,我們開始在 K8s API 的基礎上增加了一層很薄的抽象,進而把原始的 K8s API 按照現實中的協作邏輯進行了合理的拆分和分類,然後分别暴露給研發和運維去使用。這裡的原則是:研發拿到的 API 一定是研發視角的、沒有任何基礎設施的概念在裡面;而運維拿到的 API,一定是對 K8s 能力的子產品化、聲明式的描述。這樣,在理想情況下,運維(或者平台)就能夠對這些來自雙方的 API 對象進行組合,比如:應用 A + Autoscaler X,應用 B + Ingress Y。這樣組合完成後的描述對象,其實就可以完整的來描述“應用”這個東西了。
Open Application Model (OAM)
而在同社群進行交流和驗證中,我們發現:上面的這個思路正好跟當時微軟 Brendan Burns (Kubernetes 項目創始人)和 Matt Butcher (Helm 項目創始人)團隊的思路不謀而合。是以我們雙方在面對面交流了幾次之後,很快就決定共建這個項目并且把它開源出來,跟整個社群生态一起來推進這個非常具有意義的事情。
今年 10 月 17 号,阿裡雲小邪和微軟雲 CTO Mark 共同對外宣布了這個項目的開源,它的官方名字叫做
Open Application Model(OAM),同時我們還宣布了 OAM 對應的 K8s 實作——
r udr 項目。
具體來說,在設計 OAM 的時候,我們希望這個應用定義應該解決傳統應用定義的三個問題:
- 第一,不能有運作時鎖定。一套應用定義,必須可以不加修改跑到不同運作環境當中,無論是不是基于 K8s,這是解決我們在應用傳遞時遇到的問題的關鍵。這才是真正的“一次定義、随處運作”;
- 第二,這個應用定義必須要區分使用角色,而不是繼續延續 K8s 的 all-in-one API。 我們已經深刻了解到,我們所服務的應用開發人員,實際上很難、也不想關心運維以及 K8s 底層的各種概念。我們不應該讓他們原本已經很苦逼的日子變得更糟;
- 最後一個,這個應用定義必須不是在一個 YAML 裡描述所有東西。一旦一個應用定義裡把所有資訊全部耦合在一起,就會導緻應用描述和運維描述被雜糅在一起。這就會導緻這個定義的複雜度成倍提升,也會讓這個定義完全無法複用。我們希望這些不同領域的描述能夠分開,然後平台可以自由地組合搭配。
在這個思路下,我們最後設計出來的應用定義主要分為三個大塊:
- 第一部分是應用元件的描述,包括應用元件怎麼運作和該元件所依賴的各種資源。這個部分是開發負責編寫的;
- 第二部分是運維能力的描述,比如應用怎麼 scale、怎麼通路、怎麼更新等政策。這個部分是運維負責編寫的;
- 第三部分是把上述描述檔案組合在一起的一個配置檔案。比如:“ 一個應用有兩個元件。元件 A 需要運維能力 X和能力 Y,元件 B 需要運維能力 X”。是以這個配置檔案,其實才是最終的“應用”。這個配置檔案,也是運維編寫,并且送出給平台去運作的,當然,平台也可以自動生成這個檔案。
下面我們通過執行個體來看下以上三個部分對應的 YAML 檔案到底長什麼樣子?它們究竟怎麼玩?
備注:如果你想跟我一樣實際操作體驗這個流程,你隻要在 K8s 叢集裡
裝上 rudr 項目就可以實操了。
第一部分:Component
apiVersion: core.oam.dev/v1alpha1
kind: Component
metadata:
name: helloworld-python-v1
spec:
name: helloworld-python
workloadType: core.oam.dev/v1alpha1.Server
containers:
- name: foo
image: oamdev/helloworld-python:v1
env:
- name: TARGET
fromParam: target
- name: PORT
fromParam: port
ports:
- type: tcp
containerPort: 9999
name: http
parameters:
- name: target
type: string
default: World
- name: port
type: string
default: '9999'
首先我們可以看到,Component 定義的是開發關心的事情,沒有任何運維相關的概念。
它的 Spec主要分為兩大塊:
第一個參數塊是應用描述,包括 WorkloadType 字段,這個字段就是表達應用使用什麼 Workload 運作,在我們設計裡有六種預設 Workload,分别是 Server、Worker、Job 以及它們對應的單例模式,Workload 也可以擴充。Server 代表這是一個可以自動伸縮的,并且有一個端口可以通路的模式。接下來就是容器的鏡像、啟動參數之類的,這部分包含完整的 OCI spec。
第二塊是 parameters 如何運作可擴充的參數,如環境變量和端口号。這一塊參數的特點是,它們雖然是開發定義的,但是都允許運維後續覆寫。這裡的關鍵點是,關注點分離并不等于完全割裂。是以,我們設計了 parameters 清單,其實就是希望開發能告訴運維,哪些參數後續可以被運維人員覆寫掉。這樣的話很好關聯起來了,開發人員可以向運維人員提出訴求,比如運維應該使用哪些參數、參數代表什麼意思。
像這樣一個 Component 可以直接通過 kubectl 安裝到 K8s 中。
$ kubectl apply -f examples/nginx-component.yaml
componentschematic.core.oam.dev/nginx created
然後我們可以通過 kubectl 工具檢視到已經安裝好的元件有哪些:
$ kubectl get component
NAME AGE
nginx-app 27s
helloworld-python-v1 10s
是以說,我們目前的 K8s 叢集,支援兩種“應用元件”。需要指出的是,除了我們内置支援的元件之外,開發自己可以自由定義各種各樣的元件然後送出給我們。Component Spec 裡的 Workload Type 是可以随意擴充的,就跟 K8s 的 CRD 機制一樣。
第二部分: Trait
說完了開發能用的 API,我們再來看運維用的 API 長什麼樣。
在設計應用的運維能力定義的過程中,我們重點關注的是運維能力怎麼發現和管理的問題。
為此,我們設計了一個叫做 Trait 的概念。所謂 Trait,也就是應用的“特征”,其實就是一種運維能力的聲明式描述。我們能通過指令行工具發現一個系統裡支援哪些Traits(運維能力)。
$ kubectl get traits
NAME AGE
autoscaler 19m
ingress 19m
manual-scaler 19m
volume-mounter 19m
這時候,運維要檢視具體的運維能力該怎麼使用,是非常簡單的:
$ kubectl get trait ingress -o yaml
apiVersion: core.oam.dev/v1alpha1
kind: Trait
metadata:
name: ingress
namespace: default
spec:
appliesTo:
- core.oam.dev/v1alpha1.Server
- core.oam.dev/v1alpha1.SingletonServer
properties:
- description: Host name for the ingress
name: hostname
required: true
type: string
- description: Port number on the service
name: service_port
required: true
type: int
- description: Path to expose. Default is '/'
name: path
required: false
type: string
可以看到,他可以在 Trait 定義裡清晰的看到這個運維能力可以作用于哪種類型的 Workload,包括能填哪些參數?哪些必填?哪些選填?參數的作用描述是什麼? 你也可以發現,OAM 體系裡面,Component 和 Trait 這些 API 都是 Schema,是以它們是整個對象的字段全集,也是了解這個對象描述的能力“到底能幹嗎?”的最佳途徑(反正基礎設施團隊的文檔寫的也不咋地)。
上面這些 Trait 也都是用過 kubectl apply 就可以安裝到叢集當中的。
那麼既然 Component 和 Trait 都是 Schema,那麼它們怎麼執行個體化成應用呢?
第三部分:Application Configuration
在 OAM 體系中,Application Configuration 是運維人員(或者系統本身也可以)執行應用部署等動作的操作對象。在 Application Configuration 裡,運維人員可以将 Trait 綁定到 Component 上執行。
在 Application Configuration YAML 裡面,運維可以把 Component 和 Trait 組裝起來,進而得到一個可以部署的“應用”:
apiVersion: core.oam.dev/v1alpha1
kind: ApplicationConfiguration
metadata:
name: first-app
spec:
components:
- componentName: helloworld-python-v1
instanceName: first-app-helloworld-python-v1
parameterValues:
- name: target
value: Rudr
- name: port
value: '9999'
traits:
- name: auto-scaler
properties:
minimum: 3
maximum: 10
- name: ingress
properties:
hostname: example.com
service_port: 9999
在這裡我們可以看到,運維執行個體化的應用裡面包含了一個叫 hellowworld-python-v1 的 Component,它有兩個參數:一個是環境變量 target,一個是 port。需要注意的是,這兩個參數是運維人員覆寫了原先 Component yaml 中開發定義的兩個可覆寫變量。
同時,這個Component綁定了 2 個運維能力:一個是水準擴容,一個是 Ingress 域名通路。
運維人員通過 kubectl 即可把這樣一個應用部署起來:
$ kubectl apply -f examples/first-app-config.yaml
configuration.core.oam.dev/first-app created
而這時候,在 K8s 裡面,你就可以看到 OAM 插件就會自動為你建立出對應的 Deployment。
$ kubectl get deploy
NAME READY UP-TO-DATE AVAILABLE AGE
first-app 1/1 1 1 30h
同時,這個應用需要的 Ingress 也被自動建立起來了:
$ kubectl get ingress
NAME HOSTS ADDRESS PORTS AGE
first-app-ingress example.com 80 16s
這裡其實是前面提到的 Rudr 插件在起作用,在拿到 OAM 的 Application Configuration 檔案以後,識别出其中的 Component 和 Trait,将其映射到 K8s 上的資源并拉起,K8s 資源相應的生命周期都随着 OAM 的配置去管理。當然,由于 OAM 定義是平台無關的,是以除了 K8s 本身的資源,Rudr 插件的實作中也會加入外部資源的拉起。
OAM YAML 檔案 = 一個自包含的軟體安裝包
最終我們可以通過像樂高積木一樣組裝複用 OAM 的不同子產品,執行個體化出一個 OAM 的應用出來。更重要的是,這個 OAM 應用描述檔案是完全自包含的,也就是說通過 OAM YAML,作為軟體分發商,我們就可以完整地跟蹤到一個軟體運作所需要的所有資源和依賴。
這就使得現在對于一個應用,大家隻需要一份 OAM 的配置檔案,就可以快速、在不同運作環境上把應用随時運作起來,把這種自包含的應用描述檔案完整地傳遞到任何一個運作環境中。
這不僅讓我們前面提到的軟體傳遞難題得到了很好的解決,也讓更多的非 K8s 平台比如 IoT、遊戲分發、混合環境軟體傳遞等場景,能享受到雲原生應用管理的暢快。
最後
OAM 是一個完全屬于社群的應用定義模型,我們非常希望大家都能參與進來。
- 一方面,如果你有任何場景感覺 OAM 無法滿足的,歡迎你在社群提出 issue 來描述你的案例;
- 另一方面,OAM 模型也正在積極的同各個雲廠商、開源項目進行對接。
我們期待能與大家一起共建這個全新的應用管理生态。