本文来自正在规划的Go语言&云原生自我提升系列,欢迎关注后续文章。
至此,我们学习了如何将应用打包为容器,创建容器副本集以及使用Ingress控制器将访问负载均衡至各服务。可以使用所有这些对象(Pod、副本集和服务)来构建应用的单个实例。但这些对于常规管理应用新版本的每日或每周发布帮助甚少。Pod和副本集本来是与不发生修改的具体实例镜像相绑定的。
部署对象用于管理新版本的发布。部署是对所部署应用超越具体版本的表现。此外,部署可让我们从代码的一个版本迁移到另一个版本。这种发布流程是具体且谨慎的。在升级那些Pod的会等待一段时间,这一时间用户可配。还使用健康检查来保障新版本应用运行正确,在发生过多失败时会停止部署。
使用部署,我们可以简单可靠地发布新的软件版本,没有中断和错误。实际由部署执行的软件发布机制由Kubernetes集群正己运行的部署控制器所控制。也就是说部署无人看管依然可正确、安全地执行。这让部署可以简单地集成大量持续发布的工具和服务。并且在服务端运行对于从弱网或不稳定网络连接的地方发布应用变得安全。想象下在乘地铁时通过手机发布软件新版本。部署使其成为可能,并且还是安全的。
注:在Kubernetes刚发布时,展示其强大能力的最著名演示之一就是滚动更新,它展示了如何使用一条命名无缝更新运行中的应用,不产生服务中断且不丢失请求。最初的演示使用kubectl rolling-update
命令,这一命令行工具依然还在,但功能大部分都已并于Deployment对象。
第一个部署
和Kubernetes中的所有对象一样,Deployment可通过包含运行详情的声明式YAML对象表示。下例中,部署请求的是kuard
应用单实例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
apiVersion: apps/v1 kind: Deployment metadata: name: kuard labels: run: kuard spec: selector: matchLabels: run: kuard replicas: 1 template: metadata: labels: run: kuard spec: containers: - name: kuard image: gcr.io/kuar-demo/kuard-amd64:blue |
将这个YAML文件保存为kuard-deployment.yaml,然后使用如下命令创建:
1 |
$ kubectl create -f kuard-deployment.yaml |
我们来看看部署是如何运行的。我们知道副本集用于管理Pod,而部署用于管理副本集。Kubernetes中的所有关联都是通过labels和标签选择器定义,此处也不例外。可以通过查看部署对象来确定标签选择器:
1 2 3 4 |
$ kubectl get deployments kuard \ -o jsonpath --template {.spec.selector.matchLabels} {"run":"kuard"} |
通过它可以看出部署管理的是标签为run=kuard
的副本集。我们可以对副本集使用标签选择器查询来找出具体的副本集:
1 2 3 4 |
$ kubectl get replicasets --selector=run=kuard NAME DESIRED CURRENT READY AGE kuard-1128242161 1 1 1 13m |
下面实际查看部署和副本集的关系。可以使用指令式命令scale
来调整部署的数量:
1 2 3 |
$ kubectl scale deployments kuard --replicas=2 deployment.apps/kuard scaled |
此时再度列举该副本集会看到:
1 2 3 4 |
$ kubectl get replicasets --selector=run=kuard NAME DESIRED CURRENT READY AGE kuard-1128242161 2 2 2 13m |
对部署扩容同时也扩展了其所控制的副本集。
我们再反过来对副本集进行伸缩:
1 2 3 |
$ kubectl scale replicasets kuard-1128242161 --replicas=1 replicaset.apps/kuard-1128242161 scaled |
再执行get
命令获取副本集:
1 2 3 4 |
$ kubectl get replicasets --selector=run=kuard NAME DESIRED CURRENT READY AGE kuard-1128242161 2 2 2 13m |
这就奇怪了。虽然将副本集缩容为了一个副本,在相应状态中还是有两个副本。发生了什么呢?
别忘了,Kubernetes是一套在线自愈系统。顶层部署对象在管理这一副本集。在将副本数量调整为1时,与部署的期望状态不符,也即replicas
的值为2
。部署控制器注意到这一问题,执行操作来保障监测状态与期望状态保持一致,本例中就是重新将副本数量调回为两个。
如果真的想直接管理副本集,需要删除掉部署。(记得将--cascade
设置为false
,否则会同时删除副本集和Pod。)
创建部署
当然我们说过,应当优先通过Kubernetes配置来进行声明式的管理。也就是说在磁盘上的YAML或JSON文件中管理部署的状态。
首先将这一部署下载到一个YAML文件中:
1 2 |
$ kubectl get deployments kuard -o yaml > kuard-deployment.yaml $ kubectl replace -f kuard-deployment.yaml --save-config |
查看文件,内容类似下方(这里为可读性删除了大量只读和默认字段)。注意看注解、选择器和策略字段,这些关乎送回去的功能:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
apiVersion: apps/v1 kind: Deployment metadata: annotations: deployment.kubernetes.io/revision: "1" creationTimestamp: null generation: 1 labels: run: kuard name: kuard spec: progressDeadlineSeconds: 600 replicas: 1 revisionHistoryLimit: 10 selector: matchLabels: run: kuard strategy: rollingUpdate: maxSurge: 25% maxUnavailable: 25% type: RollingUpdate template: metadata: creationTimestamp: null labels: run: kuard spec: containers: - image: gcr.io/kuar-demo/kuard-amd64:blue imagePullPolicy: IfNotPresent name: kuard resources: {} terminationMessagePath: /dev/termination-log terminationMessagePolicy: File dnsPolicy: ClusterFirst restartPolicy: Always schedulerName: default-scheduler securityContext: {} terminationGracePeriodSeconds: 30 status: {} |
注:还需要运行kubectl replace --save-config
。它会添加注解,在将来发生修改时,kubectl
可以知道上次应用的配置以进行更智能的配置合并。如果只使用kubectl apply
,只需要在首次使用kubectl create -f
创建部署之后执行该步骤。
部署的spec与副本集的配置结构很像。有一个Pod模板,包含为部署所管理每个副本所创建的多个容器。除了Pod的规格外,还有一个strategy
对象:
1 2 3 4 5 6 7 |
... strategy: rollingUpdate: maxSurge: 25% maxUnavailable: 25% type: RollingUpdate ... |
strategy
对象描述了新软件发布的不同方式。部署支持两种策略:Recreate
和RollingUpdate
。本文后面会详细讨论。
管理部署
和所有Kubernetes对象一样,我们可以通过kubectl describe
命令获取部署的详细信息。这一命令对部署配置提供了概览,包含一些重要的字段,比如选择器、副本和事件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
$ kubectl describe deployments kuard Name: kuard Namespace: default CreationTimestamp: Tue, 01 Jun 2021 21:19:46 -0700 Labels: run=kuard Annotations: deployment.kubernetes.io/revision: 1 Selector: run=kuard Replicas: 1 desired | 1 updated | 1 total | 1 available | 0 ... StrategyType: RollingUpdate MinReadySeconds: 0 RollingUpdateStrategy: 25% max unavailable, 25% max surge Pod Template: Labels: run=kuard Containers: kuard: Image: gcr.io/kuar-demo/kuard-amd64:blue Port: <none> Host Port: <none> Environment: <none> Mounts: <none> Volumes: <none> Conditions: Type Status Reason ---- ------ ------ Available True MinimumReplicasAvailable OldReplicaSets: <none> NewReplicaSet: kuard-6d69d9fc5c (2/2 replicas created) Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal ScalingReplicaSet 4m6s deployment-con... ... Normal ScalingReplicaSet 113s (x2 over 3m20s) deployment-con... ... |
describe
命令的输出内容有大量重要信息。最重要的两条为OldReplicaSets
和NewReplicaSet
。这些字段指向部署当前管理的副本集对象。如果部署在滚动发布过程中,两个字段都会有赋值。在滚动发布完成后,OldReplicaSets
的值会设置为<none>
。
除describe
命令外,还有kubectl rollout
可用于部署。稍后我们会详解这一命令,现在读者可以使用kubectl rollout history
获取与具体部署相关联的发布历史。如果当前部署在进行中,可使用kubectl rollout status
获取滚动发布的状态。
更新部署
部署是一些声明式对象。对部署最常见的操作是扩缩容和应用更新。
部署扩缩容
虽然前面我们展示了使用kubectl scale
命令对部署进行命令式的扩缩容,但最佳实践是通过YAML文件声明式地管理部署,然后使用这些文件更新部署。要对部署进行扩容,可以编辑YAML文件增加副本数量:
1 2 3 4 |
... spec: replicas: 3 ... |
保存、提交这一修改后,可以使用kubectl apply
命令更新该部署:
1 |
$ kubectl apply -f kuard-deployment.yaml |
这会更新部署到期望状态,增加所管理的副本集大小,最终创建一个由部署所管理的Pod:
1 2 3 4 |
$ kubectl get deployments kuard NAME READY UP-TO-DATE AVAILABLE AGE kuard 3/3 3 3 10m |
更新镜像容器
另一个更新部署的常见用例是滚动发布在一个或多个容器中运行的软件新版本。完成这一任务,同样要编辑部署YAML文件,但这里更新的是容器镜像,而不是副本数量:
1 2 3 4 5 |
... containers: - image: gcr.io/kuar-demo/kuard-amd64:green imagePullPolicy: Always ... |
对部署添加注解来记录有关更新的一些信息:
1 2 3 4 5 6 7 8 |
... spec: ... template: metadata: annotations: kubernetes.io/change-cause: "Update to green kuard" ... |
注意:请务必在模板而不是部署中添加这一注解,因为kubectl apply
在部署对象中使用该字段。同时,在做简单的扩缩容操作时不要更新change-cause
注解。change-cause
的修改是对模板一个大变化,会导致新的滚动发布。
同样,可以使用kubectl apply
来更新部署:
1 |
$ kubectl apply -f kuard-deployment.yaml |
在更新部署后,会触发滚动发布,然后可以通过kubectl rollout
命令来进行监控:
1 2 |
$ kubectl rollout status deployments kuard deployment "kuard" successfully rolled out |
可以看到部署所管理的新老副本集以及所用的镜像。保留了新老两个副本集,是为了防止回滚:
1 2 3 4 5 |
$ kubectl get replicasets -o wide NAME DESIRED CURRENT READY ... IMAGE(S) ... kuard-1128242161 0 0 0 ... gcr.io/kuar-demo/ ... kuard-1128635377 3 3 3 ... gcr.io/kuar- |
如果在滚动发布的过程中,这时希望暂停(比如看到想要调查的系统中出现了异常行为),可以使用pause
命令:
1 2 |
$ kubectl rollout pause deployments kuard deployment.apps/kuard paused |
如果在调查后确认可安全执行滚动发布,使用resume
命令继续未完成的部分:
1 2 |
$ kubectl rollout resume deployments kuard deployment.apps/kuard resumed |
回滚历史
Kubernetes部署维护有滚动发布的历史,可用于了解部署之前的状态以及回滚到具体的版本:
可通过运行如下命令查看部署历史:
1 2 3 4 5 6 |
$ kubectl rollout history deployment kuard deployment.apps/kuard REVISION CHANGE-CAUSE 1 <none> 2 Update to green kuard |
修改历史按从旧到新排序。每次滚动发布的唯一版本号会递增。现在有两个:初始部署版本及将镜像更新为kuard:green
的版本。
如果想看具体版本的详情,可以使用--revision
标记来查看具体修改的详情:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
$ kubectl rollout history deployment kuard --revision=2 deployment.apps/kuard with revision #2 Pod Template: Labels: pod-template-hash=54b74ddcd4 run=kuard Annotations: kubernetes.io/change-cause: Update to green kuard Containers: kuard: Image: gcr.io/kuar-demo/kuard-amd64:green Port: <none> Host Port: <none> Environment: <none> Mounts: <none> Volumes: <none> |
我们再对示例做一次更新。通过修改容器版本号及更新change-cause
注解来将kuard
的版本更新回blue
。通过kubectl apply
命令进行应用。历史记录中应该有三个条目:
1 2 3 4 5 6 7 |
$ kubectl rollout history deployment kuard deployment.apps/kuard REVISION CHANGE-CAUSE 1 <none> 2 Update to green kuard 3 Update to blue kuard |
假设在调查时发现最新发布有一个问题,希望进行回滚。可以撤销上一次滚动发布:
1 2 |
$ kubectl rollout undo deployments kuard deployment.apps/kuard rolled back |
不管滚动发布处理何种阶段都可以使用undo
命令。可以撤销部分完成或全部完成的滚动发布。滚动发布的撤销其实是反向滚动发布(比如把v1到v2换成v2到v1),撤销的策略与滚动发布的策略完全相同。可以看到部署对象只是调整所管理副本集中的期望副本数:
1 2 3 4 5 6 |
$ kubectl get replicasets -o wide NAME DESIRED CURRENT READY ... IMAGE(S) ... kuard-1128242161 0 0 0 ... gcr.io/kuar-demo/ ... kuard-1570155864 0 0 0 ... gcr.io/kuar-demo/ ... kuard-2738859366 3 3 3 ... gcr.io/kuar-demo/ ... |
注意:在使用声明式文件控制生产系统时,应尽可能确保所提交的声明与集群中实际运行的保持一致。在执行kubectl rollout undo
时,这时更新生产状态在源码控制中并不会体现。
撤销滚动发布的另一种(也是更推荐的)方式是回退YAML文件,并对前一个版本执行kubectl apply
。这样,“追踪配置修改”可更紧急地追踪到集群中实际运行的内容。
我们再来看看部署历史:
1 2 3 4 5 6 7 |
$ kubectl rollout history deployment kuard deployment.apps/kuard REVISION CHANGE-CAUSE 1 <none> 3 Update to blue kuard 4 Update to green kuard |
没有了历史版本2!这在回滚之前的版本就会出现,部署只是复用了该模块并重新将其编号成为最新版本。之前的历史版本2现在成为了版本4。
之前我们看到可以使用kubectl rollout undo
命令来回滚到部署的上一个版本。此外,可以使用--to-revision
标记来回滚到历史记录中的指定版本:
1 2 3 4 5 6 7 8 |
$ kubectl rollout undo deployments kuard --to-revision=3 deployment.apps/kuard rolled back $ kubectl rollout history deployment kuard deployment.apps/kuard REVISION CHANGE-CAUSE 1 <none> 4 Update to green kuard 5 Update to blue kuard |
同样地,undo
获取了版本3,将其重新编号为版本5。
指定版本为0
是指定上一个版本的简便方式。这样kubectl rollout undo
与kubectl rollout undo --to-revision=0
是等价的。
默认,部署的最近10个版本会保留在部署对象上。如果要让部署保留更长时间,推荐对部署修改历史设置一个最大历史大小。例如,如果每天更新,可以将修改历史设置为14,来最多保留两个星期的修改(如果不需回滚到两周前的修改的话)。
完成这一设置,在部署规格中使用revisionHistoryLimit
属性:
1 2 3 4 5 6 |
... spec: # We do daily rollouts, limit the revision history to two weeks of # releases as we don't expect to roll back beyond that. revisionHistoryLimit: 14 ... |
部署策略
在修改实现服务的软件版本时,Kubernetes部署支持两种滚动发布策略,Recreate
和RollingUpdate
。我们来依次学习。
重建策略
Recreate
策略是两者是更简单的一个。它只是使用新镜像更新副本集并终止与部署相关联的那些Pod。ReplicaSet注意到不再有副本,会使用新镜像重建所有Pod。完成重建后,Pod中会运行新的版本。
虽然这种策略快速简单,但会导致工作负载的中断。因此,Recreate
策略仅用于那些可以接收受中断的测试部署。
滚动更新策略
对于面向用户的服务通常更推荐使用RollingUpdate
策略。虽然比Recreate
慢,但也更专业更健壮。使用RollingUpdate
,滚动发布新版本的同时还可以接收用户的访问,不会中断。
读者可能通过名称能推断出,RollingUpdate
策略通过一次更新一部分Pod,增量更新直到所有的Pod都运行着软件的新版本。
管理服务的多个版本
重要的是,这表示有一段时间,服务的新老版本会同时接收请求返回响应。这为构建软件提供了重要的信息。具体来说,软件的各版本及其各个客户端能够交替地与软件的较新和较老的版本对话,这会非常重要。
考虑以下的场景:我们正在滚动发布前端软件之中,一半的服务器上运行着版本1,而另一半运行着版本2。一个用户对服务发起了首次请求,下载了实现 UI 的客户端JavaScript库。这一请求由版本1服务器处理,因此用户接收到了版本1客户端库。这一客户端库在用户浏览器端运行,向服务又发起了API请求。这些API请求刚好路由到了版本本2服务器,所以这时版本1的JavaScript客户端库在请求版本2的API服务器。如果没有保障版本间的兼容性,应用会出问题。
一开始这看上去是种负担。但事实上一直会有这种问题,你只是没有注意到。具体来讲,用户可能刚好在开始更新前的时间t
发出请求。请求通过版本1服务器提供服务。在t_1
,服务更新为了版本2。在t_2
,在用户有的浏览器上运行着版本1的客户端代码,请求了由版本2提供服务的API端点。不管如何更新软件,都要对可靠更新保持后台和前向兼容。RollingUpdate
策略的本质只是使其更清晰和显式。
这不只适用于JavaScript客户端,确实客户端库会编译到其它调用你的服务的服务之中。你做了更新并不表示调用端会更新客户端库。这类向后兼容对于解耦你的服务与依赖该服务的系统非常重要。如果不规范API并进行解耦,你就必须要仔细管理依赖你的服务的系统的滚动发布。这类紧密耦合会让每周推送新版本这样的敏捷性变得极其困难,更遑论每天或每小时了。在图10-1中所示的解耦架构中,前端通过API合约和负载均衡与后端进行解耦,而在耦合的架构中,将胖客户端编译到了前端中,用于直接与后端进行连接。
图10-1. 解耦(左)和耦合(右)应用架构图
配置滚动更新
RollingUpdate
是非常普遍的策略,可用于用各种设置更新各应用。因此,滚动更新的可配置性很强,可以调优其行为来符合具体的要求。有两个参数可用于调优滚动更新:maxUnavailable
和maxSurge
。
maxUnavailable
参数设置在滚动更新时最大的不可用Pod数。可设置为一个绝对值(如3
,表示最多有3个Pod不可用)或是百分比(如20%
,表示最多有20%的期望副本数不可用)。通常来说,使用百分比对大部分服务是个好方法,因为不论部署的期望副本数都能正确应用。但是,有时可能会希望使用绝对值(比如将最大小可用Pod数设置为1个)。
本质上maxUnavailable
参数帮助调优滚动更新开展的速度。比如,如果设置maxUnavailable
为50%
,然后滚动更新会立刻将老的副本集缩容到原来的50%。如果有4个副本,就会缩容至两个副本。然后滚动更新会将删除的Pod替换为新副本集所扩容的两个副本,总数为4个副本(两个旧的,两具新的)。然后老副本集缩容至0个副本,总共有两个副本。最后,会将新副本扩容为4个副本,完成滚动发布。因此,通过将maxUnavailable
设置为50%
,滚动发布4步完成,但过程中只有50%的服务容量。
试想将maxUnavailable
设置为25%
会是什么情况。这时,每步一次只执行一个副本,因此需要花费两倍的步骤来完成滚动发布,但可用性在滚动发布期间只会降到最小75%。这描绘了maxUnavailable
是如何在滚动发布速度和可用性之间做权衡。
注:细心的读者会发现Recreate
策略与maxUnavailable
设为100%
的RollingUpdate
策略是相同的。
减少容量来成功实现滚动发布,对有循环流量模式的服务(比如夜间流量比较低)或资源有限时比较有用,因为这时扩容至比当前最大副本数更大并不可行。
但有时并不希望降到100%以下的容量,但愿意临时使用更多的资源来执行滚动发布。在这些场景中,可以将maxUnavailable
参数设置为0
,通过maxSurge
参数来控制滚动发布
maxSurge
参数控制可创建多少额外资源来实现滚动发布。为理解其运行方式,想象一下服务有10个副本。我们将maxUnavailable
设置为0
,maxSurge
设置为20%
。滚动发布首先要做的是将新副本集扩容为两个副本,服务中共有12 (120%)个副本。然后会将老副本集缩容至8个副本,服务共有10个(8个老的,2个新的)副本。持续这一过程走到滚动发布完成。在任意时刻,都保障服务的容量至少为100%,用于滚动发布的额外资源限定在总资源的20%。
注:将maxSurge
设置为100%
与蓝/绿部署一致。部署控制器先将新版本扩容至老版本的100%。一旦新版本处于健康状态,立即将老版本缩容至0%。
减慢滚动发布来保障服务健康
要阶段滚动发布表示保障滚动发布的结果是,健康、稳定的服务运行着新的软件版本。为实现这一目标,部署控制器总是等待Pod报告其准备就绪再去更新下一个Pod。
警告:部署控制器检查Pod的状态,由就绪性检测决定。就绪性检测是Pod健康检测的一部分,在Pods一章中进行过讲述。如果想要使用部署来对软件进行可靠的滚动发布,需要对Pod中的容器指定就绪性检测。没有这些检测,运行中的部署控制器就无从知晓Pod的状态。
但有时只注意到Pod变得就绪并不足以保证Pod就是正确运行的。某些错误条件不会立即发生。比如,可能存在严重的内存泄漏,可能要花几分钟才会出现,或者存在bug,仅在1%的请求中出现。在大部分真实场景中,我们会等待一段时间才能高度确信新版本正确运行,然后再更新下一个Pod。
对于部署,这一等待时间由minReadySeconds
参数定义:
1 2 3 4 |
... spec: minReadySeconds: 60 ... |
将minReadySeconds
设置为60
表示部署在发现Pod后必须等待60秒再去更新下一个Pod。
除了等待Pod变健康,我们还希望设置一个超时限定系统的等待时间。比如假定服务的新片有一个bug,马上就会死锁。它永远也不会就绪,没有超时的话,部署控制器就会永远在滚动发布上卡住。
这种场景正确的做法是为滚动发布设置超时。这会将滚动发布标记为失败。失败状态可用于触发警告,向操作人员表明滚动发布出现了问题。
注:乍一看,对滚动发布设置超时是化简为繁了。但渐渐地,像滚动发布这种操作会由全自动系统触发,很少要人工介入。在这时,超时变成极端异常,可触发全自动回滚或是创建工单/事件来让人工介入。
我们使用部署的progressDeadlineSeconds
参数来设置超时时间:
1 2 3 4 |
... spec: progressDeadlineSeconds: 600 ... |
例中将进度超时设置为10分钟。如果滚动发布中的具体阶段用10分钟未能完成,那么部署会被标记为失败,所有继续进行部署的尝试都会中断。
在部署进程中给出这一超时而不是针对整个部署长度,这点要注意。在这里,进程为部署创建或删除Pod的时长。发生问题时,超时时钟会重置为0。图10-2中显示了部署的生命周期。
图10-2. Kubernetes部署生命周期
删除部署
如果想要删除部署,可以通过指定式命令完成:
1 |
$ kubectl delete deployments kuard |
也可以使用创建时使用的声明式YAML文件来完成删除:
1 |
$ kubectl delete -f kuard-deployment.yaml |
不论是哪种方式,默认删除部署会删除整个服务。这意味着它删除的不只是部署,还有其管理的所有副本集,以及副本集所管理的所有Pod。对于副本集,如果这不是期望的行为,可以使用--cascade=false
标记来只删除部署对象。
监测部署
如果部署无法在指定时间内完成操作,就会超时。而这时,部署的状态会转换为失败状态。这一状态可通过status.conditions
数组获取,其中会有Type
为Progressing
以及Status
为False
的Condition
。这种状态的部署会失败,不再继续执行。设置部署控制器转换为此状态前应等待的时间,可使用spec.progressDeadlineSeconds
字段。
小结
最终,Kubernetes的主要目标是让构建和部署可靠分布式系统变得简单。这表示不只是一次性地初始化应用,而是有规律地管理软件服务新版本的滚动发布。部署是可靠滚动发布以及服务滚动发布管理的重要部分。下一章中我们会讲解DaemonSets,它保障Pod的单个拷贝会跨Kubernetes集群的一组节点运行。