
系列专栏《深入剖析 Kubernetes》· 基于张磊极客时间专栏思想整理 · 适合 CSDN 发布01 白话容器基础从进程说开去很多人把容器想得很神秘——好像它是一种全新的虚拟化技术和虚拟机并列。其实不然。容器技术没有任何黑魔法它的全部秘密都藏在 Linux 内核早已存在的两组机制里namespace命名空间和cgroups控制组。一句话总结容器就是一个被 namespace 隔离了视图、被 cgroups 限制了资源的、普通的 Linux 进程。理解这一点是理解后续一切 K8s 概念的基石。一、进程一切的起点我们在 Linux 上启动任何一个程序操作系统做的第一件事就是创建一个进程。看一个最朴素的例子# 启动一个 sleep 进程$sleep100[1]2845$ps-ef|grepsleeproot28451010:00 ? 00:00:00sleep100进程2845有三个关键信息PIDProcess ID进程号内核给它的唯一身份证。父 PIDPPID它的父亲这里是1通常是 init/systemd。视图它能看到哪些 PID、哪些网络、哪些挂载点由内核决定。所谓容器不过是在创建这个进程时多传了几个参数CLONE_NEWPID、CLONE_NEWNET等告诉内核“请给它一套独立的视图同时限制它的资源用量”。于是这个进程以为自己独占了这台机器其实它和宿主机上其他进程共享同一个内核。宿主机进程树简化 容器视角PID namespace 内 ┌────────────────────────┐ ┌────────────────────────┐ │ systemd (PID 1) │ │ 你的进程 (PID 1) │ │ ├─ dockerd (1023) │ │ └─ app (PID 7) │ │ ├─ kubelet (1540) │ 隔离 │ │ │ └─ sleep (2845) │ 视图 │ (对外仍是 2845) │ └────────────────────────┘ └────────────────────────┘ 真实 PID 视图 被 namespace 包装后的视图二、namespace给进程一套私人视图namespace 的本质是把内核的某一部分全局资源做按进程分组的隔离。Linux 一共提供了 7 类本文聚焦容器常用的 6 类第 7 类 Time 较少用namespace中文隔离内容创建标志UTS主机名/域名hostname、NIS domainCLONE_NEWUTSPID进程号进程编号空间CLONE_NEWPIDMount挂载点文件系统挂载树CLONE_NEWNSNetwork网络网卡、IP、端口、路由CLONE_NEWNETUser用户UID/GID 映射CLONE_NEWUSERIPC进程间通信共享内存、信号量、消息队列CLONE_NEWIPCUTS namespace让容器可以有自己的主机名这也就是为什么你在容器里hostname看到的是一串随机 ID而不是宿主机的名字。Mount namespace是容器拥有独立文件系统的关键。容器启动时把自己的 rootfs 挂载为根目录pivot_root/chroot于是容器内/和宿主机/完全脱钩。Network namespace让每个容器拥有独立的网络栈自己的lo、eth0、独立的端口空间。所以两个容器都能监听0.0.0.0:8080而不冲突——它们在各自的 netns 里。User namespace是安全刚需容器内以rootUID 0运行但在宿主机上其实映射成了一个普通的高位 UID如 100000即使容器被攻破也拿不到宿主机的真正 root 权限。实际查看一个容器的 namespace# 先拿到容器内 1 号进程的 PID宿主机视角$dockerinspect-f{{.State.Pid}}mycontainer2845# 查看该进程关联的 namespace$ls-l/proc/2845/ns/ lrwxrwxrwx... ipc -ipc:[4026532529]lrwxrwxrwx... mnt -mnt:[4026532527]lrwxrwxrwx... net -net:[4026532532]lrwxrwxrwx... pid -pid:[4026532530]lrwxrwxrwx... uts -uts:[4026532528]lrwxrwxrwx... user -user:[4026532531]方括号里的数字就是 namespace 的 inode 编号。两个进程指向同一个 inode说明它们在同一 namespace 里。Docker 正是靠共享这些 ns来把多个进程塞进同一个容器的。三、cgroups给进程套上资源紧箍咒namespace 解决看得到什么cgroupscontrol groups解决能用多少。它限制、记账、隔离进程组对 CPU、内存、IO 等资源的消耗。cgroups v1 vs v2cgroups v1按子系统subsystem分别挂载如cpu、memory、blkio。结构复杂多控制器协调困难。cgroups v22016 起、K8s 1.25 默认推荐统一层级unified hierarchy一个cgroup.controllers管理全部支持更精准的CPU 权重 硬性上限且原生支持Memory QoS。新版 Kubernetes≥1.25配合 systemd 驱动默认用 v2。限制一个进程的内存v2 示例# 创建 cgroup限制最多 100MB 内存$mkdir/sys/fs/cgroup/demo $echo100M/sys/fs/cgroup/demo/memory.max $echo2845/sys/fs/cgroup/demo/cgroup.procs限制 CPUv2限制为 0.5 核# cpu.max 配额 周期下面表示每 100000us 最多用 50000us$echo50000 100000/sys/fs/cgroup/demo/cpu.max在 Kubernetes 里你写的resources.limits/requests最终就翻译成这些 cgroup 文件resources:requests:# 调度依据 cgroup 软下限v2 的 cpu.weight / memory.lowcpu:250m# 0.25 核memory:256Milimits:# 硬上限cgroup 的 cpu.max / memory.maxcpu:500m# 超额会被 throttlememory:512Mi# 超额触发 OOM Kill四、常见误解“容器里只能跑一个进程”这是一个流传很广的误解。从技术上讲容器里完全可以跑多个进程——它终归只是个被隔离的 Linux 进程空间。你可以在容器里启动nginx sidecar cron它们共享同一个 PID namespace。那为什么大家都说一个容器一个进程这是工程实践层面的建议不是技术限制PID 1 的责任容器内第一个进程PID 1要扮演 init 的角色——回收僵尸进程、正确转发信号SIGTERM。很多普通程序不擅长这个会导致僵尸进程堆积或优雅退出失败。生命周期耦合多个进程塞一个容器扩缩容、健康检查、资源统计都变成一锅粥。K8s 的抽象Pod 才是调度单位把不同职责拆成多个容器主容器 sidecar是正道。所以正确说法是容器能跑多进程但你最好别这么干。五、经典深坑JVM 读不到真实内存这是 Java 应用在容器里最常见的生产事故。早期 JVMJava 8u131 之前启动时会去读/proc/meminfo来算本机有多少内存再据此设置堆大小Heap。问题来了cgroups 限制了容器只能用 512Mi但 JVM 读的是宿主机的/proc/meminfo比如 64GiB于是它欢快地给自己分配了几十 G 的堆——远超容器限额结果就是被 cgroup 的 OOM Killer 直接干掉。这就是俗称的“%mem” 问题容器内存超卖导致 OOM。解决方案JDK 10 默认开启8u191 需手动# 让 JVM 读取 cgroup 限额而非宿主机 meminfojava-XX:UseContainerSupport\-XX:MaxRAMPercentage75.0\# 堆最多占容器内存的 75%-jarapp.jar-XX:UseContainerSupport启用容器感知JDK 8u191 / 10 默认开。-XX:MaxRAMPercentage用百分比取代写死的-Xmx更贴合弹性环境。这也是为什么在容器化 Java 服务时务必设置容器内存 limit并显式确认 JVM 版本已支持容器感知否则明明内存够却频繁 OOM会让你排查到怀疑人生。小结 / 核心要点容器 被 namespace 隔离视图 被 cgroups 限制资源的普通进程不是虚拟化共享宿主机内核。6 类核心 namespaceUTS主机名、PID进程号、Mount挂载树、Network网络栈、UserUID 映射、IPC通信。它们共同构造出独立机器的假象。cgroups v2 是趋势统一层级、原生 Memory QoSK8s 的 requests/limits 最终落盘为 cgroup 文件。容器只能跑一个进程是误解技术上可多进程工程上建议单进程以便生命周期与扩缩容管理。JVM 容器坑老版本 JVM 读宿主机 meminfo 导致堆过大被 OOM用-XX:UseContainerSupport和MaxRAMPercentage解决。理解了容器不过是个被精心约束的进程下一讲我们再看这个进程赖以生存的文件系统——容器镜像到底是怎么一回事。