Skip to content

第5章 Docker 与 containerd ​

对应简历:技能第4条「熟练掌握 Docker / containerd 容器技术与 Dockerfile 镜像构建,熟悉私有镜像仓库托管与认证拉取管理」/ 项目一职责「在统信 UOS(ARM)环境完成 Docker 与 kubeadm 组件部署」/ 项目二职责「完成全节点标准化初始化与集群部署:containerd 运行时(Systemd cgroup)…」「构建"公网拉取 → 内网私有仓库托管 → 集群认证拉取"的镜像双轨体系,通过 ImagePullSecret 实现私有镜像安全分发」

术语约定:Kubernetes(以下简称 K8s,仅正文简写)。仓库域名一律以「内网私有仓库」指代。

5.1 三大概念:镜像/容器/仓库 与 容器 vs 虚拟机 【必背】 ​

标准回答(60 秒口径,两个问题打包答): 先说三大概念,用"类与实例"的类比:镜像 Image 是只读模板,把应用、依赖和运行环境分层打包在一起,相当于"类";容器 Container 是镜像的运行实例,在只读层之上叠一个可写层,相当于"实例",启动是秒级的——因为不用引导内核只是起进程;仓库 Registry 是集中存取镜像的服务,Docker Hub、Harbor、各云厂商私有仓库都是,docker push 和 pull 的对象就是它。

再说容器和虚拟机的区别,本质一句话:容器是"隔离的应用进程",虚拟机是"完整的计算机"。展开两条:隔离级别——容器共享宿主机内核,靠 Namespace 隔离视图(pid/net/mnt 等)、Cgroup 限制资源配额,所以体积 MB 级、启动秒级、单机密度高;虚拟机通过 Hypervisor 模拟硬件、每个 VM 跑独立内核,隔离最彻底但体积 GB 级、启动分钟级。代价是容器的隔离性弱于 VM——内核是共享的,安全要求极高的场景仍要 VM 或 Kata 这类方案。

我在银行项目是"虚拟机里跑 K8s 容器"的两层叠加用法,正好把两者的定位都用上了。 追问链:

  • 追问1:容器靠什么隔离? → 答:两套内核机制配合。Namespace 管隔离——pid 让容器内进程有自己的进程树视图、net 给独立网络栈、mnt 独立挂载点、uts 独立主机名、ipc 隔离进程间通信、user 做用户映射,一共六类,解决"看得见什么";Cgroup 管限额——限制 CPU、内存、IO、进程数等配额,解决"能用多少"。一句话总结"Namespace 管隔离,Cgroup 管限额",K8s 的 resources.limits 底层就是 Cgroup。这套我还能接一句实操:在节点上 ls /proc/PID/ns/ 能看到进程的 namespace、cat /sys/fs/cgroup/... 能看到配额,说明我不是只背名词。

5.2 Docker 架构:C/S 模型 【理解】 ​

标准回答: Docker 是 C/S 架构,一条调用链讲清:docker 客户端命令 → Docker daemon(dockerd 服务端)做镜像、容器、网络、卷的管理 → containerd 负责容器生命周期管理 → 底层 runc 真正按 OCI 标准创建容器进程。

客户端和 daemon 的通信:同机默认走 Unix socket /var/run/docker.sock,远程走 TCP 2375(明文)或 2376(TLS)——2375 不加密绝不能暴露公网,生产远程管理要么 2376 开 TLS 双向认证,要么干脆只留本机 socket 加 sudo/docker 组控制权限。

理解这条链的价值在于后面两个话题:一是 K8s 的 kubelet 就是通过 CRI 接口调链路中的 containerd 这一层(跳过了 dockerd,见 5.13);二是排查"docker 命令没响应"时知道层次——daemon 挂了是 systemctl restart docker 的事,socket 权限问题是用户组的事,分得清才不会乱试。 追问链:

  • 追问1:docker CLI 和 daemon 之间走什么? → 答:本机默认是 Unix domain socket(/var/run/docker.sock),高效且权限可由文件系统控制——普通用户想免 sudo 用 docker 就是被加进 docker 组,本质是获得 socket 的读写权限,但这近似 root 权限要谨慎授予;远程场景走 TCP,2375 明文只配内网信任环境,2376 配 TLS 证书加密。我在银行内网环境用的是纯本机 socket 方式,最简单也最安全。

5.3 Dockerfile 指令全解(区别与坑) 【必背】 ​

标准回答: Dockerfile 指令我按"必考三组 + 常规一组"讲。第一组构建执行:FROM 指定基础镜像必须是第一条(多阶段构建有多个 FROM);RUN 是构建时执行命令,每条 RUN 生成一层,所以命令要合并并清理缓存——RUN yum install -y xx && yum clean all 一条搞定,不留缓存层。第二组启动命令(最高频考点):CMD 是容器启动的默认命令,会被 docker run 后面的参数整体覆盖;ENTRYPOINT 是固定入口,run 参数不覆盖它而是追加为参数。

组合用法 ENTRYPOINT ["java","-jar"] 加 CMD ["app.jar"]:默认跑 app.jar,又能 docker run 镜像 other.jar 换包。还要强调格式:CMD 写 shell 格式(CMD java -jar app.jar)时实际是 sh -c 包一层,sh 成了 1 号进程、收不到 SIGTERM;exec 格式(JSON 数组)让 java 直接当 PID 1,优雅停止才工作——生产一律 exec 格式。

第三组文件与变量:COPY 纯拷贝首选;ADD 是超集会自动解压本地 tar 和拉 URL,语义不透明不推荐;ENV 进镜像运行期生效;ARG 只在构建期有效(--build-arg 传值),敏感信息别放 ARG 因为 history 可查。常规组:EXPOSE 只是声明不等于映射(映射要 -p);VOLUME 声明匿名卷防数据写容器层;WORKDIR 工作目录;USER 非 root 运行是安全实践。

指令作用关键点/坑
FROM基础镜像第一条必须是 FROM;多阶段构建有多个 FROM
RUN构建时执行命令每条 RUN 一层;命令要合并 + 清理缓存
CMD容器启动默认命令可被 docker run 后的参数整体覆盖;多条只最后一条生效
ENTRYPOINT容器启动固定入口不被 run 参数覆盖;与 CMD 配合:ENTRYPOINT 定命令、CMD 定默认参数
COPY拷贝本地文件进镜像首选
ADDCOPY 超集额外:自动解压本地 tar、可拉 URL;语义不透明,拷文件一律用 COPY
EXPOSE声明端口仅声明/文档作用,不等于映射,真正的映射要 -p
ENV运行时环境变量持久进镜像,docker inspect 可见
ARG构建时变量仅构建期有效,--build-arg 传值;不要放敏感信息(history 可查)
VOLUME声明匿名卷挂载点防数据写进容器层;运行时用 -v 覆盖
WORKDIR工作目录后续指令的落脚点,不存在自动创建
USER运行用户安全实践:非 root 运行

追问链:

  • 追问1(高频):CMD 和 ENTRYPOINT 区别? → 答:核心差异是对 docker run 参数的反应:CMD 是"默认值",run 后面跟的命令会整体替换它;ENTRYPOINT 是"固定入口",run 参数追加为它的参数。所以 ENTRYPOINT 适合固定程序形态(这个容器就是跑 java -jar 的),CMD 适合可变默认值(默认跑哪个 jar、默认参数是什么)。组合拳 ENTRYPOINT ["java","-jar"] + CMD ["app.jar"] 我在生产用过:平时直接跑,换包时 run 后面给新 jar 名就行,不用改镜像。
  • 追问2:CMD 写成 shell 格式和 exec 格式有什么区别? → 答:CMD java -jar app.jar 是 shell 格式,实际执行 /bin/sh -c "java -jar app.jar"——sh 成为 PID 1,java 是它的子进程,docker stop 发的 SIGTERM 被 sh 吞掉传不下去,容器只能等超时被 SIGKILL 强杀,优雅停机失效。CMD ["java","-jar","app.jar"] 是 exec 格式,java 直接是 PID 1,能正确接收并处理 SIGTERM。结论:生产一律 exec 格式,这也是 K8s 里"容器停止要 30 秒宽限期"问题的常见根因之一。
  • 迭问3:ARG 和 ENV? → 答:生命周期不同:ARG 只在构建期存在,用 --build-arg 传入,构建完就消失;ENV 被固化进镜像,容器运行期也能读到。用法分工:构建临时变量(版本号、源地址)用 ARG;镜像里要固定的配置(时区、JVM 初始参数)用 ENV。安全提醒:ARG 的值会留在镜像 history 里能被 docker history 看到,所以数据库密码这类敏感信息两个都不适合直接放——应该用运行时注入(-e 或 Secret)。

5.4 分层原理、构建缓存与镜像瘦身(多阶段构建) 【必背】 ​

标准回答: 三个概念一条线讲。分层:镜像 = 多个只读层叠加,Dockerfile 每条 RUN/COPY/ADD 生成一层,相同的层可以被多个镜像和多个容器复用(内存和磁盘都省);容器 = 所有只读层 + 顶部一个可写层,修改文件时按写时复制 CoW:先把文件从只读层完整拷到可写层再改。缓存:构建时从上往下逐层比对"指令加内容"的哈希,命中就复用不再执行——由此推出 Dockerfile 顺序铁律:变的放后面。

典型应用:先 COPY pom.xml 装依赖、再 COPY 源码编译,这样改代码时依赖层缓存命中,不用重新下载依赖,构建从十几分钟缩到一两分钟。

