我们说了如何使用StatefulSet编排有状态容器,StatefulSet其实就是对典型运维业务的容器化抽象,在不使用容器的情况下,也能自己去DIY一个类似的方案,但是一旦涉及到了升级,版本控制,Kubernetes可以突出这个好处

比如,对StatefulSet进行滚动更新,其实,只需要修改StatefulSet的Pod模板,就能自动触发滚动升级

$ kubectl patch statefulset mysql –type=’json’ -p='[{“op”: “replace”, “path”: “/spec/template/spec/containers/0/image”, “value”:”mysql:5.7.23″}]’

statefulset.apps/mysql patched

使用了kubectl patch命令,以打补丁的方式,修改API对象的指定字段,就是后面指定的

/spec/template/spec/containers/0/image

这样,StatefulSet Controller就会按照和Pod编号相反的顺序,从最后一个Pod开始,逐步更新这个StatefulSet管理的每一个Pod,如果发生了错误,这次滚动更新就会停止,而且,这种滚动更新的方式,还支持细粒度的区分,比如金丝雀发布或者灰度发布,应用中多个实例中指定的一部分更新,或者指定的一部分不更新

这个字段是

statefulSet的spec.updateStrategy.rollingUpdate的Partition字段

我们再打一个补丁

$ kubectl patch statefulset mysql -p ‘{“spec”:{“updateStrategy”:{“type”:”RollingUpdate”,”rollingUpdate”:{“partition”:2}}}}’

statefulset.apps/mysql patched

kubectl patch命令后面的参数,就是Partition字段在API对象中的路径,上述操作等于是将这个rollingUpdate字段修改为了2

这样,在镜像发生了改变,比如更新为了5.7.23,那么只有序号大于等于2的Pod会被更新到这个版本,并且删除或者重启了小于等于2的Pod,再次重启,也不会被升级为新的镜像

但是StatefulSet不仅如此,在其中还有一个变种DaemonSet,在Kubernetes中,运行一个Daemon Pod作为守护Pod

具体关于DaemonSet的作用,是在Kubernetes集群中,运行一个Daemon Pod

这个Pod呢,可以运行在Kubernetes集群中每一个节点上

每一个节点上呢,有且只有一个这样的Pod实例

每当有新的节点加入Kuberentes集群时候,这个Pod会立刻在这个新的节点上被创建出来,旧的节点删除的时候,上面的Pod也会被立刻回收掉

常见的需要用到Daemon Pod的地方又

1.各种网络插件的Agent组件,必须要在每一个节点上,处理这个节点上的容器网络

2.存储的插件,必须运行在每一个节点上,用来挂载容器的Volume目录

3.各种监控和日志组件,运行在每一个节点上,负责节点上的监控信息和日志搜集

而且,和很多编排对象不一样,DaemonSet运行时机,比Kubernetes集群中节点上线的时机都要早

这是因为DaemonSet可能是网络插件,如果这个不先进行运行,那么可能就导致整个集群的不可用

所以DaemonSet的设计,必然存在着某种过人之处

我们先看一下API的定义

apiVersion: apps/v1

kind: DaemonSet

metadata:

name: fluentd-elasticsearch

namespace: kube-system

labels:

k8s-app: fluentd-logging

spec:

selector:

matchLabels:

name: fluentd-elasticsearch

template:

metadata:

labels:

name: fluentd-elasticsearch

spec:

tolerations:

– key: node-role.kubernetes.io/master

effect: NoSchedule

containers:

– name: fluentd-elasticsearch

image: k8s.gcr.io/fluentd-elasticsearch:1.20

resources:

limits:

memory: 200Mi

requests:

cpu: 100m

memory: 200Mi

volumeMounts:

– name: varlog

mountPath: /var/log

– name: varlibdockercontainers

mountPath: /var/lib/docker/containers

readOnly: true

terminationGracePeriodSeconds: 30

volumes:

– name: varlog

hostPath:

path: /var/log

– name: varlibdockercontainers

hostPath:

path: /var/lib/docker/containers

这个DaemonSet,管理的是一个fluented-elasticsearch镜像的Pod,镜像的功能非常实用,通过fluented将Docker容器的日志转发给ElasticSearch中

而且DaemonSet和Deployment非常类似,不过没有replicas字段,同样使用了selector选择管理了所有带有name=fluented-elasticsearch标签的Pod

这些Pod模板,也是利用了template字段,在这个字段中,使用了一个fluentd-elasticsearch:1.20的镜像,分别对应挂载了宿主机的/var/log和/var/lib/docker/containers目录

显然,fluentd启动后,会从这两个目录中搜集日志,并发送给ElasticSearch来保存

而上面挂载的目录,正式Docker容器中应用的日志,保存在了/var/lib/docker/containers/{{.容器ID}}/{{.容器ID}}-json.log文件中,正是fluentd的搜集目标

