AI Infra 训练营
总览
  • 总览
  • 完整安装
  • 核心 K8s
  • Cilium 网络
  • Longhorn 存储
  • 监控日志
  • CI / GitOps
  • 安全准入
  • CI/CD 实战(MySQL+Go+Vue)
  • HPA/Ingress/Hubble 实战
  • 面试速查 + 真实踩坑
  • Day 0 · 新手接管 Runbook
  • Day 1 · 集群起步 + CNI
  • Day 2 · 控制面 + etcd
  • Day 3 · CRD + Operator + Webhook
  • Day 4 · 存储深度
  • Day 5 · 卷扩容 + 安全
  • Day 6 · 调度 + 可观测
  • Day 7 · Harbor + ArgoCD + Mesh
  • Day 8 · AI Infra
  • Day 9 · Triton + GPU
  • Day 10 · MIG + HPA + 量化
  • Day 11 · AI Agent 端到端
  • Day 12 · 灾备
  • Day 13 · Operator + 联邦 + Mesh + RAG
  • Day 14 · CKA / CKS + 总结
  • LLM 训练手册
  • RAG + Agent 手册
  • 推理优化手册
  • 上下文工程手册
  • Agent 开发手册
  • 面试深度复盘
  • 训练 v2 深度手册
  • 心智模型
  • 看懂命令输出
  • 容器网络底层
  • K8s 网络深入
  • DNS 全套
  • 故障排查方法论
  • 心智模型
  • 容器挂载完整指南
  • K8s Volumes 大全
  • PV/PVC/CSI 深入
  • NFS 深入
  • 分布式存储概览
  • 故障排查 runbook
命令手册
HiHuo 主站
GitHub
总览
  • 总览
  • 完整安装
  • 核心 K8s
  • Cilium 网络
  • Longhorn 存储
  • 监控日志
  • CI / GitOps
  • 安全准入
  • CI/CD 实战(MySQL+Go+Vue)
  • HPA/Ingress/Hubble 实战
  • 面试速查 + 真实踩坑
  • Day 0 · 新手接管 Runbook
  • Day 1 · 集群起步 + CNI
  • Day 2 · 控制面 + etcd
  • Day 3 · CRD + Operator + Webhook
  • Day 4 · 存储深度
  • Day 5 · 卷扩容 + 安全
  • Day 6 · 调度 + 可观测
  • Day 7 · Harbor + ArgoCD + Mesh
  • Day 8 · AI Infra
  • Day 9 · Triton + GPU
  • Day 10 · MIG + HPA + 量化
  • Day 11 · AI Agent 端到端
  • Day 12 · 灾备
  • Day 13 · Operator + 联邦 + Mesh + RAG
  • Day 14 · CKA / CKS + 总结
  • LLM 训练手册
  • RAG + Agent 手册
  • 推理优化手册
  • 上下文工程手册
  • Agent 开发手册
  • 面试深度复盘
  • 训练 v2 深度手册
  • 心智模型
  • 看懂命令输出
  • 容器网络底层
  • K8s 网络深入
  • DNS 全套
  • 故障排查方法论
  • 心智模型
  • 容器挂载完整指南
  • K8s Volumes 大全
  • PV/PVC/CSI 深入
  • NFS 深入
  • 分布式存储概览
  • 故障排查 runbook
命令手册
HiHuo 主站
GitHub
  • Day 0 · 环境与硬件

    • Day 0 新手现场接管 Runbook:先看懂,再动手
    • Day 0:5 节点裸 Ubuntu → K8s 装机基线
  • Week 1:K8s 内核 + 周边基础设施

    • Day 1:3 CP HA 集群 + CNI 选型 + DNS 调优
    • Day 2:控制面 deep dive + etcd 内核 + chaos drill
    • Day 3:CRD + Operator (kubebuilder 从 0 写)
    • Day 4:Longhorn 存储 + Cilium 二探(Hubble / NetworkPolicy / L7)
    • Day 5:PVC 在线扩容 + K8s 安全基线(RBAC / PSA / Secret 加密 / Kyverno)
    • Day 6:调度策略 + Prometheus / Loki 观测栈
    • Day 7:Harbor 私有镜像 + ArgoCD GitOps + Cilium WireGuard
  • Week 2:制品 + GitOps + AI Infra + 综合

    • Day 8:k3s 单节点 + NVIDIA Device Plugin + vLLM 跑 Qwen2.5-3B
    • Day 8(attempt 1):跨 WAN GPU 加入主集群(失败复盘)
    • Day 8:AlertManager 真接入 + PrometheusRule 实战
    • Day 8:集群内 CI 闭环 — Gitea + Jenkins + Kaniko
    • Day 9:Triton 多框架推理 + DCGM 跨集群可观测 + vLLM 实测
    • Day 10:MIG 硬切片 + AWQ 量化 + HPA Custom Metrics
    • Day 11:AI 业务端到端 —— chainlit + GitOps + 跨 WAN vLLM
    • Day 12:灾难恢复 + 生产事故注入
    • Day 13:LLM Operator + 多集群联邦 + Ambient Mesh + RAG
    • Day 14:CKA / CKS 真题演练 + 14 天技术栈横向汇总

