6.7 容器存储与网络
一句话
容器只有两种存储身份——临时的根文件系统(overlay,删容器即丢)与外挂的数据卷(与容器生命周期解耦);也只有两张网络——Pod 网络(CNI,一个 Pod 一个 IP)与 Service 网络(虚拟 IP + 转发规则);把这两组搞混,就会得到 “数据丢在容器层” 与 “服务发现靠记 IP” 两类事故。
存储:先问 “这份数据要活多久”
| 挂载类型 | 生命周期 | 数据在哪 | 适合 |
|---|---|---|---|
| 容器可写层 | 容器 | 节点磁盘(overlay upperdir) | 临时文件;不适合任何要保留的数据 |
| emptyDir | Pod | 节点磁盘(或 medium: Memory 用 tmpfs) | 同 Pod 内容器共享、缓存、临时落盘 |
| hostPath | 节点 | 节点目录 | 读取节点信息(如 /var/log);生产数据禁用 |
| PVC(CSI) | 独立于 Pod | 云盘 / NFS / 分布式存储 | 数据库、消息队列、上传文件 |
| tmpfs / Memory | Pod | 内存 | 敏感临时数据、极低延迟缓存 |
判断句式:这份数据要不要活过容器?要 → PVC;只在 Pod 内共享 → emptyDir;只是临时文件 → 可写层(但要意识到它会占节点磁盘)。
可写层的三个代价
- copy-up:写一个文件要先把整份从只读层拷上来(机制见 6.2 镜像与分层、写时复制)。
- ephemeral storage 配额:可写层 + 日志 + emptyDir 共用节点临时存储额度,超了会被驱逐(
Evicted)。 - 随容器消失:
docker rm/ 重建 Pod 即丢,没有任何恢复路径。
日志是最常见的 “无意识写可写层”:应用把日志写文件,文件在容器层,节点磁盘被写满 —— 正确做法是写 stdout(由 kubelet 轮转)或挂 emptyDir 并设 sizeLimit。
访问模式:最容易背错的一张表
| 模式 | 含义 | 常见误用 |
|---|---|---|
ReadWriteOnce(RWO) | 单节点读写(不是单 Pod) | 以为 “一个 Pod 独占”,实际同节点多 Pod 可同时挂 |
ReadOnlyMany(ROX) | 多节点只读 | 用在要求强一致的读多写少场景,忽略缓存 |
ReadWriteMany(RWX) | 多节点读写 | 需要文件系统支持(NFS/CephFS);块设备给不了 |
ReadWriteOncePod(RWOP) | 单 Pod 独占 | 新集群才有,老集群不可用 |
“RWO 就是独占” 是最贵的误解:两个同节点 Pod 挂同一个 RWO 卷时,文件锁与写入冲突仍然会发生。
CSI 的两个时间成本
- attach/detach:Pod 漂移到新节点时,云盘要先从旧节点摘、在新节点挂,通常 30 秒–2 分钟;这会让 “节点故障后 Pod 恢复” 变慢,是 RTO 的一部分。
- 扩容:
allowVolumeExpansion打开后可以原地扩,但缩容几乎都不支持——规划时按 “只增不减” 设计。
性能与选型(块 / 文件 / 对象、IOPS 随容量)见 10.1 块存储 / 文件存储 / 对象存储与 10.3 云盘性能模型(IOPS / 吞吐 / 时延)(待补);备份与快照见 10.4 快照与备份。
网络:两张网,别混
| 网络 | 谁提供 | 地址 | 解决什么 |
|---|---|---|---|
| Pod 网络 | CNI 插件 | 每个 Pod 一个 IP(集群内可路由) | Pod 之间怎么通 |
| Service 网络 | kube-proxy / eBPF / 云 LB | 虚拟 IP(ClusterIP),不落在任何网卡上 | 服务怎么被稳定访问 |
三层访问路径:外部 → Service(LB/NodePort)→ Pod IP → 容器端口。排错时先说清 “卡在哪一层”,否则会在 “Service 不通” 与 “Pod 不通” 之间反复假设。
Pod 网络模型:一个 Pod 一个 IP
- 同节点:veth pair + 网桥(或 eBPF 直接转发)。
- 跨节点:overlay(VXLAN / IPIP 封装)或 underlay(BGP 宣告 Pod 网段,如 Calico 的 BGP 模式)。
- MTU 坑:VXLAN 封装占 50 字节,1500 的物理 MTU 下 Pod MTU 通常要设 1450;不设会出现 “小包通、大包丢” 的诡异现象(握手成功、传文件卡死)。注意别拿本机 Docker Desktop 当参照:实测它默认网络的
com.docker.network.driver.mtu是 65535(Linux VM 内部环路),生产网卡不是这个数,按物理 MTU 减封装开销算。
| CNI 插件 | 模式 | 特点 | 适合 |
|---|---|---|---|
| Flannel | VXLAN / host-gw | 简单、够用 | 中小集群、快速起步 |
| Calico | BGP / IPIP / eBPF | 网络策略强、可 underlay | 需要 NetworkPolicy 的生产集群 |
| Cilium | eBPF | 高性能、可观测、L7 策略 | 大规模、想摆脱 iptables |
| 云厂商 CNI | VPC 原生 | Pod 直接用 VPC IP,与 LB/安全组天然集成 | 公有云托管 K8s |
Service:四种暴露方式
| 类型 | 用途 | 代价 |
|---|---|---|
| ClusterIP | 集群内访问 | 无(默认) |
| NodePort | 临时对外 / 调试 | 端口范围有限、暴露节点、无健康检查 |
| LoadBalancer | 生产对外 | 每个 Service 一个 LB,成本随服务数增长 |
Headless(clusterIP: None) | 有状态服务直连 Pod | 客户端自己做发现与负载均衡 |
生产入口通常是 “一个 Ingress / 网关 + 多个 ClusterIP”,而不是 “每个服务一个 LoadBalancer”。四层与七层的分工见 8.9 四层与七层负载均衡,南北向与东西向的区分见 47.5 南北向与东西向流量。
NetworkPolicy:默认全通
Kubernetes 的默认行为是 Pod 之间全通。NetworkPolicy 是 “白名单”,且需要 CNI 支持(Flannel 默认不支持,Calico/Cilium 支持):
| 写法 | 效果 | 注意 |
|---|---|---|
podSelector: {} + policyTypes: [Ingress] | 该命名空间所有 Pod 拒绝入站 | 必须先放行 DNS 与探针,否则 “全拒” 会连自身健康检查一起拒掉 |
只写 Ingress | 出站仍然全通 | 出站管控要单独写 Egress |
| 按 namespace 放行 | 常见的多环境隔离 | 标签拼写错误会静默放行/拒绝 |
命令:把“存储与网络”查清楚
# 存储:这个容器到底挂了什么、写在哪一层
docker inspect -f '{{json .Mounts}}' <container> | python3 -m json.tool
docker system df -v # 镜像/容器/卷各占多少(卷最容易漏)
# 网络:容器在哪个网络、拿到什么 IP、能不能通
docker network ls
docker network inspect bridge -f '{{json .IPAM.Config}}'
docker exec <container> sh -c 'ip -4 addr show eth0; cat /etc/resolv.conf'
# K8s:临时卷与持久卷的区别就写在 spec 里
volumes:
- name: cache
emptyDir: {sizeLimit: 1Gi} # 随 Pod 生命周期,删 Pod 即丢
- name: data
persistentVolumeClaim: {claimName: pg-data} # 独立生命周期,RWO 通常只能单节点挂
反模式
| 反模式 | 后果 |
|---|---|
| 数据库数据目录不挂卷 | 重建即丢数据;copy-up 还会放大 IO |
| 用 hostPath 存业务数据 | Pod 漂移后数据留在旧节点,新节点读不到 |
| 以为 RWO 是 “单 Pod 独占” | 同节点多 Pod 并发写同一卷,文件损坏 |
| 日志写容器内文件 | 节点磁盘写满,Pod 被驱逐 |
| 用 NodePort 当生产入口 | 无健康检查、端口冲突、暴露节点 IP |
| 客户端硬编码 Pod IP | Pod 重建后 IP 变化,调用全挂 |
| 不设 MTU | 小包通、大包不通,排查耗时以天计 |
| 以为 NetworkPolicy 默认拒绝 | 集群实际全通,隔离形同虚设 |
| Service 数量爆炸(每服务一个 LB) | 云账单暴涨,入口配置难以治理 |
三条铁律
🔑 数据不落容器层:要活过容器就挂 PVC;只在 Pod 内共享用 emptyDir。
🔑 RWO 是 “单节点” 不是 “单 Pod”;要独占用 RWOP,要并发写用 RWX。
🔑 默认全通:Pod 网络与 NetworkPolicy 都默认放行;隔离必须显式写,且要放行 DNS 与探针。
自检
Q1 一个 StatefulSet 的 Pod 从节点 A 漂到节点 B,恢复用了 90 秒,其中 60 秒花在存储上。怎么解释与优化?
▸ 答案:60 秒是 CSI detach/attach:云盘必须先脱离旧节点、再挂到新节点,典型 30 秒–2 分钟。优化方向:① 用支持 “多挂载/快速重挂” 的存储类型;② 让调度尽量回到原节点(nodeAffinity + 本地卷);③ 把 RTO 目标按 “含存储挂载时间” 重算,而不是只算进程启动。这是 RTO 里最容易被漏掉的一段。
Q2 为什么两个 Pod 同时挂同一个 RWO 卷会出问题,尽管 “它们都在同一个节点”?
▸ 答案:因为 RWO 保证的是单节点挂载,不是单写入者。文件系统本身没有分布式锁语义(块设备上尤其明显),两个进程并发写同一文件会产生覆盖与元数据损坏。需要独占就用 ReadWriteOncePod,需要并发写就用支持 ReadWriteMany 的文件系统,并让应用自己处理锁与冲突。
Q3 集群里 “小请求正常、传大文件卡死”,最可能是什么问题?
▸ 答案:MTU 不匹配。overlay 封装(VXLAN 50 字节)后 Pod MTU 若仍为 1500,大包会被分片或丢弃,表现为 TCP 握手成功(小包)、传数据时卡住或超时。验证:ping -M do -s 1472 <pod-ip>(1472 + 28 = 1500)逐步降包长试;修法是把 CNI 的 MTU 设为物理 MTU 减去封装开销(常见 1450)。
Q4 团队说 “我们做了网络隔离,因为配置了 NetworkPolicy”。这句话可能哪里不对?
▸ 答案:三处可能:① CNI 不支持(如默认 Flannel),策略被忽略;② 只写了 Ingress 没写 Egress,出站仍全通;③ 默认行为是全通,策略写错选择器就静默不生效。验证方式是实际从 Pod 里 curl/nc 另一个命名空间的 Pod,而不是看 YAML 是否存在。
Q5 生产入口该用 NodePort、LoadBalancer 还是 Ingress?
▸ 答案:默认选 Ingress(或网关)+ ClusterIP:一个入口复用,TLS 与路由集中管理,成本最低。LoadBalancer 适合 “少量、需要独立公网 IP 的服务” 或云托管 LB 的集成场景;NodePort 只适合调试与临时暴露——它没有健康检查、端口范围有限、还暴露节点 IP。选择依据是 “入口要治理多少个服务、TLS 在哪终结、成本按什么计”。
跨篇相关:可写层与 copy-up 的机制见 6.2 镜像与分层、写时复制;无状态化是挂卷与横向扩展的前提见 37.2 无状态化;块 / 文件 / 对象存储与云盘性能见 10.1 块存储 / 文件存储 / 对象存储、10.3 云盘性能模型 (IOPS / 吞吐 / 时延)(待补);快照与备份见 10.4 快照与备份;Pod 网络与 Service 的编排层细节见 7.7 存储卷与持久化、7.8 网络模型 (CNI / NetworkPolicy)(待补);四层与七层的分工见 8.9 四层与七层负载均衡;南北向与东西向流量见 47.5 南北向与东西向流量;TCP 重传与 MTU 的底层机制见 8.1 TCP/IP 与三次握手。