Skip to content
Charles Shao
Go back

Kubernetes 核心对象 · Pod/Deployment/Service/Ingress

views

上一篇我们把容器拆到了内核层——namespace、cgroup、镜像分层。但单机 docker run 起几个容器,离”上生产”还差着十万八千里:几百个容器怎么调度到几十台机器上?挂了怎么自愈?升级怎么不停服?流量怎么找到不断被重建、IP 一直变的容器? 这些”编排”问题,就是 Kubernetes 要解决的。

这一篇不堆 YAML 字段手册,而是把 K8s 最核心的一组对象——Pod、Deployment、Service、Ingress、ConfigMap/Secret——以及它们背后那套声明式 + reconcile 控制循环的心智讲透。理解了这套心智,你看任何 K8s 资源都是同一个套路。

本文是容器化与 K8s 部署系列的第 2 篇。 全系列 5 篇:

  1. 容器化(开篇)· Docker 原理与镜像分层
  2. Kubernetes 核心对象 · Pod/Deployment/Service/Ingress(本篇)
  3. K8s 调度、资源与弹性伸缩
  4. CI/CD 流水线 · Jenkins 构建镜像与自动部署到 K8s
  5. 广告服务上云实战 · 容器化部署与调优

一句话定位:Kubernetes 是一个”声明式的期望状态系统”——你用 YAML 声明”我想要什么”(3 个副本、这个镜像、暴露这个端口),一堆控制器不停地把集群的实际状态朝你的期望状态收敛;Pod/Deployment/Service 就是这套系统里最核心的几种”期望”。

TL;DR

Table of contents

Open Table of contents

1. 声明式与 reconcile:理解 K8s 的第一心智

学 K8s 最容易犯的错,是拿”命令式脚本”的脑子去套。命令式是:启动容器 → 绑端口 → 挂载盘 → 如果挂了我再写脚本重启K8s 是声明式的:你交给它一份 YAML,说”我要 3 个跑 bid-service:v2 镜像的副本,对外暴露 8080”,然后就不管了——剩下的”怎么达成、挂了怎么补、机器没了怎么重调度”,全是 K8s 的事。

这套魔法的核心叫 reconcile(调谐/协调)控制循环

每种资源都有 spec(期望状态,你写的)status(实际状态,系统回写的)。对应的控制器(controller) 就是一个死循环:不停地读期望、读实际、对比差异、执行动作把实际朝期望推,直到两者一致(收敛)。

Pod 挂了?副本数从 3 掉到 2,ReplicaSet 控制器发现”期望 3、实际 2”,立刻补一个。节点宕机?上面的 Pod 没了,控制器在别的节点重建。你声明的是一个”目标”,系统自己想办法维持这个目标——这就是 K8s 自愈能力的来源。记住这一条,后面所有对象都是这个模式的实例。

2. 集群架构:控制面与工作节点

K8s 集群分成两类角色:控制面(Control Plane,大脑)工作节点(Worker Node,干活的)

Kubernetes 集群架构图。上方紫色面板是 Control Plane 控制面(集群大脑),横向排列四个组件:kube-apiserver(唯一入口·REST,所有组件只与它对话)、etcd(集群状态存储,唯一真相来源基于 Raft)、scheduler(把 Pod 绑定到合适的 Node)、controller-manager(各类控制器,reconcile 期望到实际)。下方两个工作节点面板 Worker Node 1、Worker Node 2,每个节点内含:kubelet(管本节点 Pod 生命周期)、kube-proxy(维护 Service 转发规则)、容器运行时 containerd(CRI),以及两个 Pod(容器 + sidecar)。控制面的 apiserver 用虚线箭头连到两个节点,标注 kubelet 注册/汇报·watch。底部说明所有组件都只跟 kube-apiserver 通信、绝不互相直连,集群唯一持久状态在 etcd;控制面负责决策与编排,节点上的 kubelet 负责把决策落成真正运行的容器。

控制面四大件(apiserver/etcd/scheduler/controller-manager)负责编排决策,节点上的 kubelet/kube-proxy 负责把决策落成运行的容器和转发规则。所有组件只跟 apiserver 对话。

2.1 控制面组件

2.2 工作节点组件

2.3 一条铁律:万物经 apiserver

初学者常画错的架构图是”scheduler 直接指挥 kubelet""controller 直接改 etcd”。都不对。真相是:

所有组件都只跟 apiserver 通信,通过 watch/list 感知变化、通过写资源表达意图,彼此之间从不直连。 apiserver 是唯一的读写枢纽,etcd 只被 apiserver 独占访问。

这个设计的好处是解耦和可扩展:任何新组件(自定义控制器、Operator)只要会调 apiserver、会 watch 资源,就能无缝接入这套体系——这也是 K8s 生态如此繁荣的架构基础。

