Kubernetes-DaemonSet
📑 官方文档
Concept
DaemonSet的主要作用,是在Kubernetes集群里,运行一个Daemon Pod。这个Pod有如下三个特征:
- 这个Pod运行在Kubernetes集群里的每一个节点上;
- 每个节点上只有一个这样的Pod实例;
- 当有新的节点加入Kubernetes集群后,该Pod会自动地在新节点上被创建出来;而当旧节点被删除后,它上面的Pod也相应地会被回收掉。
DaemonSet的存在意义:
- 各种网络插件的Agent组件,都必须运行在每一个节点上,用来处理这个节点上的容器网络;
- 各种存储插件的Agent组件,也必须运行在每一个节点上,用来在这个节点上挂载远程存储目录,操作容器的Volume目录;
- 各种监控组件和日志组件,也必须运行在每一个节点上,负责这个节点上的监控信息和日志搜集。
YAML
看下面的fluentd-elasticsearch.yaml对一个DaemonSet的定义:
1 | apiVersion: apps/v1 |
这个DaemonSet,管理的是一个fluentd-elasticsearch镜像的Pod。这个镜像的功能非常实用:通过fluentd将Docker容器里的日志转发到ElasticSearch中。
可以看到,DaemonSet跟Deployment其实非常相似,只不过是没有replicas字段;它使用selector选择管理所有携带了name=fluentd-elasticsearch
标签多Pod。
而这些Pod的模板,也是用template字段定义的。在这个字段中,定义了一个是用fluentd-elasticsearch:1.20镜像的容器,而且这个容器挂载了两个hostPath类型的Volume,分别对应宿主机的/var/log目录和/var/lib/docker/containers目录。
显然,fluentd启动之后,它会从这两个目录里搜集日志信息,并转发给ElasticSearch保存。这样,通过ElasticSearch就可以很方便地检索这些日志了。
需要注意的是,Docker容器里应用的日志,默认会保存在宿主机的/var/lib/docker/containers/{{.ContainerID}}/{{.ContainerID}}-json.log
文件里,所以这个目录正是fluentd的搜集目标。
Problem-1
(1)DaemonSet又是如何保证每个Node上有且仅有一个被管理的Pod呢?
这是一个典型的“控制器模型”能够处理的问题。
DaemonSet Controller,首先从Etcd里获取所有的Node列表,然后遍历所有的Node。这时,它就可以很容易地去检查,当前这个Node上是不是有一个携带了name=fluentd-elasticsearch
标签的Pod在运行。
而检查的结果,可能有这三种情况:
- 没有这种Pod,那么就意味着要在这个Node上创建这样一个Pod;
- 有这种Pod,但是数量大于1,那就说明要把多余的Pod从这个Node上删除掉;
- 正好只有一个这种Pod,那说明这个节点是正常的。
其中,删除节点(Node)上多余的Pod非常简单,直接调用Kubernetes API就可以了。
(2)如何在指定的Node上创建新Pod呢?
答案是:用nodeSelector,选择Node的名字即可。
1 | nodeSelector: |
在Kubernetes里,nodeSelector是一个将要被废弃的字段了。
nodeAffinity
现在有了一个新的、功能更完善的字段可以代替nodeSelector,即:nodeAffinity。
举个例子:
1 | apiVersion: v1 |
在这个Pod里,声明了一个spec.affinity字段,然后定义了一个nodeAffinity。其中,spec.Affinity字段,是Pod里跟调度相关的一个字段。
在这里,定义的nodeAffinity的含义是:
- requireDuringSchedulingIgnoredDuringExecution:这个nodeAffinity必须在每次调度的时候予以考虑。同时,这也意味着可以设置在某些情况下不考虑这个nodeAffinity;
- 这个Pod,将来只允许运行在“metadata.name”是“node-highness”的节点上。
在这里,应该注意到nodeAffinity的定义,可以支持个国家丰富封余发,比如operator: In(即:部分匹配;如果定义operator: Equal,就是完全匹配),这也正是nodeAffinity会取代nodeSelector的原因之一。
Controller
DaemonSet Controller会在创建Pod的时候,自动在这个Pod的API对象里,加上这样 的NodeAffinity定义。其中,需要绑定的节点名字,正是当前正在遍历的这个Node。
当然,DaemonSet并不需要修改用户提交的YAML文件里的Pod模板,而是在向Kubernetes发起请求之前,直接修改根据模板生产的Pod对象。
此外,DaemonSet还会给这个Pod自动加上另外一个与调度相关的字段,叫作tolerations。这个字段意味着这个Pod,会容忍某些Node的污点。
而DaemonSet自动加上的tolerations字段,格式如下所示:
1 | apiVersion: v1 |
这个Toleration的含义就是:容忍所有被标记为unschedulable污点的Node;容忍的效果是允许调度。
而在正常情况下,被标记了unschdulable污点的Node,是不会有任何Pod被调度上去的(effect:NoSchedule)。可是,DaemonSet自动地给被管理的Pod加上了这个特殊的Toleration,就使得这些Pod可以忽略这个限制,继而保证每个节点上都会被调度一个Pod。当然,如果这个节点有故障的话,这个Pod可能会启动失败,而DaemonSet则会始终尝试下去,直到Pod启动成功。
DaemonSet的过人之处,其实就是依靠Toleration实现的。
假如当前DaemonSet管理的,是一个网络插件的Agent Pod,那么在这个DaemonSet的YAML文件里,给它的Pod模板加上一个能够容忍node.kubernetes.io/unschedulable
污点的Toleration。正如下面这个例子:
1 | ... |
在Kubernetes项目中,当一个节点的网络插件尚未安装时,这个节点就会被自动加上名为node.kubernetes.io/network-unavailable
的污点。
而通过这样一个Toleration,调度器在调度这个Pod的时候,就会忽略当前节点上的“污点”,从而成功地将网络插件的Agent组件调度到这台机器上启动起来。
这种机制,正是在部署Kubernetes集群的时候,能够先部署Kubernetes本身、再部署网络插件的根本原因;因为当时创建的Weave的YAML,实际上就是一个DaemonSet。
Daemon Set其实时一个非常简单的控制器。在它的控制循环中,只需要遍历所有节点,然后根据节点上是否有被管理Pod的情况,来决定是否要创建或者删除一个Pod。
只不过,在创建每个Pod的时候,DaemonSet会自动给这个Pod加上一个nodeAffinity,从而保证这个Pod只会在指定节点上启动。同时,他还会自动给这个Pod加上一个Toleration,从而忽略节点的unschedulable“污点”。
当然,可以在Pod模板里加上更多种类多Toleration,从而利用DaemonSet达到自己的目的。比如,在这个fluentd-elasticsearch DaemonSet里,就给它加上了这样的Toleration:
1 | tolerations: |
这是因为在默认情况下,Kubernetes集群不允许用户在Master节点部署Pod。因为,Master节点默认携带了一个叫作node-role.kubernetes.io/master的“污点”。所以,为了能在Master节点上部署DaemonSet的Pod,就必须让这个Pod容忍这个污点。
Practice
首先,创建上面的fluentd-elasticsearch.yaml对应的DaemonSet对象:
1 | kubectl create -f fluentd-elasticsearch.yaml |
需要注意的是,在DaemonSet上,一般都应该加上resources字段,来限制它的CPU和内存使用。
比如我的集群里,只有一个Pod,如下所示:
1 | kubectl get pod -n kube-system -l name=fluentd-elasticsearch |
此时通过kubectl get
查看Kubernetes集群里的DaemonSet对象:
1 | kubectl get ds -n kube-system fluentd-elasticsearch |
Kubernets里比较长的API对象都有短名字,比如DaemonSet对应的是ds,Deployment对应的是deploy。
可以看到,DaemonSet和Deployment一样,也有DESIRED、CURRENT等多个状态字段。这也就意味着,DaemonSet可以像Deployment那样,进行版本管理。这个版本,可以使用kubectl rollout history
看到:
1 | kubectl rollout history daemonset fluentd-elasticsearch -n kube-system |
接下来,把这个DaemonSet的容器镜像版本升级到v2.2.0:
1 | kubectl set image ds/fluentd-elasticsearch fluentd-elasticsearch=k8s.gcr.io/fluentd-elasticsearch:v2.2.0 --record -n=kube-system |
这个kubectl set iamge
命令里,第一个fluentd-elasticsearch是DaemonSet的名字,第二个fluentd-elasticsearch是容器的名字。
这时候,可以使用kubectl rollout status
命令看到这个“滚动更新”的过程,如下所示:
1 | kubectl rollout status ds/fluentd-elasticsearch -n kube-system |
在这次升级命令后面加上了--record
参数,所以这次升级使用到的指令就会自动出现在DaemonSet的rollout hostory里面,如下所示:
1 | kubectl rollout history daemonset fluentd-elasticsearch -n kube-system |
有了版本号,就可以像Deployment一样,将DaemonSet回滚到某个指定的历史版本了。
Problem-2
Deployment管理这些版本,靠的是“一个版本对应一个ReplicaSet对象”。DaemonSet控制器操作的直接就是Pod,没有ReplicaSet这样的对象参与其中。那么,它的这些版本又是如何维护的呢?
在Kubernetes中,任何需要记录下来的状态,都可以被用API对象的方式实现。当然,”版本“也不例外。
Kubernetes v1.7之后添加了一个API对象,名叫ControllerRevision,专门用来记录某种Controller对象的版本。比如,可以通过如下命令查看fluentd-elasticsearch对应的ControllerRevision:
1 | kubectl get controllerrevision -n kube-system -l name=fluentd-elasticsearch |
而如果使用kubectl describe
查看这个ControllerRevision
这个对象:
1 | kubectl describe controllerrevision fluentd-elasticsearch-7464ccb7c -n kube-system |
可以看到,这个ControllerRevision对象,实际上是在Data字段保存了该版本对应的完整的DaemonSet对象。并且,在Annotation字段保存了创建这个对象所使用的kubectl命令。
接下来,可以尝试将这个DaemonSet回滚到Revision=1时的状态:
1 | kubectl rollout undo daemonset fluentd-elasticsearch --to-revision=1 -n kube-system |
这个kubectl rollout undo
操作,实际上相当于读取到了Revision=1的ControllerRevision对象保存的Data字段。而这个Data字段里保存的信息,就是Revision=1时这个DaemonSet的完整API对象。
所以,现在DaemonSet Controller就可以使用这个历史API对象,对现有的DaemonSet做一次PATCH操作(等价于执行一次kubectl apply -f "旧的DaemonSet对象"
),从而把这个DaemonSet“更新”到一个旧版本。
所以,执行完这次回滚之后,DaemonSet的Revision并不会从Revision=2退回到1,而是增加成Revision=3,因为一个ControllerRevision被创建了,验证如下:
1 | kubectl rollout history daemonset fluentd-elasticsearch -n kube-system |