想象你搬进一栋三十层的高层公寓。楼里其实住了几百户人家,但你几乎感觉不到他们的存在:你有自己的门牌号、自己的路由器、自家的水电表,家里那一摞档案甚至从“第 1 号”开始登记。客厅、厨房、卧室一应俱全,住久了你会恍然觉得自己买的是独栋别墅。可事实上,大家共用同一栋楼的地基、承重墙和总水塔。要让“合租”住出“独栋”的感觉,靠的不是多盖几栋楼,而是两套精妙的制度。第一套叫 Namespace,它负责“视野”——给每户发一堵看不见的隔断墙,让住户只看得见自己的门牌、进程、网卡和数据,仿佛整栋楼只有自己一家。第二套叫 Cgroups,它负责“账本”——物业给每户装上水电表,CPU、内存这些公共资源谁也不能无限占用。在这两套制度之上,还有 eBPF 这枚“智能摄像头”,让物业能实时观察每户的水电流量,既不进屋,也不打扰。这套“视野隔离 + 资源配额”的组合,正是 Linux 容器(Docker、Kubernetes)赖以运转的地基。

岗位关联

学懂 Namespace 与 Cgroups,你就掌握了云原生世界的“力学原理”:既知道应用为何“看”不到彼此,也知道如何精确限制每个应用的资源用量。这是容器排障、资源治理与安全加固的必修课。

目录
  1. 可视化演示
  2. 隔离思想启蒙
  3. 进程编号隔离
  4. 网络空间隔离
  5. 其余四种空间
  6. 资源配额控制
  7. 统一层级革新
  8. 云原生运行时
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

演示Linux容器隔离机制:六种Namespace隔离维度、Cgroups v2资源控制、容器三层架构组合,三种模式交互切换。

本节配有交互动效(进入视口后自动加载,约 3KB)。

六种Namespace隔离维度 PID:独立进程号空间 Net:独立网络协议栈 Mount:独立文件系统视图 6种Namespace为容器提供完整隔离视图

隔离思想启蒙

你在租来的公寓里装了个智能音箱,喊一声“关灯”,客厅应声而灭。你觉得自己住的就是独栋别墅——因为你压根看不到隔壁老王家的客厅,也听不到他家的电视声。公寓楼靠“物理隔断”制造了独栋错觉。可如果是几十台服务器拼出的“云”,给每户都砌一道水泥墙就太奢侈了。内核想了个更省的办法:不给每户发独立地基,只给每户发“独立视野”。

这个“独立视野”机制在 Linux 里叫 Namespace(命名空间)。当一个进程被放进某个命名空间,它就“以为”自己独占了一台机器:能看到的进程表、网卡、挂载点、主机名,都是这个空间私有的视角。早在 UNIX 时代,chroot 只能隔离文件系统路径;如今内核一口气提供了六种 Namespace,分别隔离六个维度。

六种分别是:PID(进程编号)、NET(网络栈)、UTS(主机名)、IPC(进程间通信)、MNT(挂载点)、USER(用户映射)。它们由 clone() / unshare() 系统调用创建,子进程默认继承父进程所在的命名空间,于是形成一棵“视图继承树”。注意容器与虚拟机的根本区别:虚拟机自带独立内核,像一户人家自建地基;容器共享宿主机内核,只是视野和账本被隔开。所以一台物理机能跑几百个容器,但隔离强度不如虚拟机。

这就是公寓楼的“借景”装修:承重墙不能动,但每户都通过隔断、磨砂玻璃和独立门锁,获得“整栋楼就我一家”的体验。Namespace 就是那几扇磨砂玻璃——廉价,但够用;真正的承重墙,始终握在内核手里。

