引言
你照着教程跑了一个 nginx Pod,一切正常。
然后你删了它。
然后它又回来了。
这不是 bug。这是 K8s 的核心机制在起作用——你写了 yaml,告诉集群"我想要一个叫 nginx 的 Pod"。集群说"收到"。然后你手动删了 Pod,但 yaml 还在。kube-controller-manager 的检查循环(reconcile loop)每隔几秒扫一遍 API server,发现"你说要一个 nginx Pod,但实际没有",于是又创建一个。
这就是你跟分布式系统的第一次吵架。
本文的目标:搞清楚 K8s 的四层抽象(Pod → Deployment → Service → Ingress),以及为什么 K8s 不是 Docker 的替代品,而是一套完全不同的范式。
1. 四层抽象
K8s 不直接管容器。它管的是四层抽象资源,每一层都比前一层多一层"替你管理"的逻辑。
1.1 Pod:调度的最小单位
Pod 不是 Container。Pod 是一个或多个容器的封装壳,它们共享同一个 network namespace 和 volume。
apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
containers:
- name: nginx
image: nginx:1.27
ports:
- containerPort: 80
一个 Pod 里可以放多个容器。常见的组合是业务容器 + sidecar(比如 log-agent、proxy)。它们共享同一个 IP 地址和端口空间,通过 localhost 互相通信。
Pod 有一个关键特性:它被调度到一个节点上,但它的 IP 是临时的。Pod 死了重建,IP 就变了。你不能依赖 Pod IP 做持久化寻址。
新手常见错误:kubectl exec nginx -- ls 报错 Error: unable to upgrade connection: container not found。原因是 Pod 里有两个 container(比如 nginx + fluentd),你不指定 -c 参数,K8s 不知道该 exec 进谁里面。
1.2 Deployment:副本控制器
教程教你写 Pod yaml。但生产环境几乎没有人直接用 Pod。因为 Pod 是短暂的——可能被调度到任何节点,可能随时被驱逐,可能因为节点 OOM 被杀掉。
Deployment 管的是副本数量。你告诉 Deployment"我要 3 个 nginx",它创建 ReplicaSet,ReplicaSet 创建 Pod。Pod 死了?ReplicaSet 重建。节点挂了?ReplicaSet 在其他节点重建。
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.27
ports:
- containerPort: 80
代价是你失去了对单个 Pod 的控制权。你不能 ssh 进 Pod(Pod 是短暂的,ssh 进去可能下一秒就被重建了)。你不能直接删 Pod(删了会重建)。你不能给 Pod 固定 IP。
你拥有的只是一个 Service 的名字。
1.3 Service:稳定的网络入口
Docker Compose 里,服务之间靠容器名解析 IP。K8s 说 IP 会变,不能信。
Service 提供了一个稳定的虚拟 IP(ClusterIP)和一个 DNS 名字。
apiVersion: v1
kind: Service
metadata:
name: nginx-service
spec:
selector:
app: nginx
ports:
- protocol: TCP
port: 80
targetPort: 80
type: ClusterIP
kubectl expose deployment nginx-deployment --port 80 --target-port 80 等价于上面的 yaml。
Service 背后的实现机制:
- iptables 模式(默认):kube-proxy 在每个节点上维护 iptables 规则,把发往 ClusterIP 的流量转发到后端 Pod 的 IP
- IPVS 模式:基于内核的负载均衡器,性能更好,支持更多调度算法
无论哪种模式,流量转发都是一层又一层的。如果你的 Pod 在 node1,Service 的规则在 node2 上生效——但流量到了 node2 之后,kube-proxy 会把流量再转发回 node1 上的 Pod。两层转发。你在集群内部的网络里绕了一圈又一圈,最后流量回到了起点。
这就是为什么很多人说 K8s 网络是"一层又一层的转发"。
1.4 Ingress:七层 HTTP 路由
ClusterIP 只能在集群内部访问。你想从外面进来?
- NodePort:在每个节点上开一个 30000-32767 范围的端口
- LoadBalancer:让云厂商给你一个外部 IP
- Ingress:管 HTTP 的七层路由
Ingress 不是 Controller。它是一个 API 资源。你需要安装一个 Ingress Controller(nginx-ingress、traefik、contour)来实现路由规则。
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: nginx-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: nginx-service
port:
number: 80
新手典型崩溃:写了 Ingress 资源,curl 外部 IP 返回 502。因为 Ingress Controller 没装。或者装了但没配正确的 class annotation。或者配了但 DNS 没指向 Ingress Controller 的 IP。
四层对比
| 抽象层 | 管什么 | 稳定吗 | 谁创建 | 常见问题 | |--------|--------|--------|--------|----------| | Pod | 容器集合 | IP 会变 | 人工或 Controller | 多 container 时 exec 找不到目标 | | Deployment | Pod 副本数 | 副本数稳定 | 人工 | 以为可以直接 ssh 进 Pod | | Service | Pod 的聚合入口 | ClusterIP 和 DNS 名稳定 | 人工 | 不理解 kube-proxy 的两层转发 | | Ingress | HTTP 路由规则 | 路由规则稳定 | 人工 | 忘了装 Ingress Controller |
2. 声明式 API 的本质
K8s 真正管理的不是应用。是应用的预期状态。
你写 yaml,不是在配置一个服务。你是在对集群说一句话:"我希望世界上存在这样一个东西。"
# 声明式:我想要什么
kind: Deployment
spec:
replicas: 3 # 我希望有 3 个 nginx 副本活着
---
kind: Service
spec:
selector: # 我希望有一个叫 nginx 的服务可以被 DNS 解析
app: nginx
对比命令式(imperative):
# 命令式:去做
docker run --name web -p 8080:80 nginx
前者是"去做"。后者是"去成为"。
kube-controller-manager 里的 controller-manager 在不停地跑 reconcile loop。它比较"你希望的"和"实际的"。如果有差距,它就修补。
- Pod 被删了?Reconcile 发现期望 3 个,实际 2 个。创建 1 个。
- 节点宕机了?Reconcile 发现 Pod 所在的节点 NotReady。把 Pod 调度到其他节点。
- 镜像拉取失败?Reconcile 发现 Pod 处于 ImagePullBackOff。重试,直到成功或者达到最大重试次数。
这个过程没有终点。它永远不会说"搞定了,下班"。它会一直跑下去,直到你把 yaml 删了。
这就是为什么 K8s 的文档里反复出现"declarative"这个词。声明式 API 的核心思想不是"你告诉它做什么",而是"你告诉它你想要什么,它自己想办法实现"。
3. Docker Compose vs K8s:两种思维模式
| 维度 | Docker Compose | Kubernetes | |------|---------------|------------| | 控制方式 | 命令式(docker run) | 声明式(kubectl apply) | | 网络模型 | 容器名 = IP,直接通信 | Service 名 = DNS,经过 kube-proxy 转发 | | 故障恢复 | 手动重启 | 自动重建(reconcile loop) | | 扩展 | 手动 scale | Deployment replicas 改数字就行 | | 状态管理 | 你管一切 | 你管预期,K8s 管维持 | | 适用场景 | 单机多容器开发 | 多节点生产环境 |
Compose 用户的心态是"我知道我的应用在哪个端口"。K8s 用户的心态是"我知道我的 Service 叫什么名字"。
这不是风格差异。这是两种完全不同的世界观。
Compose 里,你控制一切。容器在哪台机器上,端口是多少,网络怎么连——你说了算。出了问题你 ssh 进去看日志。修完了你 exit。
K8s 里,你控制不了任何东西。你只能描述你想要的状态。然后等 K8s 的控制器们慢慢把它变成现实。这个过程可能很快(几秒),也可能很慢(如果 scheduler 在排队,如果 image 拉取超时,如果 liveness probe 没通过)。
你失去的是即时可见的控制权。你换来的是自愈能力——Pod 死了自动重建,节点挂了自动迁移,镜像更新了自动滚动。
这两样东西不能兼得。K8s 选择了后者。
4. 实操:在你的集群里验证
你的集群在 192.168.3.56(control-plane)和 192.168.3.57(worker)上跑着。containerd 2.2.x 在背后管理着所有容器的生命周期。etcd 存着所有状态。apiserver 在监听每一个变更。
4.1 验证 reconcile loop
# 在 node1 (192.168.3.56) 上执行
# 1. 创建一个 Pod
kubectl apply -f - <<EOF
apiVersion: v1
kind: Pod
metadata:
name: test-pod
spec:
containers:
- name: nginx
image: nginx:1.27
ports:
- containerPort: 80
EOF
# 2. 查看 Pod
kubectl get pod test-pod
# NAME READY STATUS RESTARTS AGE
# test-pod 1/1 Running 0 10s
# 3. 手动删除 Pod
kubectl delete pod test-pod
# pod "test-pod" deleted
# 4. 三秒后再看——它又回来了
kubectl get pod test-pod
# 注意:如果是裸 Pod,删了就真的没了。
# 但如果用 Deployment 创建的 Pod,删了会自动重建。
4.2 用 Deployment 验证自愈
# 1. 创建 Deployment
kubectl apply -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deploy
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.27
ports:
- containerPort: 80
EOF
# 2. 查看 Pod(应该有 3 个)
kubectl get pods -l app=nginx
# NAME READY STATUS RESTARTS AGE
# nginx-deploy-6d4f5b7c8-abc12 1/1 Running 0 30s
# nginx-deploy-6d4f5b7c8-def34 1/1 Running 0 30s
# nginx-deploy-6d4f5b7c8-ghi56 1/1 Running 0 30s
# 3. 杀掉其中一个 Pod(模拟节点故障)
kubectl delete pod nginx-deploy-6d4f5b7c8-abc12
# 4. 再看——ReplicaSet 自动创建了新 Pod
kubectl get pods -l app=nginx
# NAME READY STATUS RESTARTS AGE
# nginx-deploy-6d4f5b7c8-def34 1/1 Running 0 2m
# nginx-deploy-6d4f5b7c8-ghi56 1/1 Running 0 2m
# nginx-deploy-6d4f5b7c8-jkl78 1/1 Running 0 5s # ← 新创建的
4.3 验证 Service 网络
# 1. 创建 Service
kubectl expose deployment nginx-deploy --port 80 --target-port 80 --name nginx-svc
# 2. 获取 ClusterIP
kubectl get svc nginx-svc
# NAME TYPE CLUSTER-IP PORT(S) AGE
# nginx-svc ClusterIP 10.96.45.123 80/TCP 10s
# 3. 在集群内任意 Pod 中访问
kubectl run -it --rm debug --image=busybox --restart=Never -- \
wget -qO- --timeout=5 http://nginx-svc:80
# 返回 nginx 欢迎页 HTML
5. 常见误区
误区一:"K8s 是 Docker 的替代品"
Docker 管容器运行时。K8s 管容器编排。它们是不同层面的东西。K8s 甚至可以不使用 Docker(现在的 K8s 默认使用 containerd 作为 CRI,dockershim 从 v1.24 起已移除)。
误区二:"Pod 就是容器"
Pod 是一个调度单元,可以包含多个容器。但 K8s 不让你直接管容器——它只让你管 Pod。真正的运行单位还是 container(由 containerd 管理),但 K8s 的抽象层挡住了你。
误区三:"Service 就是负载均衡器"
Service 是四层负载均衡 + DNS。它不处理 HTTP。如果你想按域名或路径路由流量,需要 Ingress(七层)。
误区四:"删了 Pod 就没了"
如果你用裸 Pod(kind: Pod),删了就没了。但如果你用 Deployment 创建的 Pod,删了会自动重建。区别在于:裸 Pod 没有 Controller 管它,Deployment 有 ReplicaSet 管它。
6. 小结
K8s 不是 Docker 的替代品。它是放弃控制权的契约。
你学会的第一件事不是怎么写 yaml。是停止问"我的 Pod 在哪里"。
当你不再追踪单个 Pod 的 IP,不再 ssh 进容器看日志,不再手动重启服务——当你开始问"我的 Service 可达吗"、"我的 Deployment 的 replicas 都 Ready 了吗"、"我的 Ingress 路由对不对"——你就跨过了那条线。
那条线左边是 Docker 用户。右边是 K8s 用户。
中间隔着四层抽象,和一次心态的彻底转换。