了解DaemonSet

daemonSet的作用

DaemonSet 确保全部(或者某些)节点上运行一个 Pod 的副本。 当有节点加入集群时, 也会为他们新增一个 Pod 。 当有节点从集群移除时,这些 Pod 也会被回收。删除 DaemonSet 将会删除它创建的所有 Pod。

简单来说就是能够时刻同步的执行节点上的任务

DaemonSet 的一些典型用法:

  • 在每个节点上运行集群守护进程
  • 在每个节点上运行日志收集守护进程
  • 在每个节点上运行监控守护进程

一种简单的用法是为每种类型的守护进程在所有的节点上都启动一个 DaemonSet。
一个稍微复杂的用法是为同一种守护进程部署多个 DaemonSet;每个具有不同的标志, 并且对不同硬件类型具有不同的内存、CPU 要求。

DaemonSet 的特征

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

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

虽然从这些概念中, 我们感觉这个控制器非常简单 ,但是他的作用确至关重要的。
比如:

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

更重要的是,跟其他编排对象不一样,DaemonSet 开始运行的时机,很多时候比整个 Kubernetes 集群出现的时机都要早。

DaemonSet详解

上面说到了他的出现时机甚至比kubernetes集群启动的早, 这是为什么呢 下面我们来讨论一下
首先,上述这种比集群启动的早的说法对于大多数情况是错误的。但是有例外, 我等下说明
为什么呢? 因为daemonSet控制器是依赖与集群节点的, 如果节点被加入那么这个daemonset控制的节点才会被部署在集群上。 如果节点被移除, 同样会删除这个节点上的 daemonset的pod。 为什么会说DaemonSet 开始运行的时机,很多时候比整个 Kubernetes 集群出现的时机都要早。因为他启动的时间是非常早的。 他会在节点准备好之后立即启动。为什么?
因为 DaemonSet 确保了在集群中的每个节点上都会运行一个 Pod 副本。这是通过 Kubernetes 的调度器实现的,该调度器会在新节点加入集群时,立即为 DaemonSet 创建新的 Pod。
DaemonSet 通常用于运行一些系统级别的服务,例如网络代理、日志收集器或其他监控软件等。这些服务通常需要在节点开始运行其他应用 Pod 之前就启动,以确保节点的网络连接、日志收集和监控等功能正常。
在 Kubernetes 中,有一种称为 “优先级”(Priority)的概念,可以用来控制 Pod 的启动顺序。DaemonSet 控制的 Pod 通常会被赋予较高的优先级,这样在节点启动时,这些 Pod 就会被优先调度和启动。

说说什么情况下DaemonSet会启动的很早

在 Kubernetes 集群初始化过程中,某些关键组件(如网络插件)需要在其他组件之前启动,以确保集群的正常运行。例如,网络插件的 Agent 组件需要在其他应用 Pod 被调度和启动之前运行,以便为集群中的每个节点提供网络连接。
在这种情况下,DaemonSet 可能会在 Kubernetes 控制平面组件(如 API 服务器、控制器管理器和调度器)完全启动之前就运行这些关键组件。当然这些组件的启动也是依赖kubelet来启动的因为静态 Pod 的创建是由 kubelet 控制的。

1
2
3
4
5
6
7
8
9
10
11
...
template:
metadata:
labels:
name: network-plugin-agent
spec:
tolerations:
- key: node.kubernetes.io/network-unavailable
operator: Exists
effect: NoSchedule

上述这段yaml文件就给我们解释了为什么他会早启动。
:::info
当一个新节点加入 Kubernetes 集群时,节点上可能会有一个名为 **node.kubernetes.io/network-unavailable** 的 taint,表示该节点的网络尚未准备好。通常情况下,调度器不会将新的 Pod 调度到具有这个 taint 的节点上。然而,由于这个 DaemonSet 管理的 Pod(如网络插件的 Agent)具有相应的 toleration,它可以被调度到具有 **node.kubernetes.io/network-unavailable** taint 的节点上。这样一来,网络插件的 Agent 可以在节点的网络设置完成之前启动,从而确保节点尽早加入集群并开始运行工作负载。
:::
这种机制,正是我们在部署 Kubernetes 集群的时候,能够先部署 Kubernetes 本身、再部署网络插件的根本原因:因为当时我们所创建的 Weave 的 YAML,实际上就是一个 DaemonSet。

其实这个优先级的概念也是在DaemonSet的yaml文件中定义好的, 下面我们来一起看看它的yaml文件。

DaemonSet的yaml解读

这也是他的api对象的定义, 我们一起来看下。 这个 DaemonSet,管理的是一个 fluentd-elasticsearch 镜像的 Pod。这个镜像的功能非常实用:通过 fluentd 将 Docker 容器里的日志转发到 ElasticSearch 中。

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
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 跟 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/{{. 容器 ID}}/{{. 容器 ID}}-json.log** 文件里,所以这个目录正是 fluentd 的搜集目标。

