从单机容器到集群编排

Docker 解决的是「把一个应用打包成标准镜像」的问题;当你有几十上百台机器、几百个容器要一起跑时,谁来决定哪个容器跑在哪台机器上、挂了谁来拉起、升级时谁先下谁先上,就是编排工具要解决的事。Kubernetes(社区习惯叫 K8s,把中间 8 个字母缩写成数字 8)是这个领域的事实标准。

它的核心思路和 Ansible 一脉相承:你不直接指挥某个容器,而是向集群提交一份「期望状态」,比如「这个应用要跑 3 个副本」,K8s 的控制平面会不断对比现状和期望,自动把集群往期望状态拉。机器挂了,它在别处补一个新的;版本升级了,它按节奏一个一个换。

单机 Docker 是你自己照看一盆花;K8s 是一座温室,你只说要多少株、要长什么样,剩下的它自己维护。
本文核心知识点
  • Pod集群调度的最小单位,一个或一组紧耦合容器的壳。
  • Deployment声明要跑几个副本、用哪个镜像,由它管滚动升级。
  • Service给一组会漂移的 Pod 一个固定的访问入口和负载均衡。
  • 探针告诉集群这个容器是不是真的健康,挂了自动重建。
Kubernetes 容器编排配图:中央主控节点连线连接多组集装箱式容器方块集群的蓝色扁平科技插画
控制面只负责一件事:不断把集群现状,拉回到你声明的那个期望状态。

Pod:调度的最小单位

K8s 不直接跑容器,它跑 Pod——可以理解为「一组共享网络和存储的容器的小公寓」。绝大多数情况下一个 Pod 里只放一个主容器;只有像「应用 + 日志 sidecar 收集器」这种必须同生共死、共享本地文件的场景,才往一个 Pod 里塞多个容器。

Pod 是临时的:它随时可能因为节点故障、资源不足、滚动升级被销毁重建,新 Pod 会拿到新的 IP。这个特性决定了后面两件事——我们不能把任何状态存在 Pod 本地磁盘里,也不能靠 Pod IP 去互相访问。下面是一个最小 Pod 清单:

apiVersion: v1
kind: Pod
metadata:
  name: demo-pod
  labels:
    app: demo
spec:
  containers:
  - name: app
    image: nginx:1.25
    ports:
    - containerPort: 80

日常工作里你几乎不会手动创建单个 Pod——单个 Pod 挂了没人帮你重建。真正管 Pod 的是下一节的 Deployment。理解 Pod 的意义在于:它是被调度、被重建、被替换的最小单位,所有编排动作最终都落在它身上。

Deployment:声明期望副本数

Deployment 是你最常打交道的对象。它一句话说清楚三件事:用哪个镜像、跑几个副本、怎么升级。下面是一份可直接用的 Deployment:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
spec:
  replicas: 3
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
      - name: web
        image: nginx:1.25
        resources:
          requests:
            cpu: "100m"
            memory: "128Mi"
          limits:
            cpu: "500m"
            memory: "256Mi"

几个关键点:replicas: 3 表示永远维持 3 个 Pod,少一个就补一个;selector 用来告诉 Deployment 「哪些 Pod 归我管」;resources 给每个 Pod 划定 CPU 与内存的请求和上限,不设 resources 的集群早晚会互相抢资源打到雪崩,这是新人最容易漏的一步。

改镜像版本时,只需把 image: nginx:1.25 改成新版本再 kubectl apply,Deployment 就会自动开始滚动替换,而不是一次性把旧的全停掉。

Service:给一组 Pod 一个稳定入口

Pod 的 IP 会随重建漂移,那别的服务怎么找到它们?答案是 Service:它按标签选中一组 Pod,给它们一个固定的虚拟 IP 和 DNS 名字,内部还自带一层负载均衡。

apiVersion: v1
kind: Service
metadata:
  name: web-svc
spec:
  selector:
    app: web
  ports:
  - port: 80
    targetPort: 80
  type: ClusterIP

集群里别的服务只要访问 http://web-svc,请求就会被自动分发到后端那 3 个 Pod 上,它后面的 Pod 换了几轮 IP,调用方完全无感知。常见的三种类型:ClusterIP(只在集群内部可见,默认)、NodePort(在每台节点上开一个端口对外暴露)、LoadBalancer(对接云厂商的外部负载均衡器)。对外暴露的入口通常再叠一层 Ingress 做域名和路由,把多个服务收敛到一个统一入口。

对象管什么类比
Pod一个运行中的容器实例一盆花
Deployment维持副本数、管升级花圃的园丁
Service给这组 Pod 固定入口花圃的统一门牌

滚动发布与一键回滚

更新镜像时,Deployment 默认的滚动策略是:先起一个新版本 Pod,等它就绪,再停一个旧版本 Pod,如此往复直到全部换完,全程不整体停服。这个节奏由两个参数控制:maxSurge(升级时最多比期望多出几个副本)和 maxUnavailable(升级时最多允许几个副本不可用)。把后者设成 0,就能保证升级期间可用副本数不下降。

每一次 apply 都会留下一个 Revision(版本记录)。新版本上线后出问题,一条命令就能回到上一版:

# 查看这个 Deployment 的历次发布记录
kubectl rollout history deployment/web

# 一键回滚到上一版
kubectl rollout undo deployment/web

# 也可以指定回到某个历史版本
kubectl rollout undo deployment/web --to-revision=3

# 实时观察这次升级的进度
kubectl rollout status deployment/web

配合上一轮讲的 CI/CD 流水线,镜像推完、apply 一执行,滚动升级和回滚就全部交给集群了。回滚不再是「重新打包老版本再部署一遍」的体力活,而是一次版本指针的回退——这就是声明式编排相对脚本部署最大的省心之处。