从单机容器到集群编排
Docker 解决的是「把一个应用打包成标准镜像」的问题;当你有几十上百台机器、几百个容器要一起跑时,谁来决定哪个容器跑在哪台机器上、挂了谁来拉起、升级时谁先下谁先上,就是编排工具要解决的事。Kubernetes(社区习惯叫 K8s,把中间 8 个字母缩写成数字 8)是这个领域的事实标准。
它的核心思路和 Ansible 一脉相承:你不直接指挥某个容器,而是向集群提交一份「期望状态」,比如「这个应用要跑 3 个副本」,K8s 的控制平面会不断对比现状和期望,自动把集群往期望状态拉。机器挂了,它在别处补一个新的;版本升级了,它按节奏一个一个换。
单机 Docker 是你自己照看一盆花;K8s 是一座温室,你只说要多少株、要长什么样,剩下的它自己维护。
- Pod集群调度的最小单位,一个或一组紧耦合容器的壳。
- Deployment声明要跑几个副本、用哪个镜像,由它管滚动升级。
- Service给一组会漂移的 Pod 一个固定的访问入口和负载均衡。
- 探针告诉集群这个容器是不是真的健康,挂了自动重建。
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 一执行,滚动升级和回滚就全部交给集群了。回滚不再是「重新打包老版本再部署一遍」的体力活,而是一次版本指针的回退——这就是声明式编排相对脚本部署最大的省心之处。
探针与自愈:让集群自己修自己
容器进程活着,不代表它能正常服务——可能死锁了、可能依赖连不上了、可能卡在启动初始化。K8s 用三类探针来区分这几种状态,把下面这段加进 Deployment 的容器定义里:
livenessProbe:
httpGet:
path: /healthz
port: 80
initialDelaySeconds: 10
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: 80
periodSeconds: 5
三种探针各司其职:livenessProbe(存活探针)连续失败就重启这个容器;readinessProbe(就绪探针)没通过时,把这个 Pod 从 Service 的后端名单里摘下来,不发流量给它,通过了再放回去;startupProbe(启动探针)专门给启动慢的老应用兜底,在它起来之前不做存活探测,避免慢启动被误杀。
这套机制串起来就是自愈:一个 Pod 卡死了,存活探针发现、重启它;重启还没好,就绪探针先把它的流量摘掉;真起不来被节点驱逐,Deployment 再在别处补一个新 Pod。你不用写任何「挂了就重启」的脚本,把探针配好,集群自己会处理。
一份能跑的最小编排清单
落到上手的最小工程,一个应用通常就两份 YAML——Deployment 加 Service,外加一个对外的 Ingress:
k8s-manifests/
├── base/
│ ├── deployment.yaml # 镜像、副本数、resources、探针
│ ├── service.yaml # ClusterIP,内部固定入口
│ └── ingress.yaml # 域名路由到这个 Service
└── overlays/
├── dev/ # 测试环境:副本数 1
└── prod/ # 生产环境:副本数 3、资源翻倍
几个新人最容易踩的坑,提前记下来:第一,所有有状态的东西(数据库、文件上传)别图省事直接跑进容器,先用云盘或外部数据库扛,再谈上 Operator;第二,资源 requests/limits 一定要设,否则一个失控的应用能拖垮整台节点;第三,探针路径要真的能反映健康,别拿首页当就绪检查。
K8s 的概念(Namespace、ConfigMap、Secret、HPA、DaemonSet……)乍看很多,但日常一个普通业务应用,真正高频用到的就今天这四个:Pod、Deployment、Service、探针。先把这四个用熟,能把一个无状态应用稳稳跑在集群上,再去补剩下的概念,比一上来啃完全部文档有效得多。