ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Pod多容器资源共享:从Linux Namespace到Sidecar实战

Pod多容器资源共享:从Linux Namespace到Sidecar实战

1. 先搞清楚Pod里究竟共享了哪些东西

做K8S这么久,有个问题几乎每次培训都会被问到:Pod里面多容器到底共享了什么资源?很多朋友一说“共享”,第一反应就是文件目录,觉得只要把同一个Volume挂进两个容器,就算资源共享了。实际上这个理解只对了一半,而且是最浅的一层。Pod里的资源共享远比文件挂载要深,它涉及到Linux Namespace这一层,非常底层。

先给一个比较反直觉的结论:同一个Pod里所有容器,它们的网络栈是同一份。也就是说,nginx容器里监听80端口,旁边那个busybox容器访问localhost:80,能直接连通。这在普通Docker环境里是不行的,因为Docker容器各自有独立的网络命名空间,只能通过宿主机IP或者端口映射去找对方。而在K8S里,调度器创建Pod时,会先拉起一个基础设施容器(通常叫pause容器)来持有整套命名空间,后续的业务容器加入它的命名空间。所以整个Pod对外呈现的就是一台“逻辑机器”,127.0.0.1指向同一个网络栈,任何容器看到的IP和端口表都是同一份。

理解了这个机制,很多现象就能解释清楚了。比如两个容器想同时监听8080端口,在同一个Pod里必然会冲突,因为网络端口是共享的,不存在“容器A的8080和容器B的8080互不干扰”这种说法。再比如一个组件的日志或者指标暴露,如果你用容器内IP去访问另一个容器,多半会踩坑,因为Pod内根本不存在多个IP,只有共享的一个。这些特性不是K8S凭空设计的,而是源于Linux容器底层实现,理解起来要往内核方向靠。

1.1 网络命名空间:容器不是一台独立机器

要真正理解Pod资源共享,先得接受一个事实:Pod内的容器不是传统意义上“各自独立的一台机器”,更像是一台机器里多个进程组,只不过用chroot和cgroup做了隔离。K8S默认情况下,同一个Pod里所有容器共享Network Namespace,带来的结果包括:

  • 所有容器共享同一个IP地址和MAC地址,Pod被调度到哪个节点,就在节点上存在这样一个虚拟网卡。
  • 端口空间完全互通,任意容器监听某个端口,其他容器通过127.0.0.1就能访问。
  • 网络策略对流量的限制作用在Pod级别,而不是Container级别,因为从CNI插件视角来看,一个Pod就是一个终端。
  • iptables规则、路由表、socket连接,在整个Pod内是全局的。

有个典型误区是:有人想在Pod里部署一个agent容器,专门抓取其他容器的网络流量。如果你不理解共享网络栈,会觉得需要把网卡“透传”或者“桥接”进去。实际上因为容器和Pod共享同一个网络栈,agent只要打开原始socket或者使用AF_PACKET,就能直接看到所有流量,根本不需要额外配置。这个共享特性是双刃剑:调试方便了,但安全边界也变模糊了,某个容器一旦被攻破,Pod内的其他容器等同于直接暴露在同一张网上。

1.2 IPC与UTS:被默认绑定的两块资源

除了网络,还有两个容易被人忽略的共享维度:IPC和UTS。UTS Namespace管的是主机名和域名。因为Pod创建时就固定了hostname,Pod内所有容器看到的hostname都是同一个值,也就是Pod元数据里的名字。这个特性其实很有用,比如有些应用会用hostname来判断自己是哪个实例,在Pod内多容器部署时,它们拿到的hostname一致,不会出现每个容器觉得自己是不同机器的情况。

IPC Namespace则管的是进程间通信资源,比如System V信号量、消息队列、POSIX共享内存。K8S默认情况下,Pod内的容器是否共享IPC取决于运行时的具体实现。一般来说,如果你不显式开启,各容器还是有着独立的IPC隔离。但当你通过某些配置打开IPC共享后,两个容器就可以通过共享内存做非常高效的数据交换,性能远好于走socket或者文件。实际生产里用共享内存的场景不多,因为大多数应用不会在同一Pod内部署两个交互密集的进程,但遇到高吞吐数据处理类工作负载时,这个通道确实能派上用场。

2. 隔离边界与共享边界:一张表看清区别