瘦身五板斧:① 精简基础镜像(alpine/slim/distroless);② RUN 合并加清缓存(rm -rf /var/cache/yum);③ .dockerignore 排除无关文件减小上下文;④ 多阶段构建——编译阶段用全量镜像,运行阶段只 COPY 产物到精简镜像;⑤ 用固定小版本 tag 不用 latest(可复现且不会被上游悄悄换底包)。 实操模板(多阶段构建骨架,Java 示例):

dockerfile
# ---- 阶段1:构建(用全量 Maven 镜像)----
FROM maven:3.8-eclipse-temurin-8 AS builder
WORKDIR /build
COPY pom.xml .
RUN mvn dependency:go-offline            # 先装依赖,利用缓存
COPY src ./src
RUN mvn package -DskipTests
# ---- 阶段2:运行(只带 JRE 的精简镜像)----
FROM eclipse-temurin:8-jre
WORKDIR /app
COPY --from=builder /build/target/app.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java","-jar","app.jar"]

追问链:

  • 迭问1:为什么改一行代码整个 RUN 层重跑? → 答:因为缓存的失效是"向后传染"的:Dockerfile 逐层比对,某一层的内容变了,它自身的缓存失效,同时它之后的所有层都失去复用资格——层号必须连续对应。所以同样的构建逻辑,指令顺序不同效率天差地别:把 COPY src 放在装依赖之前,每次改代码依赖层全部失效重下;放之后,改代码只触发编译层。这条铁律我给开发讲过多次,是 Dockerfile 优化的第一条。
  • 迭问2(深挖):进入容器改了文件,commit 后镜像变大的原因? → 答:CoW 机制的反噬:修改可写层里的文件时,要把原文件从只读层完整拷贝到可写层再修改——一份文件变成两份占空间;删除文件也只是在新层打"删除标记"(whiteout),旧层的实体还在。所以"进容器手工改东西再 commit"是反模式,镜像越 commit 越胖还不可复现。正确姿势永远是改 Dockerfile 重建——一切变更代码化,这也是我在交付规范里写明的。

5.5 现场手写模板一:Python 应用 【必背】 ​

标准回答(面试常要求现场写,按此背): Python 应用的 Dockerfile 我按六段写:精简基础镜像 python:3.11-slim 起底;WORKDIR /app 定工作目录;两个 ENV 关闭字节码写入和打开无缓冲输出——PYTHONUNBUFFERED=1 尤其重要,不设的话 stdout 有缓冲,docker logs 看不到实时日志排障抓瞎;先 COPY requirements.txt 再 pip install——利用缓存铁律,改代码不重装依赖,-i 指定国内镜像源加速;然后 COPY . . 拷代码;最后 EXPOSE 8000 加 CMD 用 gunicorn 起四个 worker。

写完构建运行一条龙:docker build -t myapp:v1 . 然后 docker run -d --name myapp -p 8000:8000 myapp:v1。这版模板每个指令都有"为什么",被追问任何一行我都能给理由。

dockerfile
FROM python:3.11-slim                      # 精简基础镜像
WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1                     # 日志实时输出
COPY requirements.txt .                    # 先依赖后代码(缓存)
RUN pip install --no-cache-dir -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple/
COPY . .
EXPOSE 8000
CMD ["gunicorn","-w","4","-b","0.0.0.0:8000","app:app"]

追问链:

  • 迭问1:为什么加 PYTHONUNBUFFERED? → 答:Python 默认对 stdout 做缓冲,输出攒够一批才真正写出;容器场景下日志是 docker logs 或 kubectl logs 从标准输出抓的,缓冲导致日志延迟甚至看起来"是空的"。PYTHONUNBUFFERED=1 关闭缓冲让每条 print 立刻落到标准输出——这是 Python 容器化的标配参数,忘了它,第一次排障就会困惑"为什么容器跑着但没日志"。

5.6 现场手写模板二:Java 应用与 Nginx 静态站 【必背】 ​

标准回答: Java 后端镜像我以银行数币平台的后端 jar 为语境:基础镜像 eclipse-temurin:8-jre 只带运行时;WORKDIR /opt/app;COPY jar 包改名 app.jar;ENV 里配时区 Asia/Shanghai 和 JAVA_OPTS 堆参数;EXPOSE 8080;ENTRYPOINT 用 ["sh","-c","java $JAVA_OPTS -jar app.jar"]——这里 sh -c 是必须的,因为 exec 格式的 JSON 数组不做变量替换,$JAVA_OPTS 会原样传给 java 当参数报错,套一层 sh 让 shell 展开变量,这是 Java 镜像的经典坑。

前端静态站镜像更简单:nginx:1.25-alpine 做底,COPY dist 到 Nginx 的 html 目录,COPY 自定义 nginx.conf 进去——配置里预置 try_files 兜底和 /prod-api/ 反代(对应第 4 章那套),EXPOSE 80。两个镜像合起来就是数币平台的前后端容器化形态。

dockerfile
# Java 后端(银行项目 jar 语境)
FROM eclipse-temurin:8-jre
WORKDIR /opt/app
COPY dcepscapi-admin.jar app.jar
ENV TZ=Asia/Shanghai JAVA_OPTS="-Xms512m -Xmx1024m"
EXPOSE 8080
ENTRYPOINT ["sh","-c","java $JAVA_OPTS -jar app.jar"]   # 用sh -c才能吃到ENV变量
dockerfile
# Nginx 静态站(前端镜像)
FROM nginx:1.25-alpine
COPY dist/ /usr/share/nginx/html/
COPY nginx.conf /etc/nginx/conf.d/default.conf    # 自带配置:try_files + /prod-api/ 反代
EXPOSE 80

追问链:

  • 迭问1:ENTRYPOINT 里为什么要 sh -c? → 答:因为 exec 格式的 JSON 数组是"直接 exec 不经过 shell",内核层面执行 argv 数组,没有任何变量替换——ENTRYPOINT ["java","$JAVA_OPTS","-jar"] 里的 $JAVA_OPTS 会被当成字面字符串传给 java,直接报无法识别的参数。套 sh -c "java $JAVA_OPTS -jar app.jar" 后由 shell 负责展开变量。代价是 sh 成了 PID 1——好在 sh -c 会把信号转发给子进程(exec 形式的 sh -c 单命令场景),优雅停止仍可用。这个细节我在 ARM 环境构建 Java 镜像时真实处理过。
  • 迭问2:ARM 环境镜像有什么不同? → 答:核心就一条:架构必须匹配。基础镜像要用 arm64 版本,拉取前用 docker manifest inspect 镜像 看 architecture 字段确认是否 multi-arch;构建时在 ARM 机器上构建出来的自然是 arm64。银行项目所有镜像走 aarch64 专用包加离线导入(见 5.11),我在交付前做"镜像架构清单"逐个核对——这个习惯来自一次 exec format error 事故。

5.7 docker run 常用参数 【理解】 ​

标准回答: docker run 参数我按四个场景分组背。基础组:-d 后台运行、--name 命名(后续操作都靠它)、--rm 容器退出即删除(一次性任务用)、-it 交互终端。网络组:-p 宿主端口:容器端口 做映射(这是外部访问容器的入口)、--network 加入指定网络、--net=host 直接共享宿主网络栈(性能最好但无隔离)。存储组:-v 宿主目录:容器目录 绑定挂载(配置文件常用)、-v 卷名:容器目录 命名卷(数据库数据常用)。

配置组:-e KEY=VALUE 注入环境变量(密码、连接串的正确姿势)、--restart=always 开机自启加崩溃自动重启、--memory/--cpus 资源限额(cgroup 的用户态入口)。

综合示例一句背:docker run -d --name web -p 80:80 -v /data/web:/usr/share/nginx/html --restart=always nginx:1.25——后台起一个 web 容器,映射 80 端口、挂载站点目录、开机自启,这是我给客户部署单机服务时的标准起手式。 追问链:

  • 迭问1:--restart=always 和 on-failure 区别? → 答:always 是无条件重启——容器无论以什么原因退出都拉起,而且 daemon 重启(宿主机重启)后也会自动把容器拉起来,适合必须常驻的服务;on-failure 只在退出码非零(异常退出)时重启,正常退出的任务容器(跑完就退的批处理)不会被拉起。我的部署习惯:常驻服务一律 always;一次性的导数、迁移任务不用重启策略让它跑完即止。K8s 里对应的就是 restartPolicy 和 Deployment 控制器的语义了。

5.8 四种网络模式与容器间通信 【必背】 ​

标准回答: Docker 网络四种模式按使用频率排:bridge 是默认——docker0 网桥加 NAT,容器有独立网络栈,对外靠 -p 端口映射,通用场景都用它;host 直接共享宿主机网络栈,没有网络隔离、性能最好,适合端口不冲突且追求吞吐的服务;container 模式是"加入另一个容器的网络命名空间"——这就是 K8s Pod 的底层原理:先起一个 pause 沙箱容器持有 network namespace,Pod 里其他业务容器都以 container 模式 join 进来,所以同 Pod 容器共享 IP、用 localhost 互通;none 无网络,给安全测试或离线任务用。

