K8s Lab 把当前仓库文档整理成一个可阅读的网页站点

Repository Reading Site

本轮操作记录:环境验证、集群基线盘点与故障样本采集

用户确认: 基于这个前提,我这轮的目标不是“再看一遍目录”,而是做三件事: 1. 验证 5 台机器的 SSH 连通性和关键系统服务状态 2. 验证本机 `kubectl` 对集群的访问,并建立基线快照 3. 抓取几个真实故障样本,为后续学习和排障训练做材料准备 这一步的意义是: --- 我对 5 台机器都执行了同一类命令,结构如下: 远端执行的内容大致是:

Markdown01-操作记录-环境验证与故障样本采集.md2026年4月9日 17:31

本轮操作记录:环境验证、集群基线盘点与故障样本采集

本轮目标

用户确认:

  • 文档里的机器都可以免密登录
  • 希望我继续推进

基于这个前提,我这轮的目标不是“再看一遍目录”,而是做三件事:

  1. 验证 5 台机器的 SSH 连通性和关键系统服务状态
  2. 验证本机 kubectl 对集群的访问,并建立基线快照
  3. 抓取几个真实故障样本,为后续学习和排障训练做材料准备

这一步的意义是:

把之前的“仓库分析”升级成“基于真实集群状态的学习入口”。


Step 1: 验证 5 台机器能否免密 SSH 登录

实际命令模式

我对 5 台机器都执行了同一类命令,结构如下:

ssh -o BatchMode=yes -o StrictHostKeyChecking=no -o ConnectTimeout=8 root@<IP> '...'

远端执行的内容大致是:

printf "host=%s user=%s\n" "$(hostname)" "$(whoami)"
printf "kernel=%s\n" "$(uname -r)"
printf "ips=%s\n" "$(hostname -I | xargs)"
printf "kubelet=%s\n" "$(systemctl is-active kubelet 2>/dev/null || true)"
printf "containerd=%s\n" "$(systemctl is-active containerd 2>/dev/null || true)"
printf "wireguard=%s\n" "$(systemctl is-active wg-quick@wg0 2>/dev/null || true)"

为什么这么设计命令

这条 SSH 命令不是随便拼的,而是有明确目的:

  • hostname:确认登录到的到底是哪台主机
  • whoami:确认当前远端身份是 root
  • uname -r:确认宿主机内核版本
  • hostname -I:查看该节点当前拥有的所有 IP
  • systemctl is-active kubelet:确认节点代理是否活着
  • systemctl is-active containerd:确认容器运行时是否活着
  • systemctl is-active wg-quick@wg0:确认 WireGuard 虚拟内网是否活着

为什么不用更简单的 ssh root@ip hostname

因为“能连上”只是最低验证。

真正有价值的是一次远程命令里同时确认:

  • 机器身份
  • 系统版本
  • 网络身份
  • K8s 节点工作必需服务

这样一次就能建立环境可信度。


Step 2: 解释 SSH 参数背后的原理

-o BatchMode=yes

作用:

  • 禁止 SSH 在需要密码或交互确认时进入交互模式

为什么重要:

  • 如果没有免密配置,这个参数会让 SSH 直接失败
  • 这比卡在密码输入提示上更适合自动化验证

这实际上是在验证:

是否真的具备“非交互式免密登录能力”。

-o StrictHostKeyChecking=no

作用:

  • 首次连接某台机器时,不要求人工确认 host key

为什么重要:

  • 自动化验证环境时,经常不希望被 “Are you sure you want to continue connecting?” 卡住

注意:

这在实验环境中方便,但生产环境要更谨慎,因为 host key 校验本来是为了防止中间人攻击。

-o ConnectTimeout=8

作用:

  • 连接超时时间设为 8 秒

为什么重要:

  • 如果某台机器失联,不希望 SSH 一直挂着
  • 这样结果会更快返回,便于批量验证

Step 3: 为什么用 systemctl is-active

命令

远程机器上执行了:

systemctl is-active kubelet
systemctl is-active containerd
systemctl is-active wg-quick@wg0

原理

在 systemd 管理的 Linux 系统里,服务不是抽象概念,而是明确的 unit。

systemctl is-active 的好处是:

  • 输出简单,适合脚本判断
  • 不像 status 那样内容很多
  • 能快速知道服务当前是否活着

为什么只验证这 3 个服务

因为它们几乎构成了这个集群数据面的最小闭环:

  • kubelet:节点代理,负责 Pod 生命周期
  • containerd:实际拉镜像、起容器
  • wg0:跨机房统一内网,保证节点通信地址一致

如果这三者都挂了,节点基本就不具备正常参与集群工作的条件。


Step 4: SSH 验证结果解读

Master

输出摘要:

host=us480851516617a user=root
kernel=6.8.0-48-generic
ips=10.2.207.3 10.244.248.64 10.10.0.1
kubelet=active
containerd=active
wireguard=active

解读:

  • 远端身份正常
  • kubelet / containerd / wg0 都正常
  • 节点同时具备云内网 IP、Pod 相关地址、WireGuard IP

Worker-1

输出摘要:

host=us590068728056 user=root
kernel=6.8.0-107-generic
ips=10.2.207.2 10.244.119.192 10.10.0.2
kubelet=active
containerd=active
wireguard=active

解读:

  • 同样具备多层地址
  • 服务正常

Worker-2

输出摘要:

host=cp-3 user=root
kernel=6.8.0-48-generic
ips=154.9.27.60 10.0.4.169 10.10.0.3 10.244.242.0 172.17.0.1
kubelet=active
containerd=active
wireguard=active

解读:

  • 这是一个很好的教学样本,因为它同时显示了公网 IP、云内网 IP、WireGuard IP、Pod 网络相关地址和 Docker bridge 地址
  • 非常适合用来讲“多层网络身份”

Worker-3

输出摘要:

host=hk652699382121 user=root
kernel=6.8.0-48-generic
ips=10.4.225.2 172.17.0.1 172.18.0.1 10.10.0.4 10.244.169.0
kubelet=active
containerd=active
wireguard=active

解读:

  • 节点服务正常
  • 节点上存在多张桥接/容器网络相关地址

Worker-4

输出摘要:

host=wk-1 user=root
kernel=5.15.0-174-generic
ips=154.219.104.66 10.10.0.5 10.244.147.64
kubelet=active
containerd=active
wireguard=active

解读:

  • 这个节点和其他节点相比更异构:Ubuntu 22.04 + 5.15 内核 + containerd 1.7.x
  • 但仍成功作为 K8s worker 参与集群

阶段结论

5 台机器的 SSH 和关键服务验证全部通过。

这说明后续:

  • 我们可以直接进入远程宿主机层面的排障
  • 也可以放心基于真实节点状态写教学材料

Step 5: 验证本机 kubectl 是否能访问集群

命令

KUBECONFIG=~/.kube/config-k8s-lab kubectl get nodes -o wide
KUBECONFIG=~/.kube/config-k8s-lab kubectl get ns

为什么要显式写 KUBECONFIG=...

因为 Kubernetes 客户端默认读的是:

~/.kube/config

但你这里的实验环境配置文件是:

~/.kube/config-k8s-lab

通过在命令前临时指定环境变量 KUBECONFIG,可以确保:

  • 当前命令明确使用实验集群配置
  • 不会误用默认 kubeconfig
  • 不影响其它 shell 会话的默认环境

原理

kubectl 本质是一个 API 客户端。

它需要从 kubeconfig 中知道:

  • API Server 地址
  • 使用哪个用户或证书
  • 当前上下文是哪个集群

所以 kubeconfig 就是:

kubectl 的“连接配置 + 身份凭据 + 上下文切换器”。


Step 6: 为什么 kubectl get nodes -o wide 这么重要

命令

KUBECONFIG=~/.kube/config-k8s-lab kubectl get nodes -o wide

