Kubernetes 基础体系 · 第 18/83 篇。示例基于 Kubernetes 当前稳定 API;弃用、版本偏差、云厂商差异和生产风险会明确说明。

DaemonSet:节点级 Agent、调度、更新、容忍和资源治理

DaemonSet 是 Kubernetes 中用于运行“每个节点一份”或“每个符合条件的节点一份”Pod 的工作负载控制器。典型用途包括:

  • 节点日志采集 Agent;
  • 节点监控、指标和硬件探针;
  • CNI、网络策略或存储相关的节点插件;
  • 安全扫描、主机完整性检查;
  • 需要访问节点文件系统、容器运行时或内核接口的 Agent。

DaemonSet 不是“把 Pod 直接绑到节点上的静态配置”。它仍然由控制器持续计算期望状态,并通过 API Server、调度器和 kubelet 协作完成创建、调度、运行、更新和恢复。


一、先建立正确模型:DaemonSet 维护的是节点集合上的副本

Deployment 的核心问题是:

集群中应该有多少个副本?

DaemonSet 的核心问题是:

哪些节点应该各自拥有一个符合模板的 Pod?

设集群节点集合为 NN,DaemonSet 的节点筛选条件为 FF,则理论上的目标节点集合可以写成:

E={nNn 满足节点筛选条件 F}E = \{n \in N \mid n \text{ 满足节点筛选条件 } F\}

这里的 FF 通常包括:

  1. nodeSelector
  2. Pod 的 nodeAffinity
  3. 节点是否被控制面纳入调度范围;
  4. 节点 taint 是否被 Pod toleration 接受;
  5. Pod 的资源请求、端口、卷和其他调度约束是否可满足。

对于每个 nEn \in E,DaemonSet 控制器期望存在一个属于该 DaemonSet 的 Pod:

DesiredPods=E\text{DesiredPods} = |E|

但这不等于任何时刻都能观察到:

RunningPods=E\text{RunningPods} = |E|

原因包括:

  • Pod 尚未被调度;
  • 镜像尚未拉取完成;
  • kubelet 不可用;
  • 节点资源不足;
  • Pod 正在终止或滚动更新;
  • 节点处于压力状态并发生驱逐;
  • DaemonSet 使用了 maxSurge,更新期间一个节点上可能暂时存在两个 Pod;
  • 控制器、调度器和 kubelet 的状态同步存在延迟。

因此,DaemonSet 的 desiredNumberScheduledcurrentNumberSchedulednumberReadynumberAvailable 表达的是不同层次的状态,不能只看其中一个字段判断 Agent 是否正常。


二、组件关系和完整生命周期

DaemonSet 运行涉及四个主要组件:

  • DaemonSet controller:监听 DaemonSet、Node 和 Pod,计算哪些节点需要哪些 Pod;
  • kube-scheduler:为尚未绑定节点的 Pod 选择可行节点;
  • kubelet:在目标节点上创建 Pod 沙箱、启动容器、执行探针并上报状态;
  • API Server:保存期望状态和观察状态,是各组件交换状态的中心。

典型流程如下:

sequenceDiagram
    participant U as 用户
    participant API as API Server
    participant DC as DaemonSet Controller
    participant S as Scheduler
    participant K as Kubelet
    participant C as Container Runtime

    U->>API: 创建或更新 DaemonSet
    API-->>DC: Watch 到 DaemonSet 变化
    DC->>API: 为目标节点创建 Pod
    API-->>S: Watch 到未绑定节点的 Pod
    S->>S: 过滤节点、检查资源和约束
    S->>API: 写入 Pod.spec.nodeName
    API-->>K: kubelet Watch 到目标节点上的 Pod
    K->>C: 创建容器并启动
    C-->>K: 返回容器状态
    K->>API: 更新 Pod Status
    API-->>DC: Watch 到 Pod 状态变化
    DC->>API: 更新 DaemonSet Status

更新 DaemonSet 时,控制器会根据 Pod 模板计算模板哈希。模板中影响 Pod 身份的字段发生变化,例如镜像、环境变量、资源、探针或卷配置发生变化,通常会产生新的 ControllerRevision。RollingUpdate 控制器随后按照更新策略替换旧 Pod。

1. DaemonSet 控制器不负责让容器真正运行

控制器通常只负责:

  • 判断目标节点;
  • 创建或删除 Pod;
  • 处理旧版本 Pod;
  • 更新 DaemonSet 状态。

容器是否能启动,仍由 kubelet 和容器运行时负责。例如:

  • 镜像拉取失败;
  • 容器进程立即退出;
  • 挂载路径不存在;
  • 容器没有权限访问 /var/log
  • liveness 探针失败;

