搞懂 Linux 内核容器技术,才算真正吃透 Linux 内核底层 📅 2026/7/7 8:26:29 搞懂 Linux 内核容器技术才算真正吃透 Linux 内核底层一、容器是什么1.1 容器技术的定义从技术原理来讲的话容器是基于 Linux 内核的特性实现了轻量级的进程隔离。通过巧妙运用命名空间Namespaces和控制组Cgroups容器内的进程拥有自己独立的资源视图彼此之间互不干扰就像在不同的小世界里运行。这不仅保障了应用的稳定运行启动速度可达到秒级资源利用率大幅提高单机可以轻松承载数千个容器实例 这是传统部署方式难以企及的。1.2 容器与虚拟机区别很多人喜欢把容器说成是“轻量级虚拟机”这说法吧也不是全错但本质上南辕北辙。容器的核心是一组被内核“欺骗”的进程你把它理解成虚拟机就默认它有独立内核、独立硬件抽象层。实际上容器啥也没虚拟它跑的就是普普通通的 Linux 进程只不过内核通过某种手段Namespace 和 Cgroups让这个进程以为自己独占整台机器。虚拟机是基于 Hypervisor虚拟机监视器实现的硬件级虚拟化。Hypervisor 如同一个中介在物理服务器硬件之上模拟出一套完整的硬件环境每个虚拟机都需要安装独立的操作系统Guest OS拥有自己专属的 CPU、内存、磁盘等资源。这就像是在一台物理电脑里又虚拟出多台独立的电脑每台都能独立运行不同的操作系统和应用隔离性极强但代价是资源开销大。虚拟机镜像动辄数 GB启动时间往往以分钟计算在资源利用率和启动效率上存在明显短板。反观容器它属于操作系统级虚拟化所有容器共享宿主机的 Linux 内核 。容器利用命名空间实现进程、网络、文件系统等资源的隔离通过 Cgroups 来限制每个容器对 CPU、内存等资源的使用。容器只包含应用及其依赖镜像体积通常在 MB 级别启动速度能达到秒级甚至毫秒级性能表现接近原生应用。在单机部署密度上容器更是展现出碾压虚拟机的优势能极大提升硬件资源的利用效率。二、核心机制命名空间Namespaces2.1 命名空间技术进程隔离的基础先来个不那么严谨但好懂的说法Namespace 就是内核给进程画圈。一个进程被放进圈里它只能看到圈里的东西。圈外的进程、文件系统、网络接口——统统看不见。不是说它们不存在而是内核在“视野”层面做了过滤。Linux 里 Namespace 的实现方式是给每个进程绑一组“命名空间标识符”。当一个进程访问系统资源读 proc、枚举网络接口、发起 IPC内核先去查它在哪个空间里然后只返回属于那个空间的内容。Namespace 把“系统全局资源”包装成“每个空间独立的实例”——比如你在容器里ps aux只看到容器内那几个进程不是宿主机 2000 多个因为内核给你看的 PID 树是假的或者说是“被隔离过”的树。这机制和 chroot 很像但比它强太多了。chroot 只改了根目录的映射Namespace 可以隔离六七个维度PID、Mount、Network、UTS、IPC、User再加上后来加的 Cgroup Namespace。每一项都是独立的“视界”。2.1.1 进程隔离PID NamespacePID Namespace 是我最喜欢用的你跑个容器进去ps auxPID 都是从 1 开始排这个 1 号进程在容器里就是 init它是所有进程的父进程负责收养孤儿进程。但你回到宿主机上ps aux | grep一下那个容器的进程你会发现它的 PID 是别的数字比如 18723。这种“双重身份”怎么做到的内核在 PID Namespace 里维护了一个 PID 映射表。同一个进程在内核里只有一个“真 PID”但在不同的 PID Namespace 里可以有不同“视图 PID”。有一回我排查一个问题在容器里 strace 一个进程的 PID 子进程在宿主机用nsenter -t 宿主机PID -p进去挂接——搞半天 PID 对不上最后发现我进错了 Namespace。真丢人。这种低级错误写出来也挺好提醒自己别犯懒。2.1.2 文件系统隔离Mount NamespaceMount Namespace 主要负责隔离文件系统的挂载点它让每个容器都拥有自己独立的文件系统视图简单来说容器内看到的文件系统结构与宿主机以及其他容器是相互独立的就像是每个容器都有自己专属的根目录。在 Linux 系统中文件系统的挂载点决定了进程对文件的访问路径通过 Mount Namespace容器可以拥有自己的挂载点集合这些挂载点仅在容器内部可见。例如容器可以将宿主机的某个目录挂载到容器内的指定位置但这个挂载操作只对当前容器有效不会影响宿主机和其他容器的文件系统布局 。容器在启动时会基于 Mount Namespace 构建自己的文件系统层级结合联合文件系统UnionFS容器可以将多个只读层和一个可写层叠加在一起形成一个完整的文件系统容器内的进程对文件的读写操作都发生在这个独立的文件系统中不会泄露到宿主机或其他容器中确保了容器环境的独立性和安全性 。2.1.3 网络隔离Network Namespace独立的网络栈Network namespace 给每个容器独立的网络栈。自己的 lo 接口、IP 地址、路由表、iptables 规则。容器间通信靠 veth pair一端在容器里一端在宿主机上像虚拟网线一样连着Docker 默认的 bridge 网络就是这么玩的你用 ip netns exec 命令进去看会感觉像开了上帝视角。2.1.4 UTS Namespace主机名与域名的隔离UTS Namespace 主要负责隔离主机名和域名允许每个容器设置自己独立的主机名和 NISNetwork Information Service域名 。这使得容器在网络中可以呈现为一个独立的节点拥有自己独特的标识。在容器环境中不同容器可以拥有不同的主机名彼此之间互不干扰。IPC namespace 管 System V IPC 和 POSIX message queues进程间通信隔离了User namespace 最有意思能把容器里的 rootUID 0映射成宿主机的普通用户大大降低了逃逸风险。2.1.5 IPC Namespace进程间通信隔离IPC Namespace 隔离的是 System V IPC 对象和 POSIX 消息队列。这意味着容器 A 里创建的共享内存段容器 B 看不见也访问不到。这个维度在一般应用场景里用到的不多但对安全隔离很重要——你不能让恶意容器通过共享内存去偷隔壁容器的数据。2.1.6 User Namespace用户 ID 映射与安全隔离这个太重要了单独多讲几句。User Namespace 允许你在容器里是 rootUID 0但在宿主机上其实是一个普通用户比如 UID 65534nobody。怎么做到的内核维护了一个 UID/GID 映射表。比如说你可以配置容器内的 UID 0 映射到宿主机的 UID 1000容器内的 UID 1000 映射到宿主机的 UID 2000以此类推。这个特性是容器安全的基石没有 User Namespace 的时候一旦有人从容器里逃逸出来拿到了 root 权限宿主机直接沦陷。有了 User Namespace 后你逃出来了也就是个普通用户权限非常有限。但这里有个坑——User Namespace 和很多文件系统操作有兼容性问题。我记得好像是三年前我在 Docker 里开 User Namespace 然后挂载 NFS直接炸了日志里全是权限错误。原因是 NFS 服务端不认识你的 UID 映射。这类问题至今在部分场景下还存在所以生产环境启用 User Namespace 之前要做充分测试。2.2 命名空间实战咱们动手搓一个“迷你容器”先来个最简版。用unshare命令创建一个隔离的 bash 环境隔离 UTS、PID 和 Mount Namespace# 创建一个隔离的 shell拥有独立的 PID、UTS 和 Mount 命名空间sudo unshare --fork --pid --mount --uts --mount-proc /bin/bash跑完这行命令你就进了个“半容器”。ps aux一下只看到 bash 和你正在运行的进程。hostname test-container改个主机名退出后宿主机不受影响。想更自动化一点用 C 代码直接调用clone()系统调用这其实就是容器运行时的底层做法#define _GNU_SOURCE#include sched.h#include stdio.h#include stdlib.h#include unistd.h#include sys/wait.h#define STACK_SIZE (1024 * 1024)static char child_stack[STACK_SIZE];int child_func(void *arg) { /* 进入新的 PID/UTS/Mount Namespace */ sethostname(mini-container, 15); execl(/bin/bash, /bin/bash, (char *)NULL); return 0;}int main() { int flags CLONE_NEWUTS | CLONE_NEWPID | CLONE_NEWNS | SIGCHLD; pid_t child clone(child_func, child_stack STACK_SIZE, flags, NULL); if (child -1) { perror(clone failed); exit(1); } waitpid(child, NULL, 0); return 0;}编译运行gcc -o mini-container mini-container.c sudo ./mini-container。然后你就得到了一个手工版的“容器”主机名独立、进程树独立、挂载点独立。•clone()— 创建新进程并带入指定 Namespace•unshare()— 让当前进程脱离某个 Namespace•setns()— 让当前进程加入一个已存在的 Namespace三、资源管控核心控制组Cgroups3.1 Cgroups 的作用在容器技术的体系中命名空间Namespaces解决了 “资源隔离” 的问题让容器内的进程仿佛运行在独立的小世界里互不干扰 。而控制组Cgroups则聚焦于 “资源定量管控”它决定了每个容器能使用多少资源防止单个容器肆意挥霍宿主机资源影响其他容器或宿主机的正常运行。想象一下宿主机是一座公寓大楼每个容器就是大楼里的一户住户。命名空间为每个住户划分了独立的居住空间而 Cgroups 则为每个住户规定了水电等资源的使用额度。比如通过 Cgroups我们可以限制某个容器最多只能使用 1 个 CPU 核心、512MB 内存以及每秒 100MB 的磁盘读写带宽 。这样一来即使某个容器内的应用出现异常疯狂占用资源也不会拖垮整栋 “大楼”保障了其他容器的稳定运行和资源的合理分配。Cgroups 通过一系列的子系统Subsystem来实现对不同类型资源的管控。常见的子系统包括 cpu 子系统控制 CPU 分配、memory 子系统限制内存使用、blkio 子系统管理块设备 I/O等 。每个子系统都提供了对应的配置参数和接口用户可以根据实际需求灵活地为容器或进程组设置资源限制策略。3.2 Cgroups 核心Cgroups 采用树状的层级结构在这个层级结构中每个节点都代表一个控制组Cgroup可以将进程加入到不同的控制组中。根控制组是整个层级树的顶端所有其他控制组都是它的子节点。子控制组会继承父控制组的资源限制设置同时也可以拥有自己独特的限制规则这种层级结构使得资源管理更加灵活和高效。以一个 Web 服务集群为例我们可以创建一个名为 “web-services” 的父控制组限制其总的 CPU 使用量为 4 核。然后在这个父控制组下为每个 Web 服务器实例创建一个子控制组如 “web-server-1”“web-server-2” 等 。每个子控制组可以根据自身负载情况进一步细化 CPU、内存等资源的分配比如 “web-server-1” 设置为最多使用 1 核 CPU 和 1GB 内存“web-server-2” 设置为最多使用 2 核 CPU 和 2GB 内存 它们的总资源使用不会超过父控制组的限制。子系统Subsystem是 Cgroups 实现资源管控的核心模块每个子系统负责管理一种特定类型的资源 。比如cpu 子系统通过控制 CPU 时间片的分配来限制进程组的 CPU 使用率memory 子系统通过设置内存使用上限防止进程组过度占用内存blkio 子系统则通过控制块设备的 I/O 操作限制进程组对磁盘的读写速率 。不同的子系统可以根据实际需求挂载到不同的控制组层级上实现对资源的精细化管理。控制器Controller是子系统的具体实现它负责将资源限制策略应用到控制组中的进程上 。每个子系统都包含多个控制器每个控制器对应一个具体的资源限制参数。以 cpu 子系统为例它包含了 cpu.cfs_quota_us 和 cpu.cfs_period_us 等控制器 。其中cpu.cfs_period_us 定义了 CPU 调度的周期单位为微秒默认值是 100000即 100mscpu.cfs_quota_us 则定义了在这个周期内进程组最多可以使用的 CPU 时间 。通过调整这两个参数我们可以精确地控制进程组的 CPU 使用率。3.3 资源管控实战来直接实操。先创建一个叫myapp的控制组限制它最多用 256M 内存CPU 权重设低一点# 创建子 cgroupsudo mkdir /sys/fs/cgroup/myapp# 看看有哪些控制器可用cat /sys/fs/cgroup/myapp/cgroup.controllers# 输出可能是cpuset cpu io memory pids ...# 内存硬上限 256MBecho 268435456 | sudo tee /sys/fs/cgroup/myapp/memory.max# CPU 权重相对于同级 cgroup数字越低分到的 CPU 时间越少echo 100 | sudo tee /sys/fs/cgroup/myapp/cpu.weight# 限制最多进程数echo 50 | sudo tee /sys/fs/cgroup/myapp/pids.max# 把当前 shell 加进这个 cgroupecho $$ | sudo tee /sys/fs/cgroup/myapp/cgroup.procs验证一下cat /proc/self/cgroup你会看到当前进程已经归属myapp组了。然后跑个stress-ng --vm 1 --vm-bytes 300M——你会发现 OOM Killer 果断出手因为 300M 256M。Docker 做的其实就是把这些步骤自动化。你执行docker run --memory256m --cpus1Docker 在后台创建对应的 cgroup、写限制值、把容器进程 PID 写入 cgroup.procs 文件。3.4 Cgroups 与容器的资源隔离继续说上面那个myapp的例子。我再建一个app-highprio组给更高 CPU 权重sudo mkdir /sys/fs/cgroup/app-highprioecho 1000 | sudo tee /sys/fs/cgroup/app-highprio/cpu.weightecho 536870912 | sudo tee /sys/fs/cgroup/app-highprio/memory.max现在把两个进程分别放进这两个组同时跑 CPU 密集型任务你会观察到app-highprio组的进程获得更多 CPU 时间片——比例大约是 1000:100 10:1严格来说不是精确比例实际还受调度器其他因素影响但大致方向是对的。这就是 Cgroups 的威力在不改变应用代码、不虚拟硬件的情况下通过内核调度器实现了多租户间的资源公平分配。3.5 Cgroups API 与工具链除了直接操作文件还有一套命令行工具cgcreate、cgexec来自libcgroup工具包v1 时代用得多v2 推荐用systemd 的 slice 机制。不过我个人更喜欢直接操作文件透明度高。systemd-cgls可以看 cgroup 树结构systemd-cgtop实时看资源使用排行——这俩命令我在日常运维中用得最多。四、文件系统UnionFS与容器镜像4.1 容器镜像的本质容器的镜像本质是啥呢是一个分层叠加的只读文件系统每一条 Dockerfile 指令生成一层。FROM ubuntu:22.04是第一层RUN apt-get install nginx是第二层COPY app /app是第三层……每一层都是一个不可变的目录树。容器启动时在这些只读层之上叠加一个可写层形成一个统一的文件视图。这种分层结构有超过优势比如镜像层的复用性不同的容器镜像如果依赖相同的基础层如都基于 ubuntu:latest 基础镜像那么这个基础层只需在宿主机上存储一份多个镜像可以共享大大节省了存储空间 。在构建镜像时Docker 会根据指令的执行顺序依次创建每一层并为每一层生成唯一的哈希标识 。当再次构建相同指令的镜像层时Docker 可以直接复用之前的缓存层无需重新执行指令极大地提高了构建效率 。当容器启动时会在这些只读层之上挂载一个可写层Writable Layer 。所有对容器内文件系统的写操作如创建文件、修改文件内容等都发生在这个可写层中 而底层的只读镜像层始终保持不变。这就好比在一幅已经画好的油画上覆盖一层透明的薄膜在薄膜上进行的任何涂鸦都不会影响到下面的油画。这种设计既保证了镜像的不可变性使得镜像可以被安全地共享和分发又为容器运行时的动态修改提供了支持 。4.2 UnionFS 原理UnionFS联合文件系统是实现容器镜像分层存储和读写分离的核心技术它的工作原理基于 “写时复制”Copy-On-Write简称 CoW机制 。简单来说当容器需要读取文件时UnionFS 会按照从上层到下层的顺序在各个只读层中查找目标文件 。如果在某一层找到了文件就直接从该层读取不会对文件进行复制 。例如容器要读取 /etc/nginx/nginx.conf 配置文件假设这个文件位于基础镜像层UnionFS 会直接从基础镜像层加载该文件整个过程高效且快速 。当容器需要修改文件时“写时复制” 机制就会发挥作用。如果要修改的文件位于只读层UnionFS 会先将该文件从只读层复制到可写层然后在可写层中进行修改 。后续对该文件的所有读取操作都会从可写层获取修改后的版本 。比如容器内要修改 nginx.conf 文件UnionFS 会把 nginx.conf 从只读镜像层复制到可写层在可写层中完成修改后再次读取 nginx.conf 时返回的就是可写层中修改后的内容 而底层的只读镜像层中的 nginx.conf 文件依然保持原样 。当容器删除一个文件时实际上并不会真正删除只读层中的文件而是在可写层中创建一个特殊的 “删除标记” 文件也称为 whiteout 文件 。这个标记文件会覆盖掉只读层中的原始文件使得在容器内看起来文件已被删除 。这样的设计保证了只读镜像层的完整性同时也实现了文件删除操作在可写层的独立管理 。4.3 主流 UnionFS 实现OverlayFS 是目前的事实标准。自 Linux 内核 3.18 合入主线后它把 AUFS 和 Device Mapper 基本全淘汰了。overlay2驱动多 lowerdir 原生支持不用像老 overlay 那样依赖硬链接来提升 inode 利用率效率高很多。AUFS 曾经是 Docker 默认的存储驱动但一直没进内核主线维护太痛苦。Device Mapper 性能还行但配置复杂thin pool 出问题的时候排查起来很头疼——有一次生产环境的 thin pool 满了容器全部变成只读后来我把线上所有环境全迁移到 OverlayFS舒服了。OverlayFS 的挂载结构•lowerdir只读的镜像层可以有多个多个之间用冒号分隔•upperdir容器的可写层•workdir内核操作需要的工作目录用户不可见•merged对外呈现的统一视图容器启动时Docker 把这些目录组装好然后执行一次mount -t overlay挂载容器的根文件系统就出来了。退出时umountoverlay 卸载merged目录变回空壳。你可以用mount命令自己体验mount -t overlay overlay -o lowerdir/lower,upperdir/upper,workdir/work /merged。4.4 容器镜像构建流程与 Dockerfile 解析Dockerfile 的每一条指令生成一层。层与层之间通过 OverlayFS 叠加最终形成统一视图。说一个很多人不知道的细节点Dockerfile 中每条指令产生的中间层在最终镜像里是不存在的被压缩成一个 layer。但构建过程中这些中间层对于构建缓存至关重要。docker build的时候每个指令执行完成后都会创建一个中间镜像下一层如果没变直接用缓存——这就是为什么把不常变化的层放上面比如apt-get install经常变化的层放下面比如COPY . /app可以大幅加速构建。写 Dockerfile 的时候要记住Dockerfile 的每一层都是不可变的已经创建的层无法修改只能在上面叠加新层。如果你发现基础镜像某个层的配置不对只能重新构建整个镜像。4.5 容器读写操作的透明性与性能优化写时复制确实节省了存储但频繁写小文件时有性能代价。每次对只读层文件的修改都要触发一次拷贝操作——把文件从 lowerdir 拷贝到 upperdir。实际经验中日志密集型应用放 OverlayFS 上性能下降非常明显。最佳实践是用volume把频繁读写的目录比如/var/log、数据库数据目录挂载到容器外的独立存储上绕过 OverlayFS 的写时复制。五、容器生命周期5.1 容器创建容器创建三步曲命名空间隔离 → Cgroups 资源限制 → 根文件系统挂载这三个步骤是有严格顺序的每一步之间都有依赖任何一个步骤出错都会导致容器创建失败。容器运行时执行docker run背后内核做的核心工作就这三件按顺序走第一步Namespace 隔离。调用clone()或unshare()创建新的 Namespace 集合——PID、Mount、Network、UTS、IPC 等进程被丢进隔离环境。第二步Cgroups 资源限制。在 cgroupfs 里创建子组写入资源配额值把进程 PID 写入cgroup.procs文件。从这一刻起这个进程组的资源使用被内核严格监控。第三步根文件系统挂载。用 OverlayFS 把镜像层和可写层组装成merged目录pivot_root或chroot到这个目录。内核层面Docker Daemon 先通过 OverlayFS 将镜像只读层与容器读写层组装为merged目录再通过pivot_root为容器进程切换根文件系统之后执行/proc、/sys、/dev等关键虚拟文件系统的挂载确保容器内拥有一个“完整系统”该有的基础环境5.2 容器启动容器的“第一个进程”PID 1是怎么来的答案是clone()系统调用clone()系统调用通过传入 CLONE_NEWPID、CLONE_NEWNS、CLONE_NEWNET 等参数创建各种命名空间 为容器构建独立的运行环境。例如当设置 CLONE_NEWPID 参数时会创建一个新的 PID 命名空间在这个命名空间内容器的 init 进程初始化进程的 PID 为 1 就像在独立的操作系统中一样 。调用 clone () 完成后会在新创建的命名空间内执行 init 进程 。init 进程作为容器的首个进程肩负着重要使命它负责启动容器内的应用程序并管理容器内的其他进程 。如果 init 进程终止容器也会随之退出就像城市的核心枢纽停止运转整个城市也将陷入瘫痪 。在基于 systemd 的容器中init 进程通常是 systemd它会读取容器内的配置文件启动各种服务和应用程序确保容器能够正常运行 。5.3 容器运行时的状态监控与资源统计docker stats能看到 CPU、内存、网络、IO 统计。但数据是哪来的答案是 cgroup 里的memory.current、cpu.stat等文件Docker 实时读取这些文件然后把值格式化输出。也可以直接看文件cat /sys/fs/cgroup/system.slice/docker-container-id.scope/memory.current拿到的就是容器当前内存用量字节。/proc/[PID]/cgroup路径记录了一个进程归属的所有 cgroup从文件的视角观察容器的资源管控状态非常直观。5.4 容器销毁docker stop发 SIGTERM超时后 SIGKILL。进程死亡后Docker会从 cgroup 中移除进程 PID删除该子 cgroup 目录卸载 OverlayFS 的merged挂载点释放容器写入层upperdir的存储空间清除对应的 Network Namespace 和 veth pair。这里需要注意的是docker rm而不是docker stop才会触发上层可写层数据的回收。你docker stop但不docker rm数据一直留在磁盘上——这些“死容器”多了会把磁盘吃满。我就经历过一次一台服务器上留了四五百个 stopped 容器docker system prune -a清出了 40G 空间。所以说容器生命周期管理的最后一步清理才是很多人忽视的关键环节不然你的/var/lib/docker会无限膨胀。5.5 容器持久化存储与数据卷管理由于容器的可写层在容器销毁后会被删除若想保留容器运行过程中产生的数据如数据库数据、日志文件等就需要借助持久化存储技术而数据卷Volume正是实现这一需求的核心方案。数据卷通过将宿主机目录或分布式存储挂载到容器内让容器内的数据读写直接作用于持久化存储介质即使容器销毁数据也能得以保留。Docker 支持三种主要的挂载方式第一种是 bind mount绑定挂载直接将宿主机的某个目录或文件挂载到容器内指定路径例如 “docker run -v /host/path:/container/path ubuntu”这条命令将宿主机的 /host/path 目录挂载到容器的 /container/path 目录容器内对该目录的所有操作都会直接同步到宿主机对应目录适合需要直接访问宿主机文件的场景。第二种是 volume数据卷这是 Docker 推荐的持久化方式。数据卷由 Docker 统一管理默认存储在宿主机的 /var/lib/docker/volumes 目录下用户无需关心具体的存储路径。例如“docker volume create my-volume” 可以创建一个名为 my-volume 的数据卷再通过 “docker run -v my-volume:/container/path ubuntu” 将其挂载到容器数据卷可以被多个容器共享且即使挂载的容器销毁数据卷本身和其中的数据依然保留需要手动删除。第三种是 tmpfs mount临时文件系统挂载将数据存储在宿主机的内存中而非磁盘上。这种方式的优势是读写速度极快但数据在容器销毁或宿主机重启后会丢失适合存储临时数据、缓存等无需持久化的内容例如 “docker run --tmpfs /tmp ubuntu”将容器的 /tmp 目录挂载为临时文件系统。在 Kubernetes 集群环境中数据卷的管理更加灵活通过 PVCPersistent Volume Claim持久化卷声明和 PVPersistent Volume持久化卷机制实现了数据卷的动态分配和管理。PV 由管理员提前创建定义了存储资源的类型、大小等属性PVC 由用户创建用于申请所需的存储资源系统会自动匹配符合条件的 PV 并进行绑定无需用户手动关联存储设备极大地简化了容器持久化存储的配置流程满足大规模容器集群的存储需求。六、容器网络每个容器都有自己的网络命名空间Network Namespace里面装着独立的网络接口、IP、路由表、ARP表和iptables规则——就像一台迷你物理机。这样容器和宿主机、容器和容器之间就实现了网络隔离。6.1 容器网络模型容器网络遵循OCI标准所以Docker、Podman这些引擎的行为都一致。默认情况下容器会拿到一个私有IP通常来自Docker网桥的172.17.0.0/16网段这个IP只在它自己的命名空间里有效外面访问不了。要和外界通信得靠宿主机的网络转发或端口映射。除了默认的桥接模式还有几种常用模式•主机模式–nethost不创建独立命名空间直接共享宿主机的网络栈容器的IP和宿主机一样。适合需要直接使用宿主机网络资源的场景。•none模式–netnone不给任何网络配置完全手动。适合对隔离要求极高、根本不需要联网的容器。•容器模式–netcontainer:容器ID让新容器跟另一个容器共享同一个网络命名空间。6.2 虚拟以太网桥接veth pair容器跟宿主机、容器跟容器之间怎么通信靠veth pair。它就是一端插在容器里通常叫eth0另一端插在宿主机上的网桥或网络命名空间里像一根虚拟网线数据能来回传。拿Docker默认的桥接模式来说宿主机上有个叫docker0的网桥。每个容器启动时veth pair的一端连到容器的eth0另一端连到docker0。容器要跟宿主机通信数据从eth0出发穿过veth pair到docker0再由docker0转发给宿主机的网卡。双向都能走而且只在两个命名空间之间有效。既隔离又能通。6.3 容器间通信同一台宿主机上的容器默认走docker0网桥。所有连到docker0的容器都在同一个网段二层交换靠MAC地址就能直接通信。比如容器A的IP是172.17.0.2B是172.17.0.3A直接ping B就行数据不用经过宿主机的对外网卡效率很高。如果想做更细的隔离可以自己建网桥docker network create my-bridge启动容器时指定--network my-bridge。不同网桥下的容器互相看不到只有同一个网桥里的才能通。适合把不同业务隔离到不同的网络环境里。跨宿主机的容器光靠网桥不行得用覆盖网络Overlay Network。常见的网络插件有Calico、Flannel、Weave等。它们通过在宿主机之间建隧道比如VXLAN把所有宿主机上的容器拉进同一个虚拟网络。比如Flannel会给每台宿主机分配一个独立网段容器的IP从里面取跨主机的通信就像在本地一样。6.4 容器网络隔离与安全策略如iptables网络隔离不光靠命名空间还得靠安全策略iptables是Linux自带的防火墙能控制数据包的进出Docker会自动配好iptables规则。比如你执行docker run -p 8080:80 nginxDocker会在nat表里加一条端口转发规则把宿主机的8080转到容器的80同时在filter表里允许外部流量访问8080禁止直接访问容器内部。你也可以手动配比如想禁止某个容器上网就用iptables限制它的出站流量想禁止不同网段的容器互相通信就在filter表里加拒绝规则。总之只开必要的端口减少风险。更高端的方案像Calico还支持网络策略Network Policy能基于容器标签做精细化访问控制比如指定哪些容器可以访问当前容器的哪些端口。6.5 高级网络方案Service Mesh容器规模一大通信管理就变得复杂。Service Mesh服务网格它通过Sidecar代理接管容器的所有网络流量实现流量治理、熔断、限流、监控等功能而且对业务代码无侵入。以Istio为例它会为每个容器注入一个Envoy Sidecar代理。容器间的所有通信都经过这个代理。代理负责转发流量、执行安全策略、收集数据。比如某个服务挂了Sidecar自动把流量切到健康的实例或者配置限流规则防止服务被冲垮。七、容器安全7.1 Linux Capabilities最小权限原则与权限控制Linux里root权限太大了如果容器里的进程是root一旦容器逃逸攻击者就拿到了宿主机的root。Linux Capabilities把root权限拆成一个个小单元让容器只拿必要的权限遵循最小权限原则。常用的Capabilities单元CAP_NET_BIND_SERVICE允许绑定小于1024的端口CAP_CHOWN允许修改文件所有者CAP_KILL允许杀进程等。默认容器只带了部分基础权限你可以按需增减。在Docker里用--cap-add和--cap-drop来调整。比如启动Nginx需要绑定80端口docker run --cap-addCAP_NET_BIND_SERVICE nginx。如果容器不需要改文件所有者就--cap-dropCAP_CHOWN。这样即使容器被攻破攻击者也干不了太多事。7.2 Seccomp系统调用白名单与攻击防护SeccompSecure Computing Mode限制进程能执行的系统调用。攻击者常滥用危险系统调用如fork、execve、mount来突破容器。Seccomp配置白名单只允许必要的调用如read、write、open禁止危险的。Docker默认启用了Seccomp已经禁止了fork、mount、umount等。你也可以自定义。比如一个只提供静态网页的容器根本不需要execve和fork就在配置里把它们禁掉。攻击者想用这些调用时会被Seccomp直接拦住。7.3 AppArmor与SELinux强制访问控制MACAppArmor和SELinux是Linux的强制访问控制MAC机制比自主访问控制更严格。•AppArmor基于路径的策略。比如给Nginx容器配个策略禁止它访问宿主机的/etc/shadow或/proc/sys。即使容器里是root也碰不到这些敏感文件。•SELinux基于标签的策略。给每个文件、进程、设备打标签然后配置标签间的访问规则。比如给容器进程打个标签宿主机敏感文件打另一个标签规则规定容器标签不能访问敏感文件标签。Docker里用--security-opt启用这些策略。Ubuntu默认用AppArmorCentOS默认用SELinux你按需配置就行。7.4 命名空间逃逸与内核漏洞防护命名空间逃逸是容器安全的核心风险——攻击者利用内核漏洞或配置不当突破命名空间拿到宿主机权限。常见方式内核漏洞如Dirty COW、滥用Mount Namespace挂载宿主机目录、通过User Namespace提权等。防护手段•及时更新内核补丁。厂商会修漏洞比如Dirty COW打个补丁就解决了。•规范容器配置。千万别用--privileged特权模式那会放开所有Capabilities几乎等于没有隔离。限制挂载权限别让容器挂载宿主机的/、/etc、/proc。合理配置User Namespace把容器里的root映射到宿主机的普通用户。• **用安全监控工具如Falco**实时检测异常系统调用、非法挂载、提权尝试等发现异常就报警并终止容器。7.5 容器镜像安全漏洞扫描与签名验证镜像不安全容器就别想安全。镜像可能带基础镜像漏洞、应用依赖漏洞、甚至恶意代码。漏洞扫描工具如Trivy、Clair、Aqua Security会扫描镜像每一层找出系统漏洞和应用漏洞给出严重等级和修复建议。Trivy可以集成到CI/CD里在构建镜像时自动扫描从源头堵住漏洞。签名验证防止镜像被篡改。发布者用私钥给镜像签名用户拉取时用公钥验证。Docker Content TrustDCT就干这事。启用DCT后只拉取经过签名的可信镜像。另外用官方或可信的基础镜像定期更新依赖包删除镜像里不必要的组件减少攻击面。八、内核如何“协同”这3大技术启动一个容器8.1 步骤流程我来按时间线拆一个容器的诞生过程。以docker run -it ubuntu:22.04 /bin/bash为例从敲下命令到容器进程启动内核和 Docker Daemon 在这段极短的时间里完成了数十项操作。\1. Docker CLI 把请求发给 Docker Daemon通过 Unix Socket 或 TCP。\2. Daemon 检查本地有没有 ubuntu:22.04 镜像。没有就去 registry 拉。拉的过程中要解压镜像层、验证 checksum、存储到本地。\3. Daemon 创建容器的 OverlayFS 层结构——用已有的镜像只读层作为 lowerdir创建新的 upperdir 作为容器写入层建 workdir挂载 merged 目录。\4. 在 cgroupfs 里创建新的控制组通常路径类似/sys/fs/cgroup/system.slice/docker-container-id.scope/写入 CPU、内存等限制值。\5. 创建 Network Namespace配置 veth pair把 veth 的一端加入 docker0 网桥另一端放入容器的 Network Namespace 并重命名为 eth0分配 IP 地址配置路由。\6. 调用clone()创建新进程带上所有需要的CLONE_NEW*旗标子进程进入全新的 Namespace 集合。\7. 在子进程内部pivot_root到 merged 目录挂载/proc、/sys、/dev/pts等必要的虚拟文件系统。然后把子进程的 PID 写入步骤 4 创建的控制组的cgroup.procs文件。\8. 子进程exec用户指定的命令——/bin/bash。容器进程正式上线。\9. Daemon 收集容器进程的 stdout/stderr通过管道让你在终端上看到交互输出。8.2 关键节点四个节点依次执行创建命名空间 → 配置Cgroups → 挂载UnionFS → 启动init进程。命名空间划边界Cgroups设上限UnionFS给环境init进程跑应用。•创建命名空间这是容器的骨架。内核不再告诉你这个进程只是宿主机上无数进程中的一员而是给了它一个“只属于自己的世界”的幻象。•配置 Cgroups 资源限制用文件操作完成。容器运行时在 cgroupfs 里写数字内核调度器看到这些数字后会开始限制进程组的资源使用。没有这一步一个内存泄漏的容器可以让整台机器瘫痪。•挂载 UnionFSOverlayFS通过mount系统调用完成。内核把多个目录叠成单个目录容器的 rootfs 呈现在进程面前。而且镜像层只有只读所有修改都在独立的可写层。•启动 init 进程靠cloneexec组合完成。新进程带全套 Namespace 出生然后内核移交控制权给用户空间的应用容器就算“活了”。8.3 内核调用容器启动本质上就是几个系统调用的协同。•clone()创建新进程同时通过标志位为新进程创建命名空间。比如CLONE_NEWPID给新PID命名空间CLONE_NEWNET给新网络命名空间CLONE_NEWNS给新Mount命名空间。调用完成后新进程就在隔离环境里跑了。•mount()负责挂载文件系统。容器启动时mount要做两件事一是挂载UnionFS把只读层lowerdir和可写层upperdir叠加到容器的根目录二是挂载数据卷如果有把宿主机目录或分布式存储挂载到容器内指定路径。得益于Mount Namespace这些挂载只影响当前容器。•cgroupfs相关系统调用Cgroups以文件系统形式暴露在/sys/fs/cgroup下。通过mkdir在对应子系统下创建控制组目录用write把配额如CPU的50000写入cpu.cfs_quota_us再用attach_task把容器进程挂进去资源限制就生效了。三个调用的协同逻辑clone()造隔离环境mount()搭文件系统cgroupfs设资源上限。理解了它们怎么配合你就摸清了容器底层的门道排障、优化、安全防护都有了基础。