容器间通信两层讲:同主机——默认 bridge 下用容器 IP 直通(同网段),但生产推荐自建网络 docker network create app-net,自建网络带内嵌 DNS,容器名自动解析互 ping;跨主机——默认不通,靠端口映射到宿主机互访,或者上 overlay 网络(flannel/calico)——这正是 K8s CNI 要解决的问题,我在移动云集群用的 Flannel VXLAN 就是解这个。 追问链:

  • 迭问1:为什么默认 bridge 里容器名 ping 不通、自建网络可以? → 答:因为默认 bridge(docker0)没有内嵌 DNS 服务,容器里只能靠 IP 通信,而容器 IP 重启就变,写死必然翻车;用户自建的网络自带 Docker 内嵌 DNS 服务器(127.0.0.11),容器启动时把容器名注册进去,其他容器解析名字自动拿到当前 IP。这就是 docker compose 里服务名互访、以及 K8s 里 Service 名解析(CoreDNS)的同一思想:名字稳定、IP 可变、DNS 做映射。
  • 迭问2(深挖):K8s Pod 里多容器怎么共享网络? → 答:机制是"pause 容器加 container 模式":Pod 被创建时,运行时先起一个 pause(沙箱)容器,由它持有这个 Pod 的 network namespace 和 IP;业务容器创建时以 container 网络模式加入 pause 的 namespace。效果是同 Pod 所有容器共享一个 IP 和端口空间,彼此 localhost 直达、用端口区分——所以"_sidecar 和主容器 localhost 通信"是 Pod 设计的天然能力。pause 镜像极小(几百 K),在节点上 ps 能看到一堆 pause 进程,它们就是各 Pod 的网络锚点。

5.9 数据卷与挂载 【理解】 ​

标准回答: 容器数据管理的原则一句话:"容器无常、数据有常"——可写层随容器删除而消失,要持久的数据必须挂出来。三种挂载方式:命名卷 -v data:/var/lib/mysql——由 daemon 管理,实体在 /var/lib/docker/volumes/ 下,跨容器复用方便、迁移由 docker 管,是数据库等有状态服务的最佳实践;绑定挂载 -v /host/path:/container/path——直接映射宿主目录,配置文件分发、开发时代码热更新常用,注意宿主目录的属主权限要容器内进程可读写;匿名卷——Dockerfile 里 VOLUME 指令声明的,没起名,容器删了容易变成孤儿垃圾,要定期 docker volume prune。判断标准很简单:数据库目录、日志这类必须出容器层的,用命名卷或绑定挂载;无状态应用随便重建。

我在 Kuboard 部署里用的 hostPath 持久化(K8s 语境)也是同一思想(见第 6 章)。 追问链:

  • 迭问1:容器删了数据还在吗? → 答:分两层:可写层的数据随容器删除一起消失——这就是为什么不能把数据库文件留在容器层;卷和绑定挂载的数据在宿主机上,容器删了也在。所以正确的数据姿势是:有状态服务的数据目录一律挂卷(数据库的 datadir、应用的日志目录),容器本身保持无状态可随时重建——升级、迁移、故障恢复都以"换容器"为单位,数据在卷里原地不动。这也是 K8s 里 Pod 易逝设计能成立的前提。

5.10 docker compose 基础 【理解】 ​

标准回答: compose 是单机多容器编排:一个 yaml 文件声明一组服务——每个服务写镜像或 build、端口、卷、环境变量、依赖关系,docker compose up -d 一键按依赖顺序全部拉起。相对裸 docker 的三个优势:声明式管理——服务按配置创建,可重复可版本化;依赖顺序——depends_on 声明谁先起(比如数据库先于应用);同 project 默认同一自建网络——服务名直接互访(内嵌 DNS 的红利)。

常用命令五件:up -d 起、down 停并清理、ps 看状态、logs 看日志、restart 重启。它和 K8s 的边界要清晰:compose 是单机编排,没有自愈、调度、滚动更新这些集群能力;K8s 是跨节点集群编排。

我的使用口径:本地开发和单机小工具用 compose,生产多机用 K8s——移动云项目我甚至没用 compose,直接 K8s 的 Deployment/Service,因为生产环境不需要中间态。 实操模板(最小 compose 示例):

yaml
services:
  web:
    image: nginx:1.25
    ports: ["80:80"]
    volumes: ["./dist:/usr/share/nginx/html"]
    depends_on: [app]
  app:
    build: .
    expose: ["8000"]

追问链:

  • 迭问1:compose 和 K8s 的区别? → 答:三个维度:规模——compose 单机,K8s 跨节点集群;能力——compose 只管"把一组容器按顺序跑起来",容器挂了不会自动重建,K8s 有控制器调谐、自愈、滚动更新、服务发现全套;声明格式——compose 是自己的 yaml,K8s 是资源对象 API。选择标准看场景:本机开发环境、单台服务器的工具集,compose 足够且轻;生产环境要高可用和弹性,直接 K8s。我的经历正好是两边都有:银行项目 ARM 单机上的工具用类 compose 思路管理,移动云生产集群全套 K8s 对象。

5.11 镜像导出导入与离线分发(本人真实:ARM 批量 docker load) 【必背】 ​

标准回答(结合项目讲): 命令对先分清:docker save -o web.tar web:v1 把镜像导出成 tar 包(保留完整分层),docker load -i web.tar 或 cat web.tar | docker load 导入(tar.gz 能直接喂不用先解压);另一对 docker export/import 是针对容器的——把运行中容器的文件系统导出,丢掉分层和历史,一般不用。

我的真实场景讲完整:银行内网环境完全无法访问海外镜像仓库,而且 TaiShan 是 ARM 架构,所以所有镜像走"外网准备加离线分发":在外网机器上准备各镜像的 arm64 tar 包,传进内网,然后我给 9 个 BCS 数据面节点写了一个批量加载脚本——for 循环遍历镜像目录下所有 tar.gz,用 case 排除 edge-manager 开头的管理面专用镜像(那些由管理面节点加载,数据面不需要),其余全部 cat 进 docker load,加载完 docker images 核对清单。这个脚本让十几类镜像一次铺满全部数据面节点,BCS 纳管一次通过,后来沉淀进部署指南成为标准动作。 实操模板(批量加载脚本,可直接背写):

bash
#!/bin/bash
# 离线批量加载镜像:排除管理面专用镜像,其余全部导入
for f in *.tar.gz; do
    if [[ "$f" != edge-manager* ]]; then
        echo "loading $f"
        cat "$f" | docker load
    fi
done
docker images   # 加载完核对镜像清单

追问链:

  • 迭问1:为什么 ARM 镜像不能直接用 Docker Hub 的? → 答:因为多数官方镜像的历史版本只构建了 amd64 架构,在 aarch64 节点上拉取会报 no matching manifest for linux/arm64/v8,或者强行 load 了 x86 镜像后容器启动报 exec format error。处理三选一:找官方 multi-arch 镜像(docker manifest inspect 镜像 看 architecture 列表确认)、基于 arm64 基础镜像自己构建、或者拿已经在 ARM 上验证过的离线包直接分发。我的流程是交付前做"镜像架构清单"逐个确认——ARM 项目这是必踩坑,我把踩过的坑变成了检查项。

5.12 私有仓库:tag/push/pull 与 ImagePullSecret 认证拉取(项目二真实) 【必背】 ​

标准回答(三条链路): 私有仓库体系我搭过完整的三条链路。第一条推送链路(我推 Kuboard 镜像的真实流程):在能访问公网的节点 docker pull eipwork/kuboard:v3.5.2.2 拉下原镜像 → docker login 内网仓库地址 认证 → docker tag 原镜像 内网仓库/项目名/kuboard:v3.5.2.2 重打标签——tag 格式必须符合"仓库/项目/镜像:版本"的规范,不合规 push 会被拒 → docker push 上传 → 控制台或重新 pull 验证。

第二条集群认证拉取链路:私有仓库要凭据,kubectl create secret docker-registry regcred --docker-server=仓库 --docker-username=账号 --docker-password=密码 -n 命名空间 创建凭据,Deployment 里 imagePullSecrets 引用它,Pod 就能认证拉取。

第三条是整体设计——移动云项目的"镜像双轨体系":公网拉取 → 内网私有仓库托管 → 集群认证拉取;运行时策略三层:首选内网仓库、备选阿里云公共仓库、兜底二进制部署——这套策略解决了海外镜像拉不下来导致节点 NotReady 的最高频故障(避坑清单第 1 条)。 追问链:

  • 迭问1:节点级和 Pod 级认证有什么区别? → 答:三种机制层次不同。docker login 是节点级——凭据写进该节点 ~/.docker/config.json,影响这台节点上的所有拉取,粒度粗、适合可信内网;imagePullSecrets 是 namespace 级的 K8s Secret——按需授权给特定命名空间的特定工作负载,粒度细、可审计,是推荐做法;第三种是节点 containerd 直接配 registry 的 hosts 和凭据(也是节点级但作用在 containerd 层)。还有一件容易被混淆的事:仓库信任(自签证书的 CA 要下发到节点)是节点级配置,拉取凭据是 Secret 级配置——两件独立的事,我排查认证问题时分开查。
  • 迭问2:Pod 报 unauthorized/imagePullBackOff 怎么排查? → 答:固定三板斧:第一 kubectl describe pod 看 Events——"Failed to pull" 后面的原因直接分类:unauthorized 是凭据问题(secret 名字错、不在同一 namespace、密码错),manifest not found 是镜像名或 tag 错,timeout 是网络不通或仓库不可达;第二看 Pod spec——imagePullSecrets 的名字和所在 namespace 对不对、镜像地址拼写;第三到 Pod 所在节点用同一运行时手动 crictl pull 复现——能复现就是仓库/凭据/网络问题,不能复现就是 K8s 对象配置问题。这个分层复现的思路比瞎猜快得多。
  • 迭问3:Harbor 怎么装?(了解) → 答:两种方式:官方离线安装包(docker-compose 起全套组件)或 helm 部署到 K8s;核心组件是 nginx 前置入口加 registry 存储、core 管理 API、database 配置存储,生产一般 2 节点起步考虑复制。我的实际口径:生产用的是云厂商私有仓库(托管免运维),Harbor 属于课程实验加理解——概念(项目/角色/复制策略/漏洞扫描)我都懂,被深挖就如实说装过实验版。

5.13 containerd vs Docker:为什么 K8s 弃用 dockershim(项目二真实坑) 【必背】 ​