前面说了共享的部分,但Pod内的资源不是全都共享,这个边界必须划清楚,否则容易在设计多容器Pod时做错决策。我经常看到有人以为同一个Pod里的容器连文件系统都是共享的,结果发现容器A里创建的文件,在容器B里根本看不见。原因很简单:每个容器有自己的Mount Namespace和Rootfs,它们的“根目录”并不相同,哪怕镜像来自同一个仓库。

我这里整理了一张表,把Pod内资源共享情况一次性说清:

资源维度是否共享说明
Network共享所有容器共用IP、端口、路由
UTS共享hostname一致
IPC按配置需要显式共享
PID按配置开启shareProcessNamespace后共享进程表
Mount不共享各自rootfs独立
User不共享各自用户命名空间(除非开启相关特性)
Cgroup不共享每个容器单独的资源限额
Volume挂载显式共享通过Pod级Volume显式挂载实现

这张表多看几遍,你在设计Pod内多容器架构的时候会少走很多弯路。Mount隔离意味着你不能把容器A的/tmp当作“公共空间”直接给容器B用,必须显式定义一个Volume,再分别挂载到两边。Cgroup隔离意味着即使在一个Pod内,容器A打满内存,容器B并不会自动帮它扛,资源超限时容器A照样会被杀掉,不会因为“同Pod兄弟”关系而幸免。

2.1 Namespace维度逐个过一遍

把Linux的Namespace机制拿过来逐一看,你会发现K8S的设计非常克制,默认只共享Network和UTS,其他都要显式开。这样做的好处是默认安全,坏处是很多新人对“共享”的预期会落空。你在网上搜Pod资源共享,看到一堆“如何在一个Pod里给两个容器共享目录”的问题,根因就在于大家把“Pod内共享”理解成了“容器间零成本共享一切”。

如果真的希望容器A的进程能被容器B看到,需要打开shareProcessNamespace这个开关。打开之后,两个容器里的ps能看到彼此进程;不打开,大家互不“认识”。我见过一个真实案例,某团队把日志采集器以Sidecar方式放进业务Pod,日志采集器需要跟踪另一个容器写日志的文件句柄,但因为没有开启PID共享,在排查时发现采集器里看不到业务进程,导致一些基于进程发现的采集配置直接失效。后来他们要么改成文件系统轮询,要么就开启PID共享,才解决了问题。

2.2 存储共享:Volume才是真正的通道

文件共享是Pod内多容器协作最常见的方式,但很多细节比想象中复杂。K8S里Volume的生命周期绑定的是Pod,不是单个容器。所以你在Pod的spec.volumes里定义了一个emptyDir,两个容器各自声明volumeMount把同一个Volume挂到自己目录下,它们就能通过这个目录交换文件。

这里有一个关键点:Consistent到文件系统层,不代表“最终一致”。两个容器同时写同一个文件,会发生竞争。你以为“一个文件大家各写各的互不影响”,实际上还是会遇到文本覆盖、日志交错、文件锁丢失等问题。我建议的做法是:要么按目录划分地盘,比如容器A只写/data/input/,容器B只读/data/input/并写/data/output/,要么对单文件做原子写,先写临时文件再rename。否则一旦两端同时启动、同时写同一路径,查问题会相当痛苦。

存储共享的另一个隐藏门槛是挂载参数。同一个Volume可以被两个容器以不同路径挂载,但宿主机上的实际目录是同一个,所以如果容器A以只读挂载、容器B以读写挂载,它们的权限关系也需要在Volume配置层面想清楚。有个很常见的Sidecar场景:主容器写日志到/var/log/app,日志采集Sidecar读同一个目录,你应该把主容器挂载读写,采集容器挂载只读,这能有效避免误写导致的日志损坏。

3. 实操完整示例:构建一个真正共享的Pod

看到这里,你大概率已经想动手验证了。我用一个完整的YAML例子,带你把上面说的共享点逐个落地。这个例子模拟的场景是:一个Web服务容器不断生成访问日志,一个日志处理Sidecar实时消费这些日志,并且两个容器能互相看到对方的进程。

apiVersion: v1 kind: Pod metadata: name: share-demo spec: shareProcessNamespace: true containers: - name: web image: nginx:1.27 ports: - containerPort: 80 volumeMounts: - name: log-volume mountPath: /var/log/nginx - name:>
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进