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-1 | 10.0.24.31 | 控制面 | 30G | 100G(已挂 /var/lib/containerd) |
| k8s-cp-2 | 10.0.24.29 | 控制面 | 30G | 100G |
| k8s-cp-3 | 10.0.24.32 | 控制面 | 30G | 100G |
| k8s-w-1 | 10.0.24.28 | worker | 30G | 100G |
| k8s-w-2 | 10.0.24.30 | worker | 30G | 100G |
存储现状全空:
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.service | Pod attach volume 直接 fail |
nfs-common | RWX 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 三连:
- 数据持久化:删 Pod 重建,数据还在
- Snapshot/Restore:创快照 → 改数据 → 从快照恢复 → 数据是快照时刻的
- 节点 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
| 维度 | tcpdump | Cilium Hubble |
|---|---|---|
| 数据源 | libpcap / AF_PACKET | eBPF tracepoint at TC hooks |
| 性能开销 | 高(内核→用户复制全包) | 低(只导 metadata 不复制 payload) |
| 身份 | 只 IP | ns/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 条 policy 在每 namespace 设 default-deny(ingress + egress 都拒)
- 后续 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 + 稳定身份,缺一不可。
| StatefulSet | Deployment | |
|---|---|---|
| Pod 命名 | 稳定 (pg-0, pg-1) | hash 随机 |
| PVC 关系 | volumeClaimTemplates 每 replica 独立 PVC | 共享一个 PVC(RWO 互锁) |
| 网络身份 | Headless Service 每 Pod 独立 DNS | Service 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。
实战踩的坑:
- egress deny 把 DNS / apiserver 一并断了 —— Pod 启动连 CoreDNS 失败,应用层看到「莫名 hang」。生产 default-deny-egress 必须显式 allow 到
kube-dnsPod 的 UDP/TCP 53 - kubelet 的 liveness/readiness probe 也被拦 —— Pod 变 NotReady。Cilium 默认豁免但跨版本不保证,保险做法把 node CIDR 加白名单
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 Subdir | NFS | 简 | 共享配置 / 小文件 |
| Longhorn | 分布式块 | 中 | 中小集群 K8s 原生存储 |
| Rook-Ceph | 分布式块/文件/对象 | 高 | 大规模生产 |
| OpenEBS Mayastor | NVMe-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 持久化。