标准回答(三层递进): 这题我按"关系、为什么、怎么落地"三层答。第一层关系:containerd 本来就是 Docker 公司捐给 CNCF 的核心运行时组件——Docker 内部一直用它管容器生命周期,两者是上下游不是竞品。K8s 的 Docker 时代调用链是 kubelet → dockershim(K8s 为兼容 Docker 专门维护的适配层)→ dockerd → containerd,链路长、每层都有开销和维护负担。第二层为什么弃用:K8s 1.24 正式移除 dockershim,kubelet 直接通过 CRI 标准接口调 containerd,少一层转发更轻更稳,也符合"标准化解耦"的方向;用户最关心的问题——Docker 构建的镜像还能用吗?能,因为 Docker 构建出的镜像遵循 OCI 规范,containerd 直接能跑,不需要重建。

第三层落地三个关键配置(移动云项目实测,全是坑换来的):SystemdCgroup = true——containerd 与 kubelet 的 cgroup 驱动必须对齐为 systemd,不一致会导致资源统计漂移甚至节点异常;sandbox_image 换国内源——默认的 registry.k8s.io 的 pause 镜像内网拉不到,kubelet 起不来 Pod;/etc/crictl.yaml 显式绑定 socket——crictl 默认按 docker/containerd/crio 顺序遍历端点,环境里有残留配置就报连接错误,显式写死 containerd.sock 一劳永逸(这就是避坑清单第 2 条)。 实操模板(crictl.yaml + 命令对照,背):

yaml
# /etc/crictl.yaml —— 显式指定运行时端点,避免多 socket 遍历报错
runtime-endpoint: unix:///var/run/containerd/containerd.sock
image-endpoint: unix:///var/run/containerd/containerd.sock
timeout: 10
debug: false
docker ps      → crictl ps            查容器
docker images  → crictl images       查镜像
docker logs    → crictl logs / kubectl logs
docker exec    → crictl exec / kubectl exec
docker inspect → crictl inspect      查详情

追问链:

  • 迭问1:pause 镜像是干什么的? → 答:它是每个 Pod 的沙箱容器,Pod 创建时第一个被拉起,作用是持有这个 Pod 的 network namespace 和 IPC namespace;随后创建的业务容器以 container 网络模式加入 pause 的命名空间,共享同一个 IP 和端口空间——这就是同 Pod 容器能用 localhost 互通的原理。pause 本身几乎不耗资源(就是挂着的 sleep 进程),所以 Pod 里业务容器重建,网络身份不变。理解了它,"Pod 是一组共享网络容器的逻辑单元"这句话就不再是抽象定义。
  • 迭问2(深挖):Docker 和 containerd 能装在同一台节点吗? → 答:不建议。两者会争抢资源:docker0 网桥和 iptables 链的规则可能互相干扰、容器存储目录的命名体系不同、两套 daemon 并存增加排障复杂度——K8s 节点上出现"docker 命令能看到的容器和 crictl 看到的不一致"多半就是这种混装状态。正确姿势:K8s 节点只装 containerd;构建能力放独立的构建机或用 kaniko、BuildKit 这类不依赖 Docker daemon 的工具。
  • 迭问3:containerd 时代怎么构建镜像? → 答:containerd 没有 build 命令,因为它定位是运行时不是构建工具。三个替代:独立 Docker 构建机(我就是这么做的——能上网的节点 pull/tag/push,构建产物进私有仓库给集群拉);buildah 或 kaniko 在 K8s 集群内构建(kaniko 在容器里不需要特权,适合 CI/CD 流水线);Docker 的 BuildKit 也支持脱离 daemon 的独立构建模式。我的双轨体系里构建机就是"公网拉取"那一轨的载体。

5.14 容器排查命令组 【理解】 ​

标准回答(一组打天下): 容器排障我有一组固定命令链,按顺序走:第一步 docker ps -a 看状态——重点看 Exited 还是 Restarting,退出看退出码;第二步 docker logs -f --tail=200 容器 看日志(K8s 场景看上次崩溃加 --previous);第三步 docker exec -it 容器 sh 进容器自证——ps 看进程、ss 看端口、env 看变量、curl 探依赖;第四步 docker inspect 容器 核对配置——IP、挂载、环境变量、退出码、重启策略;第五步 docker stats 看实时资源有没有顶到限额。

退出码要形成条件反射:137 表示 OOMKilled 或被 SIGKILL——先查内存 limit;143 是 SIGTERM 正常终止;1 是应用自身异常——看日志。

这套顺序的逻辑是"从外到内、从状态到内容":先确认死活,再看它说了什么,再进去看环境,最后对配置。 实操模板:

bash
docker ps -a                              # 1 看状态(Exited? Restarting?)
docker logs -f --tail=200 <容器>           # 2 看日志(--previous 看 K8s 上次崩溃)
docker exec -it <容器> sh                  # 3 进容器验证(ps/ss/env/curl)
docker inspect <容器>                      # 4 看配置(IP/挂载/环境变量/退出码)
docker stats                              # 5 实时资源(CPU/内存)
docker top <容器>                          # 容器内进程

追问链:

  • 迭问1:容器反复重启怎么定位? → 答:按因果链四查:一查退出码——describe 或 inspect 里看 Last State 的 exitCode,137 先怀疑内存 limit 太小被 OOMKilled;二查崩溃前日志——logs --previous 看最后几行,配置错误、连不上数据库这类启动即退的原因都在里面;三查探针——K8s 场景 describe 看 liveness probe failed,探针太激进会把启动慢的应用杀进死循环;四查依赖——应用启动依赖的 DB/Redis/配置中心连不上。四个方向覆盖了绝大多数 CrashLoopBackOff。

5.17 docker 磁盘治理:system df / prune 与容器日志限制 【理解】 ​

标准回答: Docker 用久了节点磁盘会被三类东西吃掉,我的治理三板斧对应三类。第一镜像堆积:docker system df 一条命令看镜像、容器、卷各自占了多少——这是治理入口;docker image prune -a --filter "until=168h" 清掉一周没用的镜像(注意 -a 会删所有未被容器引用的镜像,生产确认后再跑)。第二容器可写层和停止容器:docker container prune 清停止的容器、docker system prune 一把清(卷要单独 --volumes,谨慎)。

第三容器日志——最容易被忽略的磁盘杀手:容器 stdout/stderr 默认存 /var/lib/docker/containers/<id>/<id>-json.log,无限制增长能把根分区撑爆。

根治要配 daemon.json:{"log-driver":"json-file","log-opts":{"max-size":"50m","max-file":"3"}}——单文件 50M 最多 3 份滚动,改完重启 docker 生效。我处理过的真实场景就是容器日志无声涨爆磁盘,之后"新节点初始化必配日志限额"写进了基线脚本。 追问链:

  • 迭问1:docker system prune 有什么风险? → 答:它默认删除:所有停止的容器、无容器使用的网络、悬空镜像(dangling)、无容器引用的构建缓存——不加 --volumes 不动卷、不加 -a 只删悬空镜像。风险点两个:一是"停止的容器"可能包含你想留现场的事故容器,prune 前先 docker ps -a 确认;二是加了 -a 和 --volumes 的深度清理会连"暂时没被引用"的镜像卷一起删,构建缓存全丢,下次构建变慢。我的生产习惯:先 df 看分布、按类精准 prune(image/container 分开清)、大动作在变更窗口做——批量删除类命令永远先看清单再执行。
  • 迭问2:镜像和容器日志之外还有什么占磁盘? → 答:还有三块常被漏掉:卷(/var/lib/docker/volumes——匿名卷残留,docker volume prune 清理没被引用的);构建缓存(build cache,频繁 build 的机器能占几十 G,docker builder prune 清);以及 containerd 环境下对应换到 crictl 体系:crictl images、ctr -n k8s.io images remove 或 crictl rmi 清理未使用镜像——K8s 节点的镜像堆积同样要治,节点磁盘满是 Pod 驱逐的前奏(第 6 章避坑里的关联项)。移动云节点的磁盘治理我就是按"日志限额 + 定期清未用镜像 + 磁盘 85% 告警"三件套做的。

5.18 容器资源限制与 OOM 行为 【理解】 ​

标准回答: docker run 层面的资源限制:--memory 512m 内存上限、--cpus 1.5 CPU 配额(等价于 cgroup 的 1.5 核)、--memory-swap 内存加 swap 的总上限。两类资源超限的行为差异必须分清:CPU 超限是"节流"——进程被 cgroup 限制在配额内运行,表现是变慢(throttle),不会死;内存超限是"击杀"——cgroup 内存硬顶,内核 OOM killer 直接杀进程,容器退出码 137,表现是死亡。

所以评估 limit 时内存必须留足余量(Java 应用还要考虑堆外内存),CPU 可以贴着需求设甚至超卖。还有一个高频坑:容器里看到的是宿主机的内存和 CPU 数(top/free 显示宿主值)——Java 8 老版本按宿主机内存算默认堆大小,在 2G limit 的容器里可能默认堆就超了,要么显式 -Xmx 要么用感知容器的 JDK 版本(8u191+ 或 Java 10+)。

