Appearance
第5章 Docker 镜像交付与 containerd
对应简历:技能第5条「熟练掌握 Docker 与 Kubernetes 集群部署(kubeadm 单 / 多 master 高可用,Nginx + Keepalived VIP 漂移),具备 ARM 国产化与 x86 云环境双实践」/ 项目一职责「负责 Kubernetes 集群部署与高可用改造:在统信 UOS(ARM)环境完成 Docker 与 kubeadm 部署」/ 项目二职责「完成 containerd 运行时标准化、kubeadm 三副本控制平面与 Flannel 网络部署,沉淀 9 项生产避坑清单」
实施版定位:镜像怎么"构建出来、搬进内网、分发给集群、拉取时认证"——整条镜像交付链是本章主线。容器内部机制(网络模式、overlayfs、安全基线等)深度内容回查运维版第 5 章对应节。术语约定:Kubernetes(以下简称 K8s,仅正文简写)。仓库域名一律以「内网私有仓库」指代。
5.1 三大概念:镜像/容器/仓库 与 容器 vs 虚拟机 【必背】
标准回答(交付视角,两个问题打包答): 先说三大概念,用"类与实例"的类比:镜像 Image 是只读模板,把应用、依赖和运行环境分层打包在一起,相当于"类";容器 Container 是镜像的运行实例,在只读层之上叠一个可写层,相当于"实例",启动是秒级的——因为不用引导内核只是起进程;仓库 Registry 是集中存取镜像的服务,Docker Hub、Harbor、各云厂商私有仓库都是,docker push 和 pull 的对象就是它。从实施角度讲,这三个概念对应我交付工作的三段:给客户部署应用要先准备镜像(构建或离线导入),跑起来是容器(验证状态和日志),存取靠仓库(内网环境的镜像供给线)。
再说容器和虚拟机的区别,本质一句话:容器是"隔离的应用进程",虚拟机是"完整的计算机"。容器共享宿主机内核,靠 Namespace 隔离视图、Cgroup 限制资源配额,所以体积 MB 级、启动秒级、单机密度高;虚拟机通过 Hypervisor 模拟硬件、每个 VM 跑独立内核,隔离彻底但体积 GB 级、启动分钟级。我在银行项目恰好是两者叠加用:FusionCompute 发放 15 台国产化虚拟机做环境隔离,虚拟机里再跑 K8s 容器承载应用——"VM 管边界、容器管交付",这个两层结构本身就是金融生产环境的主流形态。 追问链:
- 追问1:容器靠什么隔离? → 答:两套内核机制配合:Namespace 管隔离——pid 让容器内进程有自己的进程树视图、net 给独立网络栈、mnt 独立挂载点等六类,解决"看得见什么";Cgroup 管限额——限制 CPU、内存、IO 配额,解决"能用多少"。一句话总结"Namespace 管隔离,Cgroup 管限额"。在交付现场我验证过:节点上
ls /proc/容器PID/ns/能看到进程的 namespace,cat /sys/fs/cgroup/...能看到配额——跟客户解释"容器为什么轻"时拿这两个目录展示,比讲概念有说服力。 - 追问2:交付时怎么决定用容器还是虚拟机? → 答:我看三件事:一看应用形态——无状态 Web/API 服务适合容器化,享受快速发布和弹性;数据库这类重状态服务我坚持放虚拟机或云托管(银行项目 Vastbase 就在麒麟虚拟机里,没进 K8s)。二看客户环境——有 K8s 底座就用容器,只有虚拟化平台就先虚机交付。三看合规边界——金融客户对不同层的审批要求不一样,虚机边界清晰、容器变更快,两边取舍我在两个项目里都真实做过决策。
5.2 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,又能换包不用改镜像。还要强调格式:CMD 写 shell 格式时实际被 sh -c 包一层,sh 成了 1 号进程、收不到 SIGTERM;exec 格式(JSON 数组)让 java 直接当 PID 1,优雅停止才工作——生产一律 exec 格式。
第三组文件与变量:COPY 纯拷贝首选;ADD 是超集会自动解压 tar 和拉 URL,语义不透明不推荐;ENV 进镜像运行期生效;ARG 只在构建期有效,敏感信息别放(history 可查);EXPOSE 只是声明不等于映射;WORKDIR 定工作目录;USER 非 root 运行是安全实践。下面两套模板是我现场手写的标准答案(银行项目 jar 与前端静态站的真实语境)。
dockerfile
# Java 后端(ARM 环境注意基础镜像架构,见 5.4)
FROM eclipse-temurin:8-jre
WORKDIR /opt/app
COPY app.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 前端静态站(自带 try_files 兜底 + /prod-api/ 反代配置)
FROM nginx:1.25-alpine
COPY dist/ /usr/share/nginx/html/
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80追问链:
- 追问1:ENTRYPOINT 里为什么要 sh -c? → 答:因为 exec 格式的 JSON 数组不经过 shell,内核直接执行 argv 数组,没有任何变量替换——
ENTRYPOINT ["java","$JAVA_OPTS","-jar"]里的 $JAVA_OPTS 会被当成字面字符串传给 java 直接报错。套sh -c "java $JAVA_OPTS -jar app.jar"后由 shell 负责展开变量。这个细节我是真实处理过的:数币平台后端镜像要按环境注入不同堆参数,就是靠 ENV 加 sh -c 这套组合实现的,交付文档里还专门写了注释防止后人改错。 - 追问2:CMD 和 ENTRYPOINT 再用一句话区分? → 答:CMD 是"默认值",docker run 后面跟的命令会整体替换它;ENTRYPOINT 是"固定入口",run 参数追加为它的参数。所以 ENTRYPOINT 适合固定程序形态(这个容器就是跑 java -jar 的),CMD 适合可变默认值(默认跑哪个 jar)。记忆钩子:ENTRYPOINT 定身份、CMD 定默认参数,run 的参数永远只是"改参数"。
- 迭问3:COPY 和 ADD 选哪个? → 答:一律 COPY。ADD 比 COPY 多两个行为——自动解压本地 tar 包、可以拉 URL——但正是这些"聪明"让语义不透明:读 Dockerfile 的人不知道一个 ADD 到底是拷贝、解压还是下载。规则很朴素:需要解压的场景分两步做(COPY 加 RUN tar),可控可读;拷文件永远 COPY。这是各公司 Dockerfile 规范的共识条款,我写交付文档时把这条列进了镜像构建规范。
5.3 分层缓存与镜像瘦身(多阶段构建) 【必背】
标准回答: 三个概念一条线讲。分层:镜像 = 多个只读层叠加,Dockerfile 每条 RUN/COPY/ADD 生成一层,相同的层可以被多个镜像和容器复用;容器 = 所有只读层加顶部一个可写层,修改文件按写时复制(CoW)先把文件完整拷到可写层再改。缓存:构建时从上往下逐层比对"指令加内容"的哈希,命中就复用不再执行——由此推出 Dockerfile 顺序铁律:变的放后面。典型应用:先 COPY 依赖清单装依赖、再 COPY 源码编译,改代码时依赖层缓存命中,构建从十几分钟缩到一两分钟。这条铁律在交付现场价值很大——客户环境构建机性能一般,缓存没设计好,一次构建半小时,现场等着上线的人全都盯着你。
瘦身五板斧:① 精简基础镜像(slim/alpine);② RUN 合并加清缓存;③ .dockerignore 排除无关文件减小上下文;④ 多阶段构建——编译阶段用全量镜像,运行阶段只 COPY 产物到精简镜像;⑤ 固定小版本 tag 不用 latest,可复现且不会被上游悄悄换底包。镜像瘦身对交付的直接好处是离线分发的 tar 包小——内网环境镜像要走"外网构建、tar 搬运、内网导入",一个 1G 的镜像和一个 300M 的镜像,铺 9 个节点的差别就是十几分钟和几分钟的事。 实操模板(多阶段构建骨架,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 放在装依赖之前,每次改代码依赖层全部失效重下;放之后,改代码只触发编译层。给开发团队讲镜像优化,我第一条讲的就是这个,配上"构建时间从 12 分钟到 1 分钟"的对比数字,比讲原理有说服力。
- 迭问2:进容器手工改文件再 commit,为什么镜像越滚越大? → 答:CoW 机制的反噬:修改可写层里的文件要把原文件从只读层完整拷到可写层再改——一份变两份;删除文件也只是在新层打"删除标记",旧层实体还在。所以"进容器手改加 commit"是交付反模式:镜像越 commit 越胖还不可复现。我的交付纪律写在部署指南里:任何变更改 Dockerfile 重建,一切变更代码化——这也是文档能"照着重放"的前提。
5.4 镜像架构核对:ARM 交付的必检项 【必背】
标准回答(银行项目真实经验,实施版特色考点): ARM 环境交付,镜像问题是我踩过也固化成流程的一课。核心就一条:镜像架构必须和节点架构匹配。多数官方镜像的历史版本只构建了 amd64 架构,在 aarch64 节点上会报两类错:拉取时报 no matching manifest for linux/arm64/v8——这个 tag 没有 arm64 构建;或者离线导入了一个 x86 镜像,容器启动报 exec format error——内核执行不了另一个指令集的二进制。
我的流程化做法是交付前做"镜像架构清单":一张表列出镜像名、tag、architecture、来源四列,逐项核对后才进分发环节。核对命令两条:docker manifest inspect 镜像:tag 看 architectures 列表(外网机器上用),docker inspect 镜像 | grep Architecture 看本地已导入镜像的实际架构。处理三选一:找官方 multi-arch 版本、基于 arm64 基础镜像自己构建、拿已在 ARM 上验证过的离线包直接分发。银行项目所有镜像走第三条——外网准备 arm64 tar 包带进内网(见 5.5)。这个检查项的来源正是一次 x86 镜像混入导致的 exec format error:加了清单核对之后,同类问题在后续节点铺开时零复发——把偶发事故变成清单检查项,是我在实施岗位上最典型的沉淀方式。 追问链:
- 迭问1:什么是 multi-arch 镜像? → 答:一个 tag 指向一个 manifest list(镜像清单列表),列表里按架构列出各自的 manifest——客户端 pull 时报出自己的架构,服务端返回对应版本。这就是"同一个 nginx:1.25 在 x86 和 ARM 机器上都能拉"的原理。验证方法就是 manifest inspect,看 architectures 字段里有没有 arm64/v8。理解这个机制还能解释仓库类报错:manifest not found 有时不是镜像名错,是这个 tag 在你的架构下没有对应清单——内网交付前把架构核对做成固定动作,两类坑一起防。
- 迭问2:ARM 环境除了镜像还有什么要换? → 答:四样全要换成 aarch64 版本:操作系统(统信/麒麟的 ARM 版 ISO)、软件源(yum 用 kubernetes-el7-aarch64 这类专用源)、二进制包(Docker 当时用的是 docker-18.09.8.tgz 的 aarch64 静态包解压安装,不是仓库默认的 x86 包)、编译类软件的依赖库(源码装 Redis、Nginx 前先确认 ARM 的 devel 包齐全)。不变的也要说清:内核参数、防火墙、systemd 这些系统层操作与 x86 完全一致——差异集中在"二进制和镜像的架构"这一层。这套总结详见第 6 章 6.5 节,两边互为印证。
5.5 离线导入:docker save/load 与批量加载脚本(银行项目真实) 【必背】
标准回答(结合项目讲,实施版核心题): 命令对先分清:docker save -o web.tar web:v1 把镜像导出成 tar 包(保留完整分层和历史),docker load -i web.tar 或 cat web.tar | docker load 导入(tar.gz 能直接喂不用先解压);另一对 docker export/import 是针对容器文件系统的——丢掉分层和历史,交付场景一般不用。选择 save/load 的原因就是保分层:同一批镜像在多个节点导入时相同层只存一份的机制依然成立,而且离线包可以在外网机器上用 save 按清单一次性打好。
我的真实场景讲完整:银行内网环境完全无法访问海外镜像仓库,而且 TaiShan 是 ARM 架构,所以所有镜像走"外网准备加离线分发":在外网机器上按华为 BCS 配套的镜像清单准备各镜像的 arm64 tar.gz 包,传进内网,然后给数据面节点写批量加载脚本——for 循环遍历镜像目录下所有 tar.gz,用条件判断排除 edge-manager 开头的管理面专用镜像(那些由管理面节点单独加载,数据面不需要),其余全部 cat 进 docker load,加载完 docker images 核对清单。这个脚本让十几类镜像一次铺满全部数据面节点,BCS 纳管一次通过,脚本原样沉淀进《智能合约引擎安装部署指南》,成为公司内网项目的标准动作。 实操模板(批量加载脚本,可直接背写):
bash
#!/bin/bash
# 离线批量加载镜像:排除管理面专用镜像(edge-manager*),其余全部导入
for f in *.tar.gz; do
if [[ "$f" != edge-manager* ]]; then
echo "loading $f"
cat "$f" | docker load
fi
done
docker images # 加载完与镜像架构清单逐项核对(见 5.4)追问链:
- 迭问1:为什么排除 edge-manager 开头的包? → 答:因为华为 BCS 边缘自治版的镜像包分两类:管理面镜像(以 edge-manager 命名)只在管理面三个节点加载,数据面节点加载了既占磁盘也没用处;数据面镜像是业务运行真正需要的部分。原厂文档只写了"按需加载",没有给出批量判断方法——我这个一行条件判断把"按需"变成了脚本自动识别,九个数据面节点重复执行即可。这是我对"文档级操作做脚本化改造"的一个代表案例:交付效率的差距往往就在这些细节的一行代码上。
- 迭问2:save/load 和 export/import 什么时候用后者? → 答:几乎不用,但要知道区别应对追问:save/load 操作镜像对象,保留分层结构和构建历史,是镜像分发的正道;export/import 把一个容器的文件系统扁平化导出,丢掉所有元数据和层信息,出来的镜像不可追溯、体积也不占优。唯一接近的场景是把一个"配好环境的老容器"固化下来应急,但那是反模式——正确做法永远是补写 Dockerfile 重建。我在实施规范里写明:交付环境禁止用 export/import 产生镜像。
5.6 私有仓库分发与 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 就能认证拉取。注意 Secret 是命名空间级资源,新命名空间要再建一份。第三条是运行时策略三层:首选内网仓库、备选阿里云公共仓库、兜底二进制部署——Flannel 的 init 容器和主容器镜像我做了全局替换成内网地址,这套策略解决了海外镜像拉不下来导致节点 NotReady 的最高频故障(避坑清单第 1 条)。 追问链:
- 迭问1:Pod 报 ImagePullBackOff 怎么排查? → 答:固定三板斧:第一
kubectl describe pod看 Events——"Failed to pull"后面的原因直接分类:unauthorized 是凭据问题(secret 名字错、不在同一命名空间、密码错),manifest not found 是镜像名或 tag 错,timeout 是网络不通或仓库不可达;第二看 Pod spec——imagePullSecrets 的名字和所在命名空间对不对、镜像地址拼写;第三到 Pod 所在节点用 crictl pull 同一镜像复现——能复现就是仓库、凭据或网络问题,不能复现就是 K8s 对象配置问题。这个"分层复现"的思路我在现场排障用得最多,比逐个猜快得多。 - 迭问2:节点级 docker login 和 imagePullSecrets 怎么选? → 答:docker login 是节点级——凭据写进该节点的 ~/.docker/config.json,影响这台节点所有拉取,粒度粗,适合可信内网图省事;imagePullSecrets 是命名空间级的 K8s Secret——按需授权给特定工作负载,粒度细、可审计,是推荐做法。移动云项目我全部用 imagePullSecrets,理由跟交付有关:凭据集中管理、随应用清单走,新节点加入不需要逐台 login——环境可复制性是实施最看重的指标。
- 迭问3(了解):Harbor 装过吗? → 答:课程实验装过离线版(docker-compose 起全套),概念上知道项目隔离、RBAC、漏洞扫描、复制策略这些企业特性;生产用的是云厂商私有仓库(托管免运维)。我的口径是:私有仓库的三条链路——推送、认证拉取、双轨策略——是我生产落地过的,Harbor 自建属于"装过实验版 + 理解架构",客户真要自建我能按文档落地。
5.7 containerd 与 Docker:交付环境的两套运行时 【必背】
标准回答(三层递进 + 交付差异): 这题我按"关系、为什么、交付怎么配"三层答。第一层关系:containerd 本来就是 Docker 公司捐给 CNCF 的核心运行时组件——Docker 内部一直用它管容器生命周期,两者是上下游不是竞品。K8s 1.24 正式移除 dockershim,kubelet 直接通过 CRI 标准接口调 containerd,少一层转发更轻更稳;用户最关心的问题——Docker 构建的镜像还能用吗?能,镜像遵循 OCI 规范,containerd 直接能跑。
第二层是我的双实践:银行项目 Docker 18.09(aarch64 静态包,受 BCS 1.2.6 配套约束),移动云项目 containerd 1.7.8(K8s v1.28.2 的配套范围)——两代运行时我在生产都用过,这正好是"版本跟着配套表走"的实施纪律的注脚。第三层交付时 containerd 的三个关键配置(全是坑换来的,避坑清单第 2 条):SystemdCgroup = true——与 kubelet 的 cgroup 驱动对齐,不一致会导致资源统计漂移甚至节点异常;sandbox_image 换国内源——默认 registry.k8s.io 的 pause 镜像内网拉不到,kubelet 起不来 Pod;/etc/crictl.yaml 显式绑定 containerd socket——crictl 默认按 docker/containerd/crio 顺序遍历端点,环境有残留配置就报连接错误。 实操模板(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: falsedocker ps → crictl ps 查容器
docker images → crictl images 查镜像
docker logs → crictl logs / kubectl logs
docker exec → crictl exec / kubectl exec
docker pull → crictl pull 节点级验证拉取(排 ImagePullBackOff 用)追问链:
- 迭问1:containerd 环境怎么构建镜像? → 答:containerd 没有 build 命令,它定位是运行时不是构建工具。三个替代:独立 Docker 构建机(我的做法——能上网的节点 pull/tag/push,构建产物进私有仓库给集群拉,这就是双轨体系的"公网轨"载体);buildah 或 kaniko 在集群内构建(kaniko 不需要特权,适合流水线);BuildKit 的独立构建模式。交付场景我的建议是构建机模式:构建环境独立于生产集群,出问题不互相传染,镜像产出统一走仓库分发。
- 迭问2:pause 镜像是干什么的? → 答:它是每个 Pod 的沙箱容器,Pod 创建时第一个被拉起,作用是持有这个 Pod 的 network namespace 和 IPC namespace;随后创建的业务容器以 container 网络模式加入 pause 的命名空间,共享同一个 IP 和端口空间——这就是同 Pod 容器能用 localhost 互通的原理。它跟交付的关联很直接:内网环境 pause 镜像拉不到是"节点起不来任何 Pod"的元凶之一,所以 sandbox_image 换国内源写进了我的节点初始化基线,和 crictl.yaml 一起是 containerd 节点的标配两件套。
5.8 镜像交付验收清单与报错速查 【理解】
标准回答(清单即答案,实施版特色): 我交付一套容器化应用,镜像这个环节的自检清单十项:① 架构核对(ARM 环境逐项对 Architecture 字段,5.4);② 基础镜像选型(官方 slim/alpine、固定版本 tag);③ Dockerfile 结构(先依赖后代码的缓存顺序、RUN 合并清缓存、.dockerignore);④ 启动方式(exec 格式、sh -c 吃变量);⑤ 时区(TZ 固化 Asia/Shanghai,不然日志时间差 8 小时);⑥ 日志(输出 stdout/stderr,Python 类应用关缓冲,否则 docker logs 是空的);⑦ 配置(环境变量注入,配置不烧死在镜像里);⑧ tag 规范(禁 latest,版本可追溯);⑨ 离线包核对(tar 清单与目标节点数对得上,save 保分层);⑩ 拉取验证(部署后在节点 crictl images 核对、Pod Running、业务冒烟)。高频报错速查:exec format error 是架构不匹配;no matching manifest for linux/arm64 是该 tag 无 arm64 构建;unauthorized 是仓库凭据问题;manifest not found 是镜像名或 tag 错;port is already allocated 是宿主端口被占(ss -lntp 找占用者)——每个报错一句话根因加一条解法,这是我现场反应速度的来源。 追问链:
- 迭问1:这清单里哪几项最容易被漏? → 答:我的经验前四名:日志缓冲参数(上线排障发现 logs 是空的才想起)、时区(对时间线排障发现差 8 小时)、镜像架构(x86 构建丢到 ARM 环境 exec format error)、imagePullSecrets 的命名空间(新命名空间部署忘了建 Secret,Pod 卡在 ImagePullBackOff)。共同点是"构建时都能跑、出事都在运行期"——所以清单的执行时机是部署前而不是救火时,把运行期故障左移成部署前检查,这是交付质量管理的核心思想。
5.9 docker run 常用参数与单机服务部署 【理解】
标准回答(交付单机服务的标准起手式): docker run 参数我按四个场景分组背。基础组:-d 后台运行、--name 命名(后续操作都靠它)、--rm 容器退出即删除(一次性任务用)、-it 交互终端。网络组:-p 宿主端口:容器端口 做映射——这是外部访问容器的入口,也是交付后客户访问的入口;--network 加入指定网络;--net=host 共享宿主网络栈(性能好但无隔离)。存储组:-v 宿主目录:容器目录 绑定挂载——配置文件分发和日志外置的常用手段;-v 卷名:容器目录 命名卷——数据库类有状态服务的最佳实践。配置组:-e KEY=VALUE 注入环境变量——密码、连接串的正确姿势,比烧进镜像安全;--restart=always 开机自启加崩溃自动重启;--memory/--cpus 资源限额。
综合示例一句背:docker run -d --name web -p 80:80 -v /data/web:/usr/share/nginx/html --restart=always nginx:1.25——后台起一个 web 容器、映射 80 端口、挂载站点目录、开机自启。这是我给客户部署单机服务(门户页、工具类应用)时的标准起手式,跑起来之后 ss -lntp 验证监听、curl 验证响应、docker ps 看 (healthy) 状态,三步完成交付验证。这里要补一个部署纪律:容器化部署不等于万事大吉——端口冲突要提前查(ss -lntp 看占用)、宿主目录权限要让容器内进程可读写、时区要用 -e TZ=Asia/Shanghai 对齐,这三个小坑是单机交付现场的高频项。 追问链:
- 迭问1:--restart=always 和 on-failure 区别? → 答:always 是无条件重启——容器无论什么原因退出都拉起,而且宿主机重启后 daemon 也会把它拉起来,适合必须常驻的服务;on-failure 只在退出码非零(异常退出)时重启,跑完就退的批处理任务不会被拉起。我的部署习惯:常驻服务一律 always,一次性的导数迁移任务不带重启策略让它跑完即止——重启策略跟着服务的生命周期走。
5.10 容器排障命令组与退出码分诊 【必背】
标准回答(交付验证期的固定动作): 容器排障我有一组固定命令链,按顺序走:第一步 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 是应用自身异常——看日志找报错。实施交付里最常见的场景就是"容器装完一直重启":ps -a 看 RESTARTS 在涨,inspect 看 Last State 的 exitCode——137 我就去查资源配置和内存行为,1 我就去翻日志——两个方向的动作完全不同,分诊对了排查就是一条直线。这套命令链在联调期和交付验证期我天天走,教给客户运维也是第一课。 实操模板:
bash
docker ps -a # 1 看状态(Exited? Restarting?)
docker logs -f --tail=200 <容器> # 2 看日志
docker exec -it <容器> sh # 3 进容器验证(ps/ss/env/curl)
docker inspect <容器> | grep -E 'ExitCode|OOMKilled|RestartPolicy' # 4 配置与退出码
docker stats --no-stream # 5 资源快照追问链:
- 迭问1:容器反复重启(Restarting 状态)怎么定位? → 答:按因果链四查:一查退出码——inspect 里 Last State 的 exitCode,137 先怀疑内存 limit 被 OOMKilled;二查崩溃前日志——logs 看最后几行,配置错误、连不上数据库这类启动即退的原因都在里面;三查探针——K8s 场景 describe 看 liveness probe failed,探针太激进会把启动慢的应用杀进死循环;四查依赖——应用启动依赖的 DB/Redis/配置中心连不上。四个方向覆盖绝大多数反复重启,我的习惯是先拿退出码分诊再顺着查,不盲目重启。
5.11 docker 磁盘治理与容器日志限额 【理解】
标准回答(节点长期健康,交付后必配): Docker 用久了节点磁盘会被三类东西吃掉,我的治理三板斧对应三类。第一镜像堆积:docker system df 一条命令看镜像、容器、卷各自占多少——这是治理入口;docker image prune 清悬空镜像(反复构建的环境能堆几十 G)。第二容器可写层和停止容器:docker container prune 清停止的容器——注意事故容器想留现场的先别清。
第三容器日志——最容易被忽略的磁盘杀手:容器 stdout/stderr 默认存 /var/lib/docker/containers/ 下的 json 日志文件,无限制增长能把根分区撑爆。根治要配 daemon.json:{"log-driver":"json-file","log-opts":{"max-size":"50m","max-file":"3"}}——单文件 50M 最多 3 份滚动,改完重启 docker 生效。这条对实施交付特别重要:节点交付时配好日志限额是"初始化基线"的一部分,而不是等磁盘满了回来救火——我在部署手册的节点初始化一节里把它和 chrony、crictl.yaml 并列为预置配置三件套。配套的巡检动作:磁盘使用率 85% 告警(磁盘满是 Pod 驱逐和节点 NotReady 的前奏),加上定期清未用镜像——这两个动作写进客户交接的巡检 SOP。 追问链:
- 迭问1:为什么强调部署时就配日志限额,而不是出事再配? → 答:因为这类问题的时间特性——日志增长是无声的,交付后一两个月才爆发,那时实施方可能已经撤场,客户在自己运维期踩坑体验最差。把它前置到初始化基线,成本是配置文件一行,收益是消掉一类未来的工单。"部署时多配一行,运行期少救一次火"——这是我做交付配置取舍的基本原则,和证书到期提醒、时间同步是同一个思路:所有"慢性故障"都在交付期根治。
5.12 四种网络模式与容器互通(交付够用版) 【理解】
标准回答: Docker 网络四种模式按使用频率排:bridge 是默认——docker0 网桥加 NAT,容器有独立网络栈,对外靠 -p 端口映射,通用场景都用它;host 直接共享宿主机网络栈,没有网络隔离、性能最好,适合端口不冲突且追求吞吐的服务;container 模式是"加入另一个容器的网络命名空间"——这就是 K8s Pod 的底层原理:先起一个 pause 沙箱容器持有 network namespace,Pod 里其他业务容器都 join 进来,所以同 Pod 容器共享 IP、用 localhost 互通;none 无网络,给离线任务或安全测试用。
容器间通信两层:同主机——默认 bridge 下用容器 IP 直通,但生产推荐自建网络 docker network create app-net,自建网络带内嵌 DNS,容器名自动解析互 ping——容器 IP 重启就变,写死必翻车,"名字稳定、IP 可变、DNS 做映射"是解法;跨主机——默认不通,靠端口映射到宿主机互访,或者上 overlay 网络(flannel/calico)——这正是 K8s CNI 要解决的问题。实施交付的相关经验:给客户部署一套多容器应用时,服务间互访一律写服务名不写 IP——这条写进部署手册的注意事项,能消掉一类"重启后连不上"的售后问题。 追问链:
- 迭问1:为什么默认 bridge 里容器名 ping 不通、自建网络可以? → 答:默认 bridge(docker0)没有内嵌 DNS 服务,容器里只能靠 IP 通信;用户自建网络自带 Docker 内嵌 DNS(127.0.0.11),容器启动时把容器名注册进去,其他容器解析名字自动拿到当前 IP。这和 K8s 里 Service 名解析(CoreDNS)是同一思想——我在交付里统一要求"跨组件访问必须走服务名",就是把这个思想落成纪律。
5.13 数据卷与挂载:数据不放容器层 【理解】
标准回答: 容器数据管理的原则一句话:"容器无常、数据有常"——可写层随容器删除而消失,要持久的数据必须挂出来。三种挂载方式:命名卷 -v data:/var/lib/mysql——由 daemon 管理,实体在 /var/lib/docker/volumes/ 下,跨容器复用方便,是数据库类有状态服务的最佳实践;绑定挂载 -v /host/path:/container/path——直接映射宿主目录,配置文件分发、日志外置常用,注意宿主目录属主权限要容器内进程可读写;匿名卷——Dockerfile 里 VOLUME 指令声明的,容器删了容易变孤儿,要定期 docker volume prune。
判断标准很简单:数据库目录、应用日志这类必须出容器层的用命名卷或绑定挂载,无状态应用随便重建。这个原则在 K8s 里就是 hostPath/PV 那套(第 6 章 6.8)——我在 Kuboard 部署里用 hostPath 持久化是同一思想在集群环境的版本。交付时的验证动作也固定:部署完手动删容器/重建 Pod,数据还在才算持久化生效——"验证过"和"配置了"是两回事,这条自检我写进部署手册的验证清单。 追问链:
- 迭问1:容器删了数据还在吗? → 答:分两层:可写层的数据随容器删除消失——所以不能把数据库文件留在容器层;卷和绑定挂载的数据在宿主机上,容器删了也在。所以正确的姿势是:有状态服务的数据目录一律挂卷,容器本身保持无状态可随时重建——升级、迁移、故障恢复都以"换容器"为单位,数据在卷里原地不动。
5.14 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 对象。实施交付时 compose 还有个隐藏价值:把客户要的"一套小系统"写成一个 compose 文件交付,重启迁移都是一条命令——交付物本身就是"可重放"的。 实操模板(最小 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 的 depends_on 能保证数据库"就绪"吗? → 答:不能——depends_on 默认只保证"启动顺序"(容器起来),不保证"服务可连接";应用启动时连不上库照样崩。要让"起来了"变成"就绪了",用 healthcheck 加 condition 化依赖(depends_on 里写 condition: service_healthy)——这个坑我在单机部署时处理过,也是我理解 K8s 探针体系价值的起点:单机编排缺的就绪判断,正是 K8s readiness 探针补上的。
5.15 镜像 tag 规范与版本治理 【了解】
标准回答: 镜像 tag 看似小事,治理缺失会变成事故源。我的 tag 规范五条:禁用 latest——不可追溯且缓存不可控,"发布了等于没发"就是这么来的;版本语义化——v1.2.3 或日期构建号,能看出新旧和构建时间;仓库路径分层——仓库地址/项目/应用名:版本,权限和清理按项目治理;变更可追溯——重要镜像记录 digest(sha256),回滚和审计以 digest 为准;环境晋升不重打 tag——同一版本从测试晋升到生产改部署清单不改 tag,保证"测过的就是上线的"。这套规范在移动云双轨体系里是我定的:内网仓库按"项目/应用:版本"统一重打,从此没有"这是哪个版本"的疑问——版本治理是"能五分钟回滚"的前提条件。
追问链:
- 迭问1:为什么回滚要认 digest 不认 tag? → 答:因为 tag 是可变的引用——有权限的人或 CI 可以把 v2.1 重新指向另一个镜像内容,而 digest 是内容的哈希、全球唯一且不可变。极端场景:上午的 v2.1 有 bug,下午被修复版覆盖了同 tag——此时"回滚到 v2.1"得到的已经不是上午那个版本,线上到底是什么也说不清。"tag 给人看、digest 给机器信",这是我镜像治理的一句话原则。
5.16 本章案例
- 【真实项目·银行 ARM 离线镜像体系】背景:某银行内网环境加 TaiShan ARM 架构,9 个 BCS 数据面节点需要加载全套 arm64 镜像,内网拉不了任何海外镜像。任务:把镜像安全高效地铺到全部数据面节点。行动:外网按 BCS 配套清单准备 arm64 tar.gz 包传入内网;编写批量加载脚本——for 循环遍历目录、条件排除 edge-manager 管理面镜像、逐个 cat 进 docker load、加载前后 docker images 双向核对清单;期间处理过一次 x86 镜像混入导致的 exec format error,之后固化"镜像架构清单"检查项。结果:十几类镜像一次铺完,BCS 数据面纳管一次通过;脚本和清单沉淀进《智能合约引擎安装部署指南》,成为公司同类内网项目的标准流程。
- 【真实项目·移动云镜像双轨体系】背景:移动云内网集群部署组件时海外镜像(registry.k8s.io、ghcr.io)拉取失败,导致 Flannel Pod ImagePullBackOff、节点 NotReady。任务:建立可持续的镜像供给体系。行动:设计"公网拉取 → 内网私有仓库托管 → 集群认证拉取"双轨——能上网的节点 pull 原镜像(如 eipwork/kuboard:v3.5.2.2)、tag 成内网仓库规范格式 push;集群内 create secret docker-registry 建 ImagePullSecret、Deployment 挂 imagePullSecrets 拉取;Flannel DaemonSet 镜像全局替换为内网地址。结果:集群所有组件镜像供给稳定,NotReady 类故障根除,双轨策略写进建设方案成为标准。
- 【真实项目·crictl socket 坑】背景:移动云集群节点上执行 crictl ps 报"连接 socket 失败",影响节点级排障效率。定位:crictl 默认按 docker、containerd、crio 顺序遍历运行时端点,环境里配置一多就遍历到错误目标。行动:/etc/crictl.yaml 显式写死 runtime-endpoint 和 image-endpoint 指向 containerd.sock,并把这个文件的预置写进全节点初始化脚本。结果:crictl 命令行为在全部节点完全一致,该坑记入 9 项避坑清单——"初始化脚本预置配置文件"这个动作后来推广到 crictl、chrony、limits 等所有"默认行为不可控"的组件。
- 【经典故障】ARM 镜像 manifest 不匹配:报错 no matching manifest for linux/arm64/v8,根因是所用镜像 tag 没有 arm64 构建。解法三件套:docker manifest inspect 查 architecture 支持列表、换 multi-arch 版本或基于 arm64 基础镜像自构建、用已验证的 arm64 离线包分发。教训固化为交付前检查项——"镜像架构清单"是我 ARM 项目最重要的流程产出之一。
5.17 章末自测清单(快问快答)
- 镜像/容器/仓库一句话? → 镜像只读模板(类)、容器运行实例(+可写层)、仓库集中存取镜像。
- CMD 和 ENTRYPOINT 区别? → CMD 可被 run 参数整体替换;ENTRYPOINT 固定入口,run 参数追加给它。
- 为什么 exec 格式 + 什么时候还要套 sh -c? → exec 让 java 直接当 PID 1 收 SIGTERM;要展开 ENV 变量时套 sh -c(JSON 数组不做变量替换)。
- 缓存铁律一句话? → 变的放后面:先 COPY 依赖清单装依赖,再 COPY 源码。
- COPY 和 ADD? → 拷文件一律 COPY;ADD 多了自动解压和拉 URL,语义不透明慎用。
- exec format error 根因? → 镜像架构与节点架构不匹配(x86 镜像跑在 ARM),查 inspect 的 Architecture。
- ARM 镜像交付必检项? → 镜像架构清单:镜像名、tag、architecture、来源四列逐项核对。
- save/load 和 export/import? → save/load 镜像保分层(交付正道);export/import 容器丢历史(禁用于交付)。
- 批量加载脚本怎么排除管理面镜像? →
if [[ "$f" != edge-manager* ]]条件跳过,其余 cat 进 docker load。 - tag 重打的格式规范? → 仓库地址/项目名/镜像:版本,不合规 push 被拒。
- ImagePullSecret 三步? → create secret docker-registry → Deployment 挂 imagePullSecrets → 注意 Secret 命名空间级。
- ImagePullBackOff 三板斧? → describe 看 Events 分类原因 → 核对 spec 的 secret 与镜像名 → 节点 crictl pull 复现。
- containerd 三个关键配置? → SystemdCgroup=true、sandbox_image 国内源、/etc/crictl.yaml 显式 socket。
- 单机服务部署标准起手式? → docker run -d --name -p -v --restart=always,跑完 ss/curl/ps 三步验证。
- 退出码 137/143/1 各指什么? → 137=OOMKilled 或 SIGKILL(查内存 limit);143=SIGTERM 正常终止;1=应用异常(看日志)。
- 容器日志限额怎么配? → daemon.json 的 log-opts max-size=50m、max-file=3,交付期预置而非出事再配。
- 镜像交付十项清单里最易漏的四项? → 日志缓冲参数、时区 TZ、镜像架构、Secret 命名空间。
- 私有仓库双轨体系一句话? → 公网拉取 → 内网仓库托管 → 集群认证拉取;运行时三层:内网仓库优先、公共仓库备选、二进制兜底。
- 四种网络模式一句话? → bridge 默认 NAT、host 共享宿主、container 共享他容器网络(=Pod 原理)、none 无网络。
- 自建网络比默认 bridge 强在哪? → 内嵌 DNS,容器名可解析——名字稳定 IP 可变。
- 数据持久化三方式与交付验证? → 命名卷(有状态服务)、绑定挂载(配置/日志)、匿名卷慎用;验证=删容器重建数据还在。
- compose 和 K8s 的边界? → compose 单机编排无自愈调度;K8s 跨节点集群——单机工具用 compose,生产用 K8s。
- tag 治理一句话? → 禁 latest、版本语义化、路径分层、记录 digest、环境晋升不重打 tag。