这些问题不会由 DaemonSet 控制器直接修复。控制器可能看到 Pod 一直存在,但 Pod 的 readyavailable 状态不成立。

2. 调度器仍然参与 DaemonSet 调度

现代 Kubernetes 中,DaemonSet 控制器通常创建尚未绑定节点的 Pod,并为 Pod 注入针对目标节点的节点亲和性,调度器再完成节点绑定。

这与“控制器直接设置 spec.nodeName”不同。调度器因此仍会检查:

  • 节点资源请求;
  • 节点亲和性;
  • taint 和 toleration;
  • 端口冲突;
  • 卷拓扑;
  • 其他调度插件。

这也是为什么 DaemonSet 的 Pod 可能处于 Pending:控制器已经创建了 Pod,但调度器找不到满足条件的节点。

需要区分两个概念:

  • DaemonSet 的目标节点:控制器认为该节点应该有 Pod;
  • DaemonSet Pod 的可调度节点:调度器认为 Pod 实际可以放置的节点。

目标节点和可调度节点可能不一致。例如节点匹配了标签,但节点剩余内存不足以满足 Pod 的 requests.memory


三、节点筛选:不是所有节点都会运行 DaemonSet Pod

1. nodeSelector:最简单的等值筛选

例如:

spec:
  template:
    spec:
      nodeSelector:
        kubernetes.io/os: linux
        node-role.example.com: worker

这要求 Pod 只能调度到同时满足两个标签的节点。nodeSelector 是等值匹配,表达能力有限。

如果节点没有标签:

node-role.example.com=worker

即使它是 Linux 节点,也不会成为目标节点。

可以使用命令检查:

kubectl get nodes --show-labels
kubectl get nodes -l 'kubernetes.io/os=linux,node-role.example.com=worker'

第二条命令列出的节点只是标签筛选结果,不代表这些节点一定能运行 Pod;资源、taint、卷和其他调度约束仍可能使 Pod 无法调度。

2. nodeAffinity:表达更复杂的节点条件

DaemonSet 可以使用 requiredDuringSchedulingIgnoredDuringExecution

affinity:
  nodeAffinity:
    requiredDuringSchedulingIgnoredDuringExecution:
      nodeSelectorTerms:
      - matchExpressions:
        - key: kubernetes.io/os
          operator: In
          values:
          - linux
        - key: topology.kubernetes.io/zone
          operator: In
          values:
          - zone-a
          - zone-b

这里的含义是:

  • 节点必须是 Linux;
  • 节点区域必须是 zone-azone-b
  • IgnoredDuringExecution 表示 Pod 调度成功后,如果节点标签后来变化,Kubernetes 不会仅因为这个变化自动驱逐已经运行的 Pod。

这最后一点经常被误解。节点亲和性主要约束调度时机;它不是一个持续执行的“节点标签守卫”。若希望节点变化后移走 Pod,需要结合控制器行为、taint、驱逐或人工操作处理。

preferredDuringSchedulingIgnoredDuringExecution 是偏好而非硬约束。它会影响调度打分,但在没有合适节点时仍可能放置到不符合偏好的节点。

3. DaemonSet 自身不会提供“只在新增节点上运行”的语义

DaemonSet 的目标是当前符合条件的节点集合:

  • 新节点加入并匹配条件时,控制器会创建 Pod;
  • 节点标签变化后开始匹配时,控制器可能创建 Pod;
  • 节点不再匹配时,控制器会删除对应 Pod;
  • 节点被删除时,对应 Pod 也不再具有运行意义。

因此,节点标签和 taint 是 DaemonSet 部署范围的一部分,应当像 API 设计一样被谨慎管理。误加一个标签可能让大量节点同时接收一个高资源 Agent。


四、Taint 和 Toleration:允许调度,不是保证运行

1. Taint 的三种效果

节点 taint 由三个主要部分组成:

key=value:effect

常见效果为:

  • NoSchedule:不允许新的不匹配 Pod 调度到节点;
  • PreferNoSchedule:尽量避免调度,但不是硬拒绝;
  • NoExecute:不匹配的已运行 Pod 也可能被驱逐,同时阻止新的 Pod 调度。

Pod 的 toleration 只表示“可以接受某个 taint”。例如:

tolerations:
- key: workload.example.com
  operator: Equal
  value: node-agent
  effect: NoSchedule

它允许 Pod 调度到:

workload.example.com=node-agent:NoSchedule

但并不表示:

  • 节点一定有足够资源;
  • 容器一定启动成功;
  • Pod 一定 Ready;
  • kubelet 一定可用;
  • 节点压力下不会被驱逐。

2. DaemonSet Pod 的自动容忍