接下来,就是DaemonSet如何保证每个Node上有且只有一个被管理的Pod的呢?

这是一个典型的控制器面模型能够处理的问题

DaemonSet Controller当中,从Etcd获取到所有的Node,然后遍历所有的Node,从每个Node中检查是不是携带了一个name=fluentd-elasicsearch标签的Pod在运行

如果出现了没有

那么就需要创建一个这样的Pod

有这样的Pod,但是数量大于1,就说明要将多余的Pod进行删除

如果有且只有一个,那么说明没问题

那么如何去创建这样的Pod,在一个指定的Node上

可以使用nodeSelector,指定Node名字就可以了

不过,在Kubernetes项目中,nodeSelector是一个将要废弃的字段了,现在提倡使用的,是nodeAffiniy,举个例子

apiVersion: v1

kind: Pod

metadata:

name: with-node-affinity

spec:

affinity:

nodeAffinity:

requiredDuringSchedulingIgnoredDuringExecution:

nodeSelectorTerms:

– matchExpressions:

– key: metadata.name

operator: In

values:

– node-geektime

在这个Pod中,我们定义了一个spec.affinity字段,定义了一个nodeAffinity,其中含义为requiredDuringSchedulingIgnoredDuringExecution,意味着,这个nodeAffinity必须在每次调度的时候都给与考虑,同时意味着,在某些情况下不考虑这个nodeAffinity

而且,nodeAffinity的定义中,支持了更多更加丰富的语法,比如operator:In 说明是部分匹配,如果定义了operator:Equal,说明是完全匹配,那么这个就是可以运行在metadata.name为node-geektime的节点上

DaemonSet Controller在创建Pod的时候,会自动在这个Pod的API对象中,加上一个这个的nodeAffinity,绑定的value,正是这个Node

而且DaemonSet还会给这个pod自动加上另一个和调度相关的字段,叫做tolerations,这个字段意味着这个Pod,会容忍某些Node的污点

自动加上的tolerations字段,格式如下

spec:

tolerations:

– key: node.kubernetes.io/unschedulable

operator: Exists

effect: NoSchedule

这个Toleration的含义,是所有被标记为unschedulable的污点的Node,都可以被容忍,一般情况下,被标记了unshedulabel污点的Node,是不会有任何Pod被调度上去的,可是DaemonSet自动的给被管理的Pod加上了这个Toleration,让其可以无视污点的限制,保证每个节点上都有对应的Pod

而且,如果DaemonSet管理的,是一个网络插件Agent Pod,还需要在这个DaemonSet的YAML文件中,给其Pod模板加上一个能够容忍 node.kubernetes.io/network-unavailable的污点的Toleration

而在kubernetes中集群中,如果一个节点的网络插件还没有安装,那么这个节点会被加上名为node.kubnetes.io/network-unavailable的污点

这种机制,就是我们再部署Kubernetes集群的时候,能够先部署Kubernetes,后部署网络插件的根本原因,所以,我们创建的Weave的YAML,实际上就是一个DaemonSet

所以DaemonSet本质上是一个无状态的简单的控制器,再其控制循环中,只需要遍历所有的节点,然后根据节点上是否有管理的Pod的情况,来决定是否创建或者删除一个Pod

不过在创建每一个Pod的时候,DaemonSet会自动给每个Pod加上一个nodeAffinity,从而保证Pod在指定node上启动

还会给Pod加上Toleration,从而忽略节点的unschedulable污点

而且,还能有多种的Toleration,来帮助完成对应的目的,比如下面的tolerations

tolerations:

– key: node-role.kubernetes.io/master

effect: NoSchedule

就可以在Master节点上部署这个DaemonSet的Pod

然后,我们看下DaemonSet整体的操作流程

一般在启动这个DaemonSet对象的时候,我们都会加上resources字段,限制其CPU和内存使用,防止占用过多的宿主机资源

我们在启动完成后.利用kubectl get 来查看一下Kubenretes中的DaemonSet对象

$ kubectl get ds -n kube-system fluentd-elasticsearch

NAME                    DESIRED   CURRENT   READY     UP-TO-DATE   AVAILABLE   NODE SELECTOR   AGE

fluentd-elasticsearch   2         2         2         2            2           <none>          1h

在K8S中,较长的API对象都有简写,比如DaemonSet对应的是ds,Deployement对应的是deploy

在DaemonSet中,也会有着像DESIRED CURRENT等多个状态字段,说明也能进行版本管理

我们可以先进行更新这个DaemonSet的容器及形象版本

kubectl set image ds/fluentd-elasticsearch fluentd-elasticsearch=k8s.gcr.io/fluented-elasticsearch:v2.2.0 –record -n=kube-system