-o wide 的作用

不加 -o wide 时,kubectl 只给你基础列。

加上 -o wide 后,会补充:

  • INTERNAL-IP
  • OS-IMAGE
  • KERNEL-VERSION
  • CONTAINER-RUNTIME

为什么这很关键

因为节点问题从来不是单一维度:

  • 不是只看 Ready/NotReady
  • 也要看内核、OS、runtime、地址身份

输出解读出的关键信息

我从输出里确认了:

  • 5 个节点全部 Ready
  • 内部通信地址统一是 10.10.0.x
  • 控制面只有 1 个节点
  • 其中 1 台节点环境明显异构

这些信息后来都被我写进学习文档作为第一课的真实例子。


Step 7: 为什么再看命名空间

命令

KUBECONFIG=~/.kube/config-k8s-lab kubectl get ns

原理

命名空间可以帮助你快速判断:

  • 集群是不是只装了核心组件
  • 还是已经有大量平台能力
  • 是否有环境隔离设计

输出反映出的信息

我看到了:

  • argocd
  • gitea
  • harbor
  • monitoring
  • ingress-nginx
  • ml-platform
  • dev/staging/prod

我得到的结论

这说明当前集群已经是:

  • 基础设施层就绪
  • 平台组件层较完整
  • 业务实验层也已有内容

所以后续教学不能只停留在“刚装好 Kubernetes”的视角,而要直接按真实平台来讲。


Step 8: 盘点集群 Pod、存储和资源基线

实际命令

KUBECONFIG=~/.kube/config-k8s-lab kubectl get pods -A -o wide
KUBECONFIG=~/.kube/config-k8s-lab kubectl get sc
KUBECONFIG=~/.kube/config-k8s-lab kubectl get pvc -A
KUBECONFIG=~/.kube/config-k8s-lab kubectl top nodes

为什么要看这四条

kubectl get pods -A -o wide

作用:

  • 一次性查看所有命名空间的 Pod 状态
  • 识别当前是否存在故障样本
  • 看 Pod 分布在哪些节点

kubectl get sc

作用:

  • 看默认存储类是什么
  • 判断 PVC 是如何被动态分配的

kubectl get pvc -A

作用:

  • 看哪些组件依赖持久化存储
  • 看卷是否都 Bound

kubectl top nodes

作用:

  • 看节点当前资源快照
  • 判断当前崩溃是否可能来自整体资源耗尽

结果摘要

存储

默认存储类是:

  • nfs-dynamic

PVC 已经被这些组件使用:

  • Gitea
  • Harbor
  • Monitoring
  • Loki
  • ML Platform

节点资源

当前节点 CPU、内存整体不高,未见全局资源挤压迹象。

我得到的结论

当前集群资源整体不紧张,所以一些 CrashLoop 不太像“全局资源不够”导致,更可能是:

  • 应用配置问题
  • 平台组件集成问题
  • 探针与存储行为不匹配
  • CRD/控制器依赖不完整

Step 9: 为什么抓取故障样本

kubectl get pods -A -o wide 的输出里,我发现了几类明显异常:

  • argocd-applicationset-controllerCrashLoopBackOff
  • gitea-postgresql-0CrashLoopBackOff
  • gitea 主 Pod:Init:CrashLoopBackOff
  • monitoring-grafanaCrashLoopBackOff

这是极好的学习素材。

因为真实故障能帮你训练两件事:

  1. 从现象判断问题大概在哪一层
  2. 用证据链而不是直觉下结论

所以我决定继续抓:

  • describe
  • logs --previous

Step 10: 抓取 describelogs --previous

实际命令

KUBECONFIG=~/.kube/config-k8s-lab kubectl -n argocd describe pod argocd-applicationset-controller-... | tail -n 80
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n argocd logs argocd-applicationset-controller-... --previous

KUBECONFIG=~/.kube/config-k8s-lab kubectl -n gitea describe pod gitea-postgresql-0 | tail -n 80
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n gitea logs gitea-postgresql-0 --previous