N={ PID, NET, UTS, IPC, MNT, USR }\displaystyle N = \{\, \mathrm{PID},\ \mathrm{NET},\ \mathrm{UTS},\ \mathrm{IPC},\ \mathrm{MNT},\ \mathrm{USR}\, \}
六种 Namespace 构成一个“视野隔离工具箱”,每种工具只负责一种维度的隔断。
  1. 集合 N把所有“把视野隔开”的内核机制装进一个集合,便于整体讨论。集合中的每一个元素都是某种命名空间。
    N={ … }N = \{\, \dots \,\}
  2. PID 与 NET进程编号和网络栈,是容器用得最多的两个隔离维度,分别负责“进程隐私”与“网络隐私”。
    PID, NET\mathrm{PID},\ \mathrm{NET}
  3. UTS、IPC、MNT、USR主机名、进程间通信、挂载视图和用户映射,补齐其余四种维度,凑成完整的“独栋错觉”。
    UTS, IPC, MNT, USR\mathrm{UTS},\ \mathrm{IPC},\ \mathrm{MNT},\ \mathrm{USR}

进程编号隔离

老王家的户口本第一页写的是户主老王,第二页是孩子。你家的户口本第一页也写着“户主”。两本户口本都从 1 号开始编号,谁也不跟谁打架——因为户口本只在派出所的某个分区里登记。PID Namespace 干的就是这件事:让每个容器都拥有一本从“第 1 号”开始的私人户口本。

在 Linux 里,每个进程在宿主机上都有一个全局唯一的 PID,从 1 号(init 进程)开始递增。但进程一旦进入某个 PID 命名空间,它就会在该空间里被重新编号:空间里第一个进程变成 PID 1。容器启动时,用户指定的第一个程序(比如 nginx 或 java)就是这个“户主”。你在容器里执行 ps,只能看到本空间的进程,宿主机那几百个进程全被隔墙挡在视野之外。

PID 1 还肩负两项特殊职责:收养孤儿进程(父进程先死后,孤儿被内核挂到 PID 1 名下);转发信号(docker stop 向 PID 1 发送 SIGTERM)。经典坑点:如果容器入口程序不处理 SIGTERM,docker stop 会一直等到超时然后强杀。这就像户主不在家时,物业敲门没人应,只能破门而入。

所以排查容器问题时先得“对表”:容器内报 PID 456 的进程,在宿主机上可能是 98765。运维老手会在两本户口本之间来回翻译——这正是 docker inspect 里 Pid 字段存在的意义。

Pi↦(pidns(Pi), pidhost(Pi))\displaystyle P_i \mapsto \left( \mathrm{pid}_{\mathrm{ns}}(P_i),\ \mathrm{pid}_{\mathrm{host}}(P_i) \right)
宿主机上的每个进程,在容器内和宿主机上各有一个“身份编号”,两者构成一对映射。
  1. 宿主进程 P_i内核进程表里的某个真实进程,比如容器里 nginx 的一个 worker 线程。
    PiP_i
  2. 映射箭头把单个进程“分身”成两个编号——对内一个、对外一个,就像同一个人在家叫“爸爸”、在公司叫“王工”。
    ↦\mapsto
  3. 容器内编号进程在所属 PID 命名空间里看到的编号,每个空间自己从 1 开始重新计数。
    pidns(Pi)\mathrm{pid}_{\mathrm{ns}}(P_i)
  4. 宿主机编号全局唯一、从 0 开始递增的真实 PID,所有命名空间共用这一套“总台账”。
    pidhost(Pi)\mathrm{pid}_{\mathrm{host}}(P_i)

网络空间隔离

每户公寓都有独立的路由器接口,墙壁还是隔音的——隔壁的 Wi-Fi 信号无论怎么扫都进不了你家。这就是 NET Namespace 的比喻。网络空间隔离是六种 Namespace 中与 TCP/IP 章节关系最紧的一个:每个命名空间都拥有一整套独立的网络协议栈视图。

一个 NET Namespace 里包含:网卡设备(包括回环 lo)、IPv4/IPv6 地址、路由表、iptables / netfilter 规则、端口监听表(socket 表)。因此,容器 A 可以监听 80 端口,容器 B 同样可以监听 80 端口——它们的“插座表”分属两个独立的网络栈。这直接回答了新手最常见的问题:“两个容器为什么能用同一个端口?”

但别忘了关键边界:宿主机自己也拥有一个 netns,宿主机上的 80 端口是全楼唯一的。docker run -p 8080:80 的意思是“把容器内 80 号插座接到大楼总机的 8080 号插孔”,总机插孔依然全局唯一。默认情况下,两个 netns 之间完全不通,必须用虚拟以太网设备 veth pair(一根虚拟网线)把两户连起来,再借助 bridge(虚拟交换机)或路由让它们互通。这正是 Docker bridge 网络与 Kubernetes CNI 插件的底层原理。

