---
url: /courses/kubernetes-cluster/03-kind-cluster-validation/index.md
---
# 第三节 kind 集群验证与节点管理

::: tip 项目目标
将创建好的 `yunfan` 集群从“命令返回成功”进一步验证到“API 端点、节点 Conditions、CNI 网络、CoreDNS 与 Pod 调度能力持续就绪”；在工作区根目录编写探针工作负载 `smoke-test.yaml`，体验拓扑打散调度；完成工作节点的 `cordon`（封锁）、`drain`（驱逐）与 `uncordon`（解封）维护闭环，深入理解 Eviction API 机制。
:::

## 3.1 验收标准：集群创建成功不等于服务可用

在上一课中，kind 成功创建了包含 1 个控制平面节点与 2 个工作节点的集群。kind 命令退出码为 `0`，仅能代表 Docker Engine 完成了节点容器的拉起和基础初始化，不能单独证明集群底座的健康度。

必须通过严谨的逐层验证链，确保每个核心组件均达到可承载业务的状态：

| 验证维度 | 核心观察对象 | 仅凭 `kind create` 无法排查的隐藏风险 |
| --- | --- | --- |
| **API 连通性** | kubeconfig、context、`/readyz?verbose` 端点 | kubectl 连错集群，或 API Server 鉴权模块异常 |
| **节点健康** | Node Conditions (`Ready`, `MemoryPressure` 等) | kubelet 汇报中断或磁盘空间写满导致节点卡在 `NotReady` |
| **Pod 网络** | kindnet CNI DaemonSet | CNI 插件拉取失败导致 Pod 无法获取 IP 或跨节点通信掉包 |
| **集群 DNS** | CoreDNS Deployment 与 Service | CoreDNS 副本没有就绪，导致集群内部服务域名解析失败 |
| **工作负载调度** | Pod 的实际节点调度分布 | 调度器无法按预期将副本均匀打散到不同工作节点 |
| **节点运维可控** | `cordon` / `drain` / `uncordon` | 运维封锁或驱逐节点时，忽略了 DaemonSet 或造成无法恢复的故障 |

下面是标准的集群底座能力验收顺序：

```mermaid
flowchart LR
  A[确认 kubectl context] --> B[调用 API /readyz 检查]
  B --> C[查看 Node Conditions]
  C --> D[核对系统组件 Pod]
  D --> E[提交探针验证 DNS与调度]
  E --> F[节点 Cordon/Drain 演练]
  F --> G[节点 Uncordon 恢复]
```

## 3.2 机制解析：kind 自动化背后的 Kubernetes 初始化流程

### 3.2.1 kind 自动完成了哪些 kubeadm 步骤

在真实的生产服务器环境中，部署 Kubernetes 集群需要使用 `kubeadm init` 初始化控制平面，再使用 `kubeadm join` 逐个加入工作节点，并手动配置 CNI 网络插件与 kubeconfig。kind 将这些繁琐的操作打包封装到了节点容器内。

| kind 的执行阶段 | 对应的原生 K8s / kubeadm 动作 | 底层产生的结果与对象 |
| --- | --- | --- |
| **启动 control-plane 容器** | `kubeadm init` 自动化初始化 | 启动 etcd、API Server、Scheduler 和 Controller Manager |
| **部署集群网络 CNI** | 自动 apply kindnet YAML | 为每个 Pod 分配独立 IP，配置节点间的虚拟 Bridge 路由 |
| **启动 worker 容器** | `kubeadm join` 节点加入 | 节点上的 kubelet 携带 Token 向控制平面建立双向 TLS 认证 |
| **导出 kubeconfig** | 提取控制平面 CA 证书与 Context | 将 API 访问凭据写入宿主机 `~/.kube/config` |
| **就绪等待轮询** | 访问集群 `/readyz` 接口 | 确认所有系统 Pod 达到 `Running` 状态后返回控制权 |

观察下面的交互面板，理解 kind 命令与 Kubernetes 内部引导机制的映射关系：

### 3.2.2 深入 API Server 的 `/readyz?verbose` 健康检查端点

API Server 提供了 `/readyz` 端点用于精细诊断控制平面的各项内部子组件是否达到准备状态。

在终端中执行以下命令查看 API 健康大管家：

```bash
kubectl get --raw='/readyz?verbose'
```

**命令解析与输出解读**：

