📑 官方文档

Concept

DaemonSet的主要作用,是在Kubernetes集群里,运行一个Daemon Pod。这个Pod有如下三个特征:

  1. 这个Pod运行在Kubernetes集群里的每一个节点上;
  2. 每个节点上只有一个这样的Pod实例;
  3. 当有新的节点加入Kubernetes集群后,该Pod会自动地在新节点上被创建出来;而当旧节点被删除后,它上面的Pod也相应地会被回收掉。

DaemonSet的存在意义:

  1. 各种网络插件的Agent组件,都必须运行在每一个节点上,用来处理这个节点上的容器网络;
  2. 各种存储插件的Agent组件,也必须运行在每一个节点上,用来在这个节点上挂载远程存储目录,操作容器的Volume目录;
  3. 各种监控组件和日志组件,也必须运行在每一个节点上,负责这个节点上的监控信息和日志搜集。

YAML

看下面的fluentd-elasticsearch.yaml对一个DaemonSet的定义:

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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,管理的是一个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在运行。

而检查的结果,可能有这三种情况:

  1. 没有这种Pod,那么就意味着要在这个Node上创建这样一个Pod;
  2. 有这种Pod,但是数量大于1,那就说明要把多余的Pod从这个Node上删除掉;
  3. 正好只有一个这种Pod,那说明这个节点是正常的。

其中,删除节点(Node)上多余的Pod非常简单,直接调用Kubernetes API就可以了。

(2)如何在指定的Node上创建新Pod呢?

答案是:用nodeSelector,选择Node的名字即可。

1
2
nodeSelector:
name: <Node名字>

在Kubernetes里,nodeSelector是一个将要被废弃的字段了。

nodeAffinity

现在有了一个新的、功能更完善的字段可以代替nodeSelector,即:nodeAffinity。

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
apiVersion: v1
kind: Pod
metadata:
name: with-node-affinity
spec:
affinity:
nodeAffinity:
requireDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: metadata.name
operator: In
values:
- node-highness

在这个Pod里,声明了一个spec.affinity字段,然后定义了一个nodeAffinity。其中,spec.Affinity字段,是Pod里跟调度相关的一个字段。

在这里,定义的nodeAffinity的含义是:

  1. requireDuringSchedulingIgnoredDuringExecution:这个nodeAffinity必须在每次调度的时候予以考虑。同时,这也意味着可以设置在某些情况下不考虑这个nodeAffinity;
  2. 这个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
2
3
4
5
6
7
8
9
apiVersion: v1
kind: Pod
metadata:
name: with-toleration
spec:
tolerations:
- key: node.kubernetes.io/unschedulable
operator: Exists
effect: NoSchedule

这个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
2
3
4
5
6
7
8
9
10
...
template:
metadata:
labels:
name: network-plugin-agent
spec:
tolerations:
- key: node.kubernetes.io/network-unavailable
operator: Exists
effect: NoSchedule

在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
2
3
tolerations:
- key: node-role.kubernetes.io/master
effect: NoSchedule

这是因为在默认情况下,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
2
3
$ kubectl get pod -n kube-system -l name=fluentd-elasticsearch
NAME READY STATUS RESTARTS AGE
fluentd-elasticsearch-ntrkx 1/1 Running 0 10s

此时通过kubectl get查看Kubernetes集群里的DaemonSet对象:

1
2
3
$ kubectl get ds -n kube-system fluentd-elasticsearch
NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE
fluentd-elasticsearch 1 1 1 1 1 <none> 53s

Kubernets里比较长的API对象都有短名字,比如DaemonSet对应的是ds,Deployment对应的是deploy。

可以看到,DaemonSet和Deployment一样,也有DESIRED、CURRENT等多个状态字段。这也就意味着,DaemonSet可以像Deployment那样,进行版本管理。这个版本,可以使用kubectl rollout history看到:

1
2
3
4
$ kubectl rollout history daemonset fluentd-elasticsearch -n kube-system
daemonset.apps/fluentd-elasticsearch
REVISION CHANGE-CAUSE
1 <none>

接下来,把这个DaemonSet的容器镜像版本升级到v2.2.0:

1
2
3
$ kubectl set image ds/fluentd-elasticsearch fluentd-elasticsearch=k8s.gcr.io/fluentd-elasticsearch:v2.2.0 --record -n=kube-system
Flag --record has been deprecated, --record will be removed in the future
daemonset.apps/fluentd-elasticsearch image updated

这个kubectl set iamge命令里,第一个fluentd-elasticsearch是DaemonSet的名字,第二个fluentd-elasticsearch是容器的名字。

这时候,可以使用kubectl rollout status命令看到这个“滚动更新”的过程,如下所示:

1
2
3
$ kubectl rollout status ds/fluentd-elasticsearch -n kube-system
Waiting for daemon set "fluentd-elasticsearch" rollout to finish: 0 of 1 updated pods are available...
daemon set "fluentd-elasticsearch" successfully rolled out

在这次升级命令后面加上了--record参数,所以这次升级使用到的指令就会自动出现在DaemonSet的rollout hostory里面,如下所示:

1
2
3
4
5
6
7
8
$ kubectl rollout history daemonset fluentd-elasticsearch -n kube-system
daemonset.apps/fluentd-elasticsearch
daemonset.apps/fluentd-elasticsearch
REVISION CHANGE-CAUSE
1 <none>
2 kubectl set image ds/fluentd-elasticsearch fluentd-elasticsearch=k8s.gcr.io/fluentd-elasticsearch:v2.2.0 --record=true --namespace=kube-system

zsh: no such file or directory: daemonset.apps/fluentd-elasticsearch

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

Problem-2

Deployment管理这些版本,靠的是“一个版本对应一个ReplicaSet对象”。DaemonSet控制器操作的直接就是Pod,没有ReplicaSet这样的对象参与其中。那么,它的这些版本又是如何维护的呢?

在Kubernetes中,任何需要记录下来的状态,都可以被用API对象的方式实现。当然,”版本“也不例外。

Kubernetes v1.7之后添加了一个API对象,名叫ControllerRevision,专门用来记录某种Controller对象的版本。比如,可以通过如下命令查看fluentd-elasticsearch对应的ControllerRevision:

1
2
3
4
$ kubectl get controllerrevision -n kube-system -l name=fluentd-elasticsearch
NAME CONTROLLER REVISION AGE
fluentd-elasticsearch-7464ccb7c daemonset.apps/fluentd-elasticsearch 2 97s
fluentd-elasticsearch-76fd8fd678 daemonset.apps/fluentd-elasticsearch 1 3m19s

而如果使用kubectl describe查看这个ControllerRevision这个对象:

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
$ kubectl describe controllerrevision fluentd-elasticsearch-7464ccb7c -n kube-system
Name: fluentd-elasticsearch-7464ccb7c
Namespace: kube-system
Labels: controller-revision-hash=7464ccb7c
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-sy...
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
Resources:
Limits:
Memory: 200Mi
Requests:
Cpu: 100m
Memory: 200Mi
Termination Message Path: /dev/termination-log
Termination Message Policy: File
Volume Mounts:
Mount Path: /var/log
Name: varlog
Mount Path: /var/lib/docker/containers
Name: varlibdockercontainers
Read Only: true
Dns Policy: ClusterFirst
Restart Policy: Always
Scheduler Name: default-scheduler
Security Context:
Termination Grace Period Seconds: 30
Tolerations:
Effect: NoSchedule
Key: node-role.kubernetes.io/master
Volumes:
Host Path:
Path: /var/log
Type:
Name: varlog
Host Path:
Path: /var/lib/docker/containers
Type:
Name: varlibdockercontainers
Kind: ControllerRevision
Metadata:
Creation Timestamp: 2022-03-24T09:49:08Z
Managed Fields:
API Version: apps/v1
Fields Type: FieldsV1
fieldsV1:
f:data:
f:metadata:
f:annotations:
.:
f:deprecated.daemonset.template.generation:
f:kubernetes.io/change-cause:
f:labels:
.:
f:controller-revision-hash:
f:name:
f:ownerReferences:
.:
k:{"uid":"ebbedab9-2879-41e7-a6a1-34b5a0663869"}:
f:revision:
Manager: kube-controller-manager
Operation: Update
Time: 2022-03-24T09:49:08Z
Owner References:
API Version: apps/v1
Block Owner Deletion: true
Controller: true
Kind: DaemonSet
Name: fluentd-elasticsearch
UID: ebbedab9-2879-41e7-a6a1-34b5a0663869
Resource Version: 702879
UID: 2e00c02f-70b5-4200-ba84-61aa9e67fe9c
Revision: 2
Events: <none>

可以看到,这个ControllerRevision对象,实际上是在Data字段保存了该版本对应的完整的DaemonSet对象。并且,在Annotation字段保存了创建这个对象所使用的kubectl命令。

接下来,可以尝试将这个DaemonSet回滚到Revision=1时的状态:

1
2
$ kubectl rollout undo daemonset fluentd-elasticsearch --to-revision=1 -n kube-system
daemonset.apps/fluentd-elasticsearch rolled back

这个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
2
3
4
5
6
$ kubectl rollout history daemonset fluentd-elasticsearch -n kube-system
daemonset.apps/fluentd-elasticsearch
REVISION CHANGE-CAUSE
2 kubectl set image ds/fluentd-elasticsearch fluentd-elasticsearch=k8s.gcr.io/fluentd-elasticsearch:v2.2.0 --record=true --namespace=kube-system
3 <none>