3. 一次 kubectl apply 发生了什么

把声明式和 reconcile 串起来,走一遍 kubectl apply -f deployment.yaml(期望 3 副本)的完整链路:

一次 kubectl apply 的 reconcile 时序图。六个参与者纵向泳道:kubectl、apiserver、etcd、Deployment Controller、scheduler、kubelet。时序步骤:1. kubectl 向 apiserver 提交 apply deployment.yaml(期望 3 副本);2. apiserver 把期望状态 spec 写入 etcd;注释(声明式):你只描述想要什么,不写怎么做;3. Deployment Controller watch 到新 Deployment,对比实际=0 小于期望=3;4. Controller 自身动作:创建 ReplicaSet 并生成 3 个未调度的 Pod 对象;5. scheduler watch 到未绑定 Pod,打分选 Node 后绑定;6. kubelet watch 到本节点被分配的 Pod;7. kubelet 自身动作:调 CRI 拉镜像、起容器;8. kubelet 回写实际状态 status 为 Running;注释(reconcile 循环):控制器持续对比期望 spec 与实际 status,有差就修直到收敛。

声明式提交期望 → 各控制器 watch 到差异 → 创建 Pod、调度、拉起容器、回写状态。整个过程没人被”命令”,都是各自 watch + reconcile 出来的。

  1. 提交期望kubectl apply 把 Deployment 的 spec(3 副本、镜像、端口)发给 apiserver,apiserver 校验后写进 etcd。到这一步,集群里其实还没有任何容器——你只是登记了一个”愿望”。
  2. Deployment 控制器补齐 Pod:Deployment 控制器 watch 到这个新对象,对比”期望 3 / 实际 0”,于是创建一个 ReplicaSet;ReplicaSet 控制器再创建 3 个 Pod 对象(此时 Pod 还没被调度到任何节点,处于 Pending)。
  3. scheduler 绑定节点:scheduler watch 到这些”未绑定节点的 Pod”,给每个 Pod 打分选出合适的节点,把绑定关系写回 apiserver。
  4. kubelet 拉起容器:对应节点的 kubelet watch 到”有 Pod 分给我了”,调用 CRI 拉镜像、创建容器,Pod 真正跑起来。
  5. 回写状态:kubelet 把 Pod 的实际状态(Running)回写给 apiserver → etcd。此刻”期望 3 / 实际 3”,收敛完成。
  6. 持续维持:之后控制器仍在死循环里盯着——任何一个 Pod 挂了,实际掉到 2,立刻再补一个。这就是声明式系统的自愈。

注意整个过程里没有任何组件”命令”另一个组件。Deployment 控制器没有”告诉” scheduler 去调度,scheduler 也没”通知” kubelet;大家都只是各自 watch apiserver 上的资源变化,做自己那一步 reconcile。这种”基于共享状态的松耦合协作”是 K8s 架构的精髓。

4. 工作负载对象:Pod、ReplicaSet、Deployment

4.1 Pod:最小调度单位

Pod 是 K8s 调度和管理的最小单位,而不是容器。一个 Pod 里可以有一个或多个容器,它们:

典型的”多容器 Pod”是主容器 + sidecar:主容器跑业务,sidecar 跑日志采集、服务网格代理(Envoy)、指标导出等。

最关键的认知Pod 是”牲畜不是宠物”(cattle, not pets)。它随时可能因为节点故障、滚动更新、扩缩容被杀掉重建,重建后 IP 会变、名字会变。所以你永远不该记住某个 Pod 的 IP 去访问它——这正是需要 Service 的原因(§5)。也因此,Pod 应尽量无状态(状态沉到外部存储),才敢被随意重建。

4.2 ReplicaSet:维持副本数

直接创建的裸 Pod(bare pod)没人管——挂了就没了,不会重建。ReplicaSet 的职责就是维持指定数量的 Pod 副本:声明 replicas: 3,它就通过 reconcile 保证”任何时刻都有 3 个匹配的 Pod 在跑”,多了删、少了补。

4.3 Deployment:管版本与滚动更新

但你几乎不会直接创建 ReplicaSet——你用 Deployment。Deployment 在 ReplicaSet 之上加了版本管理和发布策略

apiVersion: apps/v1
kind: Deployment
metadata:
  name: bid-service
spec:
  replicas: 3
  selector:
    matchLabels: { app: bid-service }     # 管理哪些 Pod(靠 label 匹配)
  template:                                # Pod 模板
    metadata:
      labels: { app: bid-service }         # 必须与上面 selector 匹配!
    spec:
      containers:
        - name: bid
          image: registry/bid-service:v2
          ports: [{ containerPort: 8080 }]

它的核心能力:

滚动更新、金丝雀、优雅停机的深水区留到末篇实战 结合 发布与变更安全 展开。层级关系记住一句:Deployment 管 ReplicaSet,ReplicaSet 管 Pod。

