- Linux 中的容器依靠命名空间来隔离进程可以看到的内容,并依靠 cgroups 来限制它们可以使用的资源数量。
- 根据具体情况,命名空间(PID、NET、MNT、UTS、IPC、USER、cgroup、时间)和 cgroup 控制器(CPU、内存、I/O、pids)有多种类型,可以组合使用。
- Docker、Kubernetes 和 systemd 都依赖于这些内核原语来创建隔离环境、强制执行资源策略并统一管理服务。
- 监控 cgroup 指标和了解命名空间拓扑对于诊断内存溢出、性能问题以及确保生产环境中的可靠隔离至关重要。
如果你使用 Docker、Kubernetes或任何容器平台,迟早都需要深入了解系统的内部运作机制。容器在几秒钟内启动的“魔法”背后,是 Linux 内核的两个关键组件:命名空间和 cgroups,它们负责隔离和精细化的资源管理。理解它们,就如同从工具的简单使用走向基础设施的精心设计,两者之间有着天壤之别。
本文将以平和的语气,但不会深入探讨其工作原理。 命名空间(进程可以看到的内容)和控制组(进程可以使用的内容)Docker 和 Kubernetes 如何利用它们;以及如何使用类似这样的命令在底层对它们进行调整。 unshare, ip netns 或直接写信 /sys/fs/cgroup在生产环境中,有哪些技巧对性能、安全性和故障排除有用?
Linux 系统中的容器究竟做了什么?
当你运行类似这样的程序时 docker run不会创建神奇的虚拟机; 你将得到一个具有强大隔离性和有限资源的 Linux 进程(或一组进程)。这个“技巧”基于四个核心机制:
- 命名空间它们控制进程可以看到的内容(PID、网络、文件系统、主机名等)。
- 团体它们控制进程可以使用的资源(CPU、RAM、I/O、PID 等)。
- Chroot/rootfs 隔离它们为进程提供自己的文件系统。
- 正常的 Linux 进程归根结底,这一切都取决于内核管理的主机进程 ID (PID)。
换句话说,容器就是一个运行在一组新的命名空间中的进程,它嵌套在一个或多个 cgroup 中,并拥有一个自定义的文件系统。其余部分则由工具(Docker、containerd、runc 等)通过自动化和用户友好的 API 来处理。
Linux命名空间:隔离每个进程可以看到的内容
命名空间是内核的一个抽象层,它为进程组创建独立的“世界”。进程只能看到其命名空间内的资源;系统的其余部分仍然存在,但对进程是不可见的。
Linux 目前有几种命名空间,每种命名空间都专注于系统的特定部分。它们结合起来,使得容器即使与宿主机共享内核,也能被视为一个独立的操作系统。
命名空间的主要类型
每种命名空间隔离的维度都不同。在“严肃”的容器中,通常会同时使用多种命名空间。以下是最重要的几种:
- PID命名空间:隔离进程标识符空间。 在容器内,主进程通常是 PID 1。即使主机拥有不同的进程号,来自不同命名空间的进程也可以拥有相同的进程 ID (PID) 而不会相互干扰,并且除非主机明确允许,否则它们不能相互发送信号。
- 网络 (NET) 命名空间:创建一个独立的网络堆栈,拥有自己的接口、路由、防火墙规则、套接字等。 每个容器都有自己的环回地址、自己的 IP 地址和自己的路由。这样,您就可以在不同的命名空间中重复使用同一个 IP 地址而不会发生冲突,并应用隔离的 iptables 规则。
- 挂载 (MNT) 命名空间将视野与安装点隔开。 进程只能看到其命名空间的文件层次结构。即使底层存在绑定挂载、共享卷、覆盖层等,这些机制也能让容器看起来像是拥有自己的“/”目录,而无需触及宿主机的实际根目录。
- UTS命名空间:控制主机名和 NIS 域。在 UTS 命名空间内, 您可以更改主机名而不会影响主机系统。非常适合用于区分容器或在测试环境中模拟节点。
- IPC命名空间:隔离 System V 消息队列、信号量和共享内存。 防止不同的容器进程干扰进程间通信 (IPC)。 由于事故或设计缺陷。
- 用户命名空间或许是最微妙的。它允许 将内部命名空间 UID 和 GID 映射到其他主机 ID这使得容器内的“root”用户能够映射到主机上的非特权用户,从而大大降低了权限提升的风险并提高了安全性。 容器安全.
- Cgroup 命名空间提供虚拟化视图
/proc/self/cgroup以及 cgroups 层次结构。 从容器内部,只能看到它自己的组。这掩盖了宿主的实际结构,并加强了隔离作用。 - 时间命名空间允许提供 不同的时间感知(例如 CLOCK_MONOTONIC 或 CLOCK_BOOTTIME 等时钟) 适用于不同的进程组。它可用于测试场景或进行非常精细的隔离。
所有这些命名空间都公开在 /proc/<pid>/ns/并且可以与以下工具创建或结合使用: unshare y nsenter当属于某个命名空间的最后一个进程终止时,该命名空间也会自动销毁。
从命令行尝试使用命名空间
命令 unshare 这是实验的基本要素之一: 创建具有一个或多个新命名空间的子进程例如,要测试一个隔离的 PID 命名空间:
unshare --pid --fork --mount-proc bash
ps aux
echo $$
在容器内部,你只会看到该命名空间中的进程,你的 shell 进程 ID 会显示为 1,即使主机仍然拥有所有常规进程。这基本上就是 OCI 运行时在容器启动时执行的操作框架。
网络命名空间详解
网络方面是最灵活的方面之一。创建 .NET 命名空间时,需要用到它。 ip netns add除了回环栈之外,你会得到一个空栈。从那里你可以:
- 将该命名空间连接到主机 与一对
veth(两个虚拟接口通过逻辑电缆连接)。 - 搭建桥梁 类型
br0并将多个命名空间连接到该桥接器,以便它们可以使用私有 IP 地址相互访问。 - NAT和转发 使用 iptables 为这些“自制容器”提供互联网访问权限
/proc/sys/net/ipv4/ip_forward.
没错,从底层来看,Docker 就是通过桥接器来实现这一点的。 docker0 以及它们的默认网络: 它设置网络命名空间、veth 对等体、网桥和 NAT 规则。 这样集装箱就可以互相通信并到室外去。
根文件系统和安全的挂载点和用户命名空间
使用 `mount namespace`,您可以在不使用 Docker 的情况下尝试创建容器文件系统。典型的模式是:
- 组装或准备 最小根文件系统 (例如,下载的 Alpine minirootfs)
wget并提取到目录中)。 - 创建一个挂载命名空间并执行以下操作
pivot_rootochroot到该目录。 - 蒙塔尔
/proc,/sys以及新根目录下的其他伪系统。
这样一来,你的 shell 就只能看到那个文件系统了,但是 您仍然是一个普通的宿主进程,拥有真实的进程 ID (PID),并且可以由管理员控制。如果您还使用用户命名空间, --map-root-user你可以拥有一个位于普通用户之外的“root”用户,但他没有相应的权限。
Cgroups:每个进程可以消耗多少资源
命名空间决定了进程可以看到哪些资源,而 cgroups 则限制了一组进程可以消耗的系统资源数量。它们在内核 2.6.24 中引入,现在对于任何现代容器平台来说都至关重要。
从概念层面来说,c群是 目录 /sys/fs/cgroup 包含描述限制、优先级和指标的文件当您将 PID 添加到相应的文件时(例如, cgroup.procs该过程须遵守这些规则。
cgroups 中的资源控制器
Cgroups 由作用于特定资源的控制器(子系统)组成。其中最常用的包括:
- 中央处理器:控制分配的 CPU 时间。 股份 定义相对优先级; 配额 硬性限制(例如,每 100 毫秒周期内 CPU 超时时间为 50 毫秒); CPU集 你将进程固定到特定的核心上。
- 内存限制内存和交换空间。允许 硬限制和软限制、OOM killer 设置、统计数据和内存压力对于防止某个服务占用整个服务器资源非常有用。
- 块 I/O (blkio / io)限制每个设备的带宽和 IOPS。您可以 分配比例权重或设置字节/秒操作/秒的精确限制.
- 网络虽然存在限制带宽和数据包的机制,但支持还不够成熟,通常与 tc 和 QoS 结合使用。
- PID:标记 最大进程/线程数 它可以创建一个组,从而缓解 fork 炸弹或触发线程的代码错误。
实际操作起来非常简单:将应用程序放在一个 cgroup 中,并指定 CPU、内存、I/O 和 PID 的最大限制。之后,内核会负责执行这些规则。
cgroups v1 与 cgroups v2
在生产环境中,你会发现两种类型的 cgroups,因此明确你正在使用哪一种非常重要:
- cgroup v1每个控制器都有自己独立的层级结构。路由如下:
/sys/fs/cgroup/cpu/...,/sys/fs/cgroup/memory/...等等。 它很灵活,但大规模管理起来却很复杂。许多经典发行版和平台仍然使用或支持它以实现兼容性。 - cgroup v2将所有事物归为一类 统一层级 en
/sys/fs/cgroup这些限制是通过诸如此类的文件配置的。cpu.max,memory.max,io.max等等。 它简化了管理,并提高了资源的精细化调整能力。现代发行版通常默认使用 v2 版本,有时会采用混合模式。
En /proc/cgroups 您可以查看哪些控制器已启用、有多少个活动的 cgroup,以及您当前使用的是 v1 还是 v2 版本(在 v2 版本中,您将看到 ID 为 0 的统一层级结构)。该命令 mount | grep cgroup 它还会告诉你文件系统的类型(cgroup2fs 意味着 v2, tmpfs (通常指向 v1)。
手动创建 cgroups 和典型限制
要彻底理解 Docker 的底层工作原理,建议手动创建 cgroups。一般模式如下:
- 创建 cgroup 目录。
- 配置文件限制/优先级 (CPU、内存、I/O、PID……)
- 通过输入以下命令将 PID 添加到 cgroup:
cgroup.procsotasks(在 v1 中)。
例如,在 cgroups v2 中,您可以对 CPU 和内存进行类似这样的配置:
mkdir /sys/fs/cgroup/test_app
echo "+cpu +memory +io +pids" > /sys/fs/cgroup/cgroup.subtree_control
echo "50000 100000" > /sys/fs/cgroup/test_app/cpu.max
echo "512M" > /sys/fs/cgroup/test_app/memory.max
echo $$ > /sys/fs/cgroup/test_app/cgroup.procs
因此,当前流程仅限于此。 50% 的核心使用率和 512 MB 内存如果你开始做一些高强度的事情(a dd 例如,如果将 `/dev/null` 的值更改为 `/dev/null`,你会发现系统不会出现异常。在 cgroups v1 中,方案类似,但使用类似这样的文件: cpu.shares, cpu.cfs_quota_us, memory.limit_in_bytes等等,各自处于不同的层级结构中。
Docker、Kubernetes 和内核堆栈如何运作
在命名空间和 cgroups 之上,还有好几层软件层层传递,最终到达内核。即使工具名称有所改变,最终结果也始终相同:内核调用来创建命名空间、配置 cgroups 和挂载文件系统。
如今典型的集装箱“堆叠”看起来大致如下:
- 编排层Kubernetes、Docker Swarm、Nomad……它们负责处理 Pod/Service 的生命周期、调度、扩展和支持。 微服务架构.
- 高级容器运行时Docker Engine、containerd、CRI-O。 他们接收指挥者的指令,并将其转化为具体的行动。 (下载镜像、创建容器、管理快照等)。
- 底层 OCI 运行时Runc、crun 和类似工具实现了 OCI 规范,并直接负责实现该规范。 他们称之为
clone(),unshare(),mount()和公司 将容器实体化。 - Linux内核它提供了命名空间、cgroups、OverlayFS 以及所有其他基本功能。真正的“魔法”就在这里发生。
当你扔一个 docker run路径为:CLI 客户端 → dockerd → containerd → runc → 内核调用。在此路径上,会创建命名空间、分配 cgroups、挂载带有 OverlayFS 和卷的根文件系统,并连接网络接口和网桥。 你看到的“容器”,对内核而言,只是一组带有特定标志和特殊路径的进程。.
系统中的要求和预先检查
在服务器上开始摆弄命名空间和 cgroups 之前,最好确保硬件和内核满足合理的最低要求,特别是如果您计划设置类似于生产环境的东西。
作为参考,通常建议至少配备2 个逻辑 CPU、4 GB 内存(最好是 8 GB 或更多)、约 20 GB 的可用磁盘空间,以及 3.10 或更高版本的内核(理想情况下是 4.x 或 5.x,以便拥有 cgroups v2 的所有功能和现代特性)。
要验证内核是否支持命名空间和 cgroups,只需检查当前使用版本的配置文件即可:
grep -E "CONFIG_.*_NS" /boot/config-$(uname -r)
grep -E "CONFIG_CGROUP" /boot/config-$(uname -r)
应该会显示类似这样的选项。 CONFIG_NAMESPACES、CONFIG_PID_NS、CONFIG_NET_NS、CONFIG_CGROUPS、CONFIG_MEMCG、CONFIG_CGROUP_SCHED 标记为 =y如果没有它,你就玩不了什么游戏了。
另一个实际细节是该系统是否允许 无需root权限即可创建用户命名空间这对于测试和某些工具非常有用。它由sysctl控制。 kernel.unprivileged_userns_clone如果数值为 0,你就必须激活它(或者用你的汗水把它全部激活)。
在“真实”机器上探索 cgroups 和命名空间
理解这些概念的一个比较直接的方法是观察系统本身以及容器是如何使用它们的。以下是一些有用的提示:
ls /proc/self/ns/它会显示当前 shell 所属的命名空间(cgroup、ipc、mnt、net、pid、user、uts……)。systemd-cgtop它按 cgroup 显示 CPU、内存和 I/O 使用情况,与此非常相似。top但按服务和容器分组。- En
/sys/fs/cgroup您可以通过路径中的容器 ID 找到 Docker 或 Podman 为每个容器创建的 cgroups。cgroup.procs列出关联的PID. /proc/<pid>/cgroup它会告诉你特定进程被分配到哪些 cgroups,非常适合追踪有问题的应用程序。
你会发现容器其实只是cgroups 层级结构中的另一个分支,它们各自拥有 CPU、内存、blkio 和其他方面的限制,就像你可以手动创建它们一样。Docker 的优势在于它能够自动完成这些操作,并且提供了用户友好的 API。
性能、NUMA 和亲和策略
在高负载场景(大型数据库、密集型处理、流式传输等)中,仅仅“启动容器”是不够的。如何为每个服务分配 CPU 和内存会对性能产生显著影响。
使用cpuset驱动程序,您可以将进程或服务分配给特定的 CPU 和 NUMA 节点集。操作非常简单:
mkdir /sys/fs/cgroup/cpuset/dedicated_cpus
echo "4-7" > /sys/fs/cgroup/cpuset/dedicated_cpus/cpuset.cpus
echo "0" > /sys/fs/cgroup/cpuset/dedicated_cpus/cpuset.mems
echo 1 > /sys/fs/cgroup/cpuset/dedicated_cpus/cpuset.cpu_exclusive
它允许您为特定服务预留一组核心,并防止系统其他部分进行干扰。这可以减少噪声、线程迁移和延迟问题,这些问题通常会在所有程序争用相同 CPU 时出现。
在内存层面,将numactl与 cgroups 结合使用(或者在可用时直接使用内存控制器的 NUMA 设置)有助于将数据保存在最靠近使用该数据的内核的物理节点上。这在对延迟敏感的应用程序中尤为明显。
cgroups 和命名空间的监控和可观测性
设置资源限制是可以的,但是 如果你不衡量正在发生的事情,你可能会在不知不觉中搬起石头砸自己的脚。cgroup v2 本身在日志中提供了相当多的有用指标。 *.stat, *.events y *.pressure.
一些实际的例子:
cpu.stat它会告诉你该组使用了多少 CPU 时间,被限制了多少次等等。memory.currentymemory.max它们显示了当前消耗量和配置的限制。memory.events它会记录内存不足、达到软限制和其他异常情况。io.stat累加每个设备的读写次数。
简而言之, systemd-cgtop 它非常实用,如果你想要更工业化的工具,比如 顾问 他们将 Docker/containerd cgroup 指标导出到 Prometheus,使您能够轻松地在 Grafana 中构建完整的仪表板,非常适合…… 高级系统监视器.
调试命名空间问题时,可以使用简单的脚本遍历这些脚本。 /proc/<pid>/ns/ 比较符号链接有助于 要了解哪些进程共享完全相同的命名空间。或者,查找已卡住的“丢失”的网络命名空间。
使用 systemd 作为 cgroup 管理器
在大多数现代发行版中,systemd 大量依赖 cgroups 来管理服务、切片和作用域。当你定义一个带有资源边界的单元时,systemd 实际上会为该服务创建一个 cgroup 并配置其处理程序。
使用如下指令声明的服务:
CPUQuota=50%
CPUWeight=500
MemoryMax=512M
IOReadBandwidthMax=/dev/sda 10M
这意味着该单元进程的CPU、内存和 I/O 都将受到 cgroup 的限制。这使得“传统”服务管理能够与容器模型保持一致,并使用相同的底层内核子系统。有关 systemd 及其要求的更多详细信息,请参阅systemd 变更。
对于将传统服务与容器混合使用的团队(非常常见),依靠 systemd 来限制关键守护进程是一种优雅的方式,可以防止容器化环境中的 pod 终止资源。
在包含多种工作负载的环境中,例如数据库、监控代理和自定义应用程序,掌握命名空间和 cgroups 可以帮助您定义强大的隔离策略、调整服务质量,并确保每个组件都拥有各自独立且可控的 CPU 和内存分配。了解 Docker、Kubernetes 和 systemd 如何利用这些内核功能,能够帮助您设计更安全的多租户基础架构,调试复杂问题(从内存溢出到 I/O 瓶颈),并在不牺牲稳定性的前提下最大限度地发挥硬件性能。