KUBECONFIG=~/.kube/config-k8s-lab kubectl -n monitoring describe pod monitoring-grafana-... | tail -n 100
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n monitoring logs monitoring-grafana-... grafana --previous

为什么这里要用 --previous

这是 Kubernetes 排障里一个非常关键的参数。

在 CrashLoop 场景下,当前容器可能已经重启了,你看到的当前日志未必包含上一次崩溃前的关键信息。

--previous 的意义是:

  • 去拿上一个容器实例的日志
  • 看到“它上一次为什么死”

这对排查启动即崩溃的程序非常重要。

为什么 describe 后面加 tail

因为 describe 输出很长,而最有排障价值的部分通常集中在后半段:

  • State
  • Last State
  • Exit Code
  • Probes
  • Events

tail -n 80/100 可以更聚焦地看到这些信息。


Step 11: ArgoCD 故障的证据链

观察到的现象

describe 里看到:

  • 容器反复重启
  • Back-off restarting failed container

日志里看到关键错误:

failed to get restmapping: no matches for kind "ApplicationSet" in version "argoproj.io/v1alpha1"

我如何进一步验证

为了避免仅凭日志猜测,我又执行:

KUBECONFIG=~/.kube/config-k8s-lab kubectl get crd applicationsets.argoproj.io

返回:

  • NotFound

我得到的结论

这是非常清晰的证据链:

  1. 应用控制器在启动
  2. 它想 watch ApplicationSet
  3. API Server 里没有对应 CRD
  4. Cache sync 失败
  5. 控制器退出

这一步体现的排障原则

日志给方向,API 对象给确认。

不要只凭日志说“应该是 CRD 缺了”,而要再用 kubectl get crd 验证一次。


Step 12: Gitea PostgreSQL 故障的证据链

观察到的现象

describe 里看到:

  • CrashLoopBackOff
  • Exit Code: 137
  • Liveness / Readiness probe 大量失败

日志里看到:

  • 数据库启动恢复时间长
  • recovery 过程反复出现
  • 最后出现文件缺失

为什么这个案例有价值

因为这不是简单的:

  • 镜像错了
  • 命令写错了

而是涉及:

  • 数据恢复
  • 存储后端性能
  • 探针节奏
  • 容器重启策略

我从这里推导出的结论

更可能是:

  • 数据库恢复慢
  • 探针太急
  • kubelet 反复重启容器
  • NFS 存储使恢复过程更脆弱

这一步体现的排障原则

有状态工作负载的故障,不要只盯着 Pod 状态,一定要把探针、存储和恢复行为一起看。


Step 13: Grafana 故障的证据链

观察到的现象

Grafana 日志里出现:

Datasource provisioning error: datasource.yaml config is invalid. Only one datasource per organization can be marked as default

为什么不能只停在这句日志

因为你还需要知道:

  • 到底是谁把两个 datasource 都配成了 default

所以我继续查带 grafana_datasource=1 标签的 ConfigMap:

KUBECONFIG=~/.kube/config-k8s-lab kubectl -n monitoring get cm -l grafana_datasource=1 -o name

返回两个对象:

  • configmap/loki-loki-stack
  • configmap/monitoring-kube-prometheus-grafana-datasource

随后我分别查看配置内容。

关键发现

Prometheus 的 datasource 配置里:

  • isDefault: true

Loki 的 datasource 配置里:

  • isDefault: true

我得到的结论

这不是 Grafana 自己随机崩溃,而是两个 chart 的 provisioning 配置在集成层发生冲突。

这一步体现的排障原则

当日志说“配置冲突”时,下一步不是重启 Pod,而是顺着配置对象把冲突双方都找出来。


Step 14: 本轮为什么没有直接修复这些故障

因为这轮的用户目标不是“帮我把集群修好”,而是:

  • 建立学习主线
  • 记录操作
  • 讲清原理
  • 以成为专家为目标

