Deployment 主要表达副本总数,DaemonSet 主要表达合格节点的覆盖范围,适合节点日志采集、网络代理和设备插件。新节点符合条件后会触发协调工作,不代表服务立刻运行或就绪。节点选择、调度容量、镜像拉取、初始化和节点故障,都可能延迟真正有用的覆盖;这些环节需要分别观察。

控制器根据共享模板创建并协调 Pod,通常以每个合格节点一个受管理 Pod 为期望,而不是依赖手工维护副本总数。期望覆盖与实际就绪是两种指标。还应独立定义运维希望覆盖的节点集合,避免选择器写错后,被排除的机器同时从期望数和审计视野中消失。

每节点一个、天生如此

控制器评估节点选择器与必要亲和条件,为每个合格节点创建定向 Pod,通常再由调度器绑定。节点加入和离开会改变期望集合。DaemonSet Pod 自动容忍 unschedulable 污点,因此只执行 cordon 不会排除它们。节点注册并不保证调度、镜像获取和初始化在同一时刻完成,也不保证每台机器已经具备可用日志采集能力。

优先级可以帮助重要守护进程抢占低优先级工作负载,却不会创造容量或消除冲突的放置条件。应按真实重要性和业务影响选择 PriorityClass 与资源请求。Pod 可能一直 Pending,也可能调度后启动失败;高优先级和容忍都不能证明已经就绪,更不意味着免受节点压力驱逐。

覆盖优先于数量

RollingUpdate 会按策略替换 Pod;OnDelete 则等待旧 Pod 被删除后,才使用更新模板创建替代者。maxSurge 为零时,maxUnavailable 为一允许某个节点在更新期间暂时不可用,并不保证每节点不断档。非零 maxSurge 可以让同一节点暂时同时运行新旧 Pod,需要额外资源。更新期间存在版本差异是正常现象,阻塞会使差异持续。kubectl rollout status ds/agent 报告控制器进度,不能证明日志交付连续。

选择器与亲和可以把异构节点分成有意定义的子集,GPU 设备插件与边缘日志转发器不一定使用同一个 DaemonSet。检查各子集真正应该包含哪些节点,区分有意排除与预期代理缺席。跨池比较前还要确认标签来源可靠,不能把一个看起来合理的标签直接当成硬件或运行能力证明。

跟着节点生命周期走

控制器会自动添加文档规定的容忍,包括 not-ready、unreachable、若干压力污点及 unschedulable。控制平面或硬件自定义污点仍可能需要显式容忍。容忍只是允许跨过匹配污点,不会主动选择节点,也不保证满足其他约束。标签变化可能改变成员集合;应观察实际协调结果,不能假定不同守护集合之间是原子交接。

普通 kubectl drain 遇到 DaemonSet 管理的 Pod 时会拒绝继续,除非指定 –ignore-daemonsets;这个选项跳过它们,不负责删除它们。因此 cordon、drain 与停止节点守护服务是不同动作。实际删除 Pod、节点关闭或其他受支持维护路径,仍需要合理终止宽限与信号处理,并验证具体生命周期流程。 新节点池交付时,先把独立定义的预期节点身份与实际受管理 Pod 对照,再检查调度事件和就绪状态。不能一看到数量分叉就直接补容忍,因为资源、镜像、启动失败也可能造成缺口;盲目扩大容忍范围还可能让代理进入原本不应覆盖的节点。 守护镜像先在明确限定的节点池灰度,验证真实采集与恢复行为后再扩大。保留可用旧版本和对应配置,回滚操作也需要观察结果,不能把提交回滚请求等同于所有节点已经恢复。

灰度期间检查新旧 Pod 的节点身份、就绪与版本,确认日志连续性及 CPU、内存变化。发现异常先停止扩大范围并排查。旧镜像和配置保留多久,应依据恢复要求、镜像供应与变更政策,而不是把固定一周当成通用规则。

坏掉的样子

新污点节点池可能被整体排除,此时 desiredNumberScheduled 仍然等于 numberReady。这些字段描述控制器认为应该覆盖的集合,不代表运维原本希望覆盖的全部机器,也不是按池分组的计数。应比较预期节点、选择器、污点、所有者 UID、就绪与版本;缺口需要结合事件才能判断具体原因。

坏版本可能让多个节点失去可观测性。应选择合适的不可用与增量预算,先验证选择器限定的灰度池,再扩大更新。保留能实际取得的旧镜像和配置,确认回滚最终收敛。命令返回成功不能补回已经丢失的日志,也不能保证旧版本仍然能够启动。

比较示意预期节点集合、控制器资格及受管理 Pod 就绪状态,并说明更新、容忍与暂缓诊断的边界。
DaemonSet 覆盖审计:机制与证据边界。

快速验证

使用 Bash、Python 3,并明确设置获准查询的 CONTEXT、NAMESPACE 与 DAEMONSET。INTENDED_NODES 指向独立维护的 JSON 对象,键是预期节点名,值是对应 UID;不能从本 DaemonSet 的选择器反推该名单,否则选错节点仍可能自证成功。下面读取 JSON 字段,不再依赖表头、位置列或管道计数:

(
set -euo pipefail
: "${CONTEXT:?Set the authorized kubectl context}"
: "${NAMESPACE:?Set the DaemonSet namespace}"
: "${DAEMONSET:?Set the DaemonSet name}"
: "${INTENDED_NODES:?Set the independent node-name to UID JSON file}"
umask 077
tmp=$(mktemp -d)
trap 'rm -rf -- "$tmp"' EXIT
k=(kubectl --context "$CONTEXT" --request-timeout=15s)
"${k[@]}" -n "$NAMESPACE" get ds "$DAEMONSET" -o json > "$tmp/before.json"
"${k[@]}" get nodes -o json > "$tmp/nodes.json"
"${k[@]}" -n "$NAMESPACE" get pods -o json > "$tmp/pods.json"
"${k[@]}" -n "$NAMESPACE" get ds "$DAEMONSET" -o json > "$tmp/after.json"
python3 - "$tmp" "$INTENDED_NODES" "$NAMESPACE" "$DAEMONSET" <<'PY'
import collections, json, pathlib, sys
root, roster, namespace, name = sys.argv[1:]
def require(ok, message):
    if not ok:
        raise SystemExit(message)
def unique(pairs):
    result = {}
    for key, value in pairs:
        require(key not in result, "Duplicate JSON key")
        result[key] = value
    return result
def load(path):
    return json.loads(pathlib.Path(path).read_text(), object_pairs_hook=unique)
def ready(obj):
    return any(c.get("type") == "Ready" and c.get("status") == "True"
               for c in obj.get("status", {}).get("conditions", []))
a, b = (load(pathlib.Path(root) / (x + ".json")) for x in ("before", "after"))
for ds in (a, b):
    require(ds["kind"] == "DaemonSet" and ds["apiVersion"] == "apps/v1",
            "Wrong resource type")
    require(ds["metadata"]["namespace"] == namespace and
            ds["metadata"]["name"] == name, "Wrong DaemonSet identity")
    require(ds["metadata"].get("uid") and not ds["metadata"].get("deletionTimestamp"),
            "Missing UID or deleting DaemonSet")
require(all(a["metadata"][k] == b["metadata"][k] for k in ("uid", "generation")),
        "DaemonSet changed during collection; retry")
uid = a["metadata"]["uid"]
expected = load(roster)
require(isinstance(expected, dict) and expected and
        all(isinstance(k, str) and k and isinstance(v, str) and v
            for k, v in expected.items()), "Use a nonempty node-name to UID map")
node_items = load(pathlib.Path(root) / "nodes.json")["items"]
nodes = {n["metadata"]["name"]: n for n in node_items}
require(len(nodes) == len(node_items), "Duplicate node names")
wrong_nodes = sorted(n for n in expected if n not in nodes or
                     nodes[n]["metadata"]["uid"] != expected[n])
unready_nodes = sorted(n for n in expected if n in nodes and not ready(nodes[n]))
rows, seen = [], set()
for pod in load(pathlib.Path(root) / "pods.json")["items"]:
    m = pod["metadata"]
    require(m["namespace"] == namespace, "Wrong Pod namespace")
    if not any(o.get("controller") is True and o.get("uid") == uid and
               o.get("kind") == "DaemonSet" and o.get("apiVersion") == "apps/v1"
               and o.get("name") == name for o in m.get("ownerReferences", [])):
        continue
    require(m.get("uid") and m["uid"] not in seen, "Missing or duplicate Pod UID")
    seen.add(m["uid"])
    node = pod["spec"].get("nodeName", "")
    phase = pod.get("status", {}).get("phase", "Unknown")
    terminating = bool(m.get("deletionTimestamp"))
    rows.append(dict(pod=m["name"], uid=m["uid"], node=node, phase=phase,
                     ready=ready(pod) and phase == "Running" and not terminating,
                     terminating=terminating,
                     revision=m.get("labels", {}).get("controller-revision-hash")))
counts = collections.Counter(r["node"] for r in rows if r["node"])
ready_nodes = {r["node"] for r in rows if r["ready"] and r["node"]}
issues = dict(node_identity_mismatch=wrong_nodes, node_not_ready=unready_nodes,
              missing_ready=sorted(set(expected) - ready_nodes),
              unexpected_owned_nodes=sorted(set(counts) - set(expected)),
              multiple_pods=sorted(n for n, count in counts.items() if count > 1),
              unassigned=[r["uid"] for r in rows if not r["node"]],
              unready_pods=[r["uid"] for r in rows if not r["ready"]])
print(json.dumps(dict(namespace=namespace, daemonset=name, uid=uid,
                     intended=expected, pods=rows, issues=issues), indent=2))
sys.exit(1 if any(issues.values()) else 0)
PY
)

退出零表示所收集快照中,每个预期节点 UID 匹配且 Ready,并恰有一个非终止中、Running 且 Ready 的受管理 Pod。非零需要调查:surge 造成的重复可能合法,尚未绑定节点的 Pod 仍待诊断。输出记录 Pod UID 与版本标签,但不证明目标版本、放置资格或日志交付。不同 API 读取不是原子快照,变更期间应重新核对;节点名到 UID 的映射也不能证明 Pod 当前确在该机器执行。本轮只检查语法与合成文件控制,没有运行集群、更新或 drain,技术与运行验证仍暂缓。

来源