这套认知在 K8s 里就是 requests/limits 的基础(6.10)。 追问链:

  • 迭问1:容器 OOMKilled 和宿主机 OOM 的区别? → 答:容器 OOMKilled 是 cgroup 级别的:这个容器的内存用量触到了自己 limit 的天花板,内核选择杀掉该 cgroup 内的进程——只影响这个容器,宿主机和其他容器安全,docker inspect 里 OOMKilled 标志为 true。宿主机 OOM 是系统级的:物理内存整体耗尽,内核按 oom_score 全局挑牺牲者,可能杀掉任何进程包括 sshd。排查动作因此不同:容器 137 查自己的 limit 和应用内存行为;宿主机 OOM 查 dmesg 找被杀进程、治理整机内存超卖。我在 K8s 节点上见过 Pod 137,也警惕节点级驱逐——两级都要防。
  • 迭问2:--cpus 和 cpu-shares 什么区别? → 答:--cpus(对应 cfs quota)是硬上限——容器最多用 1.5 核,超了被节流,是确定性的限制;cpu-shares 是软权重——默认 1024,只在 CPU 争抢时按比例分配,空闲时可以吃到全部。生产配资源限制用 --cpus 保确定性;同优先级批处理任务可以用 shares 让它们在空闲时互相谦让。这个"硬配额 vs 软权重"的区分在 K8s 里对应 limits(硬)和 requests(调度权重)——同一套思想两层实现。

5.19 镜像的构成:manifest、config 与 OCI 标准 【理解】 ​

标准回答: 理解镜像构成对排查架构类问题(ARM)和仓库类问题(unauthorized、not found)很有帮助。一个镜像在仓库里由三类对象组成:manifest 清单文件——描述这个镜像有哪些层、是什么架构(architecture: arm64/amd64 字段就在这);config 配置文件——镜像的元数据,包括环境变量、CMD、ENTRYPOINT、暴露端口,docker inspect 看到的很多信息就来自它;layers 层数据——真正的文件系统内容,每层是一个压缩的 tar 包,用内容寻址(sha256)存储——相同内容的层全局只存一份,这就是"多镜像共享层"和 pull 时"已存在跳过"的原理。

manifest list(或叫 image index)是多架构镜像的关键:一个 tag 指向一个清单列表,列表里按架构列出各自的 manifest——客户端 pull 时报出自己的架构,服务端返回对应版本。

OCI 标准把"镜像格式 + 运行时(容器怎么跑)"都规范化了——这就是 Docker 构建的镜像 containerd 直接能跑的原因(5.13),也是 docker manifest inspect 能查架构的原因。 追问链:

  • 迭问1:pull 一个镜像的完整过程? → 答:客户端先请求 registry 拿 manifest(多架构镜像先拿 manifest list,按本机架构选出具体 manifest);解析出层的 sha256 列表;逐层下载 blob,本机已有的层(内容寻址相同)直接跳过——这就是增量 pull 快的原因;全部就位后组装成本地镜像,记录 config。理解这个过程能解释一堆现象:拉取卡住是某层 blob 下载慢、manifest not found 是 tag 或架构没有对应清单、digest 固定拉取(@sha256)能保证内容不变——我在排查内网仓库问题时就是按这个链路逐环节验证的。

5.20 容器安全基线(非 root / 只读根文件系统 / 能力削减)【了解】 ​

标准回答: 容器安全我做基线级的三件事加一个认知。认知先行:容器共享宿主内核,隔离强度弱于虚拟机——所以"容器 breakout 逃逸"是真实风险类别,重要业务别裸奔。三个基线动作:① 非 root 运行:Dockerfile 里 USER app 或运行时 --user 1000:1000——容器内 root 和宿主 root 是同一个 uid,被攻破后危害直接放大;② 只读根文件系统:--read-only 让容器层不可写,需要写的目录显式 --tmpfs /tmp 挂内存盘——攻击者落盘的 webshell、挖矿脚本无处安放;③ 能力削减:默认容器带一串 Linux capabilities,用 --cap-drop=ALL --cap-add=NET_BIND_SERVICE 只留必需——最小权限原则在内核能力的应用。再配 5.12 的镜像可信来源(私有仓库加签名校验),就是一套够用的容器安全底线。

我在课程实验里逐项验证过这些参数的行为,生产按环境风险等级选择性落地。 追问链:

  • 迭问1:容器内 root 为什么危险? → 答:因为 namespace 隔离的是"视图"不是"身份":容器内 uid 0 在内核看来就是 uid 0,只是它看不到宿主的进程和文件。一旦有内核漏洞或危险挂载(比如把 /var/run/docker.sock 挂进容器——等于给了容器宿主机 root 的钥匙),容器内 root 就能跨界操作宿主。所以两条铁律:应用容器一律非 root 跑;docker.sock 绝不挂给不受信容器——我把 socket 挂载列为高危审计项,这比一堆参数更救命。

5.21 容器时区、时间与本地化配置 【了解】 ​

标准回答: 容器时区问题是高频小坑:容器默认 UTC 时区,日志时间戳比北京时间早 8 小时——排障对时间线时经常被坑。解法按层次:镜像层固化——Dockerfile 里 ENV TZ=Asia/Shanghai 加 RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime(基础镜像要含 tzdata 包),这是我的标准做法(5.6 的 Java 镜像模板里就有 TZ);运行时注入——-e TZ=Asia/Shanghai,适合改不了镜像的场景;编排层——K8s 里把宿主 /etc/localtime 只读挂进容器(hostPath)也能对齐。

时间还有个更严肃的关联:容器的时间来自宿主内核(容器没有独立时钟),所以宿主时间同步就是容器时间同步——这就是 K8s 全节点 chrony 时间同步的必要性(避坑清单第 9 条:时间不同步导致 TLS 认证失败),时区是显示问题、时间是安全问题,两个维度都要管。 追问链:

  • 迭问1:为什么容器里改不了系统时间? → 答:因为时间 syscall(settimeofday/clock_settime)作用的是宿主内核的全局时钟,容器没有独立的时钟命名空间(time namespace 只影响单调时钟的偏移,不改墙上时钟)——出于安全,容器内默认无 CAP_SYS_TIME 能力,就算 root 也改不了。这反过来印证了"宿主时间同步是唯一正解":节点 chrony 对齐 NTP,所有容器自然对齐。我在初始化基线里把 chrony 放在靠前的步骤,就是因为它影响证书认证这条命脉链路。

5.22 容器网络排障路径(veth / bridge / iptables)【理解】 ​

标准回答: 容器网络不通时我的排查路径按"从容器到宿主"逐跳走:第一跳容器内——exec 进去 ping 网关、curl 目标,确认容器网络栈本身和路由默认规则;第二跳找到接口对——每个容器有一对 veth:一端在容器内是 eth0,另一端挂在宿主 docker0 网桥上,宿主 ip link 看一堆 vethxxx,用 ethtool -S 或容器内 ifindex 对应关系找到具体是哪一端,确认它的 UP 状态和桥接关系 brctl show(或 bridge link);第三跳宿主转发——sysctl 的 ip_forward 要开(容器出网的前提)、iptables 的 nat 表 iptables -t nat -L -n 看 POSTROUTING 的 MASQUERADE 规则(容器私网地址出网靠它伪装)、FORWARD 链有没有 DROP(firewalld 有时会拦 docker 的转发);第四跳 DNS——容器内 cat /etc/resolv.conf 看指向(默认 docker 内嵌 DNS 127.0.0.11),nslookup 验证。

这套路径在 K8s 环境的对应版本就是:Pod 内 → pause 的 veth → CNI 网桥/flannel.1 → 节点 iptables/ipvs——第 6 章网络排障的地基。 追问链:

  • 迭问1:容器能 ping 通外网 IP 但域名解析失败,查哪? → 答:三段定位:resolv.conf 的 nameserver 指向对不对(自建网络应指向 127.0.0.11 由 docker DNS 转发,默认桥是宿主 DNS 配置);docker 内嵌 DNS 的上游转发有没有问题(daemon.json 的 dns 配置项可显式指定上游);宿主自己的 /etc/resolv.conf 和到 DNS 服务器的连通性。"IP 通、域名不通"九成是 DNS 链路问题——这个二分法(L3 通 vs 解析通)是我排容器网络的第一刀。

5.23 docker 常见报错速查集 【理解】 ​

标准回答: 容器类故障我按报错关键词整理了速查集,面试报得出这些报错和对应根因,可信度立刻不同。① exec format error:镜像架构和节点架构不匹配(x86 镜像跑在 ARM)——查 inspect 的 Architecture 字段,换 arm64 镜像(5.11 详述)。② no matching manifest for linux/arm64:该 tag 没有 arm64 构建——manifest inspect 确认,换 multi-arch tag 或自构建。③ port is already allocated:宿主端口被占——ss -lntp 找占用者,换映射端口或停占用服务。

④ Conflict. The container name is already in use:同名容器残留——docker rm 旧容器或起用新名字;我的习惯是重建前先 docker rm -f 同名容器,写进脚本。⑤ unable to prepare context / build 慢:构建上下文太大(把 node_modules、数据目录 COPY 进去了)——补 .dockerignore。⑥ driver failed programming external connectivity:docker 重启后 iptables 规则残留冲突——重启 docker 服务重建规则链,顽固时 flush 对应链。

⑦ permission denied while trying to connect to the Docker daemon socket:当前用户无 socket 权限——加 docker 组或 sudo。⑧ Pod 侧 ImagePullBackOff/ErrImagePull:见 5.12 的三板斧分类(凭据/镜像名/网络)。每个报错对应"一句话根因 + 一条解法",这是我现场救火的反应速度来源。 追问链:

  • 迭问1:报错信息不认识怎么办? → 答:我的三层方法:先精读报错原文——容器报错信息大多直说原因,生词拆开猜(allocated、denied 都是明示);再看事件流——docker inspect 的 State 段、K8s 的 describe Events,失败动作前后的上下文最有信息量;最后拿报错关键字搜官方 issue——过滤近一年的、带 workaround 的。要强调的是我不会上来就搜:八成问题在报错原文里已经给了答案,"认真读报错"是被低估的技能。

5.24 用 systemd 托管容器与开机自愈 【了解】 ​