排查“容器间网络不通”时,先问一句:它们之间拉虚拟网线了吗?挂在同一台虚拟交换机上了吗?隔音墙太厚,没有网线就连不上——这不是 bug,是设计使然。

netnsA∩netnsB=∅\displaystyle \mathrm{netns}_A \cap \mathrm{netns}_B = \varnothing
两个网络命名空间的资源集合之交为空;想要通信,必须主动拉一条“虚拟网线”。
  1. 两个网络空间容器 A 与容器 B 各自拥有完整的网卡、路由表、端口表视图。
    netnsA, netnsB\mathrm{netns}_A,\ \mathrm{netns}_B
  2. 交集为空没有任何一个网络对象同时属于两个空间——彻底“隔音”,数据包默认无法穿越。
    ∩=∅\cap = \varnothing
  3. veth 作为唯一桥梁用一对虚拟网卡(veth pair)把两户“接线”,接上之后才谈得上通信。
    veth:netnsA↔netnsB\mathrm{veth}: \mathrm{netns}_A \leftrightarrow \mathrm{netns}_B

其余四种空间

门牌号(UTS)、内线电话(IPC)、储物间图纸(MNT)、以及“在家是主人、出门是租客”的身份切换(USER)。剩下这四种命名空间,像公寓里四个不起眼却不可或缺的设施,共同补齐了独栋错觉的最后几块拼图。

UTS Namespace 隔离主机名(hostname)和 NIS 域名。每户可以给自己的门牌起名:在容器里执行 hostname 改成 web-01,宿主机和别的容器完全无感。IPC Namespace 隔离 System V 消息队列、信号量和共享内存。每户只有自己家里的内线电话,拨不通邻居家的分机——所以两个容器即使内存共享机制开放,也无法直接通过 IPC 对象互通。

Mount Namespace 隔离挂载点视图。同一个宿主机目录,在不同容器里可以挂到不同位置,甚至套用不同的文件系统参数。容器镜像的分层文件系统(overlayfs)正是靠它做到“每个容器看到自己的根文件系统”,就像物业让每位租客拿到一张专属的储物间图纸——同一个储物间,各有各的布局图。User Namespace 则维护一张 UID 映射表:容器里的 root(编号 0)映射到宿主机普通用户(如 uid 1000),于是容器内的“管理员”在宿主上毫无特权——相当于给租客发一把“看起来是万能钥匙、实际上只能开自家房门”的钥匙。

这四个空间加上前面的 PID、NET,凑齐了容器“独栋错觉”的完整拼图。下一块拼图是全局的“账本管控”——Cgroups。

0ns⟼1000host\displaystyle 0_{\mathrm{ns}} \longmapsto 1000_{\mathrm{host}}
User Namespace 把容器内的 root(编号 0)映射成宿主机上的普通用户,特权被大幅压缩。
  1. 容器内 root进程在用户命名空间里看到的“管理员”编号 0,在容器内拥有最高权限的幻觉。
    0ns0_{\mathrm{ns}}
  2. 宿主机普通用户这个“管理员”在宿主机视野里只是 uid=1000 的普通租客,删不了别人的文件。
    1000host1000_{\mathrm{host}}
  3. 权限收缩映射关系由 uid_map 文件维护,所有文件访问都按宿主机身份判定,超出映射范围的操作会被内核拒绝。
    ⟼\longmapsto

资源配额控制

视野可以隔断,但水电总是有限的。如果不装水表电表,某户疯狂开暖气开空调,整栋楼都会跳闸。Cgroups(控制组) 就是内核里的水电表:把一组进程圈进一个“控制组”,统一计量并限制它们的 CPU、内存、IO 等资源用量。