Kubernetes 会为 DaemonSet Pod 自动添加一些与节点状态相关的 toleration。具体自动注入项属于 Kubernetes 版本和实现行为的一部分,常见项目包括:

node.kubernetes.io/not-ready:NoExecute
node.kubernetes.io/unreachable:NoExecute
node.kubernetes.io/disk-pressure:NoSchedule
node.kubernetes.io/memory-pressure:NoSchedule
node.kubernetes.io/pid-pressure:NoSchedule
node.kubernetes.io/unschedulable:NoSchedule

这些容忍的设计目的,是让节点级 Agent 在节点暂时异常时仍有机会留在节点上或重新调度,而不是像普通业务 Pod 那样立即被排除。

但这不等于 Agent 能够跨越节点故障继续工作:

  • kubelet 停止时,Pod 状态可能长时间不更新;
  • 节点断网时,容器可能仍在运行,但控制面看不到最新状态;
  • 节点发生磁盘压力时,kubelet 仍可能驱逐 Pod;
  • 节点彻底宕机时,节点上的容器不会被迁移到另一个节点,因为 DaemonSet 的语义是“一节点一份”,而不是“始终保持全局副本数”。

3. NoExecutetolerationSeconds

可以限制 Pod 对 NoExecute taint 的容忍时间:

tolerations:
- key: node.kubernetes.io/not-ready
  operator: Exists
  effect: NoExecute
  tolerationSeconds: 300

这表示节点进入 NotReady 并施加对应 taint 后,Pod 最多容忍 300 秒;超过时间仍不恢复,Pod 可能被驱逐。

节点级基础设施 Agent 是否应设置有限时间,需要结合用途决定:

  • 采集节点本地日志的 Agent 可能需要尽量留在节点上;
  • 依赖控制面通信的安全 Agent 可能希望故障节点尽快被标记为不可服务;
  • 过长容忍时间可能导致“控制面认为 Pod 存在,但节点实际上已失联”的状态持续更久。

Node Condition、Lease、NotReady 和恢复流程会直接影响这些观察结果。Ready=FalseUnknown、节点 taint、kubelet 心跳和容器实际运行状态不是同一个状态源,诊断时必须分别检查。


五、一个可运行的 DaemonSet 示例

下面的示例部署一个节点级日志 Agent。它只运行在 Linux worker 节点,访问节点的容器日志目录,并设置了资源、探针和滚动更新策略。

hostPath 会让容器访问宿主机文件系统,示例适合理解机制,不应未经安全评估直接用于生产。不同发行版和容器运行时的日志路径也可能不同。

apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: node-log-agent
  namespace: observability
  labels:
    app: node-log-agent
spec:
  revisionHistoryLimit: 5
  minReadySeconds: 10
  updateStrategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 1
      maxSurge: 0
  selector:
    matchLabels:
      app: node-log-agent
  template:
    metadata:
      labels:
        app: node-log-agent
    spec:
      serviceAccountName: node-log-agent
      nodeSelector:
        kubernetes.io/os: linux
        node-role.example.com: worker
      tolerations:
      - key: workload.example.com
        operator: Equal
        value: node-agent
        effect: NoSchedule
      containers:
      - name: agent
        image: ghcr.io/example/node-log-agent:1.4.0
        imagePullPolicy: IfNotPresent
        args:
        - --path=/var/log/pods
        - --config=/etc/node-log-agent/config.yaml
        resources:
          requests:
            cpu: 100m
            memory: 128Mi
          limits:
            cpu: 500m
            memory: 512Mi
        readinessProbe:
          httpGet:
            path: /ready
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 10
        livenessProbe:
          httpGet:
            path: /healthz
            port: 8080
          initialDelaySeconds: 30
          periodSeconds: 20
        volumeMounts:
        - name: pod-logs
          mountPath: /var/log/pods
          readOnly: true
        - name: agent-config
          mountPath: /etc/node-log-agent
          readOnly: true
      volumes:
      - name: pod-logs
        hostPath:
          path: /var/log/pods
          type: Directory
      - name: agent-config
        configMap:
          name: node-log-agent-config

还需要创建命名空间、ServiceAccount 和配置:

kubectl create namespace observability

kubectl create serviceaccount node-log-agent \
  -n observability

kubectl create configmap node-log-agent-config \
  -n observability \
  --from-literal=config.yaml='{"flush_interval":"5s"}'

kubectl apply -f node-log-agent.yaml

这些命令成立的前提是:

  • 节点存在 kubernetes.io/os=linux
  • 目标 worker 节点存在 node-role.example.com=worker
  • 镜像仓库可访问;
  • 节点上存在 /var/log/pods
  • 镜像确实提供 /ready/healthz HTTP 接口;
  • 镜像内部支持示例中的参数。