标准回答: 单机容器的开机自启有两个方案要对比着答。方案一 docker 自带:docker run --restart=always——docker daemon 启动时自动拉起带该策略的容器,前提是 docker 服务本身 enable 了;这是最简单的方式,我的默认选择。

方案二 systemd 直管:把容器起停写成 unit 文件,ExecStart=/usr/bin/docker start -a 容器名(-a 前台附着让 systemd 能感知状态)、ExecStop=/usr/bin/docker stop 容器名,配 Restart=on-failure——好处是容器生命周期纳入 systemd 体系:journalctl 统一日志、systemctl 统一管理、依赖顺序可控(After=docker.service、Requires=网络)。

我的选择逻辑:普通服务用 --restart=always 就够;需要和其他 systemd 服务编排依赖关系(比如"数据库容器就绪后再起应用容器")或要统一纳管时用 unit 方案。移动云的 node-exporter 用 apt 装自带 systemd unit,也是同一思想——二进制和容器都可以被 systemd 统一管理。 追问链:

  • 迭问1:--restart=always 和 systemd Restart 有冲突吗? → 答:要避免双重管理同一容器:如果 docker 的 restart 策略和 systemd unit 都管它,两边各自的重启逻辑可能打架(systemd stop 时 docker 又拉起来)。规范做法二选一:要么纯 docker 策略(服务不管),要么 unit 里 docker start 不带 --restart(把重启决策交给 systemd)。我的实践是普通容器走 docker 策略、关键编排走 systemd,同一个容器只归一边管——"一个对象一个管理者",这和 K8s 里"副本数只归 HPA 管"(6.12)是同一条原则。

5.25 overlayfs:容器文件系统的底层 【理解】 ​

标准回答: 容器"镜像只读层 + 可写层"的底层实现是 overlayfs(联合挂载文件系统),理解它能把 5.4 的分层现象解释到底。机制:overlayfs 把多个目录"叠"成一个视图——lowerdir 是镜像的只读层(可以有多个,自上而下叠放)、upperdir 是可写层、merged 是容器看到的合成视图。读文件时从上往下找,第一个命中的生效(新盖旧);写文件触发 copy-up——把文件从 lower 拷到 upper 再改(这就是 5.4 追问里"改一个文件变两份"的底层原因);删文件在 upper 打 whiteout 标记遮住 lower 的原文件(数据没删,只是看不见)。验证方法:docker inspect 容器 看 GraphDriver 信息里的 LowerDir/UppderDir/MergedDir 路径,到 /var/lib/docker/overlay2/ 下能亲眼看到这些目录——同一镜像起的多个容器共享同一组 lower 层(各自只多一个 upper),这就是镜像分层"省空间"的落点。

我在 ARM 项目排查"容器里改的文件重启后还在"这类困惑时,就是靠 overlayfs 的 copy-up 机制解释清楚的。 追问链:

  • 迭问1:容器里删了文件为什么磁盘空间反而可能变大? → 答:因为删除是 whiteout 标记——lower 层的原文件完好无损地占着空间,upper 层还要新增一个标记文件;所以"删大文件"之后镜像层的大文件还在、可写层又多了一条,总占用不降反微升。要真正释放镜像层里的垃圾,只能在构建期解决(多阶段、RUN 合并清理),运行期的容器层删除从来不是空间手段——这个反直觉结论是 overlayfs 原理最实用的推论,我在讲镜像瘦身时都会带上。

5.26 HEALTHCHECK:镜像自带的健康检查 【理解】 ​

标准回答: Dockerfile 的 HEALTHCHECK 让镜像自带健康状态:HEALTHCHECK --interval=30s --timeout=3s --retries=3 CMD curl -f http://localhost:8080/health || exit 1——容器周期性执行这条命令,退出码 0 为 healthy、1 为 unhealthy,docker ps 的 STATUS 列显示 (healthy)/(unhealthy),docker events 和 inspect 的 Health 字段可查历史。它和 K8s 探针的关系要讲清:K8s 不直接使用 Docker 的 HEALTHCHECK(CRI 环境下 kubelet 探针独立工作),但两者理念同源——都是"用业务可访问性代替进程存活"判断健康。它的实际价值两个场景:单机 docker run 的编排器(compose 的 depends_on condition: service_healthy 按健康状态控制启动顺序)和自检文档化——把健康检查方法固化进镜像,任何人拿到镜像就知道怎么验证它活着。我写 Dockerfile 的习惯:Web 类应用一律带 HEALTHCHECK,路径和应用探针保持一致。

追问链:

  • 迭问1:HEALTHCHECK 和 K8s 探针谁生效? → 答:K8s 环境下 kubelet 的 liveness/readiness 探针是权威——它由 kubelet 发起、失败动作(重启/摘流量)都在 K8s 层执行;镜像里的 HEALTHCHECK 在 containerd 运行时的 K8s Pod 里基本不参与调度决策。所以我的实践是:Dockerfile 里写 HEALTHCHECK 服务单机/compose 场景和自检文档;K8s 的 Deployment 里独立配 liveness/readiness(参数按 K8s 语义调)——两套并存、各管各的场景,不指望一个配置通吃两个平台。

5.27 镜像 tag 规范与仓库治理 【了解】 ​

标准回答: 镜像 tag 看似小事,治理缺失会变成事故源。我的 tag 规范五条:禁用 latest——它不可追溯(同 tag 被覆盖后回滚都找不到原镜像),一律显式版本;版本语义化——v1.2.3 或日期构建号(20261024-1),能看出新旧和构建时间;仓库路径分层——仓库地址/项目/应用名:版本,项目层对应团队或业务域,权限和清理都能按项目治理;变更可追溯——重要镜像记录 digest(sha256),回滚和审计以 digest 为准;环境晋升不重打 tag——同一 digest 从 test 晋升到 prod(改部署清单),保证"测过的就是上线的"。仓库治理三件事:定期清理旧版本(保留最近 N 个 + 永远保留生产在用版本,云仓库配自动清理策略)、账号权限最小化(推送权限和拉取权限分开)、私有仓库的可用性纳入监控(拉取失败告警——镜像仓库挂了等于发布通道断了)。

移动云的镜像双轨体系(5.12)里,tag 规范是我定的:内网仓库按"项目/应用:版本"统一重打,从此再也没有"这是哪个版本"的疑问。 追问链:

  • 迭问1:为什么回滚要认 digest 不认 tag? → 答:因为 tag 是可变的引用——有权限的人或 CI 可以把 v2.1 这个 tag 重新指向另一个镜像内容,而 digest 是内容的哈希、全球唯一且不可变。极端场景:上午发布的 v2.1 有 bug,有人下午用修复后的镜像覆盖了 v2.1 这个 tag——此时"回滚到 v2.1"得到的已经不是上午那个坏版本,也说不清线上到底是什么。所以生产部署清单记录 digest、审计以 digest 为准——"tag 给人看、digest 给机器信",这是我镜像治理的一句话原则。

5.28 容器里的世界:ps/top 视角与资源感知 【了解】 ​

标准回答: "容器里看到的资源"有一套经典陷阱要会解释。进程视角:容器里 ps aux 只能看到容器自己的进程——这是 pid namespace 隔离的效果(1 号进程是容器主进程);但宿主机 ps 能看到所有容器进程——运维排障时两头对照是常规动作。资源视角的坑:容器里 top/free 看到的是宿主机的 CPU 核数和内存总量(默认没有 lxcfs 或 cgroup 感知)——2G limit 的容器里 free 显示 64G,非常误导。后果最典型的是 JVM:老版本 JDK 按宿主机内存算默认堆(64G 的 1/4 = 16G),在 2G limit 里启动就 OOM。解法三选:启动参数显式 -Xmx(我们的 Java 镜像模板固定 -Xms512m -Xmx1024m 就是这个原因);用容器感知的 JDK(8u191+ 自动感知 cgroup);节点上部署 lxcfs 让容器内的 /proc 虚拟化出 cgroup 值。这套"容器世界 vs 宿主世界"的对照,是我讲容器隔离原理时最生动的素材。

追问链:

  • 迭问1:怎么快速验证容器资源感知是否正常? → 答:两个对照实验:容器内 cat /sys/fs/cgroup/memory/memory.limit_in_bytes(cgroup v1)直接读 cgroup 真实限制——这个值才是 limit 的"事实";再对比 free 显示值,两者不一致就说明应用层感知没做好,必须在启动参数里显式声明资源。Java 应用还有个验证动作:启动日志打印 -XX:+PrintFlagsFinal 里的 MaxHeapSize 或 jinfo 查看,确认堆真的按 limit 算的——"不要相信显示,要验证事实"是这类问题的处理原则。

5.29 docker 与 K8s 命令对照实战(排障视角) 【理解】 ​

标准回答: K8s 节点上 docker 命令全部有 crictl 对应(5.13 的对照表),实战里我的用法分层。优先 kubectl:涉及 Pod 生命周期的问题(日志、进入容器、事件)用 kubectl——它跨节点、带集群上下文,logs/exec/describe 是第一入口。节点级用 crictl:Pod 起不来看 kubelet 视角——crictl ps -a 看容器退出码和状态、crictl logs 看容器输出、crictl inspect 看挂载和环境变量;crictl 是"站在节点上看容器"的工具,配合 journalctl -u kubelet 是 NotReady/CrashLoop 类问题的节点侧组合拳。镜像层:crictl images、crictl pull 镜像 在节点上直接验证拉取(复现 ImagePullBackOff 的标准动作)。还有个冷知识要会:crictl 只能看"通过 CRI 创建的容器"(K8s 的 Pod),docker run 起的独立容器它看不到——所以构建机上的容器管理还是 docker 命令,K8s 节点上才是 crictl 的地盘,两套环境两套命令,我在双轨体系里(5.13 追问 3)就是这么分工的。