在 cgroup v1 时代,每个资源子系统各自成一棵树:cpu、memory、blkio 的目录散落在 /sys/fs/cgroup 下,一个进程可能同时挂在多棵树上。CPU 控制的核心是 CFS 带宽机制:cpu.cfs_quota_us(预算)配合 cpu.cfs_period_us(结算周期)。内存方面,memory.limit_in_bytes 设定上限,超过上限时内核先回收缓存页,还不够就触发 OOM 杀死组内进程——相当于给这户先减用水、再直接断水。

要特别分清楚两个概念:quota 是硬上限。设 50ms/100ms,表示每个 100ms 周期内最多给这组进程 50ms 的单核时间,用满就被限流(throttle),哪怕其它核此刻闲着。权重(shares)是软分配,只在带宽竞争时按比例分水,没有硬顶。类比:quota 是“这户每月最多用 100 吨水”,shares 是“全楼缺水时各户按比例分水”。

一个典型的生产事故:Java 服务在容器里识别到宿主机 32 核,拼命开线程,实际配额只有 2 个核,于是 CPU 被打爆、请求排队。解决方法是让 JVM 感知 Cgroup 配额。记住这句话:Namespace 管“看到什么”,Cgroups 管“能用多少”。

cpu_limit=cfs_quota_uscfs_period_us\displaystyle \mathrm{cpu\_limit} = \frac{\mathrm{cfs\_quota\_us}}{\mathrm{cfs\_period\_us}}
Cgroups 用“预算 / 结算周期”定义 CPU 硬上限,比值就是该控制组最多能占用的核数。
  1. cfs_period_us结算周期,默认 100000 微秒(100ms),相当于物业查一次水表的时间窗。
    cfs_period_us=100000\mathrm{cfs\_period\_us} = 100000
  2. cfs_quota_us每个周期分给该控制组的 CPU 时间预算,单位同样是微秒。
    cfs_quota_us\mathrm{cfs\_quota\_us}
  3. 比值即核数上限100000/100000 = 1 个核;50000/100000 = 0.5,最多半个核,超过就限流。
    50000100000=0.5\displaystyle \frac{50000}{100000} = 0.5

统一层级革新

老式物业的管理方式里,水费、电费、燃气费各记一本账,月底对账经常对不上。Cgroups v2 的革新就是“一户一档”:所有控制器挂到同一棵层级树上,每个控制组是一棵子树,目录里同时放着 cpu.max、memory.max 等文件,资源状态一目了然。

v2 还引入了一条重要规则:no internal process——一个控制组要么当叶子节点挂进程,要么当父节点挂子控制组,两者不可同时。这杜绝了 v1 里“账目互相拉扯”的歧义:父子节点同时有进程时,资源到底算谁的?在 v2 中这个糊涂账被彻底取消。CPU 接口也从两个文件合并成一行 cpu.max,格式为"quota period",读取与设置都更简洁。

与 eBPF 的结合是 v2 的点睛之笔。eBPF 是内核里的“智能摄像头”:把一段经过验证的字节码安全地挂到 cgroup 相关的内核探针上,能在不过问进程内部的前提下,实时统计每个控制组的网络流量、磁盘延迟、CPU 排队。其中 PSI(Pressure Stall Information) 统计进程排队等待 CPU/内存/IO 的时间占比,是判断“资源到底够不够”的黄金指标。典型组合就是:eBPF 观测 + Cgroups 限流,构成云原生可观测性与自动弹性伸缩的底座(如 Cilium 网络策略)。

物业的智能化升级,让“哪户水压不稳”从玄学变成可量化、可干预、可预测的数据。从 v1 的多本账到 v2 的一座台账,容器运行时的地基焕然一新,云原生的大规模自动调度也因此有了底气。

PSI=tstallTtotal×100%\displaystyle \mathrm{PSI} = \frac{t_{\mathrm{stall}}}{T_{\mathrm{total}}} \times 100\%
PSI(压力停顿信息)衡量进程因等待资源而被阻塞的时间占比,是 eBPF 监控资源压力的核心指标。
  1. t_stall进程因排队等待 CPU、内存或 IO 而被阻塞的时间,单位秒。
    tstallt_{\mathrm{stall}}
  2. T_total统计窗口的总时长,例如 10 秒内的总观测时间。
    TtotalT_{\mathrm{total}}
  3. 压力百分比比值越接近 100%,说明资源越紧张;eBPF 实时读取该指标并触发扩容或告警。
    PSI∈[0, 100%]\mathrm{PSI} \in [0,\ 100\%]

