k8s系列-Deployment

K8S-Deployment

介绍

Kubernetes最核心的功能:编排
Pod本质上是对容器进一步的抽象和封装,容器就好像我们平时了解到的集装箱一样,集装箱虽然好用,但是四面光秃秃的,吊车怎么来吊集装箱并且把它摆好呢?Pod对象就好像是加装了吊环的集装箱,让吊车(Kubernetes)能够能够更好的操作它,Kubernetes怎么操作它呢,就是由控制器(Controller),最常用的控制器Deployment。

下面是一个简单的名为nginx-deployment的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 2
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.7.9
ports:
- containerPort: 80

上面这个Deployment的编排很简单,确保带有标签app=nginx的Pod只有两个,等于spec.replicas指定的个数。

集群中如果有携带标签app=nginx的Pod被删除,就会有新的Pod创建起来,跟之前被删除的Pod属性一样。

这个动作能够执行全靠一个名为kube-controller-manager的组件,controller有很多种,比如下面的

  • deployment
  • job
  • statefulset
  • replicaset
  • cronjob
  • service
  • volume
  • ···

这些控制器之所以都放在controller下面,是因为他们都遵循Kubernetes项目的一个通用编排模式,控制循环。

比如用下面的这样一段伪代码可以解释一下什么叫做控制循环,一个待编排的对象的X。带有一个控制器。

1
2
3
4
5
6
7
8
9
for {
实际状态 = 获取集群中对象X的实际状态(Actual State)
期望状态 = 获取集群中对象X的期望状态(Desired State)
}
if (实际状态 == 期望状态) {
什么都不做
} else {
执行编排动作,将实际状态调整为期望状态
}

实际状态往往来自于Kubernetes集群本身,kubelet通过心跳汇报容器状态和节点状态,或者是监控系统保存的应用监控数据,或者是控制器主动收集自己感兴趣的信息。期望状态一般就是提交的YAML了。

比如Deployment对象的Replicas字段的值就往往是保存在Etcd中。

Deployment详解

控制器模型的实现

仍然以上面的名为nginx-deployment为例

  1. Deployment控制器从Etcd中获取所有携带app:nginx标签的Pod,并且统计数量,这个就是实际状态。

  2. Deployment对象的Replicas字段就是期望值。

  3. Deployment控制器将两个状态作比较,根据比较的状态,执行类似上文的伪代码,判断是要新增Pod还是删除Pod。

这个操作通常被叫做调谐(Reconcile),这个调谐的过程被称作Reconcile Loop(调谐循环)或者Sync Loop(同步循环),就像增加Pod,删除Pod,更新Pod这些都算是Kubernetes中面向API对象编程的一个直观体现。

控制器本身负责定义被管理对象的期望状态,比如replicas字段,被控制的对象则来自于一个模板,Deploymenttemplate字段,这个template字段的属性跟标准的Pod定义丝毫不差,Deploymenttemplate字段在Kubernetes中有个专有的名字叫做PodTemplate(Pod模板)

Deployment的水平扩展和收缩

ReplicaSet简介

如果你更新了Deployment的Pod模板,比如你修改了镜像的版本,那么Deployment就需要要遵循一种叫做滚动更新的策略还更新控制器,而这个能力的实现就是依靠ReplicaSet

详解

还是从上面的名为nginx-deployment的例子说到ReplicaSet

先使用如下命令创建nginx-deployment这个Deployment

1
kubectl apply -f nginx-deployment.yml

查看ReplicaSet状态

1
2
3
4
5
6
7
8
9
10
11
12
# 先使用如下命令等待Deployment都Running起来
kubectl get Deployment
# 使用如下命令获取到所有的ReplicaSet
kubectl get rs

NAME DESIRED CURRENT READY AGE
nginx-54458cd494 4 4 4 18d
nginx-deployment-76bf4969df 3 3 3 3d22h


#再如下命令编辑上文列表中的这个ReplicaSet nginx-deployment-76bf4969df
kubectl edit rs nginx-deployment-76bf4969df

获取到结果如下
image

从图中可以大概看出来ReplicaSet对象就是由副本数目定义和一个Pod模板组成的,是Deployment的一个子集,重要的是,Deployment操作的是ReplicaSet对象,而不是直接操作的Pod

分析ReplicaSet