追问链:

  • 迭问1:kubectl exec 和 crictl exec 有什么区别? → 答:kubectl exec 按 Pod 名(可带 -c 指定容器)进入,apiserver 经 kubelet 转发执行——有 RBAC 权限控制、有审计、跨节点可用,是协作环境的规范入口;crictl exec 按容器 ID 在本节点直接执行——需要在节点上有 shell 权限,适合节点侧排障和 kubelet/apiserver 不可用时的"最后手段"(比如 apiserver 挂了想看容器日志,kubectl 不可用,上节点 crictl 还是能干活)。这个"控制面失联时的节点级后手"价值,是我强调两个命令都要熟的原因。

5.30 容器里的 DNS 与 hosts、resolv 注入 【了解】 ​

标准回答: 容器解析域名的行为由三个注入点控制,排障"容器里解析不了服务名"就查这三处。--dns 参数(或 K8s 的 dnsConfig):给容器指定 DNS 服务器地址——自建网络默认是 127.0.0.11(Docker 内嵌 DNS),K8s 里是 CoreDNS 的 ClusterIP;显式 --dns 可以覆盖(比如强制走公司 DNS)。--add-host(等价 K8s 的 hostAliases):往容器 /etc/hosts 追加条目——--add-host=内部域名:10.x.x.x,临时让容器用指定 IP 访问某服务,调试和"没有 DNS 的环境"的救急手段。resolv.conf 继承:默认 bridge 容器复制宿主的 /etc/resolv.conf——宿主 DNS 配错容器跟着错,这是"宿主和容器解析行为不一致问题"的常见根源。排障顺序固定:进容器 cat /etc/resolv.conf 和 /etc/hosts 看配置 → nsloopup 验证解析链 → 对比宿主配置找差异。我在课程实验里专门做过"断 DNS 造故障再修"的练习,这三处的因果链很熟。

追问链:

  • 迭问1:--add-host 和改镜像里的 /etc/hosts 一样吗? → 答:不一样,也改不了——/etc/hosts 是挂载进容器的(由运行时按参数生成),镜像构建时写进 /etc/hosts 的内容会在运行时被覆盖,所以"固化域名映射进镜像"这条路是走不通的;正确做法就是运行时 --add-host 或编排层 hostAliases 注入,或者干脆上正经的 DNS。这个"以为是文件其实是个挂载点"的细节,是理解容器文件系统边界的很好例子——想知道哪些文件能改,先看 mount。

5.31 镜像构建的 CI 视角:构建即流水线的一环 【了解】 ​

标准回答: 把 Dockerfile 放进 CI/CD 的视角看(衔接简历 GitLab + Jenkins 技能),构建不是手工动作而是流水线的一环。典型流水线五段:代码提交触发 → 构建镜像(docker build 或 kaniko,参数化注入版本号)→ 扫描(Trivy 检测 CVE 漏洞、镜像签名)→ 推送私有仓库(tag 带构建号,5.27 的规范)→ 部署(更新 K8s 清单的镜像 tag)。运维要操心的四个点:构建环境一致——CI 上构建而不是个人机器,避免"我这能构建"的环境漂移;构建缓存——CI 上合理分层 Dockerfile 加缓存加速(依赖层不常变);凭据管理——仓库密码放 CI 的凭据库不进代码;失败即断——扫描出高危 CVE 就阻断流水线,把质量卡点前置。我的课程实验走过完整链路:Jenkins 多分支流水线构建镜像、Trivy 扫描做卡点、推送仓库后更新部署——这套流程是我对"容器化交付"的完整认知框架,面试讲 CICD 章节(第 10 章)时正好展开。

追问链:

  • 迭问1:Trivy 扫描的原理和误报处理? → 答:Trivy 扫两层:镜像层里的系统包(对照 CVE 数据库比对包版本)和依赖文件(pom.xml/requirements 锁定的库版本)。误报的典型来源:包已打了补丁但版本号没变(发行版 backport)、应用实际没用到有漏洞的代码路径。处理策略:先按严重度分级阻断(CRITICAL 阻断、HIGH 警告)、确认不可利用的做豁免记录(豁免要有理由和期限)、定期重扫——漏洞库每天在更新,镜像不重扫就是"上线时干净、半年后带洞"。扫描卡点的价值是把安全左移,比上线后被扫出来补救便宜一个量级。

5.32 容器化交付检查单(从 Dockerfile 到上线)【理解】 ​

标准回答: 我交付一个容器化应用的自检清单,按生命周期排十项:① 基础镜像选型(官方 slim/alpine、固定版本 tag、ARM 环境核对架构);② Dockerfile 结构(先依赖后代码的缓存顺序、RUN 合并清缓存、.dockerignore 齐全);③ 启动方式(exec 格式、sh -c 吃变量、非 root 用户);④ 资源意识(JVM 显式 -Xmx、HEALTHCHECK 带上);⑤ 日志(输出到 stdout/stderr、PYTHONUNBUFFERED 类缓冲参数);⑥ 时区(TZ 固化);⑦ 配置(环境变量注入、配置不烧死在镜像里);⑧ 数据(有状态目录挂卷、容器层不留数据);⑨ 构建验证(本地跑通、镜像体积合理、启动冒烟);⑩ 运行验证(重启策略、日志轮转限额、资源 limit)。这份清单是我把 5.3 到 5.29 的知识点按交付顺序串起来的——每一项背后都对应前面某一节的原因,用它自检过的镜像,上线后的"容器类工单"基本为零。

追问链:

  • 迭问1:这清单里哪几项最容易被漏? → 答:统计我的经验前四名:日志缓冲参数(上线后排障发现 logs 是空的才想起)、时区(对时间线排障时发现差 8 小时)、HEALTHCHECK/探针(没配,应用假死没人知道)、镜像架构(x86 构建丢到 ARM 环境 exec format error)。共同点是"构建时都能跑、出事都在运行期"——所以我的清单执行时机是"合入主干前"而不是"上线救火时",把运行期故障左移成构建期检查,这是容器化质量管理的核心思想。

5.33 文件的进出容器:docker cp 与挂载的选择 【理解】 ​

标准回答: "把文件放进/取出容器"有三条路,各有适用场景。docker cp:docker cp 容器:路径 本地路径 双向拷贝,不要求容器运行着(停止的也能拷)——排障时从容器里抓日志、dump 文件到宿主机是我最高频的用法;K8s 对应 kubectl cp ns/pod:路径 本地路径(同样动作,按 Pod 操作)。exec 加重定向:docker exec 容器 cat /path/log > local.log——适合单个文件,配合管道还能压缩后传出(大日志先 gzip 再 cat,省带宽)。挂载共享:-v /data/share:/share 把宿主目录挂进去——适合持续的双向交换(比如应用写导出文件到挂载目录)和配置分发。选择逻辑:一次性取文件用 cp/cat,持续性交换用挂载;重要提醒——用容器"传文件"只能是应急,正式的数据流应该走对象存储或共享存储,容器边界上频繁倒文件是架构坏味道。

追问链:

  • 迭问1:kubectl cp 和 docker cp 底层一样吗? → 答:kubectl cp 实际上是 exec tar 的封装——在 Pod 里执行 tar 打包、流式传回本地解包(所以 Pod 里必须有 tar 命令,极简镜像没有 tar 会报错,这是它的经典坑);docker cp 是 daemon 直接访问容器文件系统层,不依赖容器内工具。知道这个差异能解释"kubectl cp 报 exec: tar not found"的原因——对策是用镜像时给排障基础镜像补 tar/busybox,或改用 exec cat 单文件的方式绕过。

5.34 docker 版本、兼容与升级注意 【了解】 ​

标准回答: Docker/containerd 的版本管理我按两个环境的实践讲。版本差异:银行项目 Docker 18.09.8(aarch64 静态包,受 BCS 配套约束)、移动云 containerd 1.7.8(K8s 1.28 的配套范围)——又一次"版本跟着配套表走"。兼容性三件事:client/server API 兼容——docker 客户端比 daemon 新太多会报 API version 不匹配,DOCKER_API_VERSION 环境变量能强制对齐(应急用);K8s 兼容矩阵——每个 K8s 版本有支持的运行时版本范围(kubeadm 升级前必查);镜像格式兼容——OCI 标准稳定,老 Docker 构建的镜像在新 containerd 上照跑(5.13)。升级注意:运行时升级要逐节点 drain 滚动(K8s 的标准动作,6.19),升级后验证 Pod 能正常创建、CNI 正常;别跨大版本跳级。我的版本观一句话:"Docker 18.09 到 containerd 1.7 我都用过,版本新旧不是问题,配套关系和升级纪律才是。"

追问链:

  • 迭问1:怎么查当前环境的 docker/containerd 版本和配套信息? → 答:一组命令:docker version 分客户端和服务端两段显示版本和 API 版本;containerd --version、crictl version(还能显示 kubeadm 的信息);docker info 看存储驱动、cgroup 版本等环境全貌——这些输出是报障和查配套时的第一手材料。我的习惯是把"节点初始化完成"的验收输出(版本、驱动、参数)存档,环境对账时直接 diff——环境信息的留痕让"什么时候变的"永远可查。

5.35 容器运维的"急救五件套"(场景速答)【了解】 ​