DaemonSet 又是如何保证每个 Node 上有且只有一个被管理的 Pod 呢?

保证每个节点只有一个被管理的pod ,最核心的思想就是以下几步

  1. 获取所有的node, 然后检查是否存在DaemonSet的pod
  2. 如果没有就创建
  3. 如果多了那就删除

显然,这是一个典型的“控制器模型”能够处理的问题。
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 就可以了。
如何新的节点上的fluentd-elasticsearch 标签的 Pod呢?
答案是用 nodeSelector,选择 Node 的名字即可。

1
2
nodeSelector:
name: <Node名字>

不过,在 Kubernetes 项目里,nodeSelector 其实已经是一个将要被废弃的字段了。因为,现在有了一个新的、功能更完善的字段可以代替它,即:nodeAffinity。我来举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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。其中,spec.affinity 字段,是 Pod 里跟调度相关的一个字段。
而在这里,我定义的 nodeAffinity 的含义是:

  1. requiredDuringSchedulingIgnoredDuringExecution:它的意思是说,这个 nodeAffinity 必须在每次调度的时候予以考虑。同时,这也意味着你可以设置在某些情况下不考虑这个 nodeAffinity;
  2. 这个 Pod,将来只允许运行在“`metadata.name”是“node-geektime”的节点上。

在这里,你应该注意到 nodeAffinity 的定义,可以支持更加丰富的语法,比如 operator: In(即:部分匹配;如果你定义 operator: Equal,就是完全匹配),这也正是 nodeAffinity 会取代 nodeSelector 的原因之一。
所以,我们的 DaemonSet Controller 会在创建 Pod 的时候,自动在这个 Pod 的 API 对象里,加上这样一个 nodeAffinity 定义。其中,需要绑定的节点名字,正是当前正在遍历的这个 Node。

当然,DaemonSet 并不需要修改用户提交的 YAML 文件里的 Pod 模板,而是在向 Kubernetes 发起请求之前,直接修改根据模板生成的 Pod 对象。这个思路,也正是我在前面讲解 Pod 对象时介绍过的。
此外,DaemonSet 还会给这个 Pod 自动加上另外一个与调度相关的字段,叫作 tolerations。这个字段意味着这个 Pod,会“容忍”(Toleration)某些 Node 的“污点”(Taint)。

1
2
3
4
5
6
7
8
9
10
apiVersion: v1
kind: Pod
metadata:
name: with-toleration
spec:
tolerations:
- key: node.kubernetes.io/unschedulable
operator: Exists
effect: NoSchedule

这个 Toleration 的含义是:“容忍”所有被标记为 unschedulable“污点”的 Node;“容忍”的效果是允许调度。

1
2
3
tolerations:
- key: node-role.kubernetes.io/master
effect: NoSchedule

上面的这个就是容忍daemonset控制的fluentd让其能够调度到master节点。 并且能够收集到上面的日志。

而通过这样一个 Toleration,调度器在调度这个 Pod 的时候,就会忽略当前节点上的“污点”,从而成功地将网络插件的 Agent 组件调度到这台机器上启动起来。
这种机制,正是我们在部署 Kubernetes 集群的时候,能够先部署 Kubernetes 本身、再部署网络插件的根本原因:因为当时我们所创建的 Weave 的 YAML,实际上就是一个 DaemonSet。

至此,通过上面这些内容,你应该能够明白,DaemonSet 其实是一个非常简单的控制器。在它的控制循环中,只需要遍历所有节点,然后根据节点上是否有被管理 Pod 的情况,来决定是否要创建或者删除一个 Pod。

注意:
在默认情况下,Kubernetes 集群不允许用户在 Master 节点部署 Pod。因为,Master 节点默认携带了一个叫作node-role.kubernetes.io/master的“污点”。所以,为了能在 Master 节点上部署 DaemonSet 的 Pod,我就必须让这个 Pod“容忍”这个“污点”。如下代码

1
2
3
tolerations:
- key: node-role.kubernetes.io/master
effect: NoSchedule

DarmonSet的通信

与 DaemonSet 中的 Pod 进行通信的几种模式如下:

  • 推送(Push):配置 DaemonSet 中的 Pod,将更新发送到另一个服务,例如统计数据库
  • NodeIP 和已知端口:DaemonSet 中的 Pod 可以使用 hostPort,从而可以通过节点 IP 访问到 Pod。客户端能通过某种方法获取节点 IP 列表,并且基于此也可以获取到相应的端口。比如prometheus的node-exporter。
  • DNS:创建具有相同 Pod 选择算符的 无头服务 通过使用 endpoints 资源或从 DNS 中检索到多个 A 记录来发现 DaemonSet。

这里先介绍基本概念, 具体的使用需要在真实的生产环境中来进行。后续有机会再做这方面的分享