* `kubectl get --raw`: 绕过 kubectl 的普通资源格式化输出，直接向 API Server 发起原生 HTTP GET 请求。
* `?verbose`: 展开逐项细节。输出中每个以 `[+]` 开头的子检查项代表一个关键模块：
  * `[+]ping`: 验证 API Server HTTP 基础服务响应状态。
  * `[+]log`: 验证日志子系统挂载点。
  * `[+]etcd`: 验证 API Server 与后端存储数据库 etcd 的读写连通性。
  * `[+]poststarthook/...`: 验证 API 启动后的 hook 钩子任务（如 CRD 注册、ServiceAccount Token 生成器等）。
* 结尾显示 `readyz check passed` 表示集群控制平面完全健康。

## 3.3 基线检查：节点 Conditions 与系统组件分析

### 3.3.1 `Ready` 状态与节点 Conditions 的深层含义

运行以下命令，查看节点摘要与详细状态：

```bash
kubectl get nodes -o wide
kubectl describe node yunfan-worker
```

在 `kubectl describe node` 的输出中，重点关注 `Conditions`（节点状态列表）：

| 状态类型 (Condition) | 健康期望值 | 异常影响与底层排查思路 |
| --- | --- | --- |
| **`Ready`** | `True` | 若为 `False` 或 `Unknown`，说明 kubelet 掉线或节点 CNI/网络故障。 |
| **`MemoryPressure`** | `False` | 若为 `True`，说明节点内存使用率触发临界阈值，kubelet 开始驱逐低优先级 Pod。 |
| **`DiskPressure`** | `False` | 若为 `True`，说明节点容器运行时磁盘空间受限，无法继续拉取新镜像或创建临时文件。 |
| **`PIDPressure`** | `False` | 若为 `True`，说明节点内部进程数达到上限，阻止创建新进程。 |

### 3.3.2 区分 `kube-system` 中的三大核心组件

在 `kube-system` 命名空间中，有三个非常容易混淆的核心组件：

| 组件名称 | 部署类型 | 核心职责 |
| --- | --- | --- |
| **kindnet** | DaemonSet | **CNI 网络插件**：负责为节点分配 Pod CIDR，实现集群内 Pod 之间的网络平铺连通。 |
| **kube-proxy** | DaemonSet | **网络代理**：在每个节点维护 iptables/IPVS 规则，将 Service 的 ClusterIP 访问请求负载均衡到后端 Pod。 |
| **CoreDNS** | Deployment | **域名解析**：在集群内部提供 DNS 服务的 Pod，将 Service 名称（如 `web-smoke`）解析为具体的虚拟 IP。 |

执行以下命令，验证系统级 Pod 的运行状况：

```bash
kubectl get pods -n kube-system -o wide
```

**预期输出**：
`coredns` 部署的副本数应达到 Ready；`kindnet` 与 `kube-proxy` 必须在 3 个节点上均有一个 `Running` 状态的实例。

## 3.4 实训：用探针工作负载验证调度、网络与 DNS

仅靠检查节点和系统 Pod 仍然不够，我们需要在项目根目录下创建一个包含 Deployment 和 Service 的探针配置文件 `smoke-test.yaml`，从真实工作负载的角度验证 **Pod 调度分布** 和 **CoreDNS 名称解析**。

### 3.4.1 配置文件完整代码：`smoke-test.yaml`

在项目根目录下直接创建 `smoke-test.yaml` 文件：

```yaml title="smoke-test.yaml"
apiVersion: v1
kind: Namespace
metadata:
  name: yunfan-smoke
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-smoke
  namespace: yunfan-smoke
spec:
  replicas: 2
  selector:
    matchLabels:
      app: web-smoke
  template:
    metadata:
      labels:
        app: web-smoke
    spec:
      topologySpreadConstraints:
        - maxSkew: 1
          topologyKey: kubernetes.io/hostname
          whenUnsatisfiable: ScheduleAnyway
          labelSelector:
            matchLabels:
              app: web-smoke
      containers:
        - name: nginx
          image: nginx:1.27-alpine
          ports:
            - name: http
              containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: web-smoke
  namespace: yunfan-smoke
spec:
  selector:
    app: web-smoke
  ports:
    - name: http
      port: 80
      targetPort: http
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: dnsutils
  namespace: yunfan-smoke
spec:
  replicas: 1
  selector:
    matchLabels:
      app: dnsutils
  template:
    metadata:
      labels:
        app: dnsutils
    spec:
      containers:
        - name: dnsutils
          image: registry.k8s.io/e2e-test-images/agnhost:2.39
          imagePullPolicy: IfNotPresent
```