还是从上面的Deployment说起

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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

在这个副本数量为3的控制器中,DeploymentReplicaSetPod的关系是怎么样的

image

从上图中我么就可以看到,其实Deployment并不直接关系到Pod,他是通过ReplicaSet间接操控Pod,其中ReplicaSet通过控制器模式保证集群中Pod的数量是期望的值那样,这也正是Deployment只允许容器的restartPolicy=Always的主要原因,只有在容器保证自己始终是Running的情况下,调整Pod的个数才有意义

水平扩展/收缩

在此基础上,Deployment同样通过控制器模式来操控ReplicaSet的个数和属性,进行”水平收缩/扩展“和“滚动更新”这两个编排动作。在Deployment中水平收缩扩展比较容易实现,只需要修改ReplicaSetPod副本个数就可以了。

想要实现这个操作很简单,看下面的命令

1
2
3
4
5
6
7
8
9
10
# 第一种方式:使用scale命令进行扩展
kubectl scale deployment nginx-deployment --replicas=4
# 再使用如下命令就可以看到Deployment个数进行了增长
NAME READY UP-TO-DATE AVAILABLE AGE
nginx-deployment 4/4 4 4 3d23h


# 第二种方式: 修改yml文件的replicas属性为4,再执行下面的命令
kubectl apply -f nginx-deployment.yml
# 也能看到Pod个数变成了4

滚动更新

我们如下命令对Deployment进行删除,并且在重建的时候加上--record参数,这个参数为了记录你每次操作执行的命令方便以后查看

1
2
3
4
5
6
7
8
# 先删除
kubectl delete -f nginx-deployment.yml
# 重建
kubectl apply -f nginx-deployment.yml --record
# 查看创建好的Deployment
kubectl get Deployment
NAME READY UP-TO-DATE AVAILABLE AGE
nginx-deployment 3/3 3 3 73s

从上面的输出中可以看到,这个Deployment有四个状态字段:

  1. DESIRED:用户期望的Pod副本个数(spec.replicas)
  2. READY:当前处于Running状态的Pod数量
  3. UP-TO-DATE:当前处于最新版本Pod的个数,所谓最新版本是指Pod的Spec部分跟Deployment里模板定义的一样。(可以会存在正在进行滚动更新还没删除的老Pod)
  4. AVAILABLE:当前已经可用的Pod个数,既是Running状态,有事最新版本,而且已经处于Ready状态的Pod个数,这个属性才是我们最为期待成功的属性

我们再来尝试查看这个ReplicaSet

1
2
3
[root@localhost home]# kubectl get rs
NAME DESIRED CURRENT READY AGE
nginx-deployment-76bf4969df 3 3 3 7m18s

如上所示用户提交Deployment对象之后,Deployment Controller为立即创建一个副本数为3的ReplicaSet,这个ReplicaSet的名字是Deployment名字和一个随机字符串组成的,这个随机字符串叫做pod-template-hash。同时Pod也会加上这个字符串用于跟其他的Pod进行区分。通过这个列表可以看到,Deployment只是比ReplicaSet多了一个UP-TO-DATE字段

1
2
3
4
5
6
7
8
9
10
11
12
#可以使用如下命令看看Pod
[root@localhost home]# kubectl get pod
NAME READY STATUS RESTARTS AGE
host-pod 1/1 Running 3 8d
nginx 2/2 Running 10 9d
nginx-54458cd494-79ntm 1/1 Running 1 3d23h
nginx-54458cd494-h92mx 1/1 Running 9 18d
nginx-54458cd494-k4qwk 1/1 Running 1 3d23h
nginx-54458cd494-sddb4 1/1 Running 9 18d
nginx-deployment-76bf4969df-8xmr9 1/1 Running 0 25m
nginx-deployment-76bf4969df-n89x8 1/1 Running 0 25m
nginx-deployment-76bf4969df-plc4q 1/1 Running 0 25m

如果这个时候我修改了DeploymentPod模板,就会触发滚动更新操作,比如使用kubectl edit命令或者还是修改yaml文件再执行kubectl apply

这里我使用kubectl edit命令,升级nginx版本到1.9.1

1
2
3
4
5
[root@localhost home]# kubectl edit deployment/nginx-deployment

# 自动进入编辑器模式,修改镜像版本那一行为1.9.1