如果镜像、路径或健康接口不存在,DaemonSet 对象仍然可以创建,但 Pod 可能进入 ImagePullBackOffCrashLoopBackOff 或因探针失败而不 Ready。

检查结果:

kubectl -n observability get daemonset node-log-agent
kubectl -n observability get pods -o wide -l app=node-log-agent
kubectl -n observability describe daemonset node-log-agent

可能看到类似状态:

DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE
3         3         3       3            3

其含义不是“集群总共有三个节点”,而是“当前有三个节点满足 DaemonSet 的筛选条件,并且三个 Pod 都达到对应状态”。如果 DESIRED=3CURRENT=3READY=0,说明控制器已创建 Pod,但容器启动或就绪过程失败。

进一步诊断:

kubectl -n observability get events \
  --sort-by=.lastTimestamp

kubectl -n observability describe pod <pod-name>

kubectl -n observability logs <pod-name> -c agent

如果 Pod 处于 Pending,重点看调度事件,例如:

0/5 nodes are available:
2 Insufficient memory,
2 node(s) didn't match Pod's node affinity,
1 node(s) had untolerated taint

这三类原因分别对应:

  • 资源请求超过节点可分配资源;
  • 标签或亲和性不匹配;
  • Pod 没有对应 toleration。

六、资源治理:DaemonSet 的成本按节点线性增长

1. 资源请求如何影响调度

对一个节点,调度器主要根据 Pod 的资源请求进行可行性判断,而不是根据容器此刻的实际 CPU 使用率。

设节点可分配资源为:

AnodeA_{\text{node}}

该节点上已有 Pod 的资源请求总和为:

RexistingR_{\text{existing}}

DaemonSet Pod 的资源请求为:

RdsR_{\text{ds}}

则至少要满足:

Rexisting+RdsAnodeR_{\text{existing}} + R_{\text{ds}} \leq A_{\text{node}}

例如某节点可分配内存为 8 GiB,已有工作负载请求 7.95 GiB,DaemonSet Pod 请求 128 MiB:

7.95 GiB+0.125 GiB>8 GiB7.95\text{ GiB} + 0.125\text{ GiB} > 8\text{ GiB}

即使节点当前监控到的实际内存使用只有 5 GiB,调度器也可能拒绝该 Pod,因为调度使用的是请求账本,而不是瞬时利用率。

DaemonSet 的全局请求成本可以近似表示为:

Rcluster=E×Rone-podR_{\text{cluster}} = |E| \times R_{\text{one-pod}}

例如:

  • 100 个目标节点;
  • 每个 Agent 请求 200m CPU;
  • 每个 Agent 请求 256Mi 内存。

那么该 DaemonSet 的调度请求总量约为:

100×200m=20 CPU100 \times 200m = 20\text{ CPU}

100×256Mi=25Gi100 \times 256Mi = 25Gi

这还没有计算容器运行时、日志缓存、页缓存、网络缓冲和 kubelet 本身的开销。

2. requestslimits 的区别

  • requests 影响调度和 QoS 分类;
  • limits 约束容器可使用的资源上限;
  • CPU 超过 limit 时通常会被节流;
  • 内存超过 limit 时可能触发 OOMKill;
  • 节点内存压力下,Pod 的 QoS 和优先级会影响驱逐顺序。

如果一个节点 Agent 同时设置:

resources:
  requests:
    cpu: 100m
    memory: 128Mi
  limits:
    cpu: 500m
    memory: 512Mi

调度器只按 100m CPU 和 128Mi 内存计入资源请求,但运行时可能允许它使用到 500m CPU 和 512Mi 内存。多个 Agent 叠加后,节点可能出现“请求账本允许,但实际运行时资源不足”的情况。

3. QoS 分类

Pod 的 QoS 类别由容器资源配置决定:

  • 所有容器的 CPU、内存 request 和 limit 都设置且相等:Guaranteed
  • 至少有一个容器设置 request 或 limit,但不满足 Guaranteed 条件:Burstable
  • 所有容器都没有 request 和 limit:BestEffort

节点级 Agent 通常不应无条件使用 BestEffort,因为它可能在节点压力下最早被驱逐;但把所有 Agent 都设置为 Guaranteed 也会显著提高资源预留,必须基于实际工作负载和节点容量计算。

4. 节点级 Agent 的资源不是“免费旁路”

常见错误是把 Agent 视为系统组件,于是省略资源请求:

resources: {}

后果可能是:

  1. 调度器低估该 Agent 的资源占用;
  2. 业务 Pod 被调度到看似有余量的节点;
  3. Agent 与业务容器在运行时争抢 CPU 或内存;
  4. 节点进入 MemoryPressure;
  5. kubelet 驱逐 Pod;
  6. Agent 本身也可能丢失日志或指标。

