经过前面不少文章的铺垫,终于可以写这个大家都感兴趣的话题了,在前面两篇文章,我们讲了Kubernetes
里的 Pod和 副本集ReplicaSet (RS) 这两个API
对象。知道了Pod
是Kubernetes
里的最小调度单元,ReplicaSet
则是控制Pod
副本数的一个基础控制器。文章最后留下了一个话题:
Kubernetes里一般使用Deployment控制器而不是直接使用ReplicaSet,Deployment是一个管理ReplicaSet并提供水平扩展/收缩、Pod声明式更新、应用的版本管理以及许多其他功能的更高级的控制器。
所以部署到Kubernetes
集群里的Go
项目就是通过Deployment
这个控制器实现应用的水平扩展/收缩和更应用新管理的,它通过自己的控制循环确保集群里当前的状态始终等于Deployment
对象定义的期望状态。
我会使用《Kubernetes入门实践--部署运行Go项目》文章里用过的项目作为演示项目,演示Kubernetes
怎么对应用服务进行水平扩容、发版更新、版本回滚等操作,在演示的过程中一起探讨下面几个话题:
什么是Deployment
控制器
Deployment
的工作原理。
怎么创建Deployment
。
如何使用Deployment
滚动更新应用。
如何使用Deployment
进行应用的版本回滚。
在Kubernetes
中,建议使用Deployment
来部署Pod
和 RS
,因为它具有很多方便管理集群的内置功能,比如:
轻松部署RS(副本集)
清理不再需要的旧版RS
扩展/缩小RS里的Pod数量
动态更新Pod
(根据Pod模板定义的更新用新Pod替换旧Pod)
回滚到以前的Deployment
版本
保证服务的连续性
以下面这个Deployment
对象的定义为例,第一部分是自己的元信息(name, labels)的定义,第二部分是ReplicaSet
对象的定义(spec.replica=3....),ReplicaSet
定义里又包含了Pod
的定义(spec.template):
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.7.9
ports:
- containerPort: 80
在具体的实现上,这个Deployment
,与ReplicaSet
,以及Pod
的关系和管理层级我们可以用一张图把它描述出来:
Kubernetes
里有很多种控制器,每一个控制器,都以独有的方式负责某种编排功能。Deployment,正是这些控制器中的一种。它们都遵循 Kubernetes 项目中的一个通用编排模式,即:控制循环(control loop),每种控制器负责的编排功能就是它们自己在控制循环里实现的逻辑。
接下来,还是以上面定义的Deployment
为例,我和你简单描述一下的工作原理:
Deployment 控制器从 Etcd 中获取到所有携带了"app: nginx"标签的 Pod,然后统计它们的数量,这就是实际状态;
Deployment 对象的 Replicas 字段的值就是期望状态,Deployment 控制器将两个状态做比较;
根据比较结果,Deployment
确定是创建 Pod,还是删除已有的 Pod,还是什么不干;
这是针对Pod
副本数的编排,至于Pod
的动态更新和Deployment
对象版本的回滚文章下面再说。总而言之,控制器的核心思想就是通过控制循环不断地将实际状态调谐成定义的期望状态,一旦期望状态有更新就会触发控制循环里的调谐逻辑。
创建Deployment
前需要先声明它的对象定义,我们拿以前文章《Kubernetes入门实践--部署运行Go项目》里用到过的Deployment
定义简单解释下每部分的含义:
apiVersion: apps/v1
kind: Deployment
metadata: # Deployment的元数据
name: my-go-app
spec:
replicas: 1 # ReplicaSet部分的定义
selector:
matchLabels:
app: go-app
template: # Pod 模板的定义
metadata:
labels:
app: go-app
spec: # Pod里容器相关的定义
containers:
- name: go-app-container
image: kevinyan001/kube-go-app
resources:
limits:
memory: "128Mi"
cpu: "100m"
ports:
- containerPort: 3000
apiVersion 声明了对象的API版本,Kubernetes会去对应的包里加载库文件。
kind声明对象的种类,其实就是告诉Kubernetes去加载什么对象。
metadata就是我们这个对象的元数据。
spec.replicas 定义副本集有多少个Pod副本,而spec.selectors则是副本集匹配Pod的规则。
spec.template是Pod模板的定义,其中的内容就是一个完整的Pod对象的定义。
spec.template.spec是关于Pod里容器相关的定义。
具体里面每个字段的意思和用途我就不多说了,前面的文章里都讲过,重点强调一下容器配置里limits.memory的128Mi
代表的是内存分配给容器128兆,而limits.cpu的1000m = 1核心。100m就是分配给容器0.1核,这个在自己电脑上实践的时候尽量别分配太大,不然根本启动不起来。
写好声明文件后,使用kubectl create
命令创建Deployment
对象,Kubernetes
里所有的API对象都是这么创建的。
➜ kubectl create -f deployment.yaml --record
deployment.apps/my-go-app created
➜
对于在笔记本上实践的同学,需要先安装Minikube,具体的安装步骤可以参考:Minikube-运行在笔记本上的Kubernetes集群。
在继续使用Deployment
进行更高级的编排工作前,我们先用下面两个命令确保一下Deployment
的运行状态:
kubectl rollout status deployment 告诉我们Deployment
对象的状态变化。
➜ kubectl rollout status deployment my-go-app
deployment "my-go-app" successfully rolled out
kubectl get deployment 显示期望的副本数和正在更新的副本数,以及当前可提供服务的Pod
数量。因为我们在定义里只指定了一个副本,所以当前只有一个Pod
。
kubectl get deployment my-go-app
NAME READY UP-TO-DATE AVAILABLE AGE
my-go-app 1/1 1 1 13m
kubectl get replicaset 查看Deployment
为Pod创建的ReplicaSet
的状态。
kubectl get replicaset
NAME DESIRED CURRENT READY AGE
my-go-app-864496b67b 1 1 1 19m
默认情况下,Deployment会将pod-template-hash添加到它创建的ReplicaSet
的名称中。比如这里的my-go-app-864496b67b
最后 kubectl get pod 命令可以查看ReplicaSet
创建出来的Pod
副本的状态。
NAME READY STATUS RESTARTS AGE
my-go-app-864496b67b-ctkf9 1/1 Running 0 25m
Deployment
通过"控制器模式",来操作ReplicaSet
的个数和属性,进而实现"水平扩展 / 收缩" 和 "滚动更新" 这两个编排动作。
"水平扩展 / 收缩"非常容易实现,Deployment
只需要修改它所控制的ReplicaSet
的 Pod
副本个数就可以了。比如,把这个值从 1 改成 3,那么 Deployment
所对应的 ReplicaSet
,就会根据修改后的值自动创建两个新的Pod
,"水平收缩"则反之。这个操作的指令也非常简单,就是 kubectl scale,比如:
➜ kubectl scale --replicas=3 deployment my-go-app --record
deployment.apps/my-go-app scaled
如果你手快点还能通过上面说的命令 kubectl rollout status deployment my-go-app 看到扩展过程中Deployment
对象的状态变化:
kubectl rollout status deployment my-go-app
Waiting for deployment "my-go-app" rollout to finish: 1 of 3 updated replicas are available...
Waiting for deployment "my-go-app" rollout to finish: 2 of 3 updated replicas are available...
deployment "my-go-app" successfully rolled out
可以通过下面的命令观察到ReplicaSet的Name没有发生变化:
➜ kubectl get replicaset
NAME DESIRED CURRENT READY AGE
my-go-app-864496b67b 3 3 3 53m
这证明了 Deployment
水平扩展和收缩副本集是不会创建新的ReplicaSet的,但是涉及到Pod模板的更新后,比如更改容器的镜像,那么Deployment会用创建一个新版本的ReplicaSet用来替换旧版本。
在上面的Deployment
定义里,Pod模板里的容器镜像设置的是kevinyan001/kube-go-app,接下来比如我们的Go
项目代码更新了,用最新的代码打包了镜像 kevinyan001/kube-go-app:v0.1,部署Go项目的新镜像的过程就会触发Deployment
的滚动更新。
有两种方式更新镜像,一种是更新deployment.yaml
里的镜像名称,然后执行 kubectl apply -f deployment.yaml。一般公司里的Jenkins
等持续继承工具用的就是这种方式。还有一种就是使用kubectl set image 命令,为了方便演示我们这里就是用第二种方式进行Pod
的滚动更新。
➜ kubectl set image deployment my-go-app go-app-container=kevinyan001/kube-go-app:v0.1 --record
deployment.apps/my-go-app image updated
执行滚动更新后通过命令行查看ReplicaSet
的状态会发现Deployment
用新版本的ReplicaSet
对象替换旧版本对象的过程。
➜ kubectl get replicaset
NAME DESIRED CURRENT READY AGE
my-go-app-6749dbc697 3 3 2 19s
my-go-app-864496b67b 1 1 1 72m
➜ kubectl get replicaset
NAME DESIRED CURRENT READY AGE
my-go-app-6749dbc697 3 3 3 24s
my-go-app-864496b67b 0 0 0 72m
通过这个Deployment的Events可以查看到这次滚动更新的详细过程:
➜ kubectl describe deployment my-go-app
Name: my-go-app
Namespace: default
CreationTimestamp: Sat, 29 Aug 2020 00:31:56 +0800
Events:
.....
Normal ScalingReplicaSet 37h deployment-controller Scaled up replica set my-go-app-6749dbc697 to 1
Normal ScalingReplicaSet 37h deployment-controller Scaled down replica set my-go-app-864496b67b to 2
Normal ScalingReplicaSet 37h deployment-controller Scaled up replica set my-go-app-6749dbc697 to 2
Normal ScalingReplicaSet 37h (x2 over 37h) deployment-controller Scaled down replica set my-go-app-864496b67b to 1
Normal ScalingReplicaSet 37h deployment-controller Scaled up replica set my-go-app-6749dbc697 to 3
Normal ScalingReplicaSet 37h deployment-controller Scaled down replica set my-go-app-864496b67b to 0
当你修改了Deployment
里的Pod
定义之后,Deployment
会使用这个修改后的 Pod
模板,创建一个新的 ReplicaSet
(hash=6749dbc697),这个新的ReplicaSet
的初始Pod
副本数是:0。然后Deployment
开始将这个新的ReplicaSet
所控制的Pod
副本数从 0 个变成 1 个,即:"水平扩展"出一个副本。紧接着Deployment
又将旧的 ReplicaSet
(hash=864496b67b)所控制的旧 Pod 副本数减少一个,即:"水平收缩"成两个副本。如此交替进行就完成了这一组Pod
的版本升级过程。像这样,将一个集群中正在运行的多个 Pod
版本,交替地逐一升级的过程,就是 "滚动更新"。
用示意图描述这个过程的话就像下图这样
Deployment滚动更新的过程为了保证服务的连续性,Deployment
还会确保,在任何时间窗口内,只有指定比例的Pod
处于离线状态。同时,它也会确保,在任何时间窗口内,只有指定比例的新 Pod
被创建出来。这两个比例的值都是可以配置的,默认都是期望状态里spec.relicas
值的 25%。所以,在上面这个 Deployment
的例子中,它有 3 个 Pod
副本,那么控制器在“滚动更新”的过程中永远都会确保至少有 2 个Pod
处于可用状态,至多只有 4 个 Pod
同时存在于集群中。这个策略可以通过Deployment
对象的一个字段,RollingUpdateStrategy来设置:
apiVersion: apps/v1
kind: Deployment
...
spec:
...
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 1
上面执行变更命令的时候都使用了--record 参数,这个参数能让Kubernetes
在这个Deployment
的变更记录里记录上产生变更当时执行的命令。
执行kubectl rollout history deployment my-go-app 就能看到这个Deployment
的更新记录:
➜ kubectl rollout history deployment my-go-app
deployment.apps/my-go-app
REVISION CHANGE-CAUSE
1 kubectl scale deployment my-go-app --replicas=3 --record=true
2 kubectl set image deployment my-go-app go-app-container=kevinyan001/kube-go-app:v0.1 --record=true
假如刚才那个滚动更新的Go项目镜像有问题,我们想回退到以前的版本。借助--record参数帮我们记录的执行命令和更新记录里的修订号就可以找到想要回滚的版本修订号。
一旦确定了修订号后我们kubectl rollout undo命令就能完成Deployment
对象的版本回滚。
kubectl rollout undo deployment my-go-app --to-revision=1
deployment.apps "my-go-app"
执行完后我们会发现一个非常有意思的事情,以前那个版本的ReplicaSet
(hash=864496b67b)的Pod的数又变回了3,新ReplicaSet
(hash=6749dbc697)的Pod数变成了0。
➜ kubectl get rs
NAME DESIRED CURRENT READY AGE
my-go-app-6749dbc697 0 0 0 3m33s
my-go-app-864496b67b 3 3 3 4m30s
证明Deployment
在上次滚动更新后并不会把旧版本的ReplicaSet
删掉,而是留着回滚的时候用,所以ReplicaSet
相当于一个基础设施层面的应用的版本管理。
回滚后在看变更记录,发现已经没有修订号1的内容了,而是多了修订号为3的内容,这个版本的变更内容其实就是回滚前修订号1里的变更内容。
➜ kubectl rollout history deployment my-go-app
deployment.apps/my-go-app
REVISION CHANGE-CAUSE
2 kubectl set image deployment my-go-app go-app-container=kevinyan001/kube-go-app:v0.1 --record=true
3 kubectl scale deployment my-go-app --replicas=3 --record=true
你可能已经想到了一个问题:我们对Deployment
进行的每一次更新操作,都会生成一个新的ReplicaSet
对象,是不是有些多余,甚至浪费资源?所以,Kubernetes 项目还提供了一个指令,使得我们对 Deployment 的多次更新操作,最后只生成一个ReplicaSet
对象。具体的做法是,在更新Deployment
前,你要先执行一条 kubectl rollout pause 指令。它的用法如下所示:
➜ kubectl rollout pause deployment my-go-app
deployment.apps/my-go-app paused
这个命令的作用,是让这个Deployment
进入了一个"暂停"状态。由于此时Deployment
正处于“暂停”状态,所以我们对Deployment
的所有修改,都不会触发新的“滚动更新”,也不会创建新的ReplicaSet
。而等到我们对 Deployment
修改操作都完成之后,只需要再执行一条 kubectl rollout resume 指令,就可以把这个 它恢复回来,如下所示:
➜ kubectl rollout resume deployment my-go-app
deployment.apps/my-go-app resumed
随着应用版本的不断增加,Kubernetes
会为同一个Deployment
保存很多不同的ReplicaSet
。Deployment
对象有一个字段,叫作 spec.revisionHistoryLimit,就是 Kubernetes
为 Deployment
保留的"历史版本"个数。如果把它设置为 0,就再也不能做回滚操作了。
Kubernetes
项目对 Deployment
的设计,代替我们完成了对应用的抽象,让我们可以用一个Deployment
对象来描述应用,使用 kubectl rollout 命令控制应用的版本。
Deployment
还会保证服务的连续性,确保滚动更新时在任何时间窗口内,只有指定比例的Pod
处于离线状态,同时也只有指定比例的新 Pod
被创建出来,这样就保证了服务能平滑更新。用Go
写的HTTP
服务举例子来说,我们不需要再在代码里自己实现HTTP Server
平滑重启的功能,因为这些功能都由Deployment
在应用抽象层面替我们实现了。
希望大家都能跟着今天文章里的演示,掌握Deployment
的提供的各种功能的用法。文章里我用的镜像已经上传到DockerHub上了,创建Deployment
对象时会自动去DockerHub上拉取。如果网络受限,拉取不了镜像,可以在文章下面留言或者公众号私信我获取项目的源码和构建镜像用的Dockerfile
。
MySQL读锁的区别和应用场景分析
Go内存管理之代码的逃逸分析
如何避免用动态语言的思维写Go代码
看到这里了就点个在看支持下吧,你的「在看」是我创作的动力。
关注公众号网管叨bi叨
,「每周为您分享原创技术文章」!
“在看转发”是最大的支持