我们说了如何使用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。