生产治理通常应同时关注:

  • CPU request 是否足以保证采集循环及时运行;
  • 内存 request 是否覆盖正常缓存;
  • 内存 limit 是否能限制失控缓存;
  • 日志突发时是否会无限堆积;
  • Agent 的本地队列是否使用 emptyDir,其大小是否受控;
  • 节点是否为系统组件预留了资源;
  • Agent 是否需要高优先级,还是应该允许让位给关键业务。

priorityClassName 可以影响调度抢占和驱逐相关行为,但高优先级不是资源增加器。若每个节点都安装多个高优先级 DaemonSet,抢占可能反过来挤压业务,甚至造成系统组件之间互相竞争。


七、节点级日志 Agent 与日志架构

Kubernetes 中最常见的日志路径是:

应用进程 stdout/stderr
        ↓
容器运行时
        ↓
节点上的容器日志文件
        ↓
DaemonSet 日志 Agent
        ↓
远端日志系统

应用容器将日志写入 stdoutstderr,容器运行时负责保存和轮转。节点级 Agent 通常以 DaemonSet 运行,通过 hostPath 读取节点上的容器日志目录,再解析 Pod、Namespace、容器名等元数据。

这个架构的优点是:

  • 每个节点只运行一个采集 Agent;
  • 不需要修改每个业务镜像;
  • 能够统一处理节点上的日志;
  • Agent 可以读取容器日志文件并批量发送。

但它也有明确边界:

  • hostPath 让容器获得主机文件访问能力;
  • 容器日志文件路径依赖容器运行时和节点系统;
  • 日志轮转速度超过 Agent 读取速度时,可能发生丢失;
  • Agent 重启期间的本地缓存策略决定了是否能恢复;
  • 节点损坏时,本地尚未发送的日志通常无法恢复。

Sidecar 日志采集则把采集器放进业务 Pod,优点是隔离性和应用级配置更强,代价是每个业务 Pod 都增加一个容器,资源、网络连接和运维对象数量都会增加。DaemonSet 适合节点级统一采集,Sidecar 适合必须按应用隔离、读取非标准日志文件或需要独立生命周期的场景。

日志留存也分两层:

  1. 节点本地容器日志留存,由容器运行时轮转和节点磁盘容量约束;
  2. 远端日志留存,由日志后端的索引、对象存储和生命周期策略约束。

DaemonSet 不能替代日志后端留存策略,也不能保证节点故障时本地日志不丢失。若日志系统要求较强可靠性,需要明确发送确认、磁盘缓冲、重试上限、背压和丢弃策略。


八、更新机制:RollingUpdate 和 OnDelete

DaemonSet 的 updateStrategy 有两个稳定策略:

updateStrategy:
  type: RollingUpdate

以及:

updateStrategy:
  type: OnDelete

1. RollingUpdate

RollingUpdate 是默认策略。Pod 模板变化后,控制器逐步删除旧 Pod 并创建新 Pod。

常用配置:

updateStrategy:
  type: RollingUpdate
  rollingUpdate:
    maxUnavailable: 1
    maxSurge: 0

maxUnavailable

maxUnavailable 表示更新期间允许不可用的 DaemonSet Pod 数量,可以是整数或百分比。

如果目标节点有 10 个:

maxUnavailable: 2

通常表示更新过程中最多允许约 2 个节点上的 DaemonSet Pod 不可用。控制器会据此控制替换速度。

这里的“不可用”与 readinessProbeminReadySeconds 和 DaemonSet 状态有关,不只是容器进程是否存在。一个 Pod 进程运行但尚未 Ready,不能简单视为已完成更新。

minReadySeconds

minReadySeconds: 30

表示新 Pod Ready 后,至少稳定 30 秒才被视为 Available。它可以避免新版本刚通过探针就立刻继续推进,从而暴露启动后延迟失败的问题。

但它不会验证业务功能正确性。例如 Agent 的 /ready 只检查 HTTP 服务是否启动,却没有检查是否能读取日志或连接远端后端,那么 minReadySeconds 只能延长“表面健康”的观察时间。

maxSurge

maxSurge 允许更新期间在一个节点上临时运行额外 Pod:

updateStrategy:
  type: RollingUpdate
  rollingUpdate:
    maxUnavailable: 0
    maxSurge: 1

这可以先启动新 Pod,再终止旧 Pod,降低节点级 Agent 的空窗期。但需要注意:

  • 同一节点临时存在两个 Agent;
  • 两个 Agent 可能重复采集同一日志;
  • 主机端口、独占设备、文件锁或节点级 socket 可能冲突;
  • 资源峰值会增加;
  • 只有较新的 Kubernetes 版本支持并稳定实现 DaemonSet 的 maxSurge 行为,使用前应核对目标集群版本和 API 文档。

