Pod 多容器、InitContainer 与共享 Volume

Pod 才是 Kubernetes 的最小调度单位

Kubernetes 不直接把单个容器调度到 Node,而是调度 Pod。一个 Pod 可以包含一个或多个容器,这些容器会被当作一个整体:

  • 被调度到同一台 Node;
  • 共享同一个网络命名空间;
  • 拥有相同的 Pod IP;
  • 可以通过 Volume 共享文件;
  • 一起创建,也一起消失。

本项目每个号池只有一个 runtime Pod,但里面同时运行:

1
2
3
4
Pod runtime-0
  ├─ initContainer: initialize-config
  ├─ container: cpa
  └─ container: cpa-manager

这不是三个 Pod,而是一个 Pod 中的一个初始化容器和两个长期运行容器。

为什么 CPA 与 cpa-manager 放在同一个 Pod

cpa-manager 需要紧密访问 CPA,并与它共享持久数据。放在同一个 Pod 后,二者可以通过 loopback 通信:

1
cpa-manager → http://127.0.0.1:8317 → CPA

这里不经过 Service、集群 DNS 或 CNI 网络。对 cpa-manager 来说,CPA 就像运行在同一台机器上的另一个本地进程。

共享 Pod 的好处:

  • 本地通信简单,延迟低;
  • 两个容器一起调度,不会出现 Manager 在 A 节点、CPA 在 B 节点;
  • 可以自然共享同一个 RWO PVC;
  • 生命周期耦合关系与业务现实一致。

代价也很明确:

  • 两者不能独立扩缩容;
  • Pod 重建时二者一起重启;
  • 任一容器一直不 Ready,整个 Pod 就不 Ready;
  • 两个容器的资源总和决定这个 Pod 的调度需求。

因此“多容器 Pod”适合强耦合进程,不应只为了少写一个 Deployment 就随便合并。

共享网络不等于共享端口

同一个 Pod 的容器共享 IP 和端口空间。CPA 监听 8317,cpa-manager 监听 18317

1
2
Pod IP:8317   → CPA
Pod IP:18317  → cpa-manager

因为端口空间共享,两个容器不能同时监听同一个 IP 和端口。若都尝试监听 0.0.0.0:8317,后启动的进程会因为端口被占用而失败。

容器在 YAML 中声明 containerPort,主要用于表达和命名端口,并不会像 Docker 的 -p 那样自动把它暴露到集群外。真正的稳定入口由 Service 提供。

InitContainer:开店前先布置柜台

InitContainer 在普通容器启动前依次执行。只有所有 InitContainer 成功退出,主容器才会启动。

本项目的 initialize-config 负责:

  1. 在共享数据卷中创建目录;
  2. 从 Secret 挂载文件读取凭据;
  3. 根据 Operator Configuration 注入的 Mihomo URL 生成 CPA 配置;
  4. 只在配置文件不存在时写入,避免每次重启覆盖运行数据。

关键逻辑可以概括为:

1
2
3
if [ ! -f /data/cpa/config.yaml ]; then
  # 读取凭据并创建配置
fi

这叫幂等初始化:执行一次和重复执行多次,最终结果一致。Pod 因节点维护被重建时,PVC 上已有配置不会被覆盖。

InitContainer 失败时,主容器不会启动。排查时可查看:

1
2
3
kubectl -n subpool-demo-pool describe pod runtime-0
kubectl -n subpool-demo-pool logs runtime-0 \
  -c initialize-config

不要只看 cpa 容器日志;如果初始化阶段就失败,它甚至还没开始运行。

Volume:Pod 内的文件连接器

容器文件系统默认是临时的。Volume 让 Pod 把外部数据源或共享存储挂进容器。

本项目 runtime Pod 使用三种 Volume:

Volume来源用途是否持久
dataStatefulSet 的 PVCCPA 与 Manager 数据
credentialsSecret三个凭据文件随 Secret 存在
bootstrapConfigMap初始化脚本随 ConfigMap 存在

Volume 定义“数据从哪里来”,volumeMounts 定义“在某个容器中挂到哪里”。同一个 Volume 可以在不同容器里挂到不同路径。

一个 PVC,如何避免两个容器互相踩文件

data PVC 在 InitContainer 中挂载到 /data。主容器则使用 subPath 把其中不同部分挂到各自路径:

1
2
3
4
5
6
7
8
PVC 根目录
  ├─ cpa/
  │   ├─ config.yaml
  │   ├─ auths/
  │   └─ logs/
  └─ cpa-manager/
      ├─ usage.sqlite
      └─ data.key

