Appearance
第 11 讲|容器和编排
应用部署过程中最棘手的问题是**"同一份代码在不同机器上跑出不同结果":测试机能跑,推到生产就报缺库;A 机器有环境变量,B 机器没有;回滚时上一版的文件和配置无法精确还原。这种环境漂移**问题困扰运维多年。容器就是为彻底解决这一问题而设计。
容器把应用、依赖、启动命令、文件系统打包成一个分发制品——镜像。镜像在任何支持容器运行时的机器上启动结果都相同,从根源消除了环境差异。镜像运行起来就是容器。
当容器数量从几个增长到几百几千,需要编排系统来管理容器在集群中的调度、重启、扩缩容、服务发现、滚动发布。Kubernetes 是云原生时代编排系统的事实标准。
本讲依次讲清楚:部署环境问题的本质、镜像与容器的关系、Docker 与 Kubernetes 的分工、K8s 的核心对象、容器化后的常见故障与排查思路。
一、传统部署的环境问题
手工部署的标准流程:登录机器 → 安装运行时(JDK、Python、Node)→ 复制代码 → 改配置 → 启动进程 → 把日志路径与启停命令记到文档。

机器少时这种方式还能勉强运转,机器一多就会失控:
| 问题 | 现场表现 |
|---|---|
| 依赖不一致 | 缺动态库、Python 包版本不同、JDK 版本不同 |
| 权限不一致 | 某台机器目录不能写,上传或日志写入失败 |
| 发布不可重复 | 同样的步骤换个人执行,结果不一样 |
| 回滚困难 | 上一版的文件、配置、启动参数都对不上 |
| 扩容慢 | 新加机器要重新装运行环境,半天搞不定 |
| 配置漂移 | 长期运行的机器上,配置被各种手工操作改过,跟初装时差异巨大 |
虚拟机与容器:两条路线
虚拟机 通过完整虚拟一套操作系统隔离环境:
- 优势:完全隔离,可以跑不同操作系统、不同内核
- 劣势:镜像体积大(GB 级)、启动慢(数十秒)、分发与升级成本高、宿主机资源利用率低
容器 走的是另一条路:不隔离整个操作系统,只隔离进程的运行视图。应用直接跑在宿主机内核上,通过 Linux 的 namespace、cgroup 机制让它"看起来"独占系统。
| 对比维度 | 虚拟机 | 容器 |
|---|---|---|
| 隔离粒度 | 整个操作系统(内核 + 用户态) | 用户态进程(共享宿主机内核) |
| 镜像大小 | GB 级 | 几十到几百 MB |
| 启动速度 | 数十秒到分钟 | 几百毫秒到秒级 |
| 资源开销 | 大(每个 VM 都有完整 OS) | 小(只多一层进程隔离) |
| 跨内核兼容 | 可以(Linux VM 跑在 Windows 上) | 不可以(必须用相同内核) |
| 安全隔离 | 强 | 弱(共享内核就有内核漏洞风险) |
容器的轻量是它能成为云原生主流的核心原因——镜像几百 MB、启动几百毫秒,适合频繁部署、快速扩缩容。
二、镜像与容器
概念关系
| 对象 | 含义 |
|---|---|
| 镜像(Image) | 静态制品,包含应用代码、依赖、启动命令、文件系统 |
| 容器(Container) | 镜像运行起来后的进程环境 |
| 镜像仓库(Registry) | 保存和分发镜像的服务,如 Harbor、Docker Hub、阿里云 ACR |
| 容器运行时(Runtime) | 实际创建和管理容器的组件,如 containerd、CRI-O |
类比:镜像是程序的安装包,容器是按安装包装好后正在运行的程序实例。一个镜像可以同时启动多个容器。
镜像的分层结构
容器镜像是**分层(layered)**的——每一层是文件系统的一组增量变更,层之间共享。这种结构带来几个特性:

- 不同镜像可以共享相同的底层(如都基于
ubuntu:22.04),节省存储与下载流量 - 镜像构建时只重建变化的层,加快 CI 速度
- 推送/拉取镜像时只传输缺失的层
Dockerfile 中每条指令(FROM、RUN、COPY)都会产生一层,所以编写时要考虑层的合并(&& 连接多条 RUN)、缓存利用(把不常变化的步骤放前面)。
完整发布流程
完整 CI/CD 流程:开发提交代码 → CI 系统构建镜像 → 镜像推送到仓库 → 服务器或集群从仓库拉镜像 → 启动容器。
容器不是小型虚拟机
这是容器最容易被误解的一点。容器里的进程仍然是宿主机内核上的进程,只是通过隔离机制让它"看起来"独立:
| 隔离机制 | 隔离什么 |
|---|---|
| PID namespace | 进程列表(容器内 ps 看不到宿主机进程) |
| Network namespace | 网络栈(独立网卡、IP、路由表) |
| Mount namespace | 文件系统视图(容器看到的 / 是镜像 + 卷) |
| UTS namespace | 主机名 |
| IPC namespace | 进程间通信 |
| User namespace | 用户/组 ID 映射 |
| cgroup | 资源限额(CPU、内存、IO) |
容器与宿主机的边界
| 容器内能看到 / 控制 | 容器无法绕过 |
|---|---|
| 自己的进程列表 | 宿主机内核版本 |
| 自己的网卡和 IP | /proc/sys/ 内核参数(共享) |
| 自己的文件系统视图 | 节点物理 CPU、内存总量 |
| 自己的主机名 | 宿主机时区(除非显式设置) |
| 自己的环境变量 | 宿主机的磁盘 IO 总带宽 |
经典的排查陷阱:
容器内 top 看到 CPU 占用很低,但宿主机已经被打满——因为 cgroup 限制的是容器自己看到的"配额",宿主机层面的整体压力容器内根本看不到。
容器内 free -h 看到内存"很多",但其实是宿主机的总内存——传统的 free 命令不感知 cgroup 限制,看到的是宿主机数据,而不是容器的内存上限。新版工具(如 containerd 集成的查看命令、K8s 的 kubectl top)才能正确显示容器视角的资源使用。
排查容器问题时,必须同时看容器视角和宿主机视角,才能拼出完整的资源使用情况。
三、Docker 与 Kubernetes 的关系
刚接触容器时容易把 Docker 和 Kubernetes 当成"竞争关系"或"升级关系",实际上它们是两个层次的工具。
Docker:容器的工具集
Docker 是把镜像构建、容器运行、网络管理、卷管理打包成一个友好命令行工具集。它包含:
docker build— 构建镜像docker run— 启动容器docker push/pull— 推拉镜像dockerd— Docker 守护进程containerd— 底层容器运行时(Docker 内部依赖)
Docker 适合:本地开发、镜像构建、单机部署、学习容器。
Kubernetes:集群级编排系统
Kubernetes 解决的是**"集群里跑大量容器"**的问题:
- 容器放在哪台机器?(调度)
- 容器挂了谁拉起来?(自愈)
- 服务之间怎么互相访问?(服务发现)
- 配置和密钥怎么注入?(ConfigMap/Secret)
- 发布时怎么逐步替换?(滚动更新)
- 流量怎么进入?(Ingress)
K8s 不直接运行容器,而是通过 CRI(Container Runtime Interface) 调用底层容器运行时:
K8s 中 Docker 的现状
K8s 从 1.24 起正式移除了对 Docker 作为运行时的支持(不再有 dockershim)。现在 K8s 节点上的容器运行时主流是 containerd 或 CRI-O——这两者都符合 CRI 标准,不需要 Docker。
但 Docker 没有过时:
- 开发者本地构建镜像仍然主要用
docker build - 镜像格式与运行时格式(OCI 标准)是兼容的,Docker 构建的镜像能在 K8s 上运行
- 简单单机部署、CI/CD 流水线中仍大量使用 Docker
OCI:让工具与运行时解耦
OCI(Open Container Initiative) 是容器领域的核心标准,定义了:
- OCI Image Spec — 镜像格式
- OCI Runtime Spec — 运行时接口
这一标准让生态可以解耦:
- 构建工具:Docker、
buildah、kaniko、nerdctl都能构建符合 OCI 标准的镜像 - 镜像仓库:Harbor、Docker Hub、Quay 都存储 OCI 镜像
- 容器运行时:Docker、containerd、CRI-O、podman 都能运行 OCI 镜像
工具链可以自由组合,不必从头到尾绑定一家。
四、Kubernetes 核心概念
K8s 的核心设计思想是声明式 API + 控制器循环:
- 用户描述期望状态(我要 3 个 Pod 副本)
- 控制器持续观察当前状态
- 发现差异,自动调整使当前状态趋近期望状态
例如 Deployment 声明 3 个副本,如果某个 Pod 挂了变成 2 个,控制器自动再起一个补到 3 个。这种"自愈"能力是声明式的天然结果。
K8s 解决的核心问题与对应对象
| 问题 | K8s 用什么对象解决 |
|---|---|
| 跑几个副本 | Deployment / StatefulSet |
| 容器放在哪个节点 | Scheduler 自动调度 |
| 容器挂了怎么办 | kubelet + 控制器自动重建 |
| 服务之间怎么访问 | Service + 内置 DNS |
| HTTP 入口在哪 | Ingress / Gateway API |
| 配置怎么注入 | ConfigMap |
| 敏感信息怎么注入 | Secret |
| 持久化存储 | PV / PVC |
| 定时任务 | CronJob |
| 一次性任务 | Job |
K8s 的起源:Borg
K8s 的设计思想来自 Google 内部的 Borg 系统——一个运行了十多年的大规模集群管理系统。容器生态成熟后,Google 把 Borg 的设计(声明式 API、调度、控制器、服务发现、滚动发布)整理成开源系统,这就是 Kubernetes。
正因为 Borg 经过了 Google 多年大规模生产验证,K8s 的设计在"集群管理"这一问题上有相当的前瞻性。
五、K8s 常见对象
K8s 中反复遇到的核心对象:

| 对象 | 作用 |
|---|---|
| Pod | 最小运行单元,一个或多个紧密耦合的容器 |
| Deployment | 管理无状态应用的副本数与滚动更新 |
| StatefulSet | 有状态应用,Pod 有固定的名字与存储 |
| DaemonSet | 在每个节点都跑一个 Pod(如日志采集器) |
| Job / CronJob | 一次性任务、定时任务 |
| Service | 给一组 Pod 提供稳定的访问入口 |
| Ingress / Gateway | HTTP/HTTPS 入口,按域名/路径路由 |
| ConfigMap | 非敏感配置 |
| Secret | 敏感信息(密码、证书、Token) |
| PV / PVC | 持久化存储声明 |
| Namespace | 资源命名空间,做隔离 |
Pod:为什么需要这一层
Pod 是 K8s 的最小调度单元。Pod 内可以有多个容器,这些容器共享网络命名空间和存储卷,彼此通过 localhost 通信。
为什么不直接调度容器?Pod 的设计支持几类紧密耦合的容器组合:
- Sidecar 模式:主容器跑业务,辅助容器跑日志采集/监控/代理
- Init Container:主容器启动前先跑初始化任务
- 多容器协作:共享文件、共享网络的紧密协作
绝大多数业务 Pod 内只有一个业务容器——但 Pod 这一层抽象给"多容器协作"留了空间。
Service:解决 Pod IP 不稳定
Pod 是临时的——可能因为节点故障、滚动更新、扩缩容而被重建,重建后 IP 通常会变。如果客户端直接连 Pod IP,Pod 一变就全失效。
Service 提供一个稳定的虚拟 IP(ClusterIP)和 DNS 名,把流量负载均衡到后面的 Pod:
Pod 增删时,Service 自动更新后端列表;客户端始终通过 api-service 这个稳定名访问,完全感知不到 Pod 变化。
Service 的几种类型
| 类型 | 暴露范围 |
|---|---|
| ClusterIP(默认) | 仅集群内部可达 |
| NodePort | 集群每个节点的某端口都暴露该 Service |
| LoadBalancer | 云厂商创建外部负载均衡器,绑定 Service |
| ExternalName | 把 Service 名解析为外部 DNS 名 |
Ingress:HTTP/HTTPS 入口
NodePort 和 LoadBalancer 适合暴露单个服务,但实际业务通常有几十上百个 HTTP 服务,不可能每个都开一个 LoadBalancer(贵且管理复杂)。
Ingress 提供 HTTP/HTTPS 层的入口:
- 单一入口(一个 LoadBalancer)
- 按域名、路径分流到不同 Service
- TLS 证书统一管理
yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-ingress
spec:
rules:
- host: api.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: api-service
port:
number: 8080
- host: web.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web-service
port:
number: 80Ingress 资源本身只是路由规则的声明,实际转发由 Ingress Controller 实现(Nginx Ingress、Traefik、HAProxy Ingress、Envoy/Istio)。
ConfigMap 与 Secret
应用配置通过 ConfigMap 注入,敏感信息通过 Secret 注入:
yaml
# ConfigMap
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
log_level: "info"
database_host: "mysql.default.svc.cluster.local"
# Secret
apiVersion: v1
kind: Secret
metadata:
name: app-secret
type: Opaque
data:
database_password: cGFzc3dvcmQxMjM= # base64 编码注入到 Pod:
yaml
spec:
containers:
- name: app
image: myapp:1.0
envFrom:
- configMapRef:
name: app-config
- secretRef:
name: app-secretSecret 默认只是 base64 编码,不是加密——这是常见误解。要真正保护 Secret,需要:
- 启用 K8s 的 Encryption at Rest(etcd 中的 Secret 加密存储)
- 使用 Vault、Sealed Secrets、External Secrets Operator 等更安全的方案
六、容器化后的故障排查
容器化让发布制品稳定了(镜像一致),但排查对象的种类变多了——镜像、运行时、Pod、Service、Ingress、CNI 网络、存储卷、节点资源,任何一个环节都可能成为问题源。
Pod 状态与对应排查方向
| Pod 状态 | 含义 | 排查方向 |
|---|---|---|
Pending | 还没被调度或无法调度 | 节点资源不足、nodeSelector 不匹配、PVC 未绑定 |
ImagePullBackOff | 镜像拉取失败 | 镜像名错、tag 不存在、仓库认证、网络 |
ErrImagePull | 同上,刚开始失败 | 同上 |
CrashLoopBackOff | 容器反复崩溃重启 | 应用启动报错、配置缺失、健康检查配错 |
Running 但 Ready=False | 容器在跑但没就绪 | readinessProbe 失败、依赖未就绪 |
Running 且 Ready=True | 正常 | — |
Terminating 卡住 | 删除时卡住 | preStop hook 死循环、Volume 卸载失败 |
标准排查命令
bash
# 看 Pod 状态
kubectl get pods -n <namespace>
kubectl get pods -n <namespace> -o wide # 多列信息,包括所在节点和 Pod IP
# 看详细信息,包括 Events
kubectl describe pod <pod-name> -n <namespace>
# 看容器日志
kubectl logs <pod-name> -n <namespace>
kubectl logs <pod-name> -n <namespace> --previous # 上一次崩溃前的日志
kubectl logs <pod-name> -n <namespace> -c <container> # 多容器 Pod 指定容器
kubectl logs -f <pod-name> # 实时跟踪
# 进入容器排查
kubectl exec -it <pod-name> -n <namespace> -- sh
# 查看节点状态
kubectl get nodes
kubectl describe node <node-name>
# 看 Service 和 Endpoints
kubectl get svc -n <namespace>
kubectl get endpoints -n <namespace> # 关键!Endpoints 空就说明没有可用 Pod一次 502 故障的完整排查
用户访问 K8s 集群上的应用返回 502,在传统部署里是"Nginx 到后端不通",在 K8s 里要分多层排查:

bash
# 第 1 步:看 Ingress 是否匹配请求
kubectl get ingress -n <namespace>
kubectl describe ingress <ingress-name> -n <namespace>
# 第 2 步:看对应 Service 是否存在
kubectl get svc <service-name> -n <namespace>
# 第 3 步:看 Service 的 Endpoints 是否有 Pod
kubectl get endpoints <service-name> -n <namespace>
# 如果 ENDPOINTS 列显示 <none>,说明没有 Pod 被 Service 选中
# 第 4 步:看 Service 的 selector 和 Pod 的 label 是否匹配
kubectl get svc <service-name> -o yaml | grep -A 5 selector
kubectl get pods --show-labels -n <namespace>
# 第 5 步:看 Pod 是否 Ready
kubectl get pods -n <namespace>
# 看 READY 列是不是 1/1
# 第 6 步:看应用日志和容器内端口
kubectl logs <pod-name> -n <namespace>
kubectl exec <pod-name> -n <namespace> -- ss -lntp任一层断了用户都看到 502。按 K8s 对象层次逐层排查,而不是从应用日志一头扎进去。
容器化引入的新故障类别
| 故障类别 | 现象 | 根因 |
|---|---|---|
| 资源限制不当 | Pod 被 OOMKilled | memory limit 太小,应用内存超出 |
| CPU throttling | 应用响应慢 | CPU limit 太小,被 cgroup 节流 |
| 健康检查配错 | Pod 不停重启 | livenessProbe 路径错或超时太短 |
| 镜像太大 | 滚动更新慢 | 镜像几 GB,每次拉取耗时长 |
| 镜像层重复构建 | CI 慢 | Dockerfile 顺序不合理,缓存失效 |
| 网络策略阻断 | Pod 间不通 | NetworkPolicy 限制了流量 |
| DNS 解析失败 | 应用启动时找不到依赖 | CoreDNS 出问题、上游 DNS 异常 |
七、容器化不是银弹
容器和 K8s 解决了大量传统部署问题,但也引入了新的复杂度:
| 收益 | 代价 |
|---|---|
| 环境一致性 | 学习成本陡(K8s 概念多) |
| 资源利用率高 | 排查链路变长(对象多) |
| 滚动发布、自愈 | 监控/日志/链路追踪都要重做 |
| 弹性扩缩容 | 运维门槛上升 |
| 跨云迁移容易 | 网络模型(CNI)、存储模型(CSI)的复杂度 |
| 微服务的良好支撑 | 资源边界限制需要小心调优 |
容器化适合的场景:
- 业务模块多、需要独立部署的微服务架构
- 流量波动大、需要弹性扩缩容
- 多环境一致性要求高
- 团队有足够的运维投入
不适合直接上容器/K8s 的场景:
- 极小规模(几台机器、单体应用)
- 团队缺乏 K8s 经验且短期无投入
- 业务需求高度依赖某些底层特性(GPU、特殊硬件、高性能网络)
- 数据库等强状态服务直接迁移上 K8s 风险较高
先把单机部署或简单容器部署做好,再考虑 K8s——跳过中间阶段直接上最复杂的方案,会引入大量不必要的复杂度。