对于日志采集 Agent,maxSurge 尤其需要验证重复采集和偏移量竞争。很多 Agent 默认假设每个节点只有一个实例,不能直接开启 surge。

maxUnavailablemaxSurge 都不能只从“更新速度”考虑,还要从节点资源和数据一致性考虑。

2. OnDelete

updateStrategy:
  type: OnDelete

更新 Pod 模板后,控制器不会主动替换现有 Pod。只有旧 Pod 被删除,新的 Pod 才会按新模板创建。

该策略适合:

  • 需要人工逐节点验证;
  • 由外部系统控制重启;
  • 对自动更新有严格限制;
  • Agent 更新必须避开某些维护窗口。

代价是配置变更不会自动生效。执行以下命令只能更新 DaemonSet 模板,不会立即替换所有现有 Pod:

kubectl -n observability patch daemonset node-log-agent \
  --type='strategic' \
  -p='{"spec":{"template":{"spec":{"containers":[{"name":"agent","image":"ghcr.io/example/node-log-agent:1.5.0"}]}}}}'

随后需要逐个删除 Pod:

kubectl -n observability delete pod <pod-name>

删除后,DaemonSet 控制器会创建符合当前模板的新 Pod。

3. 执行更新、观察和回滚

更新镜像:

kubectl -n observability set image daemonset/node-log-agent \
  agent=ghcr.io/example/node-log-agent:1.5.0

观察滚动过程:

kubectl -n observability rollout status daemonset/node-log-agent
kubectl -n observability get pods -l app=node-log-agent -o wide

查看历史:

kubectl -n observability rollout history daemonset/node-log-agent

回滚:

kubectl -n observability rollout undo daemonset/node-log-agent
kubectl -n observability rollout status daemonset/node-log-agent

回滚只会恢复 Kubernetes 对象记录的旧 Pod 模板。它不会恢复:

  • 已经删除的本地日志;
  • 已经发送到远端的错误数据;
  • Agent 在节点上写入的外部状态;
  • 与镜像不兼容的外部配置。

如果新镜像因为启动失败而持续 CrashLoopBackOff,滚动更新可能长期无法完成。此时应先查看具体 Pod 的事件和容器日志,再决定暂停变更、回滚或修复配置。DaemonSet 没有 Deployment 那种“自动发现应用级功能错误并自行回滚”的能力。


九、Pod 替换、故障恢复和并发边界

1. kubelet 重启不一定导致 Pod 重建

如果 kubelet 短暂重启,容器运行时中的容器可能仍然存在。kubelet 恢复后会重新同步本地状态,Pod 不一定经历完整删除和重建。

如果节点被控制面标记为 NotReady:

  • 节点可能被添加 node.kubernetes.io/not-ready taint;
  • 普通 Pod 可能因 NoExecute 而被驱逐;
  • DaemonSet Pod 的自动 toleration 可能让它继续保留更长时间;
  • 但 kubelet 不恢复时,Pod 并不能真正提供节点级服务。

因此,“Pod 对象仍然存在”不代表“Agent 可用”。

2. 节点被删除后,DaemonSet 不会把 Pod 搬到别的节点

DaemonSet Pod 与节点绑定。节点彻底从 API Server 删除后,控制器会在其他目标节点维持各自的 Pod,但不会为了弥补一个节点而在别的节点创建第二份。

这保证了节点级语义:

每个目标节点最多需要一份\text{每个目标节点最多需要一份}

而不是:

任意节点总数达到某个副本数\text{任意节点总数达到某个副本数}

3. 更新期间可能存在短暂重复或短暂缺失

不使用 surge 时,常见流程是:

  1. 删除节点上的旧 Pod;
  2. 等待旧 Pod 终止;
  3. 创建或调度新 Pod;
  4. 等待新 Pod Ready;
  5. 继续下一个节点。

因此会有短暂空窗。

使用 surge 时,流程可能变成:

  1. 在目标节点上创建新 Pod;
  2. 新 Pod 调度并启动;
  3. 新 Pod 达到可用条件;
  4. 删除旧 Pod。

因此会有短暂重复。

控制器需要处理 API Server 延迟、Pod 删除宽限期和节点状态变化,所以在极短时间内观察到旧 Pod 和新 Pod 同时存在,或状态字段尚未收敛,并不必然是控制器失效。应结合 Pod 的 ownerReferences、模板哈希、创建时间和事件判断。


十、selector 是不可随意修改的身份边界

DaemonSet 的选择器必须匹配 Pod 模板标签:

spec:
  selector:
    matchLabels:
      app: node-log-agent
  template:
    metadata:
      labels:
        app: node-log-agent