所以当前更合理的动作是:

  1. 先把这些故障当作学习样本固化下来
  2. 在教学文档里解释它们分别对应哪一层知识
  3. 后续如果你要,我再逐个系统排查和修复

这也是工程上很重要的一点:

不是看到异常就立刻动手改,而是先判断当前任务目标是什么。


Step 15: 将结果写成学习文档

基于本轮采集的数据,我新建了:

  • 01-环境验证与第一课-认识你的真实集群.md

这份文档不是简单贴输出,而是把这轮验证转换成了第一课学习内容,重点讲了:

  • 为什么先做环境验证
  • 节点的四类 IP 分别是什么
  • 为什么 INTERNAL-IP 是 WireGuard 地址
  • 为什么同一台机器会出现这么多地址
  • kubectl get nodes -o wide 每一列是什么意思
  • 当前集群有哪些平台能力
  • 当前 3 个真实故障样本分别在教你什么

这意味着我们已经从“审查仓库”正式进入“基于真实集群讲 Kubernetes”阶段。


本轮命令清单

下面是本轮实际用到的核心命令清单,后续可以直接复用:

ssh -o BatchMode=yes -o StrictHostKeyChecking=no -o ConnectTimeout=8 root@107.148.176.193 '...'
ssh -o BatchMode=yes -o StrictHostKeyChecking=no -o ConnectTimeout=8 root@107.148.164.118 '...'
ssh -o BatchMode=yes -o StrictHostKeyChecking=no -o ConnectTimeout=8 root@154.9.27.60 '...'
ssh -o BatchMode=yes -o StrictHostKeyChecking=no -o ConnectTimeout=8 root@38.76.221.17 '...'
ssh -o BatchMode=yes -o StrictHostKeyChecking=no -o ConnectTimeout=8 root@154.219.104.66 '...'

KUBECONFIG=~/.kube/config-k8s-lab kubectl get nodes -o wide
KUBECONFIG=~/.kube/config-k8s-lab kubectl get ns
KUBECONFIG=~/.kube/config-k8s-lab kubectl get pods -A -o wide
KUBECONFIG=~/.kube/config-k8s-lab kubectl get sc
KUBECONFIG=~/.kube/config-k8s-lab kubectl get pvc -A
KUBECONFIG=~/.kube/config-k8s-lab kubectl top nodes

KUBECONFIG=~/.kube/config-k8s-lab kubectl -n argocd describe pod argocd-applicationset-controller-... | tail -n 80
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n argocd logs argocd-applicationset-controller-... --previous
KUBECONFIG=~/.kube/config-k8s-lab kubectl get crd applicationsets.argoproj.io

KUBECONFIG=~/.kube/config-k8s-lab kubectl -n gitea describe pod gitea-postgresql-0 | tail -n 80
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n gitea logs gitea-postgresql-0 --previous

KUBECONFIG=~/.kube/config-k8s-lab kubectl -n monitoring describe pod monitoring-grafana-... | tail -n 100
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n monitoring logs monitoring-grafana-... grafana --previous
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n monitoring get cm
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n monitoring get cm -l grafana_datasource=1 -o name
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n monitoring get cm monitoring-kube-prometheus-grafana-datasource -o yaml
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n monitoring get cm loki-loki-stack -o yaml

本轮最终结论

结论一:环境已经足够真实,适合进入正式深学

不是纸面环境,不是模拟集群,而是:

  • 5 台真实服务器
  • 真实 SSH 免密
  • 真实 kubelet/containerd/WireGuard
  • 真实平台组件
  • 真实业务工作负载

结论二:当前集群已经有很好的教学价值

因为它不只有“成功路径”,还已经自然存在几类非常典型的故障样本。

结论三:下一步应该进入控制面主链路讲解

也就是下一课最核心的问题:

一条 kubectl apply 请求,是怎么穿过 API Server、etcd、Scheduler、Controller、kubelet,最后变成真实容器的?

把这条主链路讲透,你后面学所有对象、网络、存储、监控和 Operator 都会更稳。