#我这边是vi打开的,使用 :wq 保存退出

kubectl edit编辑完保存退出之后,Kubernetes会马上触发滚动更新流程

通过如下命令可以看到滚动更新的流程

1
2
3
4
5
6
7
8
9
10
11
12
[root@localhost home]# kubectl describe deployment nginx-deployment

Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal ScalingReplicaSet 34m deployment-controller Scaled up replica set nginx-deployment-76bf4969df to 3
Normal ScalingReplicaSet 2m48s deployment-controller Scaled up replica set nginx-deployment-779fcd779f to 1
Normal ScalingReplicaSet 2m26s deployment-controller Scaled down replica set nginx-deployment-76bf4969df to 2
Normal ScalingReplicaSet 2m26s deployment-controller Scaled up replica set nginx-deployment-779fcd779f to 2
Normal ScalingReplicaSet 2m25s deployment-controller Scaled down replica set nginx-deployment-76bf4969df to 1
Normal ScalingReplicaSet 2m25s deployment-controller Scaled up replica set nginx-deployment-779fcd779f to 3
Normal ScalingReplicaSet 2m23s deployment-controller Scaled down replica set nginx-deployment-76bf4969df to 0

从Events中可以看出来,首先修改了Deployment的定义之后,Deployment Controller会根据模板的定义创建一个新的ReplicaSet,也就是这里的nginx-deployment-779fcd779f,老的ReplicaSet是nginx-deployment-76bf4969df

  • 2m48s,这个新的ReplicaSet控制的Pod副本数从0变成1,即水平扩展出一个副本
  • 紧接着在2m26s时,将老的ReplicaSet控制的Pod数从3变成了2,水平收缩成两个副本
  • 如此交替运行,一个扩,一个收

现在你在使用下面的命令查看可以看到从以前的2个ReplicaSet变成了3个ReplicaSet,旧的ReplicaSet每个属性的值都变成了0

1
2
3
4
5
[root@localhost home]# kubectl get rs
NAME DESIRED CURRENT READY AGE
nginx-54458cd494 4 4 4 18d
nginx-deployment-76bf4969df 0 0 0 35m
nginx-deployment-779fcd779f 3 3 3 4m19s

滚动更新的优势

比如在刚开始升级的时候,集群里只有1个新增的新版本Pod,这个新Pod启动不起来,滚动更新就停止了,从而开发和运维介入,这个介入结束之前都还是有两个旧的Pod在运行,所以服务并不会收到太多的影响。

这种情况也必须依赖于Pod使用Health Check检查应用的运行状态,而不是简单的依赖于容器的Running状态,要不然虽然容器可能已经Running了,但是服务可能还是没启动,那滚动更新的效果就达不到了。(就像我们简单的运行一个SpringBoot项目,万一它启动做了很多事,初始化一堆东西,需要几分钟,但是只要这条启动命令java -jar xxx.jar运行起来没错Pod就是Running了,然后就启动其它Pod,最后发现新三个Pod全部启动但是都还在初始化,旧的就全被删了,这样服务就存在不可用的时间节点)

为了保证服务的连续性,Deployment Controller还会保证在任何时间内,只有指定比例的Pod处于离线状态,同事也确保任何时间内只有指定比例的新Pod创建出来。这两个比例都可配置,默认是DESIRED的25%

上面的例子中,最少有2个Pod处于可用状态,最多有4个同时在集群中,这个配置还是在Deployment中的配置中,字段名为RollingUpdateStrategy,如下:

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
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
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 1

新增下面的strategy字段,maxSurge指定除了DESIRED之外在一次滚动中,最多还可以创建多少个新Pod,而maxUnavailable是指在一次滚动中最多删除多少个旧Pod,当然除了配置数值也能直接配置百分比像50%这种。滚动更新的时候示意图如下:

image

Deployment控制ReplicaSet的数量和ReplicaSet的属性

ReplicaSet中Pod的数量是由ReplicaSet Controller来控制的。

一次失败的滚动更新

这次我们做一个错误的操作,我们制定nginx镜像为一个错误镜像nginx:1.91,这样就会出现升级失败

1
2
3
#直接使用如下命令修改镜像
[root@localhost ~]# kubectl set image deployment/nginx-deployment nginx=nginx:1.91
deployment.extensions/nginx-deployment image updated