5. Service 与 Ingress:流量怎么找到 Pod

Pod 的 IP 会漂、会随重建变化,那外部或其他服务怎么稳定地访问它?答案是 Service——K8s 里最容易出问题、也最值得讲透的对象。

Service 与 Ingress 流量路径图。主链路自左向右:外部用户(HTTP/S)→ Ingress(L7 域名/路径路由 + TLS 终止)→ Service ClusterIP(稳定虚拟 IP + DNS,kube-proxy 负载均衡),Ingress 到 Service 的箭头标注按 host/path。Service 再扇出到三个 Pod(10.1.0.1 / 10.1.0.2 / 10.1.0.3),箭头标注按 Endpoints 分发,并注明 Endpoints 随 Pod 增删自动更新。下方 slate 面板"Service 三种暴露方式(由内到外)"横向列出:ClusterIP(默认,仅集群内部可达,内部服务间调用)、NodePort(每个节点开一个端口,外部经节点IP:端口进入)、LoadBalancer(云厂商 LB + 外部 IP,生产对外入口)、Ingress(非 Service 类型,一个 LB 后按 L7 规则分流到多个 Service)。底部说明 Pod 是牲畜不是宠物、IP 随时变,Service 提供稳定虚拟 IP + DNS 屏蔽后端漂移;Ingress 在 Service 之上做 L7 路由和 TLS 终止,是灰度/金丝雀分流的挂载点。

Service 用 label selector 动态关联一组 Pod、提供稳定的虚拟 IP + DNS;Ingress 在其上做 L7 路由。Pod 增删时 Endpoints 自动更新,前端流量无感。

5.1 Service:稳定入口 + 动态后端

Service 做两件事:

  1. 提供一个稳定的虚拟 IP(ClusterIP)和 DNS 名(如 bid-service.default.svc.cluster.local),这个 IP/名字在 Service 生命周期内永不变
  2. 用 label selector 动态关联一组后端 Pod:Service 声明 selector: {app: bid-service},K8s 就自动把所有 label 匹配的、健康的 Pod IP 收集成一个 Endpoints(EndpointSlice) 列表。Pod 增删/重建时,Endpoints 自动更新——前端始终访问 Service 的稳定 IP,kube-proxy 负责把流量负载均衡到当前健康的后端 Pod。
apiVersion: v1
kind: Service
metadata:
  name: bid-service
spec:
  selector:
    app: bid-service          # ← 必须匹配 Pod 的 label,否则 Endpoints 为空!
  ports:
    - port: 80                # Service 端口
      targetPort: 8080        # 转发到 Pod 的端口

selector 与 Pod label 必须匹配——这是 §8 生产事故的核心。selector 写错一个字,Endpoints 就是空的,流量进 Service 后无处可去,全部黑洞,而 Pod 本身完全健康。

5.2 三种 Service 类型

类型作用典型用途
ClusterIP(默认)只在集群内部可达的虚拟 IP微服务之间互相调用
NodePort在每个节点上开一个端口(30000–32767),外部经 节点IP:端口 访问简单对外暴露、测试
LoadBalancer云厂商创建一个外部 LB + 公网 IP,指向后端生产对外入口

5.3 Ingress:L7 路由与 TLS 终止

如果每个对外服务都开一个 LoadBalancer,成本和管理都爆炸。Ingress一个入口 LB 后面,按 L7 规则(域名 / 路径) 把流量分流到不同的 Service:

它还统一做 TLS 终止(证书集中管理)。Ingress 本身只是一份”路由规则”,真正干活的是 Ingress Controller(Nginx/Traefik/云原生网关)。它也是灰度/金丝雀按权重分流的天然挂载点(呼应 发布与变更安全,更细的流量切分可交给 Service Mesh)。

6. 配置与隔离:ConfigMap、Secret、Namespace

好的容器实践是镜像不可变、配置外置——同一个镜像跑在 dev/staging/prod,只是注入的配置不同。K8s 用两个对象承载配置:

Namespace 则是逻辑隔离的单位:把不同团队、不同环境(dev/prod)、不同项目的资源分到不同 namespace,各自有独立的名字空间、资源配额(ResourceQuota) 和 RBAC 权限边界。DNS 名里的 .default. 那一段就是 namespace。它是软隔离(不是安全边界),但对多团队共用一个集群的组织协作至关重要。

7. 声明式带来的运维范式转变

把前面的对象串起来,K8s 其实重塑了运维心智:

这套范式的代价是心智门槛和复杂度——你得理解声明式、label selector、reconcile,才不会在出问题时抓瞎。下面这个事故就是典型。

参考


views
Share this post on:

Previous Post
K8s 调度、资源与弹性伸缩
Next Post
容器化(开篇)· Docker 原理与镜像分层