标准回答: 把容器日常的五个高频"怎么办"整理成急救卡。① 容器一启动就退:先 docker ps -a 看退出码(137/1/143 的分诊,5.14),再 logs 看遗言,多半是配置错或依赖连不上。② 磁盘被容器吃满:docker system df 找大头(日志/镜像/卷),日志按 5.17 的 daemon.json 限额根治。③ 容器里改的东西重启就丢:那是改在可写层,重启策略 recreate 会重置——改 Dockerfile 或用挂载,别依赖容器内手工修改(5.4 追问的反模式)。④ 容器时间不对:时区差 8 小时是 TZ 没配(5.21),时间漂移是宿主 chrony 的事。⑤ 镜像拉不下来:公网环境查网络和镜像名;内网环境走离线 save/load(5.11)或私有仓库认证(5.12)。这五张急救卡覆盖了我被同事问到最多的容器问题——整理成"现象 → 一句话根因 → 解法"的卡片,新同学也能照着自助。

追问链:

  • 迭问1:为什么你强调"重启会丢"?docker restart 不丢吧? → 答:对,要区分两个动作:docker restart 是同一个容器重启——可写层保留,改动还在;docker rm 后重新 run、或 compose down/up、或 K8s 里 Pod 重建(容器被销毁重建)——可写层随之丢弃,改动全没。危险在于"平时 restart 都在,某天发布重建容器后配置全丢"——这种时间差让事故特别迷惑。结论不变:持久的东西放卷、放配置中心,容器层永远当临时的。

5.36 容器与 SELinux:一个真实的兼容话题 【了解】 ​

标准回答: SELinux 和容器的关系我在银行项目实际处理过(第 1 章 1.8 的延伸)。默认行为:多数发行版的容器运行时依赖 SELinux 的容器策略(container-selinux 包提供类型),容器进程跑在 container_t 类型下,对宿主文件系统的访问被类型限制住——这是容器安全的额外一层(配合 5.20 的安全基线)。典型冲突场景:挂载宿主目录进容器(-v /data:/data),如果 /data 的类型不是容器可访问的(container_file_t),容器内读写会报 Permission denied——且这种拒绝在容器内看起来像"普通权限问题"极具迷惑性(ls -Z 一查便知类型不对)。解法三选:给目录改类型 chcon -R -t container_file_t /data(或 semanage fcontext 加 restorecon 持久化);挂载时加 :z(让 docker 自动改类型,多容器共享用 :Z 私有);按环境基线直接 permissive(我们部署期的选择,1.8 追问 3 的口径)。判断"是不是 SELinux 拦的"一条命令:ausearch -m avc -ts recent 有 denied 记录就是它。

追问链:

  • 迭问1:-v 挂载的 😒 和 :Z 什么区别? → 答:小写 z 是"共享标签"——docker 把目录类型改成 container_file_t,所有容器都能访问,适合多个容器共享同一数据目录(比如几个服务共读的配置);大写 Z 是"私有标签"——除了改类型还绑定当前容器实例的专属类别(MCS 分类),只有这个容器能访问,隔离更严格,适合单容器的数据目录。选错的现象很有意思:用 Z 挂的目录换一个容器就读不了——排障时 ls -Z 看到 MLS/MCS 后缀不同就能定位。一个字母的差别,体现的是标签型访问控制的粒度。

5.37 compose 进阶:健康检查与启动顺序 【了解】 ​

标准回答: compose 的 depends_on 默认只保证"启动顺序"(先起数据库容器再起应用),不保证"数据库已就绪"——容器起来不等于服务能连,应用启动时连不上数据库照样崩(或进入重试)。两个解决手段:healthcheck——compose 的服务里定义 healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8080/health"],配 interval/retries,状态进 docker ps 的 (healthy);condition 化的依赖——depends_on: db: condition: service_healthy,应用会等 db 的健康检查通过才启动。这样"数据库容器 Running"升级成"数据库可连接",启动顺序变成启动就绪。配套细节:健康检查的 test 也可以是镜像里 Dockerfile HEALTHCHECK 的复用(compose 里可覆盖参数);重启策略 restart: on-failure 配合应用的重试逻辑,组成单机版的"自愈"。这套组合是我在课程实验里验证过的——单机编排要逼近"生产可用",差距就在就绪判断和失败处理这两块。

追问链:

  • 迭问1:这和 K8s 的探针体系比缺什么? → 答:compose 的 healthcheck 大致对应 K8s 的启动健康判断,但缺三样:没有 liveness/readiness 的职责分离(compose 的 unhealthy 只是标记,默认不重启——要配 autoheal 这类第三方容器才行动);没有"摘流量"的概念(单机没有 Service 抽象);没有调度和重调度(容器挂了靠 restart 策略原机拉起,节点挂了没人管)。所以我的总结:compose 把"单机的启动就绪"做完整了,K8s 把"集群的生存和流量"做完整了——用这个对比来定位两个工具的边界,比"compose 简单 K8s 复杂"的说法精确得多。

5.38 本章案例 ​

  • 【真实项目·银行 ARM 离线镜像体系】背景:某银行内网环境加 TaiShan ARM 架构,9 个 BCS 数据面节点需要加载全套 arm64 镜像,而内网拉不了任何海外镜像。任务:把镜像安全高效地铺到全部数据面节点。行动:我编写批量加载脚本——for 循环遍历镜像目录、case 排除 edge-manager 管理面专用镜像、逐个 cat 进 docker load 并 echo 进度、$? 判断失败即停;加载前后各做一次 docker images 清单核对;期间处理过一次 x86 镜像混入导致的 exec format error,之后固化"镜像架构清单"检查项。结果:十几类镜像一次铺完,BCS 数据面纳管一次通过;脚本和清单沉淀进《智能合约引擎安装部署指南》,成为公司同类内网项目的标准流程。
  • 【真实项目·移动云镜像双轨体系】背景:移动云内网集群部署组件时海外镜像(ghcr.io、registry.k8s.io)拉取失败,导致 Flannel Pod ImagePullBackOff、节点 NotReady(避坑清单第 1 条故障)。任务:建立可持续的镜像供给体系。行动:设计"公网拉取 → 内网私有仓库托管 → 集群认证拉取"双轨——能上网的节点 pull 原镜像(如 eipwork/kuboard:v3.5.2.2)、tag 成内网仓库规范格式 push、集群内 kubectl create secret docker-registry 建 ImagePullSecret、Deployment 挂 imagePullSecrets 拉取;运行时三层策略:内网仓库优先、阿里云公共仓库备选、二进制部署兜底。结果:集群所有组件镜像供给稳定,NotReady 类故障根除,双轨策略写进建设方案成为标准。
  • 【真实项目·crictl socket 坑】背景:移动云集群节点上执行 crictl ps 报"连接 socket 失败",影响节点级排障效率。定位:crictl 默认按 docker、containerd、crio 顺序遍历运行时端点,环境里配置一多就遍历到错误的目标。行动:/etc/crictl.yaml 显式写死 runtime-endpoint 和 image-endpoint 指向 containerd.sock,并把这个文件的预置写进全节点初始化脚本。结果:crictl 命令行为在 7 台节点完全一致,该坑记入 9 项避坑清单第 2 条——"初始化脚本预置配置文件"这个动作后来被我推广到 crictl、chrony、limits 等所有"默认行为不可控"的组件。
  • 【课程项目】容器化改造实验:评估应用兼容性 → 定制基础镜像(旧版系统库不匹配时补装依赖库)→ 精简镜像加规范 tag → yaml 编排上线,把一个传统部署的应用改造为容器化交付,资源利用率从约 40% 提升到约 80%(课程实验口径,按方案预估)。这个实验的价值是完整走了"评估—改造—验证"流程,我讲容器化改造话题时按这个框架展开。
  • 【经典故障】ARM 镜像 manifest 不匹配:报错 no matching manifest for linux/arm64/v8。根因是所用镜像 tag 没有 arm64 构建。解法三件套:docker manifest inspect 镜像 查 architecture 支持列表、换 multi-arch 版本或基于 arm64 基础镜像自构建、离线 save/load 分发验证过的 arm64 包。教训固化成交付前检查项——"镜像架构清单"是我在 ARM 项目上最重要的流程产出之一。

5.39 章末自测清单(快问快答) ​

  1. 镜像/容器/仓库类比? → 镜像是类(只读模板),容器是实例(+可写层),仓库存镜像。
  2. 容器 vs 虚拟机一句话? → 容器是共享内核的隔离进程(MB/秒级),VM 是独立内核的完整机器(GB/分钟级)。
  3. 隔离靠什么? → Namespace(隔离视图)+ Cgroup(限额资源)。
  4. CMD 和 ENTRYPOINT 区别? → CMD 可被 run 参数整体替换,ENTRYPOINT 固定入口、参数追加给它。
  5. COPY 和 ADD? → COPY 纯拷贝首选;ADD 多了自动解压和拉 URL,语义不透明慎用。
  6. EXPOSE 等于端口映射吗? → 不,只是声明,映射要 -p。
  7. ARG 和 ENV? → ARG 构建期变量(--build-arg),ENV 进镜像运行期生效。
  8. 为什么 exec 格式? → java/nginx 直接当 PID 1,能收到 SIGTERM 优雅退出;shell 格式隔了一层 sh。
  9. 缓存铁律? → 变的放后面:先 COPY 依赖清单装依赖,再 COPY 源码。
  10. 四种网络模式? → bridge(默认NAT)、host、container(共享他容器网络=Pod 原理)、none。
  11. save/load 和 export/import? → save/load 镜像(保分层);export/import 容器(丢历史)。
  12. 为什么弃用 dockershim? → 调用链长维护重;CRI 直连 containerd 更简洁,Docker 镜像 OCI 兼容照跑。
  13. containerd 三个关键配置? → SystemdCgroup=true、pause 镜像换国内源、/etc/crictl.yaml 显式 socket。
  14. 退出码 137 表示? → OOMKilled/被 SIGKILL,先查内存 limit。
  15. imagePullSecrets 作用? → namespace 级私有仓库拉取凭据,Pod spec 引用后集群认证拉取。