然后,我们可以使用kubectl rollout status命令看到滚动更新的过程

$ kubectl rollout status ds/fluentd-elasticsearch -n kube-system

Waiting for daemon set “fluentd-elasticsearch” rollout to finish: 0 out of 2 new pods have been updated…

Waiting for daemon set “fluentd-elasticsearch” rollout to finish: 0 out of 2 new pods have been updated…

Waiting for daemon set “fluentd-elasticsearch” rollout to finish: 1 of 2 updated pods are available…

daemon set “fluentd-elasticsearch” successfully rolled out

然后,因为,在升级命令后面加上了-record参数,所以升级使用的指令会自动出现在DaemonSet的rollout history里面

$ kubectl rollout history daemonset fluentd-elasticsearch -n kube-system

daemonsets “fluentd-elasticsearch”

REVISION  CHANGE-CAUSE

1         <none>

2         kubectl set image ds/fluentd-elasticsearch fluentd-elasticsearch=k8s.gcr.io/fluentd-elasticsearch:v2.2.0 –namespace=kube-system –record=true

有了版本号,就可以像是Deployement一样,将DaemonSet一样回滚到某个指定的历史版本可

那么就是一个问题

Deployment利用了RS进行版本管理

那么ReplicaSet这样的对象,利用了上面进行版本维护的呢?毕竟DaemonSet控制器对应的是Pod,不可能有RS这样的对象参与其中

但是Kubernetes也添加了一个API对象,名为ControllerRevision,用来记录某种Controller对象的版本

比如查看fluentd-elasticsearch对应的ControllerRevision

$ kubectl get controllerrevision -n kube-system -l name=fluentd-elasticsearch

NAME                               CONTROLLER                             REVISION   AGE

fluentd-elasticsearch-64dc6799c9   daemonset.apps/fluentd-elasticsearch   2          1h

然后可以看出来对应的api对象的详情

$ kubectl describe controllerrevision fluentd-elasticsearch-64dc6799c9 -n kube-system

Name:         fluentd-elasticsearch-64dc6799c9

Namespace:    kube-system

Labels:       controller-revision-hash=2087235575

name=fluentd-elasticsearch

Annotations:  deprecated.daemonset.template.generation=2

kubernetes.io/change-cause=kubectl set image ds/fluentd-elasticsearch fluentd-elasticsearch=k8s.gcr.io/fluentd-elasticsearch:v2.2.0 –record=true –namespace=kube-system

API Version:  apps/v1

Data:

Spec:

Template:

$ Patch:  replace

Metadata:

Creation Timestamp:  <nil>

Labels:

Name:  fluentd-elasticsearch

Spec:

Containers:

Image:              k8s.gcr.io/fluentd-elasticsearch:v2.2.0

Image Pull Policy:  IfNotPresent

Name:               fluentd-elasticsearch

Revision:                  2

Events:                    <none>

这个ControllerRevision对象,在Data字段中保存了这个版本对应的完整的DaemonSet的API对象,并且,在Annotation字段保存了创建这个对象所需要的kubectl命令

接下来,我们尝试将这个DaemonSet回滚到Revision=1的状态

$ kubectl rollout undo daemonset fluentd-elasticsearch –to-revision=1 -n kube-system

daemonset.extensions/fluentd-elasticsearch rolled back

但是在执行完成回滚之后,DaemonSet的Revision并不会从Revision=2回滚到1,而是增加到Revision=3,这是因为,一个新的ControllerRevision被创建了出来

那么在本章中,我们介绍了StatefulSet的滚动更新,

然后是一个重点的编排对象DaemonSet

相比较于Deploymen,DaemonSet只管理Pod对象,然后根据nodeAffinity来部署在指定节点

利用Toleration来控制Pod数量

与此同时,DaemonSet使用ControllerRevision,来保存和管理自己的版本,这种面向API对象的设计思想,简化了控制器本身的逻辑,正是Kubernetes声明式API的优势所在

而且,在整个K8S中,StatefulSet也是通过ControllerRevision进行版本控制的

利用这个ControllerRevision,避免了为每一种控制器维护一套冗余的代码和控制问题

在Kubernetes v1.11之前,DaemonSet管理的Pod的调度流程,实际上是由DaemonSet Controller自己完成的,为何?

查阅了Ds用默认调度器代替controller的设计文档

之前的做法是:

controller判断调度谓词,符合的话直接在controller中直接设置spec.hostName去调度。

目前的做法是:

controller不再判断调度条件,给每个pode设置NodeAffinity。控制器根据NodeAffinity去检查每个node上是否启动了相应的Pod。并且可以利用调度优先级去优先调度关键的ds pods。

发表评论

邮箱地址不会被公开。 必填项已用*标注