本文为携程技术分享演讲内容整理而来。
早在 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 非要等到这种冲突发生的时候才报错呢?你们就不能设计个机制自动检查一下这些插件的使用过程有没有发生冲突吗?”
第三,也是阿里巴巴上云之后我们团队特别痛的一个点。我们需要处理的应用的交付场景,除了公有云以外,还会有专有云、混合云、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 本身“不面向应用研发和运维设计”的问题就依然存在,这并不符合我们所希望的“应用定义”应该走的方向。此外,像 Application CRD 这种,它虽然是 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 实现——rudr 项目。
具体来说,在设计 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 模型也正在积极的同各个云厂商、开源项目进行对接。
我们期待能与大家一起共建这个全新的应用管理生态。