云原生运行时

把“视野隔离”和“账本管控”拼在一起,就是 Docker 的“一键出租”:用户写一份“户型图”(镜像),Docker daemon 用 clone 创建带六套 Namespace 的进程孤岛,再把它塞进 Cgroup 控制组限量运行。docker run 一句话,物业已经把户口本、路由器、水电表全部配齐。

Kubernetes 更进一步:Pod 是“合租套房”。同一个 Pod 里的容器共享同一个 NET 和 UTS Namespace——它们通过 localhost 就能互访,共用一张网卡。实现方式是一个不起眼的 pause 容器:Pod 先启动 pause 进程占住命名空间,业务容器再以“加入”的方式挂进来。资源侧,每个 Pod 对应一个 Cgroup:Guaranteed QoS 对应精确的 quota,Burstable 对应 shares 权重。

共享内核的代价是攻击面集中:一个内核漏洞可能让容器逃逸到宿主机。因此生产环境普遍叠加三层加固:User Namespace 映射(削弱容器内 root 的真实权限)、Capabilities 裁剪(去掉 CAP_SYS_ADMIN 等危险权能)、seccomp 白名单(拦截未授权的系统调用)。eBPF 安全产品(Falco、KubeArmor)则实时拦截恶意 syscall,等于在每户门口加装了智能门磁。

从一户公寓到一个社区,Namespace 保证“看不见”,Cgroups 保证“不抢食”,eBPF 保证“看得清”——这就是云原生容器世界的地基三件套。下次执行 docker run 或 kubectl apply 时,物业的管理处正在背后默默工作。

pod={c1,c2,…,cn}⟼(netnss, utss, cgroups)\displaystyle \mathrm{pod} = \{ c_1, c_2, \dots, c_n \} \longmapsto \left( \mathrm{netns}_s,\ \mathrm{uts}_s,\ \mathrm{cgroup}_s \right)
一个 Pod 是多个容器的集合,它们共享同一个网络命名空间、UTS 命名空间和同一个 Cgroup 账本。
  1. 容器集合Pod 里的一组容器,例如业务进程 + 日志代理,共享命运。
    {c1,c2,…,cn}\{ c_1, c_2, \dots, c_n \}
  2. 共享网络与主机名所有容器加入同一个 netns 与 UTS,看到同一块网卡、同一个 hostname。
    netnss, utss\mathrm{netns}_s,\ \mathrm{uts}_s
  3. 共享资源账本整个 Pod 挂在同一个 Cgroup 控制组下,配额按“一家人”统一计算。
    cgroups\mathrm{cgroup}_s

AI 应用场景

容器网络排障

阶段:监控告警与根因分析

解决的问题:微服务数量上百后,容器间网络不通、端口冲突、DNS 解析失败频发。传统方式要逐个 docker exec 进容器看路由表和 iptables,靠经验和直觉猜;日志又分散在多个 netns 里,排查链路极长。结合 eBPF 的 AI 可观测平台能自动绘制“每个容器属于哪个网络命名空间、数据包在哪一跳被丢”,直接把故障定位到某条 veth 或某条 iptables 规则,再结合 Cgroups 的 CPU/网络用量判断是否因限流导致。

具体案例:某电商大促前,订单服务调用支付服务超时。平台用 eBPF 抓包发现数据包在支付服务的 bridge 端口被 DROP——原来是安全策略误伤。随后利用 cgroup 流量统计确认并非 CPU 限流所致,半小时内定位并修复,避免了大促事故。

Kubernetes 资源治理

阶段:容量规划与自动伸缩

解决的问题:静态 request/limit 配置经常“拍脑袋”,要么过度预留浪费成本,要么配额不足频繁 OOMKill。基于 PSI 与 Cgroups 历史用量训练预测模型,AI 为每个工作负载生成更精确的 request 建议值,并在流量高峰前自动调整配额(类似 VPA 的思路)。