Day 4:Longhorn 存储 + Cilium 二探(Hubble / NetworkPolicy / L7)

Day 1 把 5 节点 K8s + Cilium CNI 装好了,但没存储、没策略、没观测。Day 4 把这三块补齐:

  • Longhorn 装分布式块存储,做 PVC 动态分配 + StatefulSet 数据持久化 + VolumeSnapshot 备份还原 + 节点 drain 数据无损迁移
  • Cilium Hubble 启用流量级观测,看 eBPF 抓到的 Pod-to-Pod flow(L4 + L7)
  • NetworkPolicy 从 default-deny 开始走 Zero Trust,再叠 CiliumNetworkPolicy 做 HTTP method 级 L7 控制

整篇 6 步走,每步一个真坑。最坑的几个:iSCSI 模块没持久化、vdb 不是空盘差点 mkfs 摧毁集群、Longhorn 默认不容忍 control-plane taint、default-deny 网络策略把节点本身的 health check 一并断掉。


集群形态

承接 Day 1 的 5 节点 K8s 1.30 + Cilium 1.16.5:

节点内网 IP角色系统盘 vda数据盘 vdb
k8s-cp-110.0.24.31控制面30G100G(已挂 /var/lib/containerd)
k8s-cp-210.0.24.29控制面30G100G
k8s-cp-310.0.24.32控制面30G100G
k8s-w-110.0.24.28worker30G100G
k8s-w-210.0.24.30worker30G100G

存储现状全空:

kubectl get sc,csidrivers,pv,pvc -A
# No resources found

从零搭。


第 1 步:装 Longhorn 之前的 4 个前置

Longhorn 装失败 99% 是前置没做。4 件事:检查数据盘、装 iSCSI 内核态、装 NFS 客户端、装 VolumeSnapshot CRD。

1.1 真坑:vdb 不是空盘

原计划:5 节点每台 100G vdb 都是空闲盘,直接 mkfs.xfs /dev/vdb 做 Longhorn 数据盘。

lsblk -f /dev/vdb
# NAME   FSTYPE FSVER LABEL UUID                                 MOUNTPOINTS
# vdb
# └─vdb1 ext4   1.0         8f306892-...                          /var/lib/containerd

vdb1 已经被挂到 /var/lib/containerd —— 这是 Day 0 / Day 1 时 IDC 默认挂的 containerd image 缓存。如果照计划 mkfs.xfs /dev/vdb,会把整个 container 镜像存储抹掉,集群所有 Pod 立刻炸。

改方案:不动 vdb,Longhorn 数据走 /var/lib/longhorn(在 vda 系统盘上,每节点 ~25G 可用),总池 ~125G。3 副本下可用 ~42G,学习够。

教训:任何动磁盘的操作(mkfs / wipefs / parted)之前必须 lsblk -f 看是否已有文件系统/挂载。「厂家给的空闲盘」不一定真空。

1.2 iSCSI + NFS + 数据目录(5 节点同操作)

# 1. iscsi_tcp 内核模块 —— 持久化(Ubuntu 22 不持久化默认 modprobe)
modprobe iscsi_tcp
echo iscsi_tcp > /etc/modules-load.d/iscsi_tcp.conf

# 2. iscsid 服务 —— Longhorn engine 用 iSCSI 把 volume 暴露给 kubelet
systemctl enable --now iscsid

# 3. NFS 客户端 —— Longhorn RWX (多读多写) 模式底层是 NFSv4
apt-get install -y nfs-common

# 4. Longhorn 数据目录
mkdir -p /var/lib/longhorn

每一项的意义:

项不做会怎样
iscsi_tcp 模块Longhorn 启不来,CSI Provision 报「iSCSI initiator not found」
modules-load.d/iscsi_tcp.conf重启后丢,半夜重启 PVC mount 全炸
iscsid.servicePod attach volume 直接 fail
nfs-commonRWX PV 挂载失败(mount.nfs: bad option)

为什么 Longhorn 走 iSCSI:从 Longhorn 视角「Volume 暴露给 kubelet」是通过 iSCSI 协议挂载的,即使数据物理上就在本节点。这个抽象让 attach / detach / replica failover 有统一接口。

1.3 装 VolumeSnapshot CRD + snapshot-controller

VolumeSnapshot 是 K8s 上游标准 CRD,不在 K8s 主仓库里(属于 kubernetes-csi/external-snapshotter 项目),所有 CSI driver 共享一个 snapshot-controller。装 Longhorn 之前先装这套,否则 Longhorn 装上后做不了快照。

SNAP_VER=v8.2.0
BASE=https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/$SNAP_VER

# CRDs(集群级类型定义)
for f in client/config/crd/snapshot.storage.k8s.io_volumesnapshotcontents.yaml \
         client/config/crd/snapshot.storage.k8s.io_volumesnapshotclasses.yaml \
         client/config/crd/snapshot.storage.k8s.io_volumesnapshots.yaml; do
  kubectl apply -f "$BASE/$f"
done

# snapshot-controller(协调 VolumeSnapshot → VolumeSnapshotContent)
for f in deploy/kubernetes/snapshot-controller/rbac-snapshot-controller.yaml \
         deploy/kubernetes/snapshot-controller/setup-snapshot-controller.yaml; do
  kubectl apply -f "$BASE/$f"
