23-容器面试深度指南
概述
本章汇总容器技术相关的高频面试题,覆盖 Namespace、Cgroup、内存管理、OOM、Kubernetes 资源模型等核心知识点,适合 AI Infra / AIOps / SRE / 云原生工程师等岗位面试准备。
一、Namespace 相关
1.1 基础问题
Q1: Linux 有哪几种 Namespace?各自隔离什么?
┌──────────────┬────────────────────────────────────────────────┐
│ Namespace │ 隔离内容 │
├──────────────┼────────────────────────────────────────────────┤
│ PID │ 进程 ID,容器内 PID 从 1 开始 │
│ Network │ 网络设备、IP 地址、端口、路由表 │
│ Mount │ 文件系统挂载点 │
│ UTS │ 主机名和域名 │
│ IPC │ 信号量、消息队列、共享内存 │
│ User │ 用户和组 ID (rootless 容器) │
│ Cgroup │ Cgroup 根目录视图 │
│ Time │ 系统时间 (较新) │
└──────────────┴────────────────────────────────────────────────┘
Q2: 容器和虚拟机的本质区别?
容器:
- 共享宿主机内核
- 通过 Namespace 隔离进程视图
- 通过 Cgroup 限制资源
- 轻量级,启动快 (毫秒级)
- 隔离性相对较弱
虚拟机:
- 运行独立的 Guest OS 内核
- 通过 Hypervisor 虚拟化硬件
- 资源开销大
- 启动慢 (分钟级)
- 隔离性强
1.2 进阶问题
Q3: 如何查看容器进程在宿主机上的真实 PID?
# 方法 1: 通过 docker inspect
docker inspect --format '{{.State.Pid}}' <container_id>
# 方法 2: 通过 /proc
cat /proc/<host_pid>/status | grep NSpid
# NSpid: 12345 1 (宿主机 PID 12345,容器内 PID 1)
# 方法 3: 进入容器的 PID namespace
nsenter -t <host_pid> -p ps aux
Q4: 容器内能修改系统时间吗?
默认不能,因为:
1. 需要 CAP_SYS_TIME 能力
2. 系统时间不属于任何 Namespace (在旧内核中)
3. Time Namespace 是 Linux 5.6 才引入
解决方案:
- 添加 --cap-add SYS_TIME (不推荐)
- 使用 libfaketime 在应用层模拟
- 使用 Time Namespace (需要新内核)
二、Cgroup 相关
2.1 基础问题
Q5: Cgroup v1 和 v2 的主要区别?
┌────────────────┬─────────────────────┬─────────────────────┐
│ 特性 │ Cgroup v1 │ Cgroup v2 │
├────────────────┼─────────────────────┼─────────────────────┤
│ 层级结构 │ 多层级,每个控制器 │ 单一统一层级 │
│ │ 独立的层级 │ │
│ 挂载点 │ 多个 │ 单个 /sys/fs/cgroup │
│ 进程绑定 │ 可属于多个层级 │ 只能属于一个节点 │
│ 内存保护 │ 无 │ memory.low/min │
│ PSI 压力监控 │ 无 │ 支持 │
│ Buffer I/O │ 不支持 │ 支持 │
└────────────────┴─────────────────────┴─────────────────────┘
Q6: memory.limit_in_bytes 限制的是什么?
限制的是:
1. RSS (匿名内存) - 堆、栈、匿名 mmap
2. Page Cache (文件缓存) - 该 Cgroup 进程读写的文件
3. 共享内存 (tmpfs/shmem)
4. 页表 (可选,需要 kmem 配置)
不限制的是:
1. 虚拟内存中未分配的部分
2. Swap (除非设置 memsw 限制)
3. 其他 Cgroup 的 Page Cache
2.2 进阶问题
Q7: 容器 RSS 很小,为什么还是 OOM?
常见原因:
1. Page Cache 占用
- 容器读写文件会产生 Page Cache
- Page Cache 也计入 memory.limit
2. 共享内存占用
- tmpfs 挂载
- 进程间共享内存
3. 内核内存占用
- 网络缓冲区
- inode/dentry 缓存
排查方法:
cat /sys/fs/cgroup/memory/<cgroup>/memory.stat
# 查看 cache、rss、mapped_file 等值
Q8: 如何禁止容器使用 Swap?
# Cgroup v1
echo $LIMIT > memory.limit_in_bytes
echo $LIMIT > memory.memsw.limit_in_bytes # 设置相同值
# Cgroup v2
echo 0 > memory.swap.max
# Docker
docker run --memory=1g --memory-swap=1g myimage # 相同值 = 禁用 swap
# Kubernetes
# Kubelet 默认禁用 Swap (failSwapOn: true)
三、内存管理与 OOM
3.1 基础问题
Q9: RSS、PSS、USS 的区别?
RSS (Resident Set Size):
- 进程实际占用的物理内存
- 包含共享库的完整大小
- 多个进程的 RSS 之和会重复计算共享库
PSS (Proportional Set Size):
- 共享内存按进程数平分
- 所有进程的 PSS 之和 = 实际物理内存使用
USS (Unique Set Size):
- 进程独占的内存
- 不包含任何共享部分
- 杀死进程后能释放的内存
关系: USS ≤ PSS ≤ RSS
Q10: OOM Killer 如何选择杀哪个进程?
1. 计算每个进程的 oom_score:
基础分 = (进程内存 / 系统总内存) × 1000
最终分 = 基础分 + oom_score_adj
2. 选择分数最高的进程
3. oom_score_adj 范围:
-1000: 永不杀死
0: 默认
1000: 优先杀死
4. Kubernetes 中:
Guaranteed: oom_score_adj = -997
Burstable: oom_score_adj = 2~999
BestEffort: oom_score_adj = 1000
3.2 进阶问题
Q11: 操作系统是怎么触发 OOM 的?
触发流程:
1. 进程请求分配内存 (malloc/mmap)
2. 内核尝试分配物理页
3. 分配失败,触发直接回收 (direct reclaim)
4. 回收 Page Cache、Swap out 匿名页
5. 回收失败,唤醒 kswapd 继续尝试
6. 仍然失败,检查 OOM 条件
7. 调用 out_of_memory()
8. select_bad_process() 选择 victim
9. oom_kill_process() 发送 SIGKILL
Cgroup OOM 的区别:
- 在 try_charge() 时检查限制
- 只从 Cgroup 内选择 victim
- 不影响其他 Cgroup 的进程
Q12: 如何防止关键进程被 OOM Kill?
# 1. 设置 oom_score_adj
echo -1000 > /proc/<pid>/oom_score_adj
# 2. Systemd 服务配置
[Service]
OOMScoreAdjust=-1000
# 3. Docker
docker run --oom-score-adj=-500 myimage
# 4. Kubernetes - 使用 Guaranteed QoS
resources:
requests:
memory: "1Gi"
cpu: "1"
limits:
memory: "1Gi" # 必须等于 requests
cpu: "1" # 必须等于 requests
四、Kubernetes 资源模型
4.1 基础问题
Q13: requests 和 limits 的区别?
requests:
- 用于调度决策
- 节点 allocatable >= sum(pod requests)
- 不创建内存 Cgroup 限制
- CPU requests 转换为 cpu.shares
limits:
- 运行时强制限制
- 创建 Cgroup 限制
- 内存超限 → OOM Kill
- CPU 超限 → Throttling
Q14: 三种 QoS 类别的判定条件?
Guaranteed:
- 每个容器都设置了 CPU 和内存的 requests 和 limits
- requests == limits
Burstable:
- 不满足 Guaranteed
- 至少一个容器设置了 requests 或 limits
BestEffort:
- 没有任何容器设置 requests 或 limits
4.2 进阶问题
Q15: kubectl top 显示的内存是什么?
kubectl top 显示的是 Working Set:
working_set = memory.usage - inactive_file
解释:
- memory.usage: Cgroup 内存使用量
- inactive_file: 非活跃文件页 (可回收的 Page Cache)
- working_set: 实际工作所需的内存
为什么不用 usage?
- usage 包含可回收的 Page Cache
- working_set 更接近真实需求
Q16: 应该设置 CPU limits 吗?
争议话题,两种观点:
不设置 (只设置 requests):
优点:
- 可以充分利用空闲 CPU
- 避免不必要的 throttling
缺点:
- 单个 Pod 可能影响其他 Pod
- 行为不可预测
设置 limits:
优点:
- 行为可预测
- 避免 noisy neighbor
缺点:
- 可能导致资源浪费
- 会产生 CPU throttling
推荐:
- 在线服务:考虑不设置
- 批处理任务:建议设置
- 根据实际情况决定
五、场景设计题
5.1 设计题
Q17: 设计一个容器内存监控告警系统
方案设计:
1. 数据采集层
- 使用 cAdvisor 采集容器指标
- 或直接读取 /sys/fs/cgroup/memory/*/
- 采集频率: 10-30s
2. 关键指标
- memory.usage_in_bytes (当前使用)
- memory.limit_in_bytes (限制)
- memory.stat 中的 rss/cache/inactive_file
- 计算 working_set = usage - inactive_file
3. 告警规则
- 使用率 > 80%: Warning
- 使用率 > 90%: Critical
- OOM 事件: Immediate
4. 告警策略
- 持续时间判断 (避免抖动)
- 分级告警 (Warning/Critical)
- 告警收敛
5. 存储与展示
- Prometheus 存储
- Grafana 展示
- AlertManager 告警
代码示例:
# Prometheus 告警规则
groups:
- name: container-memory
rules:
- alert: ContainerMemoryUsageHigh
expr: |
(container_memory_working_set_bytes /
container_spec_memory_limit_bytes) > 0.8
for: 5m
labels:
severity: warning
annotations:
summary: "Container memory usage > 80%"
- alert: ContainerOOMKilled
expr: kube_pod_container_status_restarts_total > 0
and kube_pod_container_status_last_terminated_reason{reason="OOMKilled"} == 1
for: 0m
labels:
severity: critical
Q18: 如何排查容器频繁 OOM 的问题?
排查步骤:
1. 确认 OOM 事件
kubectl describe pod <pod> | grep -A5 "Last State"
kubectl logs <pod> --previous
dmesg | grep -i "oom\|killed"
2. 分析内存配置
kubectl get pod <pod> -o yaml | grep -A10 resources
检查 requests 和 limits 是否合理
3. 查看实际使用
kubectl top pod <pod>
进入节点查看 /sys/fs/cgroup/memory/...
4. 分析内存组成
cat memory.stat
分析 rss、cache、mapped_file
5. 应用层排查
- Java: 检查堆设置 (-Xmx)
- Node.js: 检查 --max-old-space-size
- 检查是否有内存泄漏
解决方案:
- 增加内存 limits
- 优化应用内存使用
- 合理设置 JVM 参数
- 检查文件读写是否过多 (Page Cache)
六、源码级问题
6.1 内核源码
Q19: oom_badness 的计算逻辑?
// mm/oom_kill.c
long oom_badness(struct task_struct *p, unsigned long totalpages)
{
long points;
long adj;
// 不可杀的进程返回最小值
if (oom_unkillable_task(p))
return LONG_MIN;
// 获取 oom_score_adj
adj = (long)p->signal->oom_score_adj;
if (adj == OOM_SCORE_ADJ_MIN) // -1000
return LONG_MIN;
// 计算进程使用的页数
points = get_mm_rss(p->mm) + // RSS
get_mm_counter(p->mm, MM_SWAPENTS) + // Swap
mm_pgtables_bytes(p->mm) / PAGE_SIZE; // 页表
// 归一化并加上调整值
adj *= totalpages / 1000;
points += adj;
return points > 0 ? points : 1;
}
Q20: Cgroup 内存 charge 的流程?
// mm/memcontrol.c
int try_charge(struct mem_cgroup *memcg, gfp_t gfp_mask,
unsigned int nr_pages)
{
// 1. 尝试增加计数器
page_counter_try_charge(&memcg->memory, batch, &counter);
// 2. 检查是否超过 max 限制
if (page_counter_read(&memcg->memory) > memcg->memory.max) {
// 3. 尝试回收
mem_cgroup_reclaim(memcg, gfp_mask);
// 4. 回收失败触发 OOM
if (still_over_limit)
mem_cgroup_oom(memcg, gfp_mask, order);
}
// 5. 检查 high 水位 (v2)
if (page_counter_read(&memcg->memory) > memcg->high) {
// 限流,让出 CPU
schedule();
}
return 0;
}
七、快速复习清单
┌─────────────────────────────────────────────────────────────────────────┐
│ 面试前快速复习 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ Namespace: │
│ □ 8种类型: PID, Network, Mount, UTS, IPC, User, Cgroup, Time │
│ □ 容器 = 进程 + Namespace 隔离 + Cgroup 限制 │
│ □ 查看: /proc/<pid>/ns/ │
│ │
│ Cgroup: │
│ □ v1 多层级,v2 统一层级 │
│ □ memory.limit = RSS + Page Cache │
│ □ v2 新增: memory.high, memory.low, memory.min │
│ □ 路径: /sys/fs/cgroup/ │
│ │
│ 内存: │
│ □ RSS/PSS/USS 区别 │
│ □ Page Cache 计入限制 │
│ □ working_set = usage - inactive_file │
│ │
│ OOM: │
│ □ oom_score = (内存/总内存)×1000 + oom_score_adj │
│ □ -1000 永不杀, 1000 优先杀 │
│ □ Cgroup OOM 只杀组内进程 │
│ │
│ Kubernetes: │
│ □ requests: 调度用 │
│ □ limits: 创建 Cgroup 限制 │
│ □ QoS: Guaranteed > Burstable > BestEffort │
│ □ oom_score_adj: -997 / 2~999 / 1000 │
│ │
└─────────────────────────────────────────────────────────────────────────┘
相关链接
祝面试顺利!