apps/v1 中,DaemonSet 的 .spec.selector 是不可变字段。这样做是为了避免控制器突然接管或遗弃一批已有 Pod。

如果选择器和模板标签不匹配,创建请求会被 API Server 拒绝。若多个工作负载错误地使用相同标签选择器,则可能导致:

  • kubectl get pods -l ... 混入其他工作负载;
  • 诊断结果被误读;
  • 控制器管理边界混乱;
  • 删除和滚动更新时难以区分 Pod 所属。

应当使用稳定且专属于该 DaemonSet 的标签,而不是复用宽泛标签。


十一、DaemonSet 与 Deployment、StatefulSet 的差异

Deployment

Deployment 以副本数为中心:

集群中运行 N 个 Pod

它适合无状态业务服务。Pod 可以被调度到任意满足条件的节点,副本通常不与节点一一对应。

StatefulSet

StatefulSet 以稳定身份和有序生命周期为中心:

  • 稳定 Pod 名称;
  • 稳定网络身份;
  • 稳定存储绑定;
  • 有序创建和终止。

它适合有状态应用,不等价于节点级 Agent。

DaemonSet

DaemonSet 以节点覆盖范围为中心:

每个符合条件的节点运行一个 Pod

当节点数量、节点标签或节点 taint 变化时,目标副本数随之变化。因此不能用 replicas 字段扩缩 DaemonSet;DaemonSet 的规模由目标节点集合决定。


十二、失败表现和诊断方法

情况一:DESIRED=0

先检查筛选条件:

kubectl get nodes --show-labels
kubectl -n observability get daemonset node-log-agent -o yaml

重点核对:

  • nodeSelector 是否写错;
  • nodeAffinity 是否要求不存在的标签;
  • 节点操作系统是否符合条件;
  • DaemonSet 是否部署在正确命名空间;
  • 是否误以为 taint 会决定 DESIRED

某些 taint 主要影响调度,不一定改变控制器对目标节点集合的判断。因此不能只看到 taint 就断定节点不会被计入期望集合。

情况二:DESIRED 大于 CURRENT

说明控制器期望有更多 Pod,但当前 Pod 尚未全部创建、调度或被 API Server 观察到。检查:

kubectl -n observability describe daemonset node-log-agent
kubectl -n observability get events --sort-by=.lastTimestamp

还应检查控制器日志和 API Server 健康状况。若是大规模节点变更,状态短暂滞后是可能的。

情况三:CURRENT 正常但 READY=0

这通常意味着 Pod 已存在,但容器尚未就绪。检查:

kubectl -n observability get pods -l app=node-log-agent
kubectl -n observability describe pod <pod-name>
kubectl -n observability logs <pod-name> -c agent --previous

常见原因:

  • readinessProbe 路径错误;
  • 依赖的远端日志系统不可达;
  • 配置文件格式错误;
  • Agent 无法读取 hostPath
  • 运行用户没有访问文件的权限;
  • 容器启动后立即退出。

情况四:Pod 长时间 Pending

用调度事件定位:

kubectl -n observability describe pod <pod-name>

如果事件为 Insufficient cpuInsufficient memory,调整 Agent 请求、扩容节点或修改节点筛选范围。不能只降低 request 来“骗过”调度器,因为这会把真实资源压力推迟到运行时。

如果事件为 untolerated taint,应先确认该 taint 是否应该被容忍,而不是机械地加入:

tolerations:
- operator: Exists

无条件容忍所有 taint 会让节点 Agent 进入控制面、专用硬件或严重异常节点,扩大故障影响面。

情况五:更新卡住

检查:

kubectl -n observability rollout status daemonset/node-log-agent
kubectl -n observability get pods -l app=node-log-agent -o wide
kubectl -n observability describe daemonset node-log-agent

常见原因:

  • 新镜像无法拉取;
  • 新 Pod 的 readinessProbe 永远失败;
  • maxUnavailable 过小,且已有 Pod 不可用;
  • 新版本资源请求无法在目标节点放置;
  • hostPort 或设备资源冲突;
  • 节点本身处于 NotReady;
  • Pod 删除受到宽限期或容器退出阻塞。

确认旧版本仍有服务能力后,再执行回滚。盲目删除全部 Pod 可能让每个节点同时失去 Agent。


十三、生产边界:安全、可靠性和维护窗口

1. hostPath 的安全风险

访问以下路径的 DaemonSet Pod 风险不同:

  • /var/log/pods:通常是日志采集所需的较窄权限;
  • /var/lib/containerd:可能暴露容器运行时数据;
  • /etc:可能读取主机敏感配置;
  • /var/run:可能接触主机 socket;
  • /:几乎等同于暴露整个主机文件系统。