done

CRD 是集群级契约(跨所有 CSI driver 通用),snapshot-controller 是上游 reference 实现(所有 driver 共享一个),driver 内还有自己的 external-snapshotter sidecar 真正调 CSI gRPC CreateSnapshot —— 这套三层分离是 K8s 摆脱 in-tree volume plugin 的核心 pattern。


第 2 步:装 Longhorn v1.7.2 + 3 个连环坑

curl -sS https://raw.githubusercontent.com/longhorn/longhorn/v1.7.2/deploy/longhorn.yaml -o /tmp/longhorn.yaml
kubectl apply -f /tmp/longhorn.yaml

manifest 5047 行,40 个 kind(CRD + ServiceAccount + Role + Service + DaemonSet + Deployment ...)。镜像 longhornio/*:v1.7.2(Docker Hub,国内拉走 Day 1 配的 daocloud mirror)。

2.1 真坑 A:Longhorn 默认不容忍 control-plane taint

apply 完 kubectl get pods -n longhorn-system -o wide 只在 w-1/w-2 有 Pod,cp-1/2/3 完全没 longhorn-manager。原因:Longhorn manifest 没写 tolerations,集群 cp-1/2/3 有 node-role.kubernetes.io/control-plane:NoSchedule taint,所以只跑在 worker。生产上「storage 不跑 control plane」是合理隔离,但小集群学习要 5 节点全跑才能演示 3 副本跨节点反亲和。

正确改法:用 Longhorn Setting 不是直接 patch DaemonSet(否则 Longhorn controller 下次 reconcile 会覆盖你):

kubectl patch setting taint-toleration -n longhorn-system --type=merge \
  -p '{"value":":NoSchedule"}'

教训:装第三方 operator manifest 时,控制器自管的资源(DS/Deployment)必须通过 operator 自己的 API 改,不是直接 kubectl patch。Longhorn 把 toleration / node-selector 都做成了 Setting CRD,原因就在这。

2.2 真坑 B:csi-plugin startup race,CrashLoopBackOff

patch toleration 后,cp-1 / cp-3 上的 longhorn-csi-plugin 进入 CrashLoopBackOff,日志:

Failed to initialize Longhorn API client:
  Get "http://longhorn-backend:9500/v1": context deadline exceeded

根因:DaemonSet Pod 一可调度立刻起,但 longhorn-backend Service 后端的 longhorn-manager 在这些节点还没 Ready。csi-plugin 启动时连 backend 超时 → fatal exit → exponential backoff(30s / 60s / 120s)越等越慢。

修复:删 Pod 让 DS 重建(这时 backend 已 Ready),kubectl delete pod -n longhorn-system longhorn-csi-plugin-xxx。

教训:CrashLoopBackOff 不一定是代码 bug,startup ordering race 同样高频。健壮 controller 应该 graceful retry + readiness probe 不通过来表达「未就绪」,业务 Pod 有强依赖用 initContainers 阻塞。

2.3 真坑 C:Volume 调度选址是「创建那一刻」决定的

后面起 Postgres 时第一次 attach 报:

FailedAttachVolume: cannot attach volume because the data engine image
longhornio/longhorn-engine:v1.7.2 is not deployed on at least one of
the replicas' nodes or the node that the volume is going to attach to

engine-image-ei-* DaemonSet 同样没 toleration(B 步的 setting 会一并修复,5 节点会都跑),但已经创建的 Volume 的 replica 分布是冻结的 —— 创建那一刻 cp-1/2/3 没 engine-image,replica 只在 w-1/w-2 上,加完 toleration 不会自动迁移。只删 Pod 不够,必须删 PVC + Volume 重建:

kubectl delete sts pg -n storage-demo
kubectl delete pvc pgdata-pg-0 -n storage-demo
# 重新 apply 后,replica 才能分布到 3 个节点

教训:存储 plugin 的调度是一次性的,PVC 一旦 Bound 物理位置固定;K8s Pod 调度是持续的(每次重启重新调度)。后期加节点 / 加 disk 只对新 Volume 生效,老 Volume 要手动 migrate(Longhorn UI 里有按钮)—— 大集群必须前期统一规划 storage layout。

2.4 验收清单

kubectl get nodes.longhorn.io -n longhorn-system   # 5 节点 SCHEDULABLE=True
kubectl get sc                                     # longhorn (default)
kubectl patch svc longhorn-frontend -n longhorn-system -p '{"spec":{"type":"NodePort"}}'   # UI 暴露

需要全部满足:SC longhorn 自动建好且是 default-class;5 节点 longhorn-manager / longhorn-csi-plugin / engine-image DS 全 Running;csi-{attacher, provisioner, resizer, snapshotter} Deployment 各 3 副本 Running。

注意 reclaimPolicy: Delete 是 SC 默认值 —— 生产数据敏感场景必须改 Retain,否则 kubectl delete pvc 一键灭数据。


第 3 步:StatefulSet + Snapshot + drain 三件套实测

Litmus test 三连:

  1. 数据持久化:删 Pod 重建,数据还在
  2. Snapshot/Restore:创快照 → 改数据 → 从快照恢复 → 数据是快照时刻的
  3. 节点 drain 容忍:drain 一个 replica 节点,Pod 不重启、数据无损

测试用 Postgres 16-alpine(单写场景,RWO accessMode 完美匹配)。

3.1 StatefulSet 三件套

# storage-demo namespace
apiVersion: v1
kind: Namespace
metadata: {name: storage-demo}
---
# Headless Service —— StatefulSet 给每个 Pod 提供稳定 DNS(pg-0.pg.storage-demo)
apiVersion: v1
kind: Service
metadata: {name: pg, namespace: storage-demo}
spec:
  clusterIP: None           # 关键:headless
  selector: {app: pg}
  ports: [{port: 5432, name: pg}]
---
apiVersion: apps/v1
kind: StatefulSet
metadata: {name: pg, namespace: storage-demo}
spec:
  serviceName: pg           # 关联 headless Service
  replicas: 1
  selector: {matchLabels: {app: pg}}
  template:
    metadata: {labels: {app: pg}}
    spec:
      tolerations: [{operator: Exists}]   # 允许调度到 control plane
      containers:
      - name: pg
        image: postgres:16-alpine
        env:
        - {name: POSTGRES_PASSWORD, value: bootcamp}
        - {name: POSTGRES_DB, value: lab}
        volumeMounts:
        - {name: pgdata, mountPath: /var/lib/postgresql/data}
  volumeClaimTemplates:    # 关键:每个 replica 自动建独立 PVC
  - metadata: {name: pgdata}
    spec:
      storageClassName: longhorn
      accessModes: [ReadWriteOnce]
      resources: {requests: {storage: 5Gi}}

三件套缺一不可:Headless Service(clusterIP: None)给每个 Pod 提供稳定 DNS <pod>.<svc>.<ns>;volumeClaimTemplates 让每个 replica 自动建独立 PVC(命名 <vct>-<sts>-<ordinal>,跟 Pod 名绑定);稳定身份(pg-0、pg-1)让 Pod 重建后名字 + PVC 都不变。

Deployment + PVC 是经典反模式:Pod 名是 hash 重建后名字变,PVC 绑定关系没法维护。要状态用 StatefulSet。

3.2 Litmus #1:删 Pod 不丢数据

kubectl apply -f sts.yaml
kubectl wait --for=condition=ready pod -n storage-demo pg-0 --timeout=120s

# 写数据
kubectl exec -n storage-demo pg-0 -- psql -U postgres -d lab -c \
  'CREATE TABLE bootcamp(id serial primary key, msg text, ts timestamp default now());'
kubectl exec -n storage-demo pg-0 -- psql -U postgres -d lab -c \
  "INSERT INTO bootcamp(msg) VALUES ('msg1'), ('msg2');"

# 删 Pod,等 StatefulSet 重建
kubectl delete pod pg-0 -n storage-demo
kubectl wait --for=condition=ready pod -n storage-demo pg-0 --timeout=120s

# 验数据
kubectl exec -n storage-demo pg-0 -- psql -U postgres -d lab -c 'SELECT * FROM bootcamp;'
#  id |  msg  |             ts
# ----+-------+----------------------------
#   1 | msg1  | 2026-05-26 05:00:36.521755
#   2 | msg2  | 2026-05-26 05:00:36.521755

id + msg + ts 完全一致 —— PVC 没动,Pod 重建只换了运行时。

3.3 Litmus #2:VolumeSnapshot 备份还原

先建 VolumeSnapshotClass(每个 CSI driver 一个),然后创快照引用 PVC:

apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshotClass
metadata: {name: longhorn-snapshot-class}
driver: driver.longhorn.io
deletionPolicy: Delete
parameters: {type: snap}    # longhorn 内置快照,不导出 S3;要 backup 改 type: bak
---
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshot
metadata: {name: pg-snap-1, namespace: storage-demo}
spec:
  volumeSnapshotClassName: longhorn-snapshot-class
  source: {persistentVolumeClaimName: pgdata-pg-0}
kubectl apply -f snap.yaml
kubectl get volumesnapshot -n storage-demo
# pg-snap-1   READYTOUSE=true   SOURCEPVC=pgdata-pg-0   RESTORESIZE=5Gi   AGE=8s

# 快照后再写 3 行(这些不该出现在恢复的卷里)
kubectl exec -n storage-demo pg-0 -- psql -U postgres -d lab -c \
  "INSERT INTO bootcamp(msg) VALUES ('AFTER1'), ('AFTER2'), ('AFTER3');"
# 现在 table 共 5 行

从快照恢复到新 PVC(不能 in-place),关键字段 spec.dataSource:

apiVersion: v1
kind: PersistentVolumeClaim
metadata: {name: pg-restored, namespace: storage-demo}
spec:
  storageClassName: longhorn
  accessModes: [ReadWriteOnce]
  resources: {requests: {storage: 5Gi}}
  dataSource:
    name: pg-snap-1
    kind: VolumeSnapshot
    apiGroup: snapshot.storage.k8s.io

挂临时 Pod 验数据:

# 起 pg-verify Pod 挂 pg-restored,查数据
kubectl exec -n storage-demo pg-verify -- psql -U postgres -d lab -c 'SELECT * FROM bootcamp;'
#  id |  msg  |             ts
#   1 | msg1  | 2026-05-26 05:00:36.521755
#   2 | msg2  | 2026-05-26 05:00:36.521755
# (2 rows)

恢复出来正好是快照时刻的 2 行,timestamp 完全一致。原 pg-0 仍是 5 行(快照不阻塞 IO,主卷继续写)。

关键点:创快照不阻塞 IO(Longhorn redirect-on-write:新写入到新 block,旧 block 是快照);从快照恢复必须新建 PVC 不能 in-place;存储层 snapshot 是 block 一致性(适合大数据 / 不可中断业务),pg_dump/mysqldump 是 logic 一致性(更小、跨版本可移植)。

3.4 Litmus #3:drain 节点不丢数据

先看 replica 分布(Longhorn 默认 anti-affinity,3 个分散在 w-2 / w-1 / cp-3):

kubectl get replicas.longhorn.io -n longhorn-system \
  -o custom-columns=NAME:.metadata.name,NODE:.spec.nodeID,STATE:.status.currentState
# pvc-...-r-2898289e   k8s-w-2    running
# pvc-...-r-3642e3b3   k8s-w-1    running
# pvc-...-r-3c97ecdf   k8s-cp-3   running

Pod pg-0 跑在 w-2,挂的是 w-2 本地 replica(localhost engine)。drain cp-3(一个 replica 所在节点,Pod 不在那):

kubectl drain k8s-cp-3 --ignore-daemonsets --delete-emptydir-data --force --timeout=60s

kubectl get pod -n storage-demo pg-0
# pg-0   1/1   Running   0   3m       <- 没重启

kubectl get replicas.longhorn.io -n longhorn-system | grep pvc-
# r-2898289e   k8s-w-2    running
# r-3642e3b3   k8s-w-1    running
# r-3c97ecdf   k8s-cp-3   stopped     <- 关键

# 数据查询 + 新写入都 OK
kubectl exec -n storage-demo pg-0 -- psql -U postgres -d lab -c \
  "SELECT count(*) FROM bootcamp; INSERT INTO bootcamp(msg) VALUES ('drain_ok');"
# count: 5  /  INSERT 0 1

kubectl uncordon k8s-cp-3
# 30s 内 cp-3 上的 replica 自动 state=running,跟主卷增量同步

Longhorn 区分「临时不可调度」和「永久故障」:cordon/drain 时 replica state=stopped 等节点回来(默认 replica-replenishment-wait-interval=600s);删 node 或长时间不通才 evict + rebuild。如果一刀切立刻 rebuild,drain 节点维护 5 分钟就会触发全集群数据迁移 IO 风暴 —— 这个 600s 等待是关键工程设计。


第 4 步:Cilium Hubble —— eBPF 流量级观测

Day 1 装了 Cilium 1.16.5,但只用了它的 CNI 功能(Pod 网络互通)。Cilium 真正的卖点:Hubble(流量观测)+ NetworkPolicy(L3/L4/L7 防火墙)。

4.1 启用 Hubble + Relay + UI

# 装 cilium / hubble CLI
curl -L https://github.com/cilium/cilium-cli/releases/download/v0.16.20/cilium-linux-amd64.tar.gz \
  | tar -xz -C /usr/local/bin
curl -L https://github.com/cilium/hubble/releases/download/v1.16.5/hubble-linux-amd64.tar.gz \
  | tar -xz -C /usr/local/bin

# 启用 hubble + UI(~40s 等三个 Deployment Running)
cilium hubble enable --ui

# UI NodePort
kubectl patch svc hubble-ui -n kube-system -p '{"spec":{"type":"NodePort"}}'

# CLI 连 relay
kubectl port-forward -n kube-system svc/hubble-relay 4245:80 &
hubble status --server localhost:4245
# Current/Max Flows: 20,475/20,475 (100.00%)
# Flows/s: 102.53
# Connected Nodes: 5/5

架构:Hubble(cilium-agent 内组件)抓节点级 eBPF 事件 → Hubble Relay 聚合 5 节点 socket 成单一 stream → Hubble UI Web 看 service map。

集群默认基线 ~102 flow/s(健康检查 + CoreDNS + kubelet heartbeat),ring buffer 满后新事件覆盖老的。

4.2 实测:跨命名空间 HTTP 流量

kubectl create ns cilium-demo
kubectl run nginx -n cilium-demo --image=nginx:1.27-alpine --port=80 --labels=app=nginx
kubectl expose pod nginx -n cilium-demo --port=80 --name=nginx-svc

# 从 pg-0 发几次 wget
for i in {1..5}; do
  kubectl exec -n storage-demo pg-0 -- wget -qO- --timeout=3 http://nginx-svc.cilium-demo
done

hubble observe --server localhost:4245 --pod storage-demo/pg-0 --last 25

关键 flow 节选:

storage-demo/pg-0 -> 169.254.20.10:53            FORWARDED (UDP)    DNS,走 Day 1 配的 node-local-dns
storage-demo/pg-0 <- 169.254.20.10:53            FORWARDED (UDP)
storage-demo/pg-0 -> cilium-demo/nginx:80        FORWARDED (TCP SYN)
storage-demo/pg-0 <- cilium-demo/nginx:80        FORWARDED (TCP SYN, ACK)
storage-demo/pg-0 -> cilium-demo/nginx:80        FORWARDED (TCP ACK, PSH)   HTTP request
storage-demo/pg-0 <- cilium-demo/nginx:80        FORWARDED (TCP ACK, PSH)   HTTP response
storage-demo/pg-0 -> cilium-demo/nginx:80        FORWARDED (TCP ACK, FIN)

跟 tcpdump 比 Hubble 三个不可替代:Pod 身份明示(storage-demo/pg-0 不是裸 IP,Cilium 自己维护 IP→endpoint→Pod 映射);完整 TCP 状态机 + 方向 + verdict(每条带 -> / <- 和 FORWARDED / DROPPED);L4 + L7 都抓(一次 wget 触发 2 次 DNS UDP + 1 次完整 TCP 连接全记下)。

4.3 Hubble vs tcpdump

维度tcpdumpCilium Hubble
数据源libpcap / AF_PACKETeBPF tracepoint at TC hooks
性能开销高(内核→用户复制全包)低(只导 metadata 不复制 payload)
身份只 IPns/pod/labels/identity
L7 解析无HTTP / gRPC / DNS / Kafka
集群级聚合每节点单独抓Relay 聚合
历史pcap 文件ring buffer(默认 4096 events/agent)

性能 + 身份 + L7 是 tcpdump 做不到的。tcpdump 仍有用 —— 抓完整 payload 做应用层 debug。

4.4 真坑:ring buffer 溢出

EVENTS LOST: HUBBLE_RING_BUFFER CPU(0) 1

102 flow/s 淹没默认 buffer。生产改 helm value:

helm upgrade cilium cilium/cilium -n kube-system --reuse-values \
  --set hubble.eventBufferCapacity=16384

Hubble 是 sampling observability,不保证 100% 抓全。要审计配合外部 export(Hubble → OTEL → Loki / Tempo)—— 跟 Prometheus scrape 一样是 eventually-complete 模型。


第 5 步:NetworkPolicy default-deny + 白名单

K8s NetworkPolicy 默认是 opt-in:没 policy 时所有 Pod 互通(trust-all)。Zero Trust 生产姿势:

  1. 第 1 条 policy 在每 namespace 设 default-deny(ingress + egress 都拒)
  2. 后续 policy 逐个白名单(来源 ns + pod label + 端口)

5.1 default-deny-all

baseline:cilium-demo 无 policy,pg-0 → nginx 全通(curl 返回 HTML)。

apply 第一条:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: {name: default-deny-all, namespace: cilium-demo}
spec:
  podSelector: {}             # 空选择器 = ns 内所有 Pod
  policyTypes: [Ingress, Egress]   # 既禁入又禁出
  # 注意:没有 ingress: / egress: 字段,所以白名单为空 = 全 deny

这一条等于「本 namespace 进入 Zero Trust 状态」。

kubectl exec -n storage-demo pg-0 -- wget -qO- --timeout=3 http://nginx-svc.cilium-demo
# wget: download timed out

Hubble 看 verdict:

storage-demo/pg-0 <> cilium-demo/nginx:80 policy-verdict:none INGRESS DENIED (TCP SYN)
storage-demo/pg-0 <> cilium-demo/nginx:80 Policy denied DROPPED (TCP SYN)

policy-verdict:none 表示没匹配项 → default deny 生效,SYN 在内核 eBPF hook 处被 drop。

5.2 真坑:一刀切 default-deny 把节点 health check 一并断掉

第一次试 default-deny 时几分钟后 nginx Pod 变 NotReady —— kubelet 的 liveness/readiness probe 也被 ingress deny 拦了。kubelet 从 node IP 发 probe 到 Pod IP,这条流量在 NetworkPolicy 眼里跟「外部 Pod 来访问」一样,全 deny 意味着 probe 也 fail。

更隐蔽的是 egress deny 把 Pod 访问 CoreDNS / kube-apiserver / metadata 一并断掉。Pod 启动时连 DNS 失败,应用层看到「莫名 hang」,根因藏在网络层。

生产 default-deny 实战姿势:egress 至少要放开 DNS(白名单 kube-system/kube-dns Pod 的 UDP/TCP 53)。probe 流量处理两种:Cilium 1.x 默认豁免 kubelet health check(但跨版本不保证),保险做法用 ipBlock 放开 node CIDR 到 Pod 的入站(K8s NP 没法直接 select kubelet,只能走 IP 段)。

5.3 加业务白名单

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: {name: allow-pg-to-nginx, namespace: cilium-demo}
spec:
  podSelector: {matchLabels: {app: nginx}}    # 作用于 nginx pod
  policyTypes: [Ingress]
  ingress:
  - from:
    - namespaceSelector:
        matchLabels: {kubernetes.io/metadata.name: storage-demo}
      podSelector:
        matchLabels: {app: pg}
    ports:
    - {protocol: TCP, port: 80}

测:

# pg-0 (app=pg)
kubectl exec -n storage-demo pg-0 -- wget -qO- http://nginx-svc.cilium-demo
# <!DOCTYPE html>...     ✅

# other-tester (app=other)
kubectl run other-tester -n storage-demo --image=alpine --labels='app=other' \
  --rm -i --restart=Never -- wget -qO- --timeout=3 http://nginx-svc.cilium-demo
# wget: download timed out  ✅ 被拦

Hubble verdict 对比:

storage-demo/pg-0         → cilium-demo/nginx:80  policy-verdict:L3-L4 INGRESS ALLOWED
storage-demo/other-tester → cilium-demo/nginx:80  policy-verdict:none  INGRESS DENIED

policy-verdict:L3-L4 INGRESS ALLOWED 表示按 IP + 端口命中白名单。

5.4 NetworkPolicy 易错点

  • namespaceSelector + podSelector 在同一 from 项里是 AND 关系,不是 OR
  • Cilium 原生支持 networking.k8s.io/v1,不需要换 CiliumNetworkPolicy(除非要 L7 / FQDN)
  • egress 比 ingress 难写 —— Pod 出去要 DNS / metadata / apiserver / 外部 API,常忘开导致莫名 hang。Hubble 看 egress DENIED 一目了然
  • kubernetes.io/metadata.name 是 K8s 1.21+ 自动给每个 ns 加的 built-in label,可直接 select ns 名

第 6 步:CiliumNetworkPolicy —— L7 HTTP method 级控制

L4 policy 只能说「允许 80 端口」,但80 端口里 GET / POST / DELETE 全平等。L7 policy 能说「只允许 HTTP GET」,拦截写操作。

6.1 Cilium L7 vs Istio sidecar

传统方案要 sidecar(Istio / Linkerd)每 Pod 多一个 Envoy 进程。Cilium 1.16 把 cilium-envoy 拆成独立 DaemonSet —— 节点级 Envoy,不是 Pod 级 sidecar。HTTP 流量被 Cilium TC hook 截流 → 节点 cilium-envoy 解析 + apply policy → 转发。零 sidecar,资源 / 运维成本远低于 Istio。代价:启用 L7 后流量必经 Envoy 一跳,约 +10-20% latency;Pod 视角的 src IP 可能变成 envoy 的。

6.2 GET 通 / POST 拦

先删 K8s NetworkPolicy(原生 NP 不支持 L7),换 CiliumNetworkPolicy:

kubectl delete networkpolicy default-deny-all allow-pg-to-nginx -n cilium-demo
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata: {name: allow-pg-get-only, namespace: cilium-demo}
spec:
  endpointSelector:
    matchLabels: {app: nginx}
  ingress:
  - fromEndpoints:
    - matchLabels:
        app: pg
        k8s:io.kubernetes.pod.namespace: storage-demo
    toPorts:
    - ports:
      - {port: '80', protocol: TCP}
      rules:
        http:
        - {method: GET}      # L7! 只允许 GET

测:

# GET
kubectl exec -n storage-demo pg-0 -- wget -qO- http://nginx-svc.cilium-demo/
# <!DOCTYPE html>...  ✅

# POST
kubectl exec -n storage-demo pg-0 -- sh -c \
  'apk add curl >/dev/null; curl -sf -X POST http://nginx-svc.cilium-demo/ -d test'
# Access denied  ✅ 被拦

Hubble L7 verdict:

hubble observe --server localhost:4245 --type l7 --last 10

storage-demo/pg-0 → cilium-demo/nginx:80  http-request FORWARDED
  (HTTP/1.1 GET http://nginx-svc.cilium-demo:80/)
storage-demo/pg-0 → cilium-demo/nginx:80  http-request DROPPED
  (HTTP/1.1 POST http://nginx-svc.cilium-demo/)

Hubble 直接打印出 HTTP method + URL —— L4 policy / tcpdump 永远做不到。

6.3 L7 policy 的生产用法

  • 内部 admin API 路径 /admin/* 限制只允许来自 ops namespace
  • 数据库 metric 接口 /metrics 只允许 Prometheus pod 访问
  • 第三方回调 endpoint 只接收特定 IP + 特定 method
  • FQDN policy:允许 Pod egress 到 *.googleapis.com,拒其他公网 IP(Cilium DNS-aware)

只在需要 L7 控制的 namespace 用 L7 policy,性能敏感场景 benchmark 对比 L4-only vs L7 的差距。


集群留存

Day 4 完成后集群多了:StorageClass longhorn(default)、VolumeSnapshotClass longhorn-snapshot-class、Hubble Relay + UI(NodePort 暴露)、cilium-demo + storage-demo namespace、pg-0 PostgreSQL(持久化数据 + 快照)、pg-snap-1 VolumeSnapshot + pg-restored PVC、CiliumNetworkPolicy allow-pg-get-only。


面试常见题

Q1:K8s 存储栈从用户写 PVC 到 Pod 挂载,链路怎么走?

5 层:PVC(用户声明)→ SC(admin 定义的产品规格)→ CSI Controller(external-provisioner sidecar 调 gRPC CreateVolume)→ PV(写入 etcd)→ CSI Node Plugin(kubelet 调 NodeStageVolume + NodeMountVolume 挂载到容器)。

CSI 替换的是 K8s 1.13 之前的 in-tree volume plugin —— 当时所有存储 driver(EBS / Ceph / NFS / GCE PD ...)都在 K8s 主仓库,每加一种存储要改主分支,不可持续。现在 CSI 把「K8s 调存储接口」和「厂商实现」解耦,4 个 external sidecar(provisioner / attacher / resizer / snapshotter)+ driver 厂商代码各司其职。

Q2:AccessMode RWO/ROX/RWX/RWOP 和 reclaimPolicy Retain/Delete 区别?

AccessMode:RWO(ReadWriteOnce,单节点读写,块存储天生这样)/ ROX(ReadOnlyMany,多节点只读)/ RWX(ReadWriteMany,多节点读写,必须 NFS / CephFS / Longhorn-RWX)/ RWOP(ReadWriteOncePod,1.22+ 单 Pod 读写防同节点多 Pod 抢)。

reclaimPolicy:Retain(PVC 删了 PV 保留,数据安全必选)/ Delete(动态分配默认值,kubectl delete pvc 一键灭数据)/ Recycle(已废弃)。生产 SC 必须显式设 reclaimPolicy: Retain + allowVolumeExpansion: true。

Q3:StatefulSet 跟 Deployment 用 PVC 区别?

StatefulSet 三件套:Headless Service + volumeClaimTemplates + 稳定身份,缺一不可。

StatefulSetDeployment
Pod 命名稳定 (pg-0, pg-1)hash 随机
PVC 关系volumeClaimTemplates 每 replica 独立 PVC共享一个 PVC(RWO 互锁)
网络身份Headless Service 每 Pod 独立 DNSService LB,IP 不稳定
重建后 PVC跟 Pod 名走,绑定不变hash 变后绑定无法维护

实战 Postgres / MySQL / Kafka / Redis cluster 都必须 StatefulSet,绝不能 Deployment + PVC。

Q4:NetworkPolicy 默认是 allow-all 还是 deny-all?default-deny 怎么写?踩过哪些坑?

K8s NetworkPolicy 默认 opt-in:没 policy 时 allow-all。一旦某 Pod 被任何 policy 选中(podSelector 命中)就进入「白名单生效模式」,不在白名单的流量被 deny。

default-deny 写法:podSelector: {} + policyTypes: [Ingress, Egress],不写 ingress / egress 字段 = 白名单为空 = 全 deny。

实战踩的坑:

  1. egress deny 把 DNS / apiserver 一并断了 —— Pod 启动连 CoreDNS 失败,应用层看到「莫名 hang」。生产 default-deny-egress 必须显式 allow 到 kube-dns Pod 的 UDP/TCP 53
  2. kubelet 的 liveness/readiness probe 也被拦 —— Pod 变 NotReady。Cilium 默认豁免但跨版本不保证,保险做法把 node CIDR 加白名单
  3. namespaceSelector + podSelector 同一 from 项是 AND 不是 OR,写错关系全 deny

Q5:Cilium Hubble 怎么拿到流量数据?跟 tcpdump 区别?为什么能做 L7 解析?

数据流:内核 socket / TC hook(eBPF 程序零拷贝抽 metadata)→ cilium-agent ring buffer → hubble-relay 集群级聚合 → CLI / UI。

跟 tcpdump 三个本质差异:

  • tcpdump 抓包(libpcap / AF_PACKET 内核到用户复制完整包),Hubble 抽 metadata(不复制 payload,开销低 1-2 量级)
  • Hubble 知道 Pod 身份 —— Cilium 维护 IP → endpoint → Pod 映射,输出 storage-demo/pg-0:59440 不是裸 IP
  • L7 解析 —— Cilium 1.16 把 envoy 拆成节点级 DaemonSet,HTTP 流量被 TC hook 截流到 envoy → 解析 method/path/status → metadata 上报 Hubble

L7 解析不破坏 TLS:明文 HTTP 能看到 method / path / header,HTTPS 加密内容 Hubble 看不见(除非用 Cilium 拦截 TLS,要装 CA cert)。

Q6:Longhorn / Ceph / OpenEBS 怎么选?

方案类型部署难度适合场景
local-path-provisioner本地盘极简dev / 学习 / 无状态 cache
NFS SubdirNFS简共享配置 / 小文件
Longhorn分布式块中中小集群 K8s 原生存储
Rook-Ceph分布式块/文件/对象高大规模生产
OpenEBS MayastorNVMe-oF高高 IOPS 数据库
EBS / GCE PD CSI云盘简公有云

5-20 节点学习 / 中小生产用 Longhorn(最简单 + UI + Snapshot/Backup 全套);50+ 节点 + 同时要块/文件/对象上 Rook-Ceph;公有云别折腾分布式直接用云盘 CSI;高 IOPS 数据库考虑 OpenEBS Mayastor(NVMe-oF 比 iSCSI 快 3-5×)。


下一步

Day 4 结束集群具备「持久化存储 + 流量观测 + L3/L4/L7 网络策略」三层基础设施。Day 5 进 Observability 主线:Prometheus + Grafana + Loki + Alertmanager 全栈监控,把 Longhorn / Cilium / Postgres 的 metric 接入 Grafana,Hubble 流量经 OTEL 导出到 Loki 持久化。

在 GitHub 上编辑此页
Prev
Day 3:CRD + Operator (kubebuilder 从 0 写)
Next
Day 5:PVC 在线扩容 + K8s 安全基线(RBAC / PSA / Secret 加密 / Kyverno)