因为我们这里其实使用的是一个错误的镜像版本,DockerHub中并不存在的镜像,Deployment的滚动更新会报错停止。

现在我们来看看现在集群里面的ReplicaSet

1
2
3
4
5
6
7
[root@localhost ~]# kubectl get rs
NAME DESIRED CURRENT READY AGE
nginx-54458cd494 4 4 4 19d
nginx-deployment-76bf4969df 0 0 0 11h
nginx-deployment-779fcd779f 3 3 3 11h
nginx-deployment-79dccf98ff 1 1 0 4s
nginx-deployment-strategy-76bf4969df 3 3 3 7m40s

能够看到名为nginx-deployment-79dccf98ff的ReplicaSet是新创建出来的,因为我们之前指定了maxSurge属性可以让他新创建的Pod数为1,现在这个ReplicaSet的DESIREDCURRENT都是1,但是READY确是0,因为拉取不到指定的镜像,我们也可以使用如下命令查看现在集群中的Pod,发现有一个Pod的状态是ImagePullBackOff

image

滚动更新失败的回滚

回滚Deployment版本其实很简单,只需要执行kubectl rollout undo命令,就能把Deployment回滚到上个版本。

1
2
3
4
5
6
7
8
9
[root@localhost ~]# kubectl rollout undo  deployment/nginx-deployment
deployment.extensions/nginx-deployment rolled back
[root@localhost ~]# kubectl get rs
NAME DESIRED CURRENT READY AGE
nginx-54458cd494 4 4 4 19d
nginx-deployment-76bf4969df 0 0 0 11h
nginx-deployment-779fcd779f 3 3 3 11h
nginx-deployment-79dccf98ff 0 0 0 9m21s
nginx-deployment-strategy-76bf4969df 3 3 3 16m

执行完成之后我们再次查看ReplicaSet,发现之前升级镜像版本指定的1.91创建的ReplicaSet的所有值全部在此变回了0

如果想回退到更早的版本,可以使用kubectl rollout history查看每次修改Deployment对应的版本,由于我们之前创建Deployment的时候加上了--record参数,所以我们执行的kubectl命令都被会记录下来。

1
2
3
4
5
[root@localhost ~]# kubectl rollout history deployment/nginx-deployment
deployment.extensions/nginx-deployment
REVISION CHANGE-CAUSE
1 kubectl create --filename=/home/nginx-deployment.yml --record=true
2 kubectl create --filename=/home/nginx-deployment.yml --record=true

像这里2就是之前那个失败的操作,指定镜像版本1.91那个,使用如下命令可以看到2的细节

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[root@localhost ~]# kubectl rollout history deployment/nginx-deployment --revision=2
deployment.extensions/nginx-deployment with revision #2
Pod Template:
Labels: app=nginx
pod-template-hash=79dccf98ff
Annotations:kubernetes.io/change-cause: kubectl create --filename=/home/nginx-deployment.yml --record=true
Containers:
nginx:
Image: nginx:1.91
Port: 80/TCP
Host Port: 0/TCP
Environment: <none>
Mounts: <none>
Volumes: <none>

可以看到镜像版本1.91,有了这个我们同样也可以制定Deployment回滚到哪个版本,同时也是按照滚动更新的方式进行回滚,通过如下命令

1
2
3
# --to-revision指定回滚到哪个版本

[root@localhost ~]# kubectl rollout undo deployment/nginx-deployment --to-revision=2

更高级的更新

每次对Deployment进行一次更新操作都会触发创建一个新的ReplicaSet,这样弄下来是不是ReplicaSet太多了你觉得,Kubernetes提供了一种操作,让你多次更新只创建一个ReplicaSet,具体如下:

1
2
3
4
5
6
7
# 我们先查看这个nginx-deployment的Deployment有两个ReplicaSet,中间的两个
[root@localhost ~]# kubectl get rs
NAME DESIRED CURRENT READY AGE
nginx-54458cd494 4 4 4 19d
nginx-deployment-76bf4969df 2 2 2 155m
nginx-deployment-79dccf98ff 2 2 0 155m
nginx-deployment-strategy-76bf4969df 3 3 3 3h

更新之前,首先执行下面的这条命令,目的是让Deployment先进入一个暂停状态:

1
2
[root@localhost ~]# kubectl rollout pause deployment/nginx-deployment
deployment.extensions/nginx-deployment paused

执行完这条命令之后,我们对Deployment的修改都不触发滚动更新,也不会创建新的ReplicaSet。

同时我们执行下面这三条命令,先把镜像版本改成1.92,第二步再改成1.93,发现ReplicaSet并没有任何改变。

1
2
3
4
5
6
7
8
9
10
[root@localhost ~]# kubectl set image deployment/nginx-deployment nginx=nginx:1.92
deployment.extensions/nginx-deployment image updated
[root@localhost ~]# kubectl set image deployment/nginx-deployment nginx=nginx:1.93
deployment.extensions/nginx-deployment image updated
[root@localhost ~]# kubectl get rs
NAME DESIRED CURRENT READY AGE
nginx-54458cd494 4 4 4 19d
nginx-deployment-76bf4969df 2 2 2 159m
nginx-deployment-79dccf98ff 2 2 0 159m
nginx-deployment-strategy-76bf4969df 3 3 3 3h4m

有了上面的暂停状态,自然也能恢复回来,执行下面这条命令,就能恢复到正常状态,这两条命令之间执行的多个更新操作最后只会生成一个ReplicaSet。现在执行下面命令进行恢复状态:

1
2
3
4
5
6
7
8
9
10
11
12
[root@localhost ~]# kubectl rollout resume deployment/nginx-deployment
deployment.extensions/nginx-deployment resumed

#这条命令可以看到新增的ReplicaSet

[root@localhost ~]# kubectl get rs
NAME DESIRED CURRENT READY AGE
nginx-54458cd494 4 4 4 19d
nginx-deployment-5575f94c85 2 2 0 8s
nginx-deployment-76bf4969df 2 2 2 161m
nginx-deployment-79dccf98ff 0 0 0 161m
nginx-deployment-strategy-76bf4969df 3 3 3 3h6m

虽然上面的操作能够减少ReplicaSet的数量,但是多次操作还是很多,可能我们并不需要保留这么多的历史操作记录,Deployment对象有个字段spec.revisionHistoryLimit,就是用来限制Kubernetes为Deployment保留的历史个数,如果直接设置为0,就不能做回滚操作

金丝雀发布(Canary Deployment)和蓝绿部署(Blue-Green Deployment)

蓝绿部署

蓝绿部署:蓝绿部署是不停老版本,部署新版本然后进行测试,确认OK,将流量切到新版本,然后老版本同时也升级到新版本。

灰度发布

灰度发布(金丝雀发布)是指在黑与白之间,能够平滑过渡的一种发布方式。AB test就是一种灰度发布方式,让一部分用户继续用A,一部分用户开始用B,如果用户对B没有什么反对意见,那么逐步扩大范围,把所有用户都迁移到B上面来。灰度发布可以保证整体系统的稳定,在初始灰度的时候就可以发现、调整问题,以保证其影响度,而我们平常所说的金丝雀部署也就是灰度发布的一种方式。

金丝雀发布名字的来源

注释:矿井中的金丝雀
17世纪,英国矿井工人发现,金丝雀对瓦斯这种气体十分敏感。空气中哪怕有极其微量的瓦斯,金丝雀也会停止歌唱;而当瓦斯含量超过一定限度时,虽然鲁钝的人类毫无察觉,金丝雀却早已毒发身亡。当时在采矿设备相对简陋的条件下,工人们每次下井都会带上一只金丝雀作为“瓦斯检测指标”,以便在危险状况下紧急撤离。

金丝雀发布

有了Deployment是不是这些都很简单,金丝雀发布先用金丝雀做探视,就好像我们的Deployment先部署新的ReplicaSet开始新建Pod做测试,成功就继续增加新的Pod,新增新的Pod减少老的Pod。

少量金丝雀接受流量,再全量发布。

优点:

  • 用户体验小,金丝雀发布过程中只影响少量用户

流量发布图如下:

image

蓝绿部署

蓝绿部署其实很简单,就是先把新的部署好,再把流量全部切换过去。
可以先创建blue.yaml部署起来运行一段时间,现在复制一个green.yaml,修改需要改变的信息,然后让Service指向这个gree.yaml新建的Pod。

image