具体案例:数据分析团队跑 Spark 任务,内存 limit 设为 8G,实际峰值只有 4G。AI 观察 30 天 PSI 数据后建议降到 5G,集群整体装箱率从 40% 提升到 65%;大促期间模型又预测到内存压力上升,提前扩容配额,避免了 OOM 重启。

多租户安全隔离

阶段:安全检测与响应

解决的问题:SaaS 平台让几千个客户共享一个 Kubernetes 集群,传统方案给每个客户单独一套环境,成本高、利用率低。利用 User Namespace 映射、seccomp、Capabilities 裁剪把租户放进同一集群,再用 eBPF 做实时异常行为检测,实现租户之间“看得见但摸不着”。

具体案例:某 SaaS 厂商用 eBPF 规则监控可疑系统调用(如尝试 mount、ptrace 其他进程)。某次攻击尝试在容器内提权,eBPF 在毫秒级产生告警,并自动将该 Pod 对应 Cgroup 的网络带宽降为零,阻止了横向扩散。

Python 演示

python55 行
"""Namespace 与 Cgroups 概念模拟器: 用纯 Python 复现三种核心机制."""import random  def show_namespaces():    """列出六种 Namespace 各自隔离的视野."""    scopes = {        "PID":  "进程列表",        "NET":  "网卡/路由/端口",        "UTS":  "主机名",        "IPC":  "消息队列",        "MNT":  "挂载视图",        "USER": "用户ID映射",    }    for ns, scope in scopes.items():        print("[%s] 容器内只看得到本空间的%s, 宿主机被墙挡住" % (ns, scope))  def pid_remap(host_pids):    """把宿主机 PID 列表映射为容器内从 1 开始的新编号."""    mapping = {host: i + 1 for i, host in enumerate(host_pids)}    print("宿主机 PID -> 容器内 PID")    for host_pid, inner_pid in mapping.items():        print("%8d -> %d" % (host_pid, inner_pid))    return mapping  def net_isolation():    """演示两个 netns 可以同时监听 80 端口."""    print("netns_a: eth0 监听 :80")    print("netns_b: eth0 监听 :80")    print("两份端口表互不可见, 只能经 veth 对连线通信")  def cgroup_cpu_quota(quota_ms=60, period_ms=100):    """按 quota/period 模型模拟 10 个周期的限流过程."""    random.seed(7)    print("CPU 配额: %dms / %dms (每周期最多 %d%% 单核)" % (quota_ms, period_ms, quota_ms))    for t in range(1, 11):        used = 20 if t <= 5 else random.randint(70, 95)        if used > quota_ms:            print("周期%2d: 用量%3dms -> 限流!" % (t, used))        else:            print("周期%2d: 用量%3dms -> 正常" % (t, used))  if __name__ == "__main__":    print("== 六种 Namespace ==")    show_namespaces()    print("\n== PID 重新编号 ==")    pid_remap([1234, 1235, 1236])    print("\n== 网络隔离 ==")    net_isolation()    print("\n== Cgroups CPU 限额 ==")    cgroup_cpu_quota()
目的:用纯 Python 模拟六种 Namespace 的隔离视野、PID 重新编号和 Cgroups 的 CPU 限额(quota/period),无需 root 权限即可跑通容器两大地基机制的教学代码。
  1. scopes = {用字典把六种 Namespace 与它们隔离的对象一一对应,是整篇文章最浓缩的“知识表”。
  2. for ns, scope in scopes.items():遍历六种空间,就像物业逐一检查每面隔断墙有没有装好。
  3. mapping = {host: i + 1 for i, host in enumerate(host_pids)}一行完成“宿主机 PID → 容器内 PID”的重新编号,模拟 PID Namespace 眼中从 1 开始的户口本。
  4. for host_pid, inner_pid in mapping.items():打印翻译对,模拟运维同学在容器内外“对表”的日常操作。
  5. print("netns_a: eth0 监听 :80")两个 netns 都能监听 80,因为端口表是各自网络栈的私有财产。
  6. used = 20 if t <= 5 else random.randint(70, 95)前 5 个周期低负载、后 5 个周期高负载,制造限流对比。
  7. if used > quota_ms:用量超过 quota 即触发“限流”,对应 Cgroups 的 throttle 行为。

常见误区

误区容器就是体积更小、启动更快的虚拟机,跑在轻量 hypervisor 上,有自己的内核。所以在容器里装个 Linux 发行版,就能当一台独立服务器用。

正解容器与宿主机共享同一个内核,Namespace 只隔离“视野”,Cgroups 只限制“用量”,并没有第二套内核。由此推出两个硬结论:Linux 容器只能跑 Linux 应用;一旦有内核漏洞,攻击者可能逃逸到宿主机。比喻:别墅和公寓都叫“家”,但别墅自带地基,公寓只有隔断和门锁。

为何常见:docker run 的使用体验太像“开一台新机器”,加上名称里带"container",让人自然联想到轻量虚拟机。

误区既然两个容器都能同时监听 80 端口,那宿主机上我也可以随便监听 80,反正大家都是隔离的。

正解容器之间的端口不冲突,是因为它们分属不同的 netns;但宿主机自己也有一个 netns,宿主机的 80 端口属于宿主机网络栈,与所有容器都不共享。若宿主机已监听 80,任何容器都无法通过 -p 80:80 发布新的端口映射。比喻:每户路由器独立,但大楼总进线只有一根。

为何常见:混淆了“容器与容器之间”与“容器与宿主机之间”两个不同层次的隔离关系。

误区容器里的 1 号进程得是 systemd 之类的 init 系统,否则容器“系统”起不来。

正解容器的 PID 1 就是用户指定的入口进程(nginx、java 都可以),内核不要求它做设备初始化——因为设备初始化由共享的宿主内核完成。但 PID 1 必须承担两个义务:收养孤儿进程、转发信号。否则 docker stop 无法优雅停机,僵尸进程还会堆积。

为何常见:沿用了传统 Linux 服务器“init 系统管理一切”的心智模型,没意识到容器只是“进程 + 视图”,不是完整操作系统。

误区给容器配了 2 核配额,它就一定能获得 2 核的性能,至少不会低于 2 核。

正解quota/period 是上界而不是下界:2 核配额 = 每个结算周期最多用 2 核的时间片,宿主机繁忙时实际可能远低于这个数。想要最低性能保障,得靠权重(shares)配合调度,或者为关键负载预留整机资源。换句话说,quota 是“封顶”,不是“低保”。

为何常见:“配额”在中文里容易被理解成“保障额度”,实际上它写的是“最多允许你用多少”。

误区有了 Namespace 这堵隔墙,容器就是安全的;容器里的 root 想干什么就干什么,反正出不去。

正解Namespace 是“视图”隔离,不是“权限”隔离。容器内 root 若未经过 User Namespace 映射,在宿主机上仍是真实的 root;即使做了映射,挂载宿主目录、保留 CAP_SYS_ADMIN 等配置仍可能造成逃逸。生产安全必须叠加 seccomp、Capabilities 裁剪、只读根文件系统和 eBPF 监控。

为何常见:“隔离”一词容易被理解成“安全边界”,实际上它更像“信息屏蔽”,而不是“装甲”。

面试题

Q1. 容器与虚拟机最本质的区别是什么?

单选题 · easy

  • A.容器必须运行在 Linux 上
  • B.容器共享宿主机内核← 正确答案
  • C.虚拟机体积通常更小
  • D.容器不支持网络

答案:B。虚拟机像自建地基的独栋别墅,自带独立内核;容器像共享大楼地基的公寓,只靠 Namespace 隔视野、Cgroups 限量。正因为共享内核,Linux 容器无法跑 Windows 镜像,内核漏洞也可能波及整栋楼。

Q2. 以下哪个不是 Linux Namespace 的类型?

单选题 · easy

  • A.PID 命名空间
  • B.NET 命名空间
  • C.CPU 命名空间← 正确答案
  • D.USER 命名空间

答案:C。Namespace 负责“视野”隔离,维度包括 PID、NET、UTS、IPC、MNT、USER。CPU 是资源用量,归属 Cgroups 管理——就像水电看电表,不看户口本。所以不存在“CPU 命名空间”。

Q3. 网络命名空间为容器提供哪些独立资源?

多选题 · mid

  • A.路由表← 正确答案
  • B.iptables 规则← 正确答案
  • C.CPU 时间片
  • D.回环网卡 lo← 正确答案

答案:ABD。netns 拥有一整套网络栈:网卡(含 lo)、路由表、iptables、端口表、socket。CPU 时间片不是网络对象,由 Cgroups 管理。每个容器都有独立的 lo 网卡,所以容器内 localhost 可用。

Q4. 容器内第一个进程 PID=1,它的特殊职责是什么?

单选题 · mid

  • A.定期清理磁盘
  • B.收养孤儿进程并转发信号← 正确答案
  • C.启动 systemd 管理服务
  • D.决定 CPU 时间片

答案:B。PID 1 是容器内的“户主”兼值班大爷:内核把孤儿进程交给它收养,把 SIGTERM 等信号交给它转发。若入口程序不做这两件事,docker stop 会超时强杀、僵尸进程堆积。启动 systemd 并不是必须的。

Q5. Cgroups v2 相对 v1 的核心变化是什么?

单选题 · hard

  • A.移除了内存限制功能
  • B.每个 controller 独立成树
  • C.统一层级树且进程是叶子← 正确答案
  • D.只支持 root 用户操作

答案:C。v1 是多个子系统各自一棵树(水费电费各记一本账),v2 统一成一个层级树,并规定进程只能挂在叶子节点(no internal process 规则),所有控制器在同一棵树上协作。B 是 v1 的特征,A 与事实相反,D 无此限制。

Q6. eBPF 与 Cgroups 结合可以实现哪些能力?

多选题 · mid

  • A.实时统计容器网络流量← 正确答案
  • B.在丢包时动态调整带宽← 正确答案
  • C.修改宿主机 BIOS 固件
  • D.感知容器 CPU 排队压力← 正确答案

答案:ABD。eBPF 像物业的智能摄像头:既能观测(流量统计、PSI),也能干预(动态限流、丢包)。但 eBPF 是内核里的虚拟机,碰不到 BIOS/固件层,所以 C 错误。

Q7. 为什么两个容器可以在同一宿主机上同时监听 80 端口?

单选题 · mid

  • A.内核为多个网卡做了负载均衡
  • B.TCP 端口是每个 netns 私有资源← 正确答案
  • C.宿主机的 80 端口恰好空闲
  • D.80 端口是全部进程共享的

答案:B。端口表属于网络栈,而网络栈属于 netns。两个 netns 各有各的“80 号插座”,自然不冲突。但宿主机端口属于宿主 netns,-p 发布端口时依然全局唯一,A、D 都错。

Q8. 以下哪些做法属于容器安全加固?

多选题 · extreme

  • A.启用 user namespace 映射 root← 正确答案
  • B.配置 seccomp 白名单← 正确答案
  • C.裁剪危险 Capabilities← 正确答案
  • D.挂载宿主机 /proc 目录

答案:ABC。加固的核心是“最小化暴露”:user ns 削弱 root、seccomp 限制系统调用、裁剪 Capability 去掉危险权能。而挂载宿主 /proc 会把全楼户口本(进程表)暴露给容器,反而扩大攻击面,属于危险配置而非加固。

Q9. UTS Namespace 隔离的是什么?

单选题 · easy

  • A.用户 ID 编号
  • B.主机名与 NIS 域名← 正确答案
  • C.内存用量
  • D.文件系统类型

答案:B。UTS = Unix Timesharing System,隔离 hostname 和 NIS domain name,相当于每户门牌号独立。容器里改 hostname 不影响宿主机。用户 ID 归 USER namespace,内存用量归 Cgroups。

Q10. 容器 CPU 配额设置为 50ms/100ms,其含义是什么?

单选题 · hard

  • A.保证容器至少获得 50% CPU
  • B.每 100ms 周期最多用 50ms 单核← 正确答案
  • C.每次进程调度最长 50ms
  • D.容器只能用半个物理核

答案:B。50ms/100ms 表示每个 100ms 结算周期内最多使用 50ms 单核时间,即最多半个核,是“封顶”而非“保证”——宿主机繁忙时可能连一半都用不到。A 把上限误当保障,C 混淆了调度时长与配额,D 的说法不严谨。