Linux 与容器基础
环境变量与运行环境
环境变量是进程启动时继承的一组 key-value,用于传递配置、路径、鉴权和运行模式。它不是全局状态,而是每个进程自己的环境快照。
常见变量
| 变量 | 作用 | 问题 |
|---|---|---|
| PATH | 查找可执行文件 | 命令找不到或执行了错误版本 |
| LD_LIBRARY_PATH | 动态库查找路径 | 缺库、ABI 不兼容 |
| CUDA_VISIBLE_DEVICES | 控制 GPU 可见性 | 容器内卡号和宿主机卡号映射混淆 |
| HTTP_PROXY | 网络代理 | 下载失败或访问内网异常 |
容器隔离三件套全景
| 技术 | 内核版本 | 作用 | 核心含义 |
|---|---|---|---|
| Namespace | 2.6+(逐步加全) | 视图隔离 | "你看不到外面的世界"——PID、网络、挂载点等资源各 namespace 独立 |
| Cgroups | 2.6.24(v1)/ 4.5(v2) | 资源限制 | "你只能用这么多"——限制 CPU/内存/IO/进程数等配额 |
| rootfs / UnionFS | — | 文件系统隔离 | "你有自己的根目录"——独立文件系统视图,镜像分层+CoW |
容器不是虚拟机:没有独立内核,所有容器共享宿主机内核,隔离靠内核机制而非 hypervisor。KVM 虚拟机有独立内核,隔离更强但开销更大。
什么是 Namespace:从"全局资源"到"私有视图"
Linux 是一个全局资源共享的操作系统——默认情况下,所有进程共用一张 PID 表、一套挂载点、一个网络栈、一个 hostname。这在单台机器上没问题,但如果想让一组进程"以为自己独占了整台机器",就需要一种机制把这些全局资源包装起来,给不同进程组呈现不同的视图。这就是 Namespace。
核心思想(参考 Red Hat、Julia Evans、"Containers are not special" 等经典博客):
- 本质是内核里的一张"包装表":每种全局资源(PID、挂载点、网络……)在内核中有一个全局结构体;Namespace 把这些结构体替换成每个 namespace 一份的私有拷贝,进程通过
task_struct->nsproxy指针访问自己的那份 - 不是虚拟化,是"视图欺骗":同一时刻所有容器仍共享同一个内核,没有 hypervisor,没有 Guest OS;Namespace 只改变进程"能看到什么",不改变内核本身
- 类比:chroot 的进化版:chroot 只隔离文件系统根目录(相当于 mnt namespace 的雏形);Namespace 把这种"换一个视角"的思路扩展到了 PID、网络、主机名、IPC、用户、cgroup 等 7 类资源
- 只解决"你能看到什么",不解决"你能用多少":能看到的 CPU/内存/IO 有多少是 Cgroups 管的事,Namespace 不做资源限制
三个关键系统调用(面试必问):
| 系统调用 | 作用 | 典型场景 |
|---|---|---|
clone(flags) | 创建新进程时通过 flag 指定同时创建并加入新的 namespace | docker run 创建容器进程的核心调用,一次性带上 CLONE_NEWPID/CLONE_NEWNET/CLONE_NEWNS 等 flag |
unshare(flags) | 将当前进程/线程从原 namespace 中分离,加入新创建的 namespace(不创建新进程) | unshare --pid --mount --fork bash 命令行创建隔离环境;容器运行时在已有进程中做 namespace 切换 |
setns(fd, flags) | 将当前进程加入一个已存在的 namespace(通过 /proc/<pid>/ns/ 下的文件描述符) | nsenter -t <pid> -n -p -- bash 进入容器调试;调试工具"附着"到运行中容器 |
理解路径:当你在容器里执行 ps aux 只看到自己的几个进程时,不是因为其他进程不存在了,而是容器进程的 PID namespace 让它只看到了"私有 PID 表"中从 1 开始的那部分;当你 ls / 看到独立的文件系统,是 mnt namespace + pivot_root 切换了根目录视图;当你 ifconfig 看到自己的 eth0 和 IP,是 net namespace 给了它独立的网络设备和路由表。这一切都是同一份内核、不同份视图。
Namespace 七类
| Namespace | 隔离内容 | 系统调用参数 | 内核版本 | 容器中的表现 |
|---|---|---|---|---|
| Mount (mnt) | 文件系统挂载点 | CLONE_NEWNS | 2.4.19 | 容器内 mount/umount 不影响宿主机和其他容器;每个容器有自己独立的根目录视图 |
| PID | 进程 ID 空间 | CLONE_NEWPID | 2.6.24 | 容器内 PID=1 是 init 进程,容器内看不到宿主机和其他容器的进程;容器退出后其内进程全部销毁 |
| Network (net) | 网络设备、IP、路由表、iptables、端口 | CLONE_NEWNET | 2.6.29 | 容器有独立的网络栈(veth pair + 网桥),自己的 IP/路由/防火墙规则,端口互不冲突 |
| UTS | 主机名、NIS 域名 | CLONE_NEWUTS | 2.6.19 | 容器有自己的 hostname,hostname 命令看到的是容器名 |
| IPC | System V IPC、POSIX 消息队列 | CLONE_NEWIPC | 2.6.19 | 容器内进程间通信(共享内存、信号量、消息队列)互相隔离,跨容器不能用 IPC 通信 |
| User | 用户/用户组 ID 映射 | CLONE_NEWUSER | 3.8 | 容器内 root(UID 0)可以映射到宿主机上的普通用户(如 UID 100000),容器提权不影响宿主机 |
| Cgroup | cgroup 根目录视图 | CLONE_NEWCGROUP | 4.6 | 容器内 /proc/self/cgroup 看到自己的 cgroup 路径,看不到宿主机其他 cgroup |
常用操作命令:
- 查看进程所属 namespace:
ls -la /proc/<pid>/ns/ - 进入容器 namespace 调试:
nsenter -t <pid> -n -p -- bash(进入该进程的 net+pid namespace) - 创建新 namespace 运行命令:
unshare --pid --mount --fork --mount-proc bash
什么是 Cgroups:从"nice 值"到"进程分组+资源配额"
Linux 传统上用 nice、ulimit、setrlimit 控制单进程资源,但这些机制只能限制单个进程,无法对"一组进程"(比如一个容器里的所有进程、一个 systemd 服务的所有子进程)做统一限额和统计。Cgroups(Control Groups)就是为解决这个问题而生的内核机制——把进程组织成树形分组,在每个节点上挂载资源控制器,限制这一组进程合计能用多少 CPU/内存/IO/PID。
核心思想(参考 Red Hat、Kernel Docs、cgroupv2 官方文档):
- 树形层级:cgroup 以树状目录组织(
/sys/fs/cgroup/),每个目录是一个 cgroup 节点;子 cgroup 从父 cgroup 继承,父节点的限制不能被子节点突破(父限 4 核,所有子加起来最多 4 核) - 进程归属:每个进程属于且仅属于一个 cgroup(v2),通过向
cgroup.procs文件写入 PID 把进程移入;子进程 fork 时自动继承父进程的 cgroup - 资源控制器(Controller):每个控制器管一种资源,通过目录下的接口文件配置——cpu(CPU 带宽/权重)、memory(内存上限+OOM)、io(块设备 IOPS/BPS)、pids(进程数上限)、cpuset(绑核/NUMA)、devices(设备访问黑白名单)等
- 不仅限制,还统计:每个 cgroup 目录下有大量
*.stat、*.current文件,提供细粒度的资源使用数据,是 Prometheus/cAdvisor/K8s metrics 的数据源 - 和 Namespace 互补但独立:Namespace 解决"看到什么"(视图),Cgroups 解决"用多少"(配额);两者可以独立使用——systemd 服务用 cgroup 限制资源但不隔离视图,
unshare隔离视图但不做资源限制
Cgroups v1 vs v2:
| 维度 | cgroups v1 | cgroups v2 |
|---|---|---|
| 架构 | 每个 subsystem 独立挂载(cpu、memory、cpuset 等各自一棵树) | 统一层级(unified hierarchy),所有资源在同一棵树 |
| 进程绑定 | 进程可以在不同 subsystem 中属于不同 cgroup | 进程只能绑定到一个 cgroup,所有资源控制器统一管理 |
| 资源使用 | 各 subsystem 独立计账,可能不一致 | 统一计账,eBPF 集成更好 |
| 内核版本 | 2.6.24+ | 4.5+ 实验性,5.2+ 稳定生产可用 |
| 现代发行版 | 旧版默认 | Ubuntu 22.04+ / Debian 11+ / RHEL 9+ / K8s 1.25+ 推荐/默认 |
核心 subsystems(子系统):
| 子系统 | 作用 | Docker 对应参数 |
|---|---|---|
| cpu | 限制 CPU 使用份额(shares 相对权重)和 CFS 带宽(cfs_quota/cfs_period) | --cpus=2(限 2 核)、--cpu-shares=512(相对权重) |
| cpuset | 绑定进程到指定 CPU 核和 NUMA 节点 | --cpuset-cpus=0-3、--cpuset-mems=0 |
| memory | 限制内存使用量(硬限制+软限制),统计 RSS/cache/swap,OOM 触发 | -m 1g(限 1GB)、--memory-swap=-1(禁 swap) |
| blkio / io | 限制块设备 IO 带宽和 IOPS(相对权重或绝对限制) | --device-read-bps、--blkio-weight |
| pids | 限制进程/线程数量(防 fork bomb) | --pids-limit=100 |
| devices | 控制能访问哪些设备(黑白名单) | --device、--cap-drop=ALL |
| freezer | 暂停/恢复 cgroup 中的所有进程(不终止) | docker pause/unpause |
| hugetlb | 限制 HugePage 使用量 | --hugetlb-limit |
常用查看路径:
- cgroup 挂载点:
/sys/fs/cgroup/(v2 unified)或各 subsystem 子目录(v1) - 查看进程 cgroup:
cat /proc/<pid>/cgroup - 查看容器 cgroup:
/sys/fs/cgroup/system.slice/docker-<container-id>.scope/(systemd 驱动) - 内存统计:
memory.current(v2)/memory.usage_in_bytes(v1) - CPU 统计:
cpu.stat(v2)/cpuacct.usage(v1) - OOM 控制:
memory.oom_control(v2 中memory.oom.group)
rootfs 与 UnionFS(OverlayFS)
rootfs 是容器启动时看到的文件系统(根目录)。Docker 镜像通过 UnionFS(联合文件系统)将多个层(layer)挂载成一个统一的视图。
镜像分层:
- Dockerfile 中每条指令(RUN/COPY/ADD)产生一个只读层(layer),层可以复用和缓存
- 多个镜像可以共享基础层(base image,如 ubuntu:22.04),节省磁盘和拉取时间
- 容器启动时在所有只读层之上加一个可写层(容器层),所有运行时修改写入这层
OverlayFS(Linux 主流联合文件系统):
| 目录 | 作用 |
|---|---|
| lowerdir | 只读层,可以有多个(镜像层叠),按顺序叠加 |
| upperdir | 可写层(容器层),容器运行时所有修改写在这里 |
| merged | 合并后的挂载点,容器进程看到的统一视图 |
| workdir | OverlayFS 内部原子操作所需的工作目录(empty) |
Copy-on-Write(写时复制)机制:
- 读文件:文件在 lowerdir 中时直接从 lowerdir 读;如果在 upperdir 中有新版本则从 upperdir 读
- 修改文件:首次修改时从 lowerdir 拷贝文件到 upperdir,再在 upperdir 上修改(copy-up)。后续修改直接在 upperdir 上操作
- 删除文件:在 upperdir 中创建 whiteout 文件(字符设备 0/0),遮蔽 lowerdir 中对应文件;实际不删除下层文件
- 新增文件:直接写入 upperdir
注意事项:
- 首次写大文件时 copy-up 开销大(延迟尖刺)
- 容器层(upperdir)随容器删除而删除,持久化数据必须用 Volume 挂载
- 容器文件系统性能略低于原生文件系统(多了一层 overlay 寻址),高频 IO 场景建议用 volume 或 bind mount
三者如何协作:以 docker run 为例
- 创建 namespace:Docker 调用
clone()带着 CLONE_NEWPID/CLONE_NEWNET/CLONE_NEWNS 等 flag 创建容器进程,让它拥有独立 PID/网络/挂载/UTS/IPC 视图 - 准备 rootfs:通过 OverlayFS 将镜像层(lowerdir)+ 容器可写层(upperdir)挂载到
/var/lib/docker/overlay2/<id>/merged,作为容器根目录 - 配置网络:创建 veth pair,一端连容器 netns(eth0),一端连 docker0 网桥;分配 IP、设置路由、配置 iptables NAT
- 设置 cgroups:在
/sys/fs/cgroup/下为容器创建子目录,写入 cpu/memory/pids 等限制参数,将容器 PID 写入 tasks/cgroup.procs - 切换根目录:调用
pivot_root或chroot将进程根目录切换到 merged 目录 - 启动 init:在隔离环境中执行用户指定的 ENTRYPOINT/CMD(PID=1)
本质区别:虚拟机通过 hypervisor 模拟硬件,每个 VM 有独立内核,Guest OS 和 Host OS 完全隔离;容器共享宿主机内核,隔离全部靠 Linux 内核机制(namespace+cgroups)。VM 是"硬件级隔离",容器是"进程级隔离"。安全风险:容器隔离比 VM 弱——容器内进程直接与宿主机内核交互,内核漏洞可以逃逸;User namespace 将容器 root 映射为宿主机非特权用户可以降低风险,但默认 Docker 没开启 user namespace。生产环境多租户场景建议用 Kata Containers(轻量 VM+容器接口)或 gVisor(用户态内核)增强隔离。
--cpus=2 是硬限制,通过 CFS(Completely Fair Scheduler)带宽控制实现:设置 cpu.cfs_quota_us 和 cpu.cfs_period_us(默认 100000us=100ms),--cpus=2 对应 quota=200000us,即每 100ms 周期内最多用 200ms CPU 时间(可在多核上并行)。--cpu-shares=512 是相对权重(默认 1024),只在 CPU 竞争时生效——CPU 充裕时不限制,多个容器争抢时按 shares 比例分配。面试重点:--cpus 限制上限(类似 K8s limits),cpu-shares 控制权重(类似 K8s requests 的相对优先级)。
排查:(1) dmesg | grep -i oom 看内核 OOM 日志;(2) kubectl describe pod 看 Last State 中 OOMKilled;(3) cat /sys/fs/cgroup/memory/docker-<id>/memory.oom_control 看 oom_kill 计数。防止:(1) 设置合理的 memory limit(-m),不要太小;(2) 优化应用内存使用,避免内存泄漏;(3) K8s 中设置 memory.limit_in_bytes 足够大并配置 readiness/liveness 探针;(4) 调 oom_score_adj(越低越不容易被 kill,-1000 禁止 kill,但慎用);(5) 注意 Page Cache 会计入 cgroup memory,大文件读写会占用容器内存额度,必要时调低 vm.dirty_ratio 或定期 drop_caches。
Docker 与 Kubernetes 的分工
| 问题 | Docker/Runtime | Kubernetes |
|---|---|---|
| 环境复现 | 镜像、rootfs | 镜像版本、拉取策略 |
| 资源限制 | 写 cgroup | requests/limits、QoS、调度 |
| 失败恢复 | 单机重启策略 | Deployment/Job/StatefulSet controller |
| 服务发现 | 基本不解决 | Service、DNS、EndpointSlice |
QoS、RSS 和 Usage
RSS 是进程实际驻留物理内存;cgroup usage 是容器级内存统计,包括匿名页、page cache、部分内核内存等。Pod QoS 根据 requests/limits 分为 Guaranteed、Burstable、BestEffort,影响 OOM 和驱逐优先级。
先把概念说清楚
容器运行时不等于 Docker
Docker 是面向用户的容器产品,包含 CLI、API、build、network、volume 等能力;containerd 是更底层的容器运行时,专注镜像、容器生命周期、snapshot 和 task 管理;runc 是更底层的 OCI runtime,真正调用 Linux kernel 能力创建容器进程。
| 概念 | 是什么 | 面试里怎么说 |
|---|---|---|
| CRI | Kubernetes 定义的 Container Runtime Interface,kubelet 通过它调用运行时 | CRI 是 kubelet 和 runtime 的标准 gRPC 接口,不是具体 runtime |
| containerd | 高层容器运行时 daemon | 管镜像、content store、snapshot、container metadata、task、CRI plugin |
| runc | OCI low-level runtime | 根据 OCI runtime spec 调 Linux namespace、cgroup、mount 等创建容器 |
| OCI | Open Container Initiative 规范集合 | image spec 定义镜像格式,runtime spec 定义如何运行容器 |
| containerd-shim | containerd 和容器进程之间的托管层 | 负责 stdio、exit status、事件上报,让 containerd 重启不杀容器 |
| pause container | Pod sandbox 的基础容器 | 持有 Pod network namespace / Pod IP,让业务容器共享 Pod 网络身份 |
| CNI | Container Network Interface | 给 Pod sandbox 配网络,如 veth、IP、route、iptables/eBPF |
| CSI | Container Storage Interface | 给 Pod 准备和挂载 volume |
运行时链路图
Pod 启动链路
关键点:
RunPodSandbox先于业务容器启动,因为 Pod 需要先有网络 namespace 和 Pod IP。PullImage走 CRI ImageService,containerd 会维护 content store 和 snapshot。CreateContainer只是创建容器配置和 rootfs,StartContainer才启动进程。- runc 通常不是常驻 daemon,它创建容器后退出;常驻托管进程是 shim。
containerd 内部对象
| 对象 | 解释 | 常见追问 |
|---|---|---|
| Content | 镜像 blob 内容,按 digest 存储 | 为什么 digest 比 tag 更可靠 |
| Image | 镜像元数据,指向 manifest / config / layer | tag 和 digest 的区别 |
| Snapshot | rootfs 的可写层和只读层组合 | overlayfs、copy-on-write |
| Container | containerd 的容器元数据,不等于正在运行的进程 | container 和 task 区别 |
| Task | 正在运行的进程对象 | start/kill/exec/wait 都是 task 操作 |
| Sandbox | Pod 级运行环境 | pause 容器、Pod namespace、Pod IP |
Docker、containerd、runc 的关系
面试要避免两种说法:
| 错误说法 | 正确说法 |
|---|---|
| Kubernetes 不用 Docker 后,Docker 镜像不能跑了 | 错。dockershim 移除的是 kubelet 到 Docker Engine 的内置适配层,OCI/Docker 镜像格式仍兼容 |
| containerd 直接 fork 出业务容器 | 不准确。containerd 通常启动 shim,shim 再调用 runc,容器进程由 shim 托管 |
| runc 负责镜像拉取 | 错。runc 只负责按 OCI runtime spec 创建容器,镜像和 snapshot 是 containerd 负责 |
| pause 容器没用 | 错。pause 是 Pod namespace 锚点,业务容器重启时 Pod 网络身份可以保持稳定 |
常用排障命令
# 看 kubelet 看到的 Pod / Container 状态
kubectl describe pod <pod> -n <ns>
kubectl get events -n <ns> --sort-by=.lastTimestamp
# 节点侧:用 CRI 视角看 runtime
crictl ps -a
crictl pods
crictl images
crictl inspect <container_id>
crictl inspectp <pod_sandbox_id>
crictl logs <container_id>
crictl pull <image>
# containerd 视角
ctr -n k8s.io containers list
ctr -n k8s.io tasks list
ctr -n k8s.io images list
ctr -n k8s.io snapshots list
# 日志和进程
journalctl -u kubelet -f
journalctl -u containerd -f
ps -ef | grep containerd-shim
# CNI / 网络
ip netns list
ip link
ip route
ls /etc/cni/net.d/
crictl 更适合 Kubernetes 节点排障,因为它走 CRI;ctr 是 containerd 自带低层调试工具,命名空间常用 k8s.io;nerdctl 更像 Docker CLI 体验,适合人工运行容器。
常见面试问题
containerd 是高层 runtime daemon,负责镜像拉取、content store、snapshot、容器元数据、task 生命周期和 CRI 服务。runc 是 OCI low-level runtime,负责根据 OCI spec 调 Linux kernel 创建容器。containerd-shim 位于 containerd 和容器进程之间,负责托管容器进程、转发 stdio、收集 exit status 和上报事件。
如果 containerd 直接成为所有容器进程的父进程,那么 containerd 重启或升级时会影响正在运行的容器。shim 把容器进程和 containerd daemon 解耦:containerd 可以重启,shim 继续托管容器;shim 还负责保留 stdio、等待容器退出、收集 exit code、上报事件和清理资源。
Pod 不是一个容器,而是一组共享网络等 namespace 的容器。pause 容器是 Pod sandbox 的基础容器,它先启动并持有 Pod 的 network namespace、Pod IP 和部分共享 namespace。业务容器启动时加入这个 sandbox。这样业务容器重启时,Pod 的网络身份仍然可以保持稳定。
能。dockershim 移除的是 kubelet 内置的 Docker Engine 适配层,不是移除 Docker 镜像格式。只要镜像符合 OCI / Docker image spec,containerd 和 CRI-O 都能拉取和运行。变化在节点链路:以前是 kubelet → dockershim → Docker Engine → containerd,现在是 kubelet → CRI → containerd。
先看 Pod Events 里的错误类型:镜像名/tag 是否存在,registry 是否可达,imagePullSecret 是否正确,节点 DNS/代理/证书是否正常,是否触发 registry rate limit。然后到节点侧用 crictl pull 复现,用 journalctl -u containerd 看 runtime 具体错误。
kubectl describe pod -n
kubectl get secret -n
crictl pull
journalctl -u containerd -n 200
ContainerCreating 表示 Pod 已经调度到节点,但节点侧执行还没完成。排查顺序是 Events、kubelet 日志、containerd 日志、CNI 日志/配置、CSI mount、镜像拉取和 sandbox 创建。常见原因包括 CNI 分配 IP 失败、CSI 挂载超时、sandbox 创建失败、镜像拉取慢、节点磁盘压力。
镜像由多层只读 layer 组成,containerd 把这些 layer 存在 content store 中。启动容器时,snapshotter 会基于这些只读层准备 rootfs,并给容器加一个可写层。overlayfs 常用于把多个只读 lowerdir 和一个 writable upperdir 合成一个统一视图。容器内写文件时触发 copy-on-write,不会修改原始镜像层。
Kubernetes 的 requests 主要用于调度,limits 会通过 kubelet / runtime 写到 cgroup。CPU limit 常体现为 CFS quota,内存 limit 体现为 cgroup memory 上限,超过可能触发容器 OOMKilled。GPU 这类设备资源通常通过 device plugin 注入设备文件、环境变量或 runtime hook;GPU 显存本身不一定被 cgroup 原生限制,需要厂商 runtime 或平台策略配合。
Infra 面试回答结构
如果面试官问“介绍一下容器运行时”:
可以这样组织:
- 容器不是轻量 VM,本质是 Linux namespace、cgroup、rootfs、capability、seccomp 等能力的组合。
- Kubernetes 不直接调用 Docker/containerd 私有 API,而是通过 CRI 调运行时。
- containerd 是高层 runtime,负责镜像、snapshot、容器元数据和 task;runc 是 OCI runtime,负责创建 Linux 容器。
- Pod 先创建 sandbox/pause 容器,持有 Pod 网络 namespace;业务容器再加入 sandbox。
- 节点侧问题要按链路排:调度是否完成、kubelet 是否 SyncPod、containerd 是否拉镜像/建 sandbox、CNI/CSI 是否成功、kernel cgroup/namespace 是否正常。
参考资料
- containerd docs: Architecture of The CRI Plugin
- containerd docs: Runtime v2
- Kubernetes docs: Container Runtimes
- Kubernetes blog: Dockershim Removal FAQ