Skip to content

第 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 节点上的容器运行时主流是 containerdCRI-O——这两者都符合 CRI 标准,不需要 Docker。

但 Docker 没有过时:

  • 开发者本地构建镜像仍然主要用 docker build
  • 镜像格式与运行时格式(OCI 标准)是兼容的,Docker 构建的镜像能在 K8s 上运行
  • 简单单机部署、CI/CD 流水线中仍大量使用 Docker

OCI:让工具与运行时解耦

OCI(Open Container Initiative) 是容器领域的核心标准,定义了:

  • OCI Image Spec — 镜像格式
  • OCI Runtime Spec — 运行时接口

这一标准让生态可以解耦:

  • 构建工具:Docker、buildahkanikonerdctl 都能构建符合 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 Service Ingress 分别解决运行寻址和入口问题

对象作用
Pod最小运行单元,一个或多个紧密耦合的容器
Deployment管理无状态应用的副本数与滚动更新
StatefulSet有状态应用,Pod 有固定的名字与存储
DaemonSet在每个节点都跑一个 Pod(如日志采集器)
Job / CronJob一次性任务、定时任务
Service给一组 Pod 提供稳定的访问入口
Ingress / GatewayHTTP/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: 80

Ingress 资源本身只是路由规则的声明,实际转发由 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-secret

Secret 默认只是 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容器反复崩溃重启应用启动报错、配置缺失、健康检查配错
RunningReady=False容器在跑但没就绪readinessProbe 失败、依赖未就绪
RunningReady=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 里要分多层排查:

K8s 中 502 排查要看 Ingress Service Endpoints 和 Pod Ready

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 被 OOMKilledmemory 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——跳过中间阶段直接上最复杂的方案,会引入大量不必要的复杂度。