应限制:

  • hostPath.path 的范围;
  • 挂载是否只读;
  • 容器是否需要 privileged
  • 是否需要 hostNetworkhostPIDhostIPC
  • ServiceAccount 是否拥有过宽 RBAC 权限;
  • Pod Security Admission 是否允许这些能力。

节点 Agent 需要的权限应按功能拆分,而不是因为“它是系统 Agent”就授予所有主机能力。

2. 更新策略要匹配数据语义

对于监控 Agent,短暂重启可能只造成少量指标缺口;对于日志 Agent,重启可能导致文件偏移、重复采集或丢失。

因此更新前应明确:

  • Agent 的 checkpoint 存在哪里;
  • checkpoint 是否在容器重建后保留;
  • 新旧实例同时存在是否会重复发送;
  • 远端系统如何去重;
  • 节点维护时是否允许采集空窗;
  • 是否需要按区域、节点池或标签分批更新。

DaemonSet 的 maxUnavailable 只控制 Kubernetes 层面的替换节奏,不理解日志事件是否重复,也不会验证远端数据完整性。

3. PDB 不是 DaemonSet 更新的替代品

PodDisruptionBudget 主要约束符合条件的 Pod 在自愿中断场景下的可用数量,例如节点排空使用 Eviction API 的过程。它不能代替 DaemonSet 的 maxUnavailable,也不应被当成滚动更新的唯一保护机制。

DaemonSet 更新使用自己的更新策略;节点排空还会受到 taint、驱逐、kubelet 和 Pod 终止行为影响。两者需要分别验证。


十四、版本和实现差异

以下内容应特别注意版本范围:

  1. DaemonSet 使用稳定的 apps/v1 API;不要继续使用早期 beta API。
  2. maxUnavailableminReadySecondsOnDelete 等字段属于稳定 DaemonSet 能力,但具体默认值和校验行为应以目标集群版本的 OpenAPI schema 为准。
  3. DaemonSet 的 maxSurge 是较新的更新能力。老版本集群、发行版补丁版本或旧客户端可能不支持或行为不同,部署前应检查:
    kubectl explain daemonset.spec.updateStrategy.rollingUpdate
    
  4. 节点日志目录不是所有系统都相同。容器运行时、Linux 发行版、日志配置和云厂商节点镜像都可能改变路径及轮转行为。
  5. 托管 Kubernetes 服务可能对控制面组件、系统节点、污点、标签和日志路径做额外处理。不能把某个云厂商节点池的标签或 taint 假设为 Kubernetes 的通用规范。
  6. 自动注入的 DaemonSet toleration 属于控制器行为的一部分,若依赖某个具体 taint 的容忍效果,应在目标版本上通过:
    kubectl -n observability get pod <pod-name> -o yaml
    
    检查最终 Pod 规格,而不是只看提交的模板。

十五、用一组不变量理解 DaemonSet

可以用下面几条不变量检查设计是否合理:

节点覆盖不变量

对于每个目标节点 nEn \in E,最终应存在一个该 DaemonSet 的可运行 Pod;但在调度、更新和故障期间允许暂时偏离。

资源可行不变量

对每个节点:

PodRequests+DaemonSetRequestsNodeAllocatable\sum \text{PodRequests} + \text{DaemonSetRequests} \leq \text{NodeAllocatable}

如果这个条件不成立,Pod 即使在逻辑上“应该存在”,也无法被调度。

容忍边界不变量

Toleration 只解除指定 taint 带来的调度或驱逐限制:

TolerationResourceGuarantee\text{Toleration} \neq \text{ResourceGuarantee}

TolerationNodeHealthGuarantee\text{Toleration} \neq \text{NodeHealthGuarantee}

更新安全不变量

新版本被视为可用之前,必须满足实际需要的健康条件,而不仅是容器进程启动:

AvailableReadyminReadySeconds 已满足\text{Available} \Rightarrow \text{Ready} \land \text{minReadySeconds 已满足}

但反向关系通常不成立:

Ready⇏日志已可靠发送\text{Ready} \not\Rightarrow \text{日志已可靠发送}

因为就绪探针只验证它实现的检查项。


DaemonSet 的本质不是“自动在每个节点执行一个容器”,而是把节点集合、Pod 调度、节点状态、资源预算和版本替换连接成一个持续收敛的控制过程。理解这一点后,nodeSelector 决定覆盖范围,调度器决定能否放置,toleration 决定能否接受特定 taint,资源请求决定账面容量,探针决定可用性,更新策略决定替换节奏,而 kubelet 和节点状态决定最终运行结果。


系列导航与关联阅读

官方资料

本文依据 Kubernetes、CNCF 与相关项目官方文档重新梳理;正文和生产清单由 WR BLOG 编写。