Docker-12 高频面试题(原理与生产篇):30 题带答题思路
使用方式
原理题建议沿着“用户命令 → Docker API → 运行时 → Linux 内核 → 应用结果”回答;生产题建议沿着“现象 → 证据 → 假设 → 验证 → 修复 → 预防”回答。不要只说结论,要说明边界:Docker 的隔离强度取决于内核、运行时和启动参数。
1. namespace、cgroup 和文件系统
1.1 namespace 隔离了什么?
namespace 改变进程能看到的系统视图:PID、Mount、Network、UTS、IPC、User、Cgroup、Time 等。它不是资源限制,也不是完整安全沙箱;例如 PID namespace 让进程看不到宿主其他 PID,但 cgroup 才决定它最多使用多少 CPU/内存,capability/LSM/seccomp 才进一步限制它能做什么。
1.2 为什么容器里的进程 PID 是 1?
运行时创建新的 PID namespace 后,第一个进程在该 namespace 内编号为 1,通常就是镜像的 ENTRYPOINT。PID 1 对信号处理和孤儿进程回收有特殊语义;shell form entrypoint 可能不转发 SIGTERM,子进程也可能变成僵尸。可使用 exec form 或 --init,但应用仍要实现优雅关闭。
1.3 namespace 是怎么创建和切换的?
内核提供 clone()/clone3() 创建带 namespace 的子进程,unshare() 让当前进程脱离并创建新 namespace,setns() 加入已有 namespace。nsenter 通过打开 /proc/<pid>/ns/* 并调用 setns 进入容器视图。能否进入还受 user namespace、capability 和目标进程权限控制。
1.4 cgroup v1 和 v2 有什么区别?
v1 每个 controller 可以挂在不同层级,配置和协同复杂;v2 使用统一层级、统一接口和更明确的资源分配语义,现代发行版逐渐默认使用 v2。面试不应只说“v2 更快”,而要指出实际支持取决于内核、systemd、Docker 版本和 controller delegation。
1.5 --cpus、--cpu-shares 和 --cpuset-cpus 的区别?
--cpus=2 通过 CPU quota/period 限制带宽,超出会 throttling;--cpu-shares 是竞争时的相对权重,没有竞争时不限制绝对值;--cpuset-cpus=0,1 把进程限制在指定 CPU 集合。它们分别解决绝对带宽、相对优先级和 CPU 亲和性问题,不能互相替代。
1.6 memory.max、memory.high 和 swap 怎么解释?
memory.max 是 cgroup 内存硬上限,超出可能触发 cgroup OOM;memory.high 是压力阈值,超过后通常先回收和节流;swap 是否可用、如何计费取决于 v2 配置和 Docker 参数。设置内存上限要给线程栈、堆外内存、page cache 和运行时留余量,不能只按应用堆大小计算。
1.7 overlayfs 如何合并镜像层?
镜像层是 lowerdir,容器写层是 upperdir,overlayfs 通过 merged 目录提供统一视图。读取从上层向下层查找;修改 lowerdir 文件会 copy-up 到 upperdir;删除下层文件会生成 whiteout。大文件修改和大量小文件创建会放大 copy-up、元数据和 I/O 成本,持久化数据库应使用合适的 volume/块存储。
1.8 镜像层和容器可写层有什么区别?
镜像层不可变,可被多个容器共享;容器可写层属于单个运行实例,删除容器通常丢失其中内容。docker commit 可以把容器层固化成新镜像,但会隐藏构建过程、破坏可复现性,也不适合数据库或生产发布。正确做法是修改 Dockerfile、重新构建并通过 registry 发布。
2. Docker 运行时
2.1 dockerd、containerd、runc 分别做什么?
dockerd 提供 Engine API 并管理 Docker 对象;containerd 负责镜像传输、snapshot 和容器任务生命周期;runc 按 OCI Runtime Spec 创建 namespace、mount、cgroup、seccomp 并启动进程。它们不是三个重复的 daemon,而是从产品控制面到低层运行时的分层。不同版本会有 shim、snapshotter 和 runtime 替换,但职责边界大致稳定。
2.2 containerd-shim 为什么存在?
shim 把容器任务和 containerd/dockerd 管理进程解耦,持有 IO、退出状态和子进程关系。这样 containerd 或 dockerd 重启时,已有容器通常可以继续运行并在 daemon 恢复后同步状态。shim 不是跨主机高可用;宿主内核重启、断电、磁盘损坏仍会中断任务。
2.3 OCI bundle 和 Dockerfile 有什么关系?
Dockerfile 是构建镜像的声明文件;OCI bundle 是某次运行实例的 config.json 加 rootfs。Docker/Compose 将镜像配置、运行参数和安全选项转换成 bundle,runc 读取它创建容器。一个镜像可以启动出多个不同 bundle,改变网络、挂载、用户和资源限制。
2.4 为什么 Docker daemon 重启后容器可能不退出?
containerd-shim 持有任务,live-restore 还可以减少 daemon 重启期间的影响。这个能力只覆盖 daemon 管理进程重启,不覆盖内核、宿主机、网络和存储故障;升级 daemon/runtime 时还要评估 API、snapshot 和网络兼容性。不能把它当成高可用方案。
3. 网络原理题
3.1 bridge 网络中一个包如何出宿主机?
容器 eth0 通过 veth pair 接到 docker0 bridge;容器 namespace 的路由把包发向 bridge 网关;宿主机路由和 netfilter 决定是否转发,出站通常通过 conntrack/SNAT 把源地址改成宿主地址,返回包再还原到容器。每一步都可能受 rp_filter、firewall、MTU、代理和路由策略影响。
3.2 EXPOSE、-p 和 host network 的区别?
EXPOSE 只是镜像元数据,不开放端口;-p host:container 在宿主机配置发布和转发;host network 让容器共享宿主 network namespace,不需要端口映射但隔离变弱并可能产生端口冲突。对外发布应明确绑定地址,避免无意监听 0.0.0.0。
3.3 为什么容器能访问外网,外部却访问不了容器?
出站通常有 SNAT,返回流量属于已建立连接;入站没有自动从宿主端口转到容器端口,必须使用 -p/Compose ports、宿主路由或负载均衡。还要检查应用是否监听 0.0.0.0、宿主防火墙、云安全组和端口映射规则。
3.4 Compose 中为什么不能用 localhost 访问数据库?
每个容器默认有独立 network namespace,localhost 只代表当前容器自身。Compose 同网络提供服务名 DNS,应用应连接 db:5432。只有 network_mode: host 或 container:<name> 等共享网络 namespace 的场景才共享 localhost,后者也会共享端口并产生冲突。
3.5 Docker DNS 如何工作?
Compose 网络中的容器将服务名解析到 Docker 内置 DNS,再得到当前容器 IP;容器重建后 IP 可以变化,服务名保持稳定。DNS 解析成功不等于 TCP、TLS 或业务成功;排查要分别检查 /etc/resolv.conf、getent hosts、端口连接、证书和应用协议。
3.6 如何排查“容器网络不通”?
从外到内保存证据:
ss -lntp
docker port api
docker inspect api
docker exec api ss -lntp
docker exec api getent hosts db
docker exec api curl -v http://db:5432
sudo tcpdump -ni any port 5432
先区分 DNS、路由、端口监听、TLS 和应用认证,不要把所有失败都叫“Docker 没网络”。必要时用 nsenter -t <pid> -n 进入目标 namespace。
4. 存储与数据题
4.1 volume、bind mount、tmpfs 如何选择?
named volume 由 Docker 管理,适合数据库和应用持久数据;bind mount 使用指定宿主路径,适合开发源码、配置和受控日志,但会带来宿主路径/权限/泄露风险;tmpfs 放在内存,适合临时文件和短期敏感数据,停止后丢失。选择标准是生命周期、备份、权限、性能和迁移方式,而不是简单比较命令长度。
4.2 为什么挂载目录后镜像里的文件不见了?
挂载点会遮住目标路径原有内容,不会自动合并目录。将空宿主目录挂载到 /app/config 后,镜像中同路径的默认配置就不可见。解决方案是准备完整配置、使用初始化逻辑或调整挂载目标;先用 docker inspect .Mounts 和容器内 mount 验证。
4.3 容器内 UID 10001 为什么写不了宿主目录?
bind mount 的权限由内核按数字 UID/GID 检查,容器内用户名与宿主用户名没有自动关联。让宿主目录拥有对应数字 UID/GID,或使用 ACL/组权限;SELinux 主机还需正确标签。不要用 chmod 777 作为默认修复。
4.4 如何备份 Docker 中的 PostgreSQL?
优先使用 pg_dump/WAL 等数据库原生机制,保证一致性、可校验和时间点恢复;volume tar 只能作为普通文件迁移手段,不能在数据库持续写入时直接复制底层目录当热备份。备份要加密、异地保存、限制访问并定期恢复演练,验证 RPO/RTO。
5. 安全题
5.1 为什么 Docker socket 等价于 root?
能调用 daemon API 的主体可以请求创建容器、挂载宿主根目录、访问设备或修改其他容器,权限边界通常接近宿主 root。把 socket 挂到业务容器不是普通文件共享。应使用受限代理、专用构建节点、rootless daemon 或不暴露 API;同时审计 socket 访问。
5.2 --privileged 做了什么?
它会显著放宽 capability、设备访问、LSM/seccomp 等限制,具体细节随版本和平台变化。它可能让容器接近宿主高权限,不应作为“程序报权限错误”的通用解决方案。应分析缺少的 capability、设备或挂载,逐项最小授权并设置有效期。
5.3 capability、seccomp 和 SELinux 的职责是什么?
capability 将 root 权限拆分为细粒度能力;seccomp 过滤系统调用;SELinux/AppArmor 等 LSM 根据标签/策略限制对象访问。它们是互补层:只 drop capability 不能阻断所有 syscall,只启用 seccomp 也不能解决错误挂载。生产要保留默认策略,并为必要例外建立可审计 profile。
5.4 rootless Docker 是否绝对安全?
rootless 使用 user namespace 把容器 root 映射为宿主普通用户,降低 daemon/容器直接修改宿主 root 的风险,但共享内核、用户态代理、挂载和设备仍有攻击面。低端口、网络性能、cgroup delegation、overlayfs/NFS 能力也可能受限。应按威胁模型、内核和业务兼容性评估。
5.5 如何防止镜像供应链攻击?
锁定依赖和基础镜像版本,构建时不要带入秘密,生成 SBOM 并扫描,验证来源和签名,推送到受控 registry,部署使用 digest 而非可移动 tag。持续更新比永久锁死 digest 更重要;漏洞修复要重新构建和测试,而不是进入生产容器手工升级。
6. 生产排障和设计题
6.1 容器 OOM 如何定位?
先看 docker inspect 的 OOMKilled 和退出码,再看 docker stats、cgroup memory.current/memory.events、应用堆外指标和宿主 dmesg。区分容器 cgroup OOM、宿主全局 OOM、应用主动退出和人为 kill。修复可能是降低峰值、调整内存上限、限制并发、修正缓存或升级节点容量;不能只简单调大 limit。
6.2 容器 CPU 很高但宿主 CPU 不满,为什么?
容器可能受到 cpu.max 配额限制而 throttling,宿主仍有其他 CPU 空闲;也可能是单线程热点、锁竞争或 I/O 等待。检查 docker stats、cgroup cpu.stat、应用 pprof 和线程状态,区分“使用率高”和“被限速”。
6.3 如何设计一个可靠的 Compose 服务?
镜像使用多阶段、非 root 和固定版本;服务使用健康检查、优雅退出、资源和日志限制;数据库/缓存使用明确 volume 和备份;内部服务只加入所需网络,公网只暴露入口;配置与秘密外置,启动前 docker compose config;CI 做测试、扫描、SBOM 和签名,发布记录 digest 并准备回滚。
6.4 如何实现零停机发布?
单机 Compose 的 up 通常是替换容器,不能天然保证零停机。真正零停机需要多个副本、入口负载均衡、连接排空、兼容的数据库迁移、readiness 检查和自动回滚,通常由 Kubernetes/Nomad/云编排平台完成。Docker 镜像只是交付单元,不能单独提供完整发布语义。
6.5 Docker 如何做日志和监控?
应用写结构化 stdout/stderr,Docker logging driver 或 Fluent Bit 等采集;设置日志大小和保留周期,避免磁盘被打满。监控容器 CPU、throttling、内存、OOM、网络、I/O、重启、健康状态和应用延迟/错误率;docker stats 适合临时排查,长期使用 Prometheus/cAdvisor、宿主 exporter 和 tracing。
6.6 如何排查容器反复重启?
docker ps -a
docker inspect app --format '{{.RestartCount}} {{.State.ExitCode}} {{.State.OOMKilled}}'
docker logs --since 30m app
docker events --since 30m
然后验证 entrypoint/配置/依赖 DNS、端口冲突、健康检查过严、OOM/CPU 限制、权限/证书和外部服务。先保存现场,再修复 Dockerfile、配置或部署策略,避免在容器内临时改文件。
6.7 daemon 无法启动,如何恢复?
先读取 systemctl status docker、journalctl -u docker,检查 daemon.json JSON、重复命令行参数、data-root 权限/磁盘和存储驱动;确认不是误改 socket 或 registry TLS。修复配置后用 dockerd --validate/jq 检查,再启动并验证 docker info、镜像、容器和卷。不要直接删除 /var/lib/docker。
6.8 为什么 Docker Desktop 和 Linux 服务器表现不同?
Desktop 在 Linux VM 中运行 daemon,文件共享、磁盘、CPU/内存和网络多了一层虚拟化;宿主路径、I/O 延迟、端口转发和内核版本都可能不同。开发验证要说明运行平台,生产性能与故障演练必须在接近目标 Linux 环境的 runner 上复现。
7. 进阶开放题
7.1 如果需要容器逃逸防护,你会怎么做?
先缩小攻击面:及时修复宿主内核和 runtime,使用 rootless 或沙箱运行时,禁止业务访问 Docker socket,禁止 --privileged/宿主根目录/不必要设备,非 root、drop capability、seccomp/LSM、只读挂载、网络最小化、资源限制和审计告警。高隔离场景用 VM/microVM;同时准备补丁、隔离、取证和密钥轮换流程。
7.2 如何解释“容器不是虚拟机”?
容器是利用宿主内核机制隔离的一组进程,不包含独立内核;虚拟机包含 Guest OS 并在硬件虚拟化边界内运行。容器因此启动快、密度高,但共享内核、设备和部分资源;选择取决于内核兼容性、隔离强度、运维成本和工作负载。
7.3 如何解释一次完整的 docker run?
CLI 发 Engine API 请求,dockerd 解析镜像和运行参数,必要时从 registry 拉取 layer;containerd 准备 snapshot 和 task,创建 shim;runc 读取 OCI bundle,调用 clone/unshare、mount/pivot_root、cgroup、capability、seccomp,配置 veth/端口规则后执行 entrypoint;应用成为 PID 1,stdout/stderr 由日志驱动收集,退出状态回传给上层。
8. 场景题:用证据而不是猜测回答
8.1 “发布后 API 不断重启,你怎么处理?”
先停止无关变更并保存现场:
docker ps -a --no-trunc
docker inspect api > api.inspect.json
docker logs --timestamps --since 20m api > api.log 2>&1
docker events --since 20m > docker.events.log
然后按顺序判断:
RestartCount是否增长,ExitCode是 0、1、137 还是 143。.State.OOMKilled、cgroupmemory.events和宿主dmesg是否支持 OOM。- entrypoint、工作目录、架构、动态链接器、用户和挂载是否正确。
- 环境变量、secret、证书、DNS、数据库和迁移是否可用。
- healthcheck 是否过严,是否把下游短暂波动误判成 liveness 失败。
临时恢复可以回滚到已验证 digest或降低流量,根因修复必须落在 Dockerfile、配置、资源、代码或发布流程中;不要只把 restart 次数调大。
8.2 “容器能解析数据库,但连不上 5432,你怎么答?”
解析成功只证明 DNS 返回了地址:
docker exec api getent hosts db
docker exec api sh -c 'nc -vz -w 3 db 5432'
docker exec db ss -lntp
docker inspect api | jq '.[0].NetworkSettings.Networks'
docker inspect db | jq '.[0].NetworkSettings.Networks'
确认 API 和 DB 是否共享网络、DB 是否监听 0.0.0.0/容器地址、端口是否写错、数据库是否 ready、网络是否被 internal/防火墙限制。TCP 成功后仍要检查 TLS、用户名、密码、数据库名、连接池和迁移状态。
8.3 “磁盘快满但镜像不多,为什么?”
docker system df -v
du -xhd1 /var/lib/docker | sort -h
du -xhd1 /var/lib/docker/containers | sort -h
docker ps -a --size
docker volume ls
常见来源是 json-file 日志、BuildKit cache、容器可写层、volume 或 Desktop VM 磁盘。先识别 owner,再限制日志、清理构建缓存、归档或扩容 volume;不能只删除未使用镜像,更不能直接删 data-root 内部目录。
8.4 “如何证明容器真的受到了内存限制?”
docker run -d --name mem-lab --memory 128m alpine:3.20 sleep 600
PID=$(docker inspect -f '{{.State.Pid}}' mem-lab)
docker inspect mem-lab --format '{{.HostConfig.Memory}}'
sudo cat /proc/$PID/cgroup
sudo cat /sys/fs/cgroup$(awk -F: '$1=="0" {print $3}' /proc/$PID/cgroup)/memory.max
docker exec mem-lab cat /sys/fs/cgroup/memory.max
容器内外看到的路径可能因为 cgroup namespace 不同而不同,但应能对应到同一个 controller。验证“受限”还要通过受控压力测试和 memory.events 观察,不能只看 docker stats 的一次快照。
docker rm -f mem-lab
8.5 “如何解释 Docker 组权限事故?”
用户加入 docker 组后可以免 sudo,但 socket API 允许创建挂载宿主根目录、访问设备和读取其他容器环境变量的容器,因此在安全模型上接近 root。回答方案时要同时说:减少共享 daemon、多租户使用 rootless/隔离 runner、限制 CI 凭据、审计 socket、禁止业务容器挂 socket,而不是只说“把用户加组”。
8.6 “Compose 和 Kubernetes 都能重启容器,怎么选?”
Compose 的 restart 是单机 Docker Engine 容器级策略;depends_on.restart 是 Compose 显式操作依赖时的联动;Kubernetes 的 liveness/readiness、Deployment、ReplicaSet 和 controller 提供多副本、摘流、自愈、滚动更新和跨节点调度。单机开发用 Compose,生产多节点和高可用用编排平台,同时避免两层控制器重复负责同一生命周期。
9. 面试前自测清单
- 能画出 CLI → dockerd → containerd → shim → runc → kernel。
- 能解释 namespace、cgroup、overlayfs 各自解决什么问题。
- 能从宿主监听、端口映射、容器监听、DNS、应用协议逐层排查网络。
- 能说明容器层、volume、bind mount 的生命周期和备份边界。
- 能指出 docker socket、
--privileged、root 和秘密进入 layer 的风险。 - 能用 OOM、重启、磁盘满、daemon 故障案例给出证据驱动的排障流程。
- 能设计包含健康检查、优雅退出、资源限制、日志指标和回滚的发布方案。
10. 本篇小结
原理面试的核心是把 Docker 术语还原成 Linux 事实;生产面试的核心是把结论还原成可验证的证据和可执行的 runbook。回答“为什么”时说清边界,回答“怎么办”时给出命令、指标、回滚和预防措施,通常比罗列更多参数更有说服力。