### 3.4.2 关键 YAML 语法与调度限制解析

* **`topologySpreadConstraints`（拓扑分布约束）**：
  * `maxSkew: 1`: 允许不同主机间 Pod 数量的最大偏差值为 1，强制调度器尽量将 2 个 `web-smoke` 副本分布在不同的工作节点上（`yunfan-worker` 与 `yunfan-worker2`）。
  * `topologyKey: kubernetes.io/hostname`: 依据节点的拓扑标签 `kubernetes.io/hostname` 进行域名打散。
  * `whenUnsatisfiable: ScheduleAnyway`: 软约束策略。当可用工作节点不足时，依然允许调度，避免阻塞。
* **`dnsutils` 探针容器**：镜像内置了 `nslookup` 工具，专门用于发起 DNS 验证查询。

### 3.4.3 部署探针与网络/DNS 连通性测试

在终端中依次执行以下命令：

**1. 部署探针资源并等待就绪**：

```bash
kubectl apply -f smoke-test.yaml
kubectl wait -n yunfan-smoke --for=condition=Available deployment/web-smoke --timeout=120s
kubectl wait -n yunfan-smoke --for=condition=Available deployment/dnsutils --timeout=120s
```

**2. 检查 Pod 调度分布**：

```bash
kubectl get pods -n yunfan-smoke -o wide
```

**预期输出**：
你应该能看到 2 个 `web-smoke` 副本分别被调度到了 `yunfan-worker` 和 `yunfan-worker2` 节点上，验证了跨节点调度正常。

**3. 测试 CoreDNS 域名解析**：

```bash
kubectl exec -n yunfan-smoke deploy/dnsutils -- nslookup web-smoke.yunfan-smoke.svc.cluster.local
```

**命令解析与结果**：
在 `dnsutils` Pod 内向集群 DNS 节点查询 `web-smoke` 服务的全限定域名（FQDN）。正确返回了 `web-smoke` Service 的 ClusterIP，证明 **CoreDNS 服务完美工作**。

**4. 测试 Service 连通性**：

```bash
kubectl exec -n yunfan-smoke deploy/web-smoke -- wget -qO- http://web-smoke
```

该命令测试了 Pod 到 Service 再转发至另一个 Pod 的完整 HTTP 连通链路。

## 3.5 运维演练：完成 cordon、drain 与 uncordon 节点维护闭环

当某个工作节点需要升级 Docker、维护硬件或重启时，不能直接强制关闭节点容器。必须使用标准的节点运维三部曲：`cordon` -> `drain` -> `uncordon`。

### 3.5.1 三大运维动作的本质区别与 Eviction API 机制

| 命令动作 | 节点调度状态 (Scheduling) | 节点现有 Pod 的变化 | 底层执行机制与原则 |
| --- | --- | --- | --- |
| **`kubectl cordon`** | 变为 `SchedulingDisabled` | **无变化**。现有的 Pod 继续正常运行。 | 阻止新 Pod 调度到该节点，但不主动打扰已有工作负载。 |
| **`kubectl drain`** | 保持 `SchedulingDisabled` | **驱逐**业务 Pod，并在其他节点重建副本。 | 向 API 发送 **Eviction API** 请求优雅删除 Pod。**必须配合 `--ignore-daemonsets`** 忽略 CNI 等节点守护进程。 |
| **`kubectl uncordon`** | 恢复为 `Ready (可调度)` | **无变化**。已迁走的 Pod **不会**自动搬回该节点。 | 解除封锁。K8s 维持调度稳定性，不会强制把 Pod 迁回原节点。 |

观察下面的节点维护与 Pod 动态迁移过程，体会 Eviction API 驱动下副本平移的效果：

### 3.5.2 执行节点维护全流程演练

选择工作节点 `yunfan-worker` 进行维护演练。在终端中依次执行以下命令：

**1. 步骤一：封锁节点 (Cordon)**

```bash
# 将 yunfan-worker 标记为不可调度
kubectl cordon yunfan-worker
kubectl get nodes
```

**预期观察**：`yunfan-worker` 的 STATUS 列变为 `Ready,SchedulingDisabled`。

**2. 步骤二：驱逐节点工作负载 (Drain)**

