尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Linux Namespace详解:容器隔离的原理、操作与排障实践

Linux Namespace详解:容器隔离的原理、操作与排障实践 1. 容器不是轻量虚拟机理解Namespace之前必须修正的认知我见过太多刚接触容器的朋友上来就把容器理解成轻量虚拟机然后遇到各种诡异问题容器里能ping通外网但curl不通某个端口、容器里ps看到的进程号和宿主机完全对不上、一个容器改了/etc/hosts结果另一个容器也跟着变了……这些问题的根源几乎都能追溯到同一个概念Namespace。我甚至可以说没搞懂Namespace你对容器的理解就始终隔着一层窗户纸。Namespace的英文直译是命名空间但在容器语境下它的作用远不止给资源起名字那么简单。它是Linux内核提供的一种资源隔离机制核心作用是把一组进程圈进一个独立的视图里让这组进程只能看到被分配给它们的资源看不到宿主机的其他部分。进程、网络、文件系统挂载点、主机名、用户ID……这些在普通场景下全局共享的资源在Namespace的隔离下每个容器都像拥有了一个独立的操作系统。那为什么有了虚拟机我们还需要Namespace这种隔离方案关键在于虚拟机的隔离是硬件级的。每个虚拟机里跑着一个完整的Guest OS有独立的内核通过Hypervisor做底层虚拟化隔离性极强但代价也极其高昂——每个虚拟机都要消耗一部分CPU和内存来维持那个假操作系统的运行启动时间动不动就是几十秒。而容器的思路完全相反它不模拟硬件不装完整内核而是共享宿主机的内核只通过内核提供的隔离机制让容器里的进程以为自己运行在一个独立的系统里。这个隔离机制的核心就是Namespace再加上cgroup控制资源配额、rootfs提供文件系统视图三者共同构成了容器运行时的底层三角。我用一个日常类比来帮你建立直观印象虚拟机像是给每个租客盖一栋独立的房子有独立的墙、独立的水电表互不干扰但成本高容器则像一个隔断式公寓所有人住在一栋楼里共用同一套水电主管道共享内核但每间房的门锁、窗户、视野都是独立的Namespace同时物业还会给每家装独立的电表水表限制每家每月的用量上限cgroup。这就是Namespace存在的根本意义在共享内核的前提下给进程制造独立的错觉。带着这个认知再去看容器很多行为就说得通了。比如你在容器里执行ps aux看到PID 1是一个很特殊的进程这跟你在宿主机上看到的一堆系统进程完全不同——这是因为容器里的进程被隔离在了一个独立的PID Namespace中它看不到宿主机上的其他进程。再比如你在容器里执行ip addr看到的是 eth0、lo 这样的网络接口完全没有宿主机上那堆物理网卡的踪影——这是Network Namespace在起作用。理解到这一层你再看容器就不会再把它当成小虚拟机而会把它看作一群被Namespace圈起来、被cgroup限制了资源的普通进程。这个认知的转变是后面所有实操和排障的地基。2. 八类Namespace逐个拆解它们各自隔离了什么Linux内核目前提供了八类NamespaceTime Namespace是后来加入的Cgroup Namespace也是较新版本才有每一类负责隔离一个特定的资源维度。刚刚接触时容易觉得这是八个独立的机制很难记全。我的建议是不要死记硬背而是跟着我过一遍每个Namespace隔离了什么以及容器里对应什么现象从现象反推原理记起来就顺了。2.1 Mount Namespace文件系统的平行世界Mount Namespace是整个Namespace家族里最基础也最关键的一类。它隔离的是文件系统的挂载点视图。什么意思呢在一个Mount Namespace里执行的mount和umount操作只影响当前这个Namespace里的挂载点列表其他Namespace完全感知不到。这就是为什么在容器里你可以随意执行mount -t tmpfs none /mnt来挂一个临时文件系统而宿主机的挂载表纹丝不动。反过来宿主机上的挂载操作如果发生在容器创建之后容器里的挂载视图也不会自动同步看到。这里有个非常隐蔽的坑Mount Namespace在创建的时候会复制父Namespace的挂载点列表作为初始状态这个是CLONE_NEWNS创建时默认的行为。也就是说容器创建时能看到哪些挂载点取决于创建那一瞬间父Namespace的挂载状态。如果你在创建容器之后才在宿主机上挂载了一个新磁盘容器里是看不到这个新挂载的除非你显式做挂载传播mount propagation设置。后面我会专门讲这个坑。在容器场景里Mount Namespace还承担着一个重要任务容器内的rootfs切换。容器启动时runC会通过pivot_root或chroot把容器的根目录切换到镜像的rootfs上这个操作本质上也是依赖于Mount Namespace的隔离能力——正因为每个容器有独立的挂载视图才可以各自拥有完全不同的根文件系统一个容器里是CentOS另一个是Ubuntu它们互不干扰。2.2 PID Namespace让容器里的进程以为自己拥有完整的进程表PID Namespace隔离的是进程号。在一个新的PID Namespace里第一个创建的进程PID会被分配为1这个进程拥有和宿主机init进程PID 1同等的特殊地位——它在自己所在的Namespace里是所有孤儿进程的收养者。这带来一个直接后果容器里的PID 1进程如果提前退出整个容器的进程树就会因为失去祖先而陷入混乱孤儿进程没人收养最终可能导致容器无法正常终止或出现僵尸进程堆积。这也是Kubernetes里容器的livenessProbe探针经常要去检查PID 1进程是否存活的原因。刚才提到的那种容器里ps看到的进程号和宿主机对不上的现象就是因为进程在宿主机上有一个全局PID同时又保存着它在每个PID Namespace里看到的局部PID。同一个进程在宿主机上可能是PID 2048但在容器里看却是PID 1。当你从宿主机执行docker exec进入容器时实际上是调用了setns系统调用把当前进程塞进了目标进程的PID Namespace然后在那个Namespace里fork一个新进程所以你在容器里ps看到的就是容器内的局部PID视角。2.3 Network Namespace容器网络的隔离基石Network Namespace隔离的是网络相关的所有资源网卡、路由表、防火墙规则iptables/nftables、网络协议栈、socket等。每个Network Namespace里都有一套独立的网络协议栈可以拥有自己的IP地址、路由规则和防火墙策略。Docker默认的bridge网络模式本质上就是创建一个Linux bridge网桥docker0或自定义网桥然后为每个容器创建一个veth虚拟网卡对一端塞进容器的Network Namespace另一端挂在网桥上。容器里的eth0看到的是一个172.x.x.x的私网地址这个地址只在容器的Network Namespace里有效出了宿主机路由层面再把它NAT翻译成宿主机IP。整个链路的起点就是Network Namespace把每个容器的网络视野框死了容器才可能各自拥有独立的IP。理解了这个很多人问的为什么容器里能ping通宿主机IP但ping不通另一个容器IP这类问题就有了解题的框架先看两个容器是否在同一个Network Namespace里再看网桥、路由、防火墙的规则是否放行。容器间通信的排障起点永远是先确认网络命名空间的归属。2.4 UTS、IPC、User、Cgroup、Time剩下的五类补充拼接图UTS Namespace隔离的是主机名hostname和NIS域名。这就是为什么你可以在容器里通过hostname命令设置一个和宿主机完全不同的主机名而宿主机和其他容器不受影响。某些应用在启动时会读取主机名作为身份标识如果没有这个隔离所有容器都共享宿主机的主机名场景就会错乱。IPC Namespace隔离的是进程间通信资源主要包含System V IPC对象消息队列、信号量、共享内存和POSIX消息队列。如果一个应用使用共享内存做多进程通信而容器之间没有隔离IPC Namespace两个容器里的进程可能意外访问到同一块共享内存造成数据污染。User Namespace隔离的是用户ID和组IDUID/GID。它在容器安全方面意义重大你可以让容器里的root用户UID 0映射到宿主机上的一个普通用户比如UID 1000这样即便容器被攻破攻击者在宿主机视角也只是个普通用户而不是真正的root从而有效减小了逃逸攻击的破坏面。不过因为权限映射的配置复杂度高实际默认配置下很多容器并没有启用User Namespace。Cgroup Namespace隔离的是cgroup根目录的视图。它让容器里的进程看到的cgroup路径是干净的比如显示成/而不是宿主机上一长串的/docker/xxxx路径。这个隔离主要是为了改善容器内进程对自身资源限制的可见性让像systemd这样的工具在容器内能正常工作。Time Namespace是最晚加入的一类Linux 5.6开始支持它隔离的是系统启动时间和单调时钟。默认情况下所有进程共享同一个系统时间Time Namespace允许容器内有不同的时间偏移。这个场景相对小众主要用于需要时间旅行测试的调试场景。2.5 一张表格快速对照八类Namespace我把八类Namespace的核心参数整理成了一张表方便你在写代码或排查问题时快速查对Namespace类型clone()参数隔离的资源容器中的典型现象MountCLONE_NEWNS文件系统挂载点容器内mount不影响宿主机UTSCLONE_NEWUTS主机名、NIS域名容器内hostname独立IPCCLONE_NEWIPCSystem V IPC、POSIX消息队列容器间共享内存互不可见PIDCLONE_NEWPID进程号容器内PID从1开始NetworkCLONE_NEWNET网卡、路由、防火墙、协议栈容器有独立IP和路由表UserCLONE_NEWUSERUID/GID映射容器内root对应宿主机普通用户CgroupCLONE_NEWCGROUPcgroup根目录视图容器内cgroup路径被简化TimeCLONE_NEWTIME系统启动时间、单调时钟容器内时间偏移量独立这八类隔离叠加在一起就给容器内的进程营造了一种我在一台独立机器上的整体感觉。但注意我特意用了感觉这个词——因为Namespace只管隔离视图不管资源上限。容器能看到的资源范围被限定了但能用多少CPU、多少内存那是cgroup的工作。这个边界必须拎清楚后续排障才不会混淆方向。3. 动手操作创建、进入、退出Namespace的三种姿势理论部分讲完下面进入实操。Namespace不是只能通过Docker间接使用你完全可以在命令行里直接操控它。掌握这三种操作方式你才能真正理解容器运行时在底层做了什么。3.1 clone()从零创建一个新Namespace创建新的Namespace最根本的方式是使用clone()系统调用。clone()用于创建新进程它有一个flags参数当你传入CLONE_NEWNS、CLONE_NEWPID、CLONE_NEWNET等标志位时新进程就会处在一个全新的对应Namespace中。Docker底层用的容器运行时runC本质上就是调用clone()或更底层的clone3()创建容器进程并传入了一组Namespace标志位。我刚才说table里的CLONE_NEWXXX参数就是在clone()里用的。如果你想在纯Linux环境下感受一下可以用unshare命令代替手写C代码# 创建新的Mount、UTS、IPC、PID Namespace并执行bash unshare --mount --uts --ipc --pid --fork bash注意我加了--fork因为单独创建PID Namespace时unshare命令本身不算新Namespace里的进程必须fork一个子进程进去这个子进程才会成为新PID Namespace里的PID 1。这是新手最常踩的坑不加--fork在unshare --pid bash里执行ps看到的进程号还是宿主机视角的。3.2 setns()让当前进程加入一个已存在的Namespacesetns()是进入已有Namespace的关键系统调用。它的用法是打开一个指向Namespace文件的fd通常通过/proc/[pid]/ns/目录下的符号链接获得然后调用setns()把当前进程加入到目标Namespace中。docker exec命令里面就藏着这个逻辑它会把docker-cli发来的exec请求转给dockerddockerd再通过containerd和shim组件找到目标容器主进程的PID打开/proc/[pid]/ns/下的各个Namespace文件mnt、pid、net等对需要进入的Namespace逐一调用setns()最后在目标Namespace里fork出新进程来执行你要跑的/bin/bash或别的命令。这也就是为什么docker exec进入容器后你能看到容器内的进程列表、网络配置和挂载视图——你的shell已经穿越进了容器的Namespace。同样在命令行里nsenter命令封装了setns()的逻辑用法如下# 假设容器主进程PID是1234进入它的Mount和PID Namespace并执行bash nsenter -t 1234 -m -p bash这个命令在排查故障时极其有用。比如某个容器挂了但进程还在你想进到它的Mount Namespace看看里头的文件系统状态就可以用nsenter直接钻进去诊断比docker exec更底层、更直接。3.3 unshare()绕开先创建再进入的两步操作unshare()和setns()正好互补。setns()是加入别人的Namespaceunshare()则是把当前进程移动到一个全新的Namespace。它们都不需要创建新进程直接作用于当前进程本身所以常用于把某个子进程启动到隔离环境中。比较常见的unshare()使用场景是编写自定义的容器运行时或者沙箱工具。比如你想让某个命令运行在一个独立的主机名环境中又不想跑完整的Docker就可以写一小段C代码先调用unshare(CLONE_NEWUTS)再调用sethostname()设置新主机名最后exec要执行的程序。3.4 通过/proc观察Namespace排查问题的核心手段理解了三种系统调用后最重要的事是学会看Namespace。所有进程的Namespace信息都会以符号链接的形式暴露在/proc/[pid]/ns目录下$ ls -l /proc/self/ns/ total 0 lrwxrwxrwx 1 root root 0 Nov 9 10:21 cgroup - cgroup:[4026531835] lrwxrwxrwx 1 root root 0 Nov 9 10:21 ipc - ipc:[4026532233] lrwxrwxrwx 1 root root 0 Nov 9 10:21 mnt - mnt:[4026531841] lrwxrwxrwx 1 root root 0 Nov 9 10:21 net - net:[4026531993] lrwxrwxrwx 1 root root 0 Nov 9 10:21 pid - pid:[4026531836] lrwxrwxrwx 1 root root 0 Nov 9 10:21 uts - uts:[4026531838]方括号里的数字是这个Namespace的唯一标识inode号。两个进程的某个Namespace inode相同说明它们处于同一个Namespace中。这是排查容器隔离问题最核心的判断依据。我举一个具体场景有同事反馈某个容器里的进程通过网络连到了另一个容器的端口怀疑两个容器Network Namespace没隔离好。排查方法很简单找到两个容器的主进程PID分别执行readlink /proc/[pid]/ns/net对比inode号是否一致。如果一致说明确实共享Network Namespace如果不一致说明网络是隔离的问题出在路由或者防火墙层。另外一个实用技巧你可以通过/proc/[pid]/ns/下符号链接的文件描述符来固定一个Namespace防止它被释放。这在跨进程管理Namespace时非常有用比如你想让一个长期运行的后台进程负责管理一组容器的Namespace就可以先open住这些Namespace的fd保证即使容器退出Namespace也不会被内核回收。4. 一条容器启动链路从docker run到Namespace全部就位理解了Namespace单独怎么用我们再把它们装回容器引擎的完整流程里看看一条docker run命令到底是如何一步步把各种Namespace铺开的。4.1 docker run命令背后的运行时链条当你执行docker run -it --name demo ubuntu bash时表面看就是Docker拉了个镜像起了个容器。实际调用链是这样的Docker CLI把请求通过gRPC发给dockerd守护进程。dockerd解析镜像、创建容器配置包括要启用的Namespace列表然后通过containerd的API接口下发容器创建任务。containerd调用runC或其他符合OCI Runtime规范的运行时去真正创建容器进程。runC基于OCI规范中的config.json文件读取里面声明的Namespaces配置调用clone()创建容器进程并传入对应的CLONE_NEWXXX标志位把所有指定的Namespace一次性创建出来。在runC层面config.json里的linux.namespaces字段就是开关Namespace的清单典型内容长这样linux: { namespaces: [ {type: mount}, {type: pid}, {type: network}, {type: uts}, {type: ipc}, {type: cgroup} ] }注意这里没有默认启用User Namespace这是很多容器安全加固方案要专门去补配的原因之一。4.2 容器进程诞生瞬间的Namespace初始化顺序runC创建容器的过程细节非常多但关键点在于先劈Namespace再切根文件系统最后启动进程这个顺序先后错一点都不行。第一步clone()创建出容器进程此时Mount、UTS、IPC、Network等Namespace已经和宿主机分道扬镳。但网络相关的初始化还没开始因为此时容器进程的网络栈只有一个孤零零的loopback网卡甚至loopback都可能处于down状态。第二步runC会在宿主机侧配置好veth网卡对把一端放入容器的Network Namespace并在容器内设置IP地址、路由、启用lo网卡。这一步做完容器才通了网。第三步runC把容器的根目录从宿主机的/切换到镜像rootfs上。这里用的核心机制是pivot_root而pivot_root要求进程处于一个新的Mount Namespace中且当前root必须是挂载点。所以Mount Namespace必须在前一步就位否则pivot_root无法执行。第四步设置容器内的hostnameUTS Namespace生效、挂载/proc等虚拟文件系统这些在容器里必须有很多命令依赖/proc然后exec真正的容器启动命令比如bash。经过这一串初始化一个看起来独立的容器才算真正诞生。这也就是为什么我说Namespace是容器独立感的根基所在。4.3 PID Namespace里的特殊公民容器内PID 1进程的责任容器启动后那个在PID Namespace里编号为1的进程不是普通进程。它接管了该Namespace里所有孤儿进程的收养责任同时承担着容器生命周期管理的核心角色。在Linux中PID 1进程与其他进程有一个显著不同只有它接收不到那些没有显式安装signal handler的信号。更具体地说内核默认会忽略发给PID 1的、没有对应处理函数的信号。这对容器的优雅退出影响很大。举个例子你执行docker stop容器Docker引擎会先向容器内PID 1进程发送SIGTERM信号等它自己清理资源后退出。但如果这个PID 1进程是个普通的shell或者没写信号处理逻辑的简单程序SIGTERM可能被忽略容器就一直卡在停止中。这也是为什么现代容器镜像里的入口进程要专门处理信号或者直接用tini这类init程序作为PID 1来兜底信号转发和僵尸进程回收。看到这里你应该能理解Kubernetes里为什么要单独强调容器的主进程要处理好信号转发——根子就在PID Namespace改变了进程的层级和组织方式。还有僵尸进程问题。容器内某个子进程退出了它的父进程没有调用wait回收这个子进程就会变成僵尸进程挂到PID 1进程名下等待被收养。如果PID 1进程自己也不回收僵尸进程会越积越多最终可能拖垮整个容器。这就是为什么某些长时间运行的容器会莫名其妙内存上涨或者进程数爆炸——排除业务本身的问题后优先怀疑僵尸进程堆积然后检查PID 1进程是否有回收子进程的逻辑。5. 实战踩坑Namespace相关的故障与排查思路理论链路讲通了但真正值钱的是实操里碰到的坑。下面这几个问题都是我在实际容器运维和开发中见到过、也亲手排查过的类型每一个都和Namespace有直接关系。5.1 热词中的inconsistent namespace mapping其实是另一个世界我在搜索相关热词时看到不少人在问Inconsistent namespace mapping这个报错而且它出现在java.sql.SQLException的错误上下文里。这个报错很容易让刚学Namespace的人误以为跟Linux Namespace有问题。这里必须先做个澄清这个错误通常来自Oracle数据库的报错它说的是数据库实例内部的数据字典和对象映射关系不一致跟Linux内核的Namespace机制没有任何关系。这背后的经验是排障时第一件事不是翻报错文案里有没有namespace这个词而是先确认报错来源。同样的单词在不同技术栈里含义天差地别。Linux Namespace是内核级隔离机制Oracle的Namespace是数据库存储层级里的对象命名空间Kubernetes里的Namespace是资源对象的逻辑分组DNS里的Namespace又另有所指。如果你在一个技术上下文里套用另一个领域的知识排查方向会整个跑偏。我在看这类报错时习惯先抓这句话是谁抛出来的再决定要不要往Linux内核方向联想。5.2 同一个Pod里的容器到底共享了哪些NamespaceKubernetes的Pod是一个特别容易让人误解Namespace的模型。很多人以为Pod里的多个容器是完全隔离的但实际上同一个Pod内的多个容器会共享同一个Network Namespace、UTS Namespace和IPC Namespace而PID Namespace和Mount Namespace默认是各自独立的除非显式开启shareProcessNamespace。这个设计带来的直接后果是同一个Pod里的容器共享IP地址和端口空间。比如Pod里有两个容器一个容器监听了8080端口另一个容器想监听8081没问题但如果也去监听8080就会端口冲突——它们俩在同一个Network Namespace里socket端口空间是一致的。这也是为什么Pod里多个容器要互相访问时直接访问127.0.0.1加对方端口就可以通不需要走Service。排查这类问题时我会直接对比两个容器的Network Namespace inode# 获取Pod中两个容器的PIDs # 找到容器主进程PID后执行 readlink /proc/PID/ns/net如果两个PID的net inode一致确认共享网络栈那么问题的排查范围就缩小到端口冲突、iptables规则、socket连接状态这些网络栈内部的问题完全不需要再怀疑容器间网络不通。这个判断思路在实际排障中非常省时间。5.3 mount泄漏问题为什么容器里umount不掉宿主机的东西mount泄漏是Mount Namespace相关的经典坑。现象是你在容器里挂载了一个宿主机目录比如docker run -v /data:/data然后想在容器里umount掉它发现报错Device or resource busy甚至umount成功了宿主机上的挂载也没了。为什么会这样根源在于bind mount的本质。docker run -v /data:/data大致等价于把宿主机/data目录bind mount到容器的/data目录。这个mount条目会同时出现在宿主机的挂载表和容器的Mount Namespace挂载表里因为容器创建的时候复制了父Namespace的初始挂载状态。所以你在容器里umount实际上影响的是这个共享的挂载条目本身。如果该挂载点还有其他进程在使用哪怕是别的Namespace里的进程umount就会失败即使umount成功宿主机上的/data挂载也没了直接影响其他容器和宿主机进程。正确的做法是不要在容器里umount通过-v挂载进来的宿主机目录。如果业务上确实需要容器内隔离某个挂载应该用非共享挂载比如tmpfs或者在容器启动脚本里先umount后再重新挂载但操作前必须想清楚影响面。这类问题再次印证了一点Mount Namespace创建时会继承父级的挂载点列表容器和宿主机之间、容器和容器之间的挂载关系比你想象的更容易纠缠。5.4 Mount Propagation为什么宿主机新挂载的磁盘在容器里看不到和上面问题相对的是另一个方向宿主机挂载了一个新磁盘但容器里怎么看都找不到这个挂载点。这其实不是一个故障而是Namespace隔离的正常行为。因为容器创建时Mount Namespace挂载点列表是那一刻的快照之后宿主机上新增的挂载默认不会同步进容器除非设置了共享挂载传播。Kubernetes里volume的实现和这个机制密不可分——kubelet在挂载volume时会在容器启动之前或运行中通过特定机制确保mount事件能传播到容器的Mount Namespace里。如果你确实需要让容器感知宿主机的挂载变化需要在挂载时设置传播属性。Docker的--mount参数里支持bind-propagation选项取值一般是shared、slave、private# 以slave方式挂载宿主机挂载事件会单向同步进容器 docker run -v /data:/data --mount typebind,source/data,target/data,bind-propagationslave myimage但请注意传播模式是把双刃剑。shared模式虽然双向同步挂载事件但也会让容器内的挂载反向影响宿主机容易引发上一条说的umount灾难。我的建议是能用private就用private需要接受宿主机挂载事件的场景用slaveshared模式除非你完全清楚后果否则谨慎使用。5.5 容器退出后留在Namespace里的逃逸进程还有一个平时不太注意、但很可能会在排障尾期遇到的坑docker stop之后容器已经显示退出了但宿主机上还能找到几个无主的进程它们的Namespace和已经死掉的容器绑定在一起。这些进程通常是容器里的子进程因为各种原因被托孤到了宿主机PID命名空间之外或者是在容器退出时没有正常回收的进程。它们最大的危害是让容器的Namespace无法被内核回收导致挂载点泄漏、网络资源占用长期积累会消耗宿主机的内核资源。排查方法是在宿主机上按Namespace inode搜索# 找网络命名空间的inode # 假设容器曾用Network Namespace inode为4026532901 ls -l /proc/[0-9]*/ns/net | grep 4026532901找到这些进程后根据情况决定是kill掉还是保留观察。这类问题的根因通常要回溯到容器启动命令的设计——PID 1进程有没有正确托管子进程、有没有处理信号、有没有回收僵尸前面4.3节说的内容在这里全都会用上。Namespace是内核资源不主动清理就不会自动消失这也是为什么一个Pod反复重启后宿主机上有时会出现大量残留Namespace的原因之一。6. Namespace只是隔离拼图的一半别忘了补齐另外半边前面讲了这么多Namespace隔离的能力但有一个事实我必须反复强调Namespace只管看得到什么不管能用多少。一个容器即使隔离了PID、网络、挂载它还是可以和宿主机上的其他进程抢CPU、抢内存。要限制资源使用量必须靠cgroup。这是容器体系里缺一不可的另一半。cgroup控制组负责给一组进程设定资源上限包括CPU时间片、内存配额、磁盘IO带宽、网络带宽等。内核通过cgroup把这些进程塞进一个组里再对这个组整体做配额限制。Docker里的--cpus、--memory参数Kubernetes里的requests和limits最终都是转化成cgroup的配置写进内核的。举个例子你启动容器时指定了resources.limits.memory为512MiKubernetes的kubelet就会在cgroup文件系统里创建对应的内存控制组写入memory.limit_in_bytes536870912。当容器内所有进程加起来的RSS内存超过这个值内核就开始回收甚至OOM Kill容器进程。可以看到Namespace和cgroup的职责是完全正交的Namespace决定你能看到哪些资源cgroup决定你最多能用多少资源。两者配合容器才算真正被关起来。除了cgroup容器隔离和安全还有另外几块关键的拼图Capabilities把root的权限拆分成几十个独立的capability容器默认只保留一小部分比如CHOWN、SETUID这种相对安全的而像SYS_ADMIN、NET_ADMIN这类危险的capability默认都会被去掉。seccomp限制容器内的进程能调用哪些系统调用。即使攻击者拿到了容器内代码执行权限很多危险的系统调用也被提前封死。SELinux/AppArmor提供强制访问控制MAC给容器和容器内的文件打安全标签限制进程的文件访问范围和网络访问行为。只读根文件系统把容器的根文件系统设为只读写操作必须显式挂载临时卷这样攻击者想在容器里写文件就要多绕几道弯。讲这些是想强调一个观点Namespace是容器隔离的基石但它只是起点不是终点。生产环境真正的容器安全靠的是Namespace、cgroup、Capabilities、seccomp、安全模块这几层机制的纵深配合。任何一层被单独拿出来说这就是容器隔离都是不完整的。我在帮团队做容器安全基线的时候第一件事也是逐项检查这几层有没有配置到位而不是只看Namespace是否正确隔离了。最后分享一个我在实际项目里反复验证过的小技巧排查任何容器隔离失败类问题时不要凭感觉猜测先把这张检查清单走一遍对比Namespace inode确认隔离边界→查cgroup配额确认资源限制→查Capabilities和seccomp配置确认权限边界→查安全上下文标签确认MAC策略。按这个顺序排查90%以上的容器隔离异常问题都能在十分钟内定位到具体层级。Namespace是必须搞懂的第一块基石但只有把它和整个容器隔离体系放在一起看你的排查思路才是完整、闭环的。
返回列表