CPA 获得:

1
2
3
cpa/config.yaml → /CLIProxyAPI/config.yaml
cpa/auths       → /CLIProxyAPI/auths
cpa/logs        → /CLIProxyAPI/logs

cpa-manager 获得:

1
cpa-manager → /data

这样既共享同一个 PVC,又保持目录边界清楚。

需要注意:subPath 不是权限隔离机制。容器是否能看到其他目录取决于挂载方式,但底层仍是同一个卷;真正的安全还依赖进程身份、文件权限和应用设计。

Secret 与 ConfigMap 为什么挂成文件

本项目把凭据投影为 /run/secrets/... 下的文件,只选择约定的 key:

1
2
3
cpa_client_key
cpa_management_key
cpamp_admin_key

相较于直接把敏感值写进命令参数,文件方式能减少它出现在进程参数、YAML 和日志中的机会。初始化脚本读取后把必要内容写入权限受限的持久配置。

ConfigMap 中的初始化脚本则以只读、可执行模式挂到 /bootstrap/initialize.sh。普通配置和敏感凭据被明确分开:脚本可以进 Git,Secret value 不可以。

Pod SecurityContext 与 Container SecurityContext

Pod 级配置决定共同的进程和文件身份:

1
2
3
4
5
runAsNonRoot: true
runAsUser: 65532
runAsGroup: 65532
fsGroup: 65532
seccompProfile: RuntimeDefault

容器级配置进一步限制:

1
2
3
allowPrivilegeEscalation: false
capabilities.drop: [ALL]
runAsNonRoot: true

fsGroup 很重要:PVC 挂载后,kubelet 会尽量让卷中的文件对这个组可访问,否则非 root 容器可能无法写入数据卷。

安全配置不是“字段越多越安全”。它必须与镜像实际行为匹配。如果应用硬编码要求 root 或尝试写只读目录,Pod 会启动失败。正确做法是修正镜像和写路径,而不是第一时间取消安全约束。

terminationGracePeriodSeconds 的意义

项目为 runtime Pod 设置 30 秒优雅终止时间。Pod 被删除时,大致经历:

  1. kubelet 向容器主进程发送 SIGTERM
  2. 应用有机会停止接收请求、刷新文件、关闭数据库;
  3. 最多等待 30 秒;
  4. 仍未退出时才会被强制终止。

对于使用 SQLite 或持久配置的应用,优雅退出比无状态 HTTP 服务更重要。应用也必须正确处理 SIGTERM,否则 YAML 中的等待时间只是空等。

镜像拉取策略

本项目按镜像字符串选择策略:

  • :latest 结尾:Always
  • 其他标签或 digest:IfNotPresent

开发时 latest 方便快速替换,生产更推荐不可变 digest:

1
registry.example/cpa@sha256:...

同一个 tag 的内容可能变化,导致重建后的 Pod 与之前运行的并非同一版本。digest 把“我想运行哪个镜像”变成可审计的精确声明。

如何观察多容器 Pod

1
2
3
4
5
6
kubectl -n subpool-demo-pool get pod runtime-0
kubectl -n subpool-demo-pool describe pod runtime-0
kubectl -n subpool-demo-pool logs runtime-0 -c cpa --tail=100
kubectl -n subpool-demo-pool logs runtime-0 -c cpa-manager --tail=100
kubectl -n subpool-demo-pool logs runtime-0 \
  -c initialize-config --previous

kubectl logs 面对多容器 Pod 时应明确 -c--previous 用于查看同一个容器上一次崩溃实例的日志,对 CrashLoopBackOff 很有用。

还可以查看每个容器的状态:

1
2
kubectl -n subpool-demo-pool get pod runtime-0 \
  -o jsonpath='{range .status.containerStatuses[*]}{.name}{"\t"}{.ready}{"\t"}{.restartCount}{"\n"}{end}'

一个容易误解的地方:两个容器不等于高可用

CPA 与 cpa-manager 在同一个 Pod 中,是为了协作和共享状态,不是冗余副本。这个 StatefulSet 的 replicas 是 1,PVC 又是 RWO,所以数据面仍是单实例。

Operator 自己运行两个副本并通过 leader election 提供控制面接管能力,但那不会把号池 runtime 自动变成双活。控制面高可用和数据面高可用必须分开讨论。

理解 Pod 的关键不是记住字段,而是看清“哪些进程必须同生共死、共享什么、通过什么通信”。本项目把 CPA 和 cpa-manager 放进同一个 Pod,正是对这种强耦合关系的明确表达。

使用 Hugo 构建
主题 StackJimmy 设计