```bash
# 驱逐 yunfan-worker 节点上的 Pod，忽略 DaemonSet
kubectl drain yunfan-worker --ignore-daemonsets --timeout=120s
kubectl get pods -n yunfan-smoke -o wide
```

**参数解析与预期观察**：

* `--ignore-daemonsets`: 必需参数。DaemonSet（如 kindnet、kube-proxy）必须在每个节点运行，无法被驱逐到其他节点。此参数指示 drain 忽略它们。
* 此时运行在 `yunfan-worker` 上的 `web-smoke` Pod 被优雅终止，Deployment 在 `yunfan-worker2` 节点上自动拉起了替代 Pod！

**3. 步骤三：恢复节点调度 (Uncordon)**

```bash
# 恢复 yunfan-worker 的可调度状态
kubectl uncordon yunfan-worker
kubectl get nodes
```

**预期观察**：节点状态恢复为 `Ready`。`yunfan-worker2` 上运行的 Pod 继续运行，不会自动搬回 `yunfan-worker`。

## 3.6 实验交付与资源清理

### 3.6.1 本课提交成果清单

完成本课实训后，提交以下三项成果即可：

1. **配置文件**：项目根目录下的 `smoke-test.yaml`。
2. **第一张截图 `03-health-baseline.png`**：同一终端画面中包含 `kubectl get nodes`、`kubectl get pods -n kube-system` 和 `kubectl get pods -n yunfan-smoke -o wide` 的验证结果。
3. **第二张截图 `03-drain-uncordon.png`**：包含 `kubectl cordon`、`kubectl drain` 和 `kubectl uncordon` 完整执行与节点状态变化的画面。

{{reflection-checkpoint:kind-cluster-operations-check}}

### 3.6.2 常见错误与排错矩阵

| 故障现象 | 可能原因 | 修复排错路径 |
| --- | --- | --- |
| `/readyz` 返回 500 或 unready | API Server 或 etcd 未完全启动 | 稍等 30 秒；若仍报错，使用 `docker logs yunfan-control-plane` 查看控制平面容器日志 |
| `nslookup` 解析超时或报错 `NXDOMAIN` | CoreDNS 副本异常或处于 Pending 状态 | 运行 `kubectl get pods -n kube-system -l k8s-app=kube-dns`，检查 CoreDNS 日志 |
| `kubectl drain` 报错 `cannot delete DaemonSet-managed Pods` | 缺少必要的跳过参数 | 必须显式加上 `--ignore-daemonsets` 参数 |
| `uncordon` 后 Pod 没有回到节点 1 | 属于 Kubernetes 正常调度行为 | 不需要修复。K8s 保证的是期望副本数（Replicas），不会随意移动稳定运行的 Pod |

::: details 挑战任务：探索控制平面的静态 Pod 清单
运行 `docker exec -it yunfan-control-plane bash` 进入控制平面节点容器，查看 `/etc/kubernetes/manifests/` 目录下的配置文件。这里的 YAML 文件是节点 kubelet 用来自动加载并启动 `kube-apiserver`、`etcd` 等核心组件的静态 Pod 清单（Static Pod）。你可以尝试打开其中一个文件，观察 Kubernetes 最核心的控制平面组件是如何通过声明式配置定义的。
:::

### 3.6.3 清理探针资源

实训验证完成后，删除探针命名空间，保留 `yunfan` 集群供下一课使用：

```bash
kubectl delete namespace yunfan-smoke
```

## 本章小测

{{assessment:kind-cluster-validation}}

## 本章小结

完成本课后，你已经掌握了：

1. **集群底座验证能力**：理解了不能仅凭创建命令返回 0 断定集群可用，学会了通过 context、API `/readyz?verbose` 和 Node Conditions 建立健康基线。
2. **Kubernetes 核心系统组件架构**：理清了 kindnet (CNI)、kube-proxy 与 CoreDNS 的分工与关系。
3. **声明式探针与调度约束**：独立编写了 `smoke-test.yaml`，并掌握了 `topologySpreadConstraints` 拓扑分布限制属性的写法。
4. **节点运维三部曲**：掌握了 `cordon`、`drain` 和 `uncordon` 的完整运维闭环，理解了 Eviction API 驱逐机制与 `--ignore-daemonsets` 的底层逻辑。

下一课中，我们将使用 Label、Annotation、API 发现和 Namespace 进一步为业务建立规范的资源治理规则！
