引言

你照着教程跑了一个 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 用户。

中间隔着四层抽象,和一次心态的彻底转换。

参考资料