本文面向会用 Docker/容器、但尚未系统学习过 Kubernetes 的读者。每一节从一个你已经在头疼的问题出发,引出 K8s 对应的概念与解法。所有技术断言以 Kubernetes v1.33 官方文档为基准。
第一章:从一台机器说起——容器不只是 docker run
1.1 我会用 Docker,还有什么问题?
你已经能熟练地 docker build、docker run、docker compose up。单机上的容器化给你带来了极大的便利:环境一致性、快速启动、镜像复用。
然后业务长大了:
- 你用
docker-compose.yml管理 5 个服务,手动up/down/restart,还行 - 变成 20 个微服务后,compose 文件膨胀到 800 行,依赖关系开始混乱
- 半夜某个容器挂了,你被叫起来重启它
- 流量高峰来了,你要手动
docker run --scale扩容,高峰过去再缩回来 - 你加了两台服务器。问题变成了:哪些容器跑在哪台机器上?它们怎么互相找到彼此?
这些问题不是某个编排工具的 bug——它们是分布式系统固有的复杂性。容器的优点是轻量和可复制,但当你需要在上百个容器、多台机器上维持一个期望的运行状态时,仅凭 docker run 和脚本是撑不住的。
你需要的不是更多脚本,而是一个控制回路:你告诉它"我想要什么",它持续地驱动实际状态朝目标靠拢。
这就是 Kubernetes。
1.2 第一个问题:容器们需要"抱团"——Pod
场景
你有一个 Web 应用容器,还有一个日志收集 agent 容器。它们需要:
- 共享同一个网络(agent 通过
localhost拉取 Web 的日志) - 共享同一个存储卷(Web 写日志文件,agent 读同一文件)
- 同生共死——Web 容器没了,agent 留着也没意义
在裸 Docker 中,你会把它们塞进同一个 docker-compose 的 network_mode: "service:..." 里,或者共享 volume。但这是一个"额外配置"而非"内置语义"。
K8s 的答案是 Pod——最小的调度单元,是一组共享上下文的容器的集合。
┌─────────────────────────────────────┐
│ Pod │
│ ┌──────────┐ ┌──────────────┐ │
│ │ Web │◄───│ Log Agent │ │
│ │ 容器 │ │ (sidecar) │ │
│ └────┬─────┘ └──────┬───────┘ │
│ │ │ │
│ └──── 共享上下文 ──┘ │
│ ┌────────────────────────────────┐ │
│ │ 共享: 网络命名空间 (同一IP) │ │
│ │ 存储卷 │ │
│ │ IPC 命名空间 │ │
│ │ Pod 级 cgroup │ │
│ └────────────────────────────────┘ │
└─────────────────────────────────────┘
Pod 内所有容器共享同一个 IP 地址和端口空间。所以 Web 容器监听 0.0.0.0:8080,agent 容器可以通过 localhost:8080 直接访问。但这也意味着同一个 Pod 内端口不能冲突。
这种模式就是边车模式(Sidecar)——主容器做业务,sidecar 注入辅助能力:
- 日志收集(Fluentd, Filebeat)
- 服务网格代理(Envoy/Istio sidecar)
- 配置热更新
v1.29 起 Sidecar Container 特性:传统上 init 容器在主容器启动前顺序执行并退出。从 v1.29 开始(v1.32 GA),Init Container 可设置
restartPolicy: Always,使 sidecar 在整个 Pod 生命周期内保持运行,且确保在应用容器之前启动——这对服务网格等"代理须先于应用就绪"的场景至关重要。
Pod 的定义(你的第一个 YAML)
| |
要点:
- Pod 是 K8s 中可创建和调度的最小单位——你不能直接调度一个容器,必须通过 Pod
- 同 Pod 容器共享网络和存储,但各自有独立的文件系统和进程空间
- Pod 是易逝的(ephemeral)——它被删除后不会复活,IP 地址也会变
这个"易逝性"正是下一节要解决的问题。
1.3 用标签(Labels)在混乱中建立秩序
Pod 多了之后,你需要一种灵活的方式来组织和选择它们。K8s 使用标签(Labels):
| |
标签是任意的 key-value 对,贴在任意 K8s 对象上。与之配合的是选择器(Selector)——不是通过名字硬编码关联,而是通过标签动态匹配:
Service 通过 selector: {app: nginx} 找到所有带有该标签的 Pod
Deployment 通过 selector 管理属于它的 Pod 和 ReplicaSet
标签是 K8s 中唯一的松耦合粘合剂。你不需要预先注册标签 schema——贴标签、改标签、按标签查询,都是动态的。这就是 K8s 能够在运行时动态组建服务拓扑的基石。
第二章:Pod 不稳定——怎么让服务"稳定地可用"?
2.1 Pod 的 IP 会变,下游怎么办?——Service
现在你通过 Pod 部署了 3 个 nginx 副本。每个 Pod 都有一个 IP(比如 10.244.1.5、10.244.2.7、10.244.1.9)。你的后端应用需要访问 nginx——它该用哪个 IP?
更致命的是:Pod 挂了被重建,新 Pod 的 IP 就变了(比如变成 10.244.3.12)。所有依赖方都得跟着改配置,这在几十个微服务之间根本不可行。
需求很明确:不管后端 Pod 怎么变,前端始终用一个固定的地址访问。K8s 的答案是 Service。
Service 做三件事:
- 给自己申请一个固定的集群内部 IP(叫 ClusterIP)——这个 IP 在 Service 存在期间永远不变
- 通过你在 1.3 节学到的标签选择器,自动找到属于它的 Pod
- 当请求到达 ClusterIP 时,把流量转发到某个健康的后端 Pod
Client Pod
│
│ DNS: nginx → 10.96.0.10 (ClusterIP,永远不变)
▼
┌──────────────────────────────────────┐
│ Service "nginx" │
│ ClusterIP: 10.96.0.10:80 │
│ selector: {app: nginx} │
│ │
│ "只要 Pod 有这个标签,就是我的后端" │
└──────────┬──────────┬───────────────┘
│ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│ Pod A │ │ Pod B │ │ Pod C │
│10.244. │ │10.244. │ │10.244. │
│ 1.5:80 │ │ 2.7:80 │ │ 1.9:80 │
│app:nginx│ │app:nginx│ │app:nginx│
└────────┘ └────────┘ └────────┘
对应的 YAML 很简短:
| |
创建这个 Service 后,K8s 为它分配一个 ClusterIP(例如 10.96.0.10),并持续监控带有 app: nginx 标签的 Pod——有新的自动加入,挂掉的自动剔除。你的应用代码只需要连接 nginx:80(Service 的名字),再也不用关心 Pod 在哪个节点、IP 是什么、是不是刚重启过。
流量具体是怎么从 ClusterIP 到达 Pod 的? 这是每个节点上运行的 kube-proxy 组件负责的——它监听 Service 和 Pod 的变更,在 Linux 内核中写入转发规则。但现在你不必深究:把 Service 理解成一个"自动更新的负载均衡器"就够了。kube-proxy 的细节放在第四章集群架构中展开。
Service 不止 ClusterIP 一种类型。当你需要从集群外部访问服务时(第九章),你会用到 NodePort 和 LoadBalancer;当你需要为每个 Pod 提供独立 DNS 名时(第六章 StatefulSet),你会用到 Headless Service。但现在,记住 ClusterIP 就够了——它就是微服务间通信的核心。
2.2 怎么找到 Service?——DNS 服务发现
Service 创建后,K8s 内置的 DNS 服务器自动为其生成一条记录:
nginx → 10.96.0.10
你的应用代码不需要知道 10.96.0.10 这个 ClusterIP,只需要连接 nginx(Service 名)。K8s 在每个 Pod 的 /etc/resolv.conf 中预设了 DNS 搜索域,使同 Namespace 内的 Pod 直接用 Service 名就能解析。跨 Namespace 则用 nginx.frontend(<service>.<namespace>),全集群全局则用 nginx.frontend.svc.cluster.local(完整域名)。
这个内置 DNS 服务叫 CoreDNS——它是 K8s 集群搭建时自动部署的基础设施组件,对应用完全透明,你不必关心它怎么运行。
2.3 我要 3 个副本永远跑着——ReplicaSet
Service 解决了"找到 Pod"的问题,但没有解决"保证 Pod 数量"的问题。你手动创建了 3 个 nginx Pod,半夜某个 Pod 的进程 OOM 退出了——谁来启动一个新的?
ReplicaSet 的职责单一而明确:保证指定数量的 Pod 副本在任何时刻都在运行。
| |
ReplicaSet 持续检查 actual(Pod count) == desired(replicas)。不等则行动——少了创建,多了删除。
通常你不直接创建 ReplicaSet。 你会用 Deployment——它在 ReplicaSet 之上加了滚动更新和回滚。直接操作 ReplicaSet 就像直接操作汽车的变速箱齿轮而不握方向盘——虽然能工作,但你应该使用更高层的抽象。
2.4 我要零停机更新——Deployment
现在需求升级了:你要发布 nginx 的新版本,但不能中断服务。你要做到:
- 逐步用 v2 替换 v1(滚动更新)
- 如果 v2 有问题,一键回退到 v1
- 更新过程中,始终有指定数量的 Pod 在提供服务
Deployment 正是为此而生。它不直接管理 Pod——它在 ReplicaSet 之上一层,通过管理多个 ReplicaSet 实现滚动更新与回滚:
Deployment (nginx)
│
├── ReplicaSet (v1) replicas: 2 → 1 → 0 ← 逐步缩容
│ ├── Pod (v1)
│ └── Pod (v1)
│
└── ReplicaSet (v2) replicas: 1 → 2 → 3 ← 逐步扩容
├── Pod (v2)
├── Pod (v2)
└── Pod (v2)
滚动更新的参数(控制更新的"激进"程度):
maxSurge:更新期间允许比期望副本数多出几个 Pod(默认 25%)maxUnavailable:更新期间允许最多几个 Pod 不可用(默认 25%)
假设 replicas: 10, maxSurge: 2, maxUnavailable: 2——更新过程中,最多 12 个 Pod(10+2),最少 8 个(10-2),新旧比例逐步变化。
回滚:K8s 保留了旧 ReplicaSet(默认保留 10 个版本),一个命令即可回退:
| |
暂停/恢复:
| |
Deployment 的滚动更新机制是 K8s 声明式 API 的教科书案例:你只改了一个字段(
image: nginx:2.0),K8s 自动创建新 ReplicaSet、逐步替换、保留旧版本、记录变更历史。一个字段修改,背后的控制回路完成了所有编排工作。
第三章:应用要配置、要存数据——将状态从容器中分离
Docker 的哲学是"容器是无状态的"。但现实中的应用怎么可能没有状态——配置文件、数据库密码、上传的文件、数据库数据本身——这些都是状态。只不过,状态应该由平台管理,而非硬编码在容器镜像中。
3.1 开发/生产环境配置不同——ConfigMap
你把数据库连接字符串写死在镜像里,每次切换环境都要重新构建镜像——不应该是这样。
ConfigMap 将配置从镜像中解耦出来,以键值对存储:
| |
注入 Pod 的三种方式:
| 方式 | 配置内容 | 热更新 | 适用场景 |
|---|---|---|---|
| 环境变量 | envFrom[].configMapRef | ❌ 需重启 | 简单的一对一键值 |
| 命令行参数 | $(ENV_VAR) 语法 | ❌ 需重启 | 传递启动参数 |
| 卷挂载 | 每个 key 投影为文件 | ✅(需应用重载) | 配置文件、证书 |
| |
挂载后 /etc/app/config/database-url 文件中就写着 postgres://db-prod:5432/mydb。
ConfigMap 支持标记为不可变(
immutable: true),适合大规模集群中数千 Pod 共用同一份不变配置的场景——但对于现在的你,这个优化还不需要关心。
3.2 密码不能明文写在 YAML 里——Secret
数据库密码、API Key、TLS 私钥——这些不能和 ConfigMap 一样以明文管理。
Secret 专为敏感数据设计:
| |
使用时和 ConfigMap 完全一样——按需以环境变量或卷挂载注入 Pod。
Secret 有两个关键的运行时保障:
- 内存挂载:kubelet 将 Secret 写入 tmpfs(内存文件系统),不落盘——即使节点被攻破,磁盘上找不到 Secret 明文
- etcd 默认 Base64 编码:注意,Base64 是编码不是加密——就好像把一封信的字母全部后移一位,任何人拿到编码后都能解码。生产环境须对 etcd 启用静态加密——这属于集群运维话题,不展开
使用方式上,Secret 和 ConfigMap 完全一致——以环境变量或卷挂载注入 Pod。但从安全角度,敏感数据一定要用 Secret 而非 ConfigMap:因为 Secret 专享 tmpfs 内存挂载、独立审计日志、以及第八章将介绍的 RBAC 权限控制。
3.3 容器重启数据没了——Volume
Pod 中的容器重启后,容器文件系统的所有变更都丢失。这对于无状态应用没问题——但你的应用需要写缓存到 /tmp,或者需要访问配置文件。
Volume 是 Pod 级别的存储定义——生命周期与 Pod 绑定,但独立于容器重启:
| |
emptyDir 是最简单的卷类型——Pod 被调度到节点时创建一个空目录。容器重启不影响数据,Pod 删除则数据消失。
K8s 还有其他卷类型,你现在需要认识的是:不同卷类型的"寿命"不同。emptyDir 随 Pod 而生随 Pod 而死;而有一种特殊的卷——persistentVolumeClaim——它的寿命独立于 Pod。那就是接下来要讲的持久存储。
3.4 Pod 重建后数据还在——PV 与 PVC
emptyDir 的问题:Pod 被删除(或所在节点宕机,Pod 被重新调度到其他节点),数据就没了。你的 PostgreSQL Pod 需要的是独立于 Pod 生命周期的持久存储。
K8s 通过供给与消费分离解决这个问题:
管理员 集群 开发者
│ │ │
│ 创建 PV │ │
├──────────────────────→│ │
│ "我这里有一块 100G │ │
│ 的 SSD 存储" │ │
│ │ 创建 PVC │
│ │←────────────────────┤
│ │ "我要 100G SSD" │
│ │ │
│ │ 系统匹配 PV ↔ PVC │
│ ├────────────────────→│
│ │ 绑定 (一对一) │
│ │ │
│ │ Pod 引用 PVC │
│ │←────────────────────┤
- PersistentVolume(PV):集群中的一块存储(由管理员或 StorageClass 动态创建)。它独立于任何 Pod 存在
- PersistentVolumeClaim(PVC):用户对存储的请求——“我要 100G,ReadWriteOnce 访问模式”
PV 与 PVC 是一对一绑定的——一个 PV 被某个 PVC 绑定后,独占该 PV。PVC 删除后的行为取决于 PV 的回收策略(persistentVolumeReclaimPolicy):
Delete:后端存储随 PV 一起删除(默认,适用于云环境动态供给)Retain:保留 PV 和底层存储,需要管理员手动清理
访问模式决定了多少节点可以同时挂载:
| 模式 | 并发节点数 | 典型存储 |
|---|---|---|
ReadWriteOnce (RWO) | 1 | 云盘(EBS、Azure Disk) |
ReadOnlyMany (ROX) | 多 | 只读数据卷 |
ReadWriteMany (RWX) | 多 | NFS、CephFS |
ReadWriteOncePod (RWOP) | 1(严格单 Pod) | v1.22 GA,比 RWO 更严格 |
PV/PVC 的绑定是一对一的——一旦绑定,这块存储就被独占。这个特性在后面讲到有状态应用(StatefulSet,第六章)时至关重要:每个 Pod 实例可以绑定自己专属的 PVC,Pod 重新调度后自动重新挂载同一块存储。
3.5 我不想每个存储都要手动建 PV——StorageClass 动态供给
如果每次需要存储都要管理员手动创建 PV,那 K8s 的自动化承诺就破功了。
StorageClass 定义了存储的"类型"和自动供给方式:
| |
现在开发者只需要创建 PVC 并指定 storageClassName: fast-ssd——CSI Provisioner 自动创建 PV 和底层存储。
volumeBindingMode 的两种模式值得理解:
| 模式 | 何时分配 PV | 何时创建后端存储 | 适用场景 |
|---|---|---|---|
Immediate | PVC 创建时立即 | 绑定后立即 | 不考虑拓扑,简单场景 |
WaitForFirstConsumer | 有 Pod 引用时才绑定 | 绑定后才创建 | 确保存储与 Pod 在同一可用区(拓扑感知) |
拓扑感知延迟绑定:在云环境中,
WaitForFirstConsumer至关重要。如果 PV 在可用区 A 创建,而 Pod 被调度到了可用区 B——某些存储类型(如 AWS EBS)不支持跨可用区挂载。延迟到 Pod 被调度后才决定在哪个可用区创建存储,从根本上避免了这个问题。
CSI(Container Storage Interface) 是 K8s 与存储实现的标准化接口——各云厂商和存储厂商提供自己的 CSI Driver,K8s 通过统一接口调用。核心代码不绑定任何特定的存储实现(AWS EBS、GCP PD、Ceph、NFS 等都是通过各自的 CSI Driver 接入的)。这种"定义接口,外包实现"的可插拔设计,在 K8s 中是一个反复出现的模式——网络、存储、运行时都遵循同样的思路。
第四章:机器多了——集群架构是怎么运转起来的
前面三章我们主要聚焦在"怎么管理 Pod"——这在一个节点上就能理解。现在你有了 10 台、50 台、200 台服务器,K8s 是怎么在这么多机器上协调工作的?
4.1 问题:多节点集群需要什么?
当你从单机走到多节点,一系列新的本质性问题浮出水面:
- 状态存哪里? “有 3 个 nginx Pod 在运行”——这句话本身是状态,必须可靠地持久化,所有节点都要能读到
- 谁来决定? 新的 Pod 应该放在哪台机器上?这不是某个节点自己能决定的
- 怎么感知故障? 节点宕机了,谁负责把上面的 Pod 搬到健康节点?
- 节点之间怎么通信? Pod 可能在任意节点上,IP 地址怎么分配、怎么路由?
K8s 的架构把集群分为两个截然不同的角色平面:
┌───────────────────────────────────────────────────────┐
│ Control Plane(大脑) │
│ │
│ 负责全局决策——调度、状态存储、API 入口 │
│ │
│ ┌──────────┐ ┌───────────┐ ┌──────────────────┐ │
│ │ API │ │ Scheduler │ │Controller Manager│ │
│ │ Server │ │ │ │ (一组控制器) │ │
│ └────┬─────┘ └─────┬─────┘ └────────┬─────────┘ │
│ │ │ │ │
│ │ ┌────┴────┐ │ │
│ └─────────┤ etcd ├───────────┘ │
│ │(数据库) │ │
│ └─────────┘ │
└──────────────────────┬──────────────────────────────┘
│
┌──────────────────────┼──────────────────────────────┐
│ Data Plane(手脚) │
│ │
│ 负责运行用户负载 │
│ │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
│ │ Node 1 │ │ Node 2 │ │ Node N │ │
│ │ │ │ │ │ │ │
│ │ kubelet │ │ kubelet │ │ kubelet │ │
│ │ kube-proxy│ │ kube-proxy│ │ kube-proxy│ │
│ │ Container │ │ Container │ │ Container │ │
│ │ Runtime │ │ Runtime │ │ Runtime │ │
│ └───────────┘ └───────────┘ └───────────┘ │
└──────────────────────────────────────────────────────┘
核心设计原则:组件之间不直接通信。API Server 是唯一的数据总线——Scheduler 不直接命令 kubelet,Controller Manager 不直接修改 Pod。所有通信都经过 API Server(读写 etcd)。
这意味着什么?你可以独立地:
- 扩展 API Server 实例数(无状态,前挂 LB 即可)
- 替换调度器(只需把
schedulerName指向新的调度器,原有的不动) - 增加节点(新 kubelet 注册到 API Server 后,集群自动可用新容量)
- 审计一切(所有操作都经过 API Server,audit log 完整记录)
4.2 唯一的入口——API Server
API Server 是 K8s 控制平面的前端,所有内部和外部请求都要经过它。它做了三件事:
- 认证(Authentication):你是谁?(X.509 证书、Bearer Token、OIDC 等)
- 授权(Authorization):你能做什么?(RBAC、Webhook 等模式)
- 准入控制(Admission Control):这个请求合规吗?(Mutating/Validating Admission Webhook)
API Server 是无状态的(stateless)——它不存储任何持久数据。你可以运行多个 API Server 副本,前面挂一个负载均衡器(如 kube-apiserver.service 的 VIP 或云 LB)。
Watch 机制:API Server 支持客户端通过 HTTP 长连接 watch 资源变更——不是轮询,而是当对象发生变化时推送增量事件。这是整个 K8s 控制回路的基础:
kubectl apply → API Server → etcd → API Server push event → Scheduler watch → 做调度决策 → API Server → etcd → API Server push event → kubelet watch → 创建容器
每一步都是事件驱动的松耦合——没有组件在"等待"另一个组件,也没有组件直接调用另一个组件。
4.3 真理之源——etcd
K8s 集群中所有的状态数据都存储在 etcd 中——所有 API 对象(Pod、Service、ConfigMap、Secret……)加上集群元数据。
etcd 是一个分布式键值存储,使用 Raft 共识算法保证一致性:
- 写:所有写操作通过 Leader 节点,经 Raft 协议复制到多数派(quorum)后才确认成功
- 读:默认为线性一致性读(linearizable read),保证读到的一定是已提交的最新数据
- 规模:推荐 3 或 5 个节点的奇数集群——3 节点容忍 1 节点故障,5 节点容忍 2 节点故障
生产实践诫条:
- etcd 部署在独立节点上,用 SSD 存储——etcd 的磁盘 I/O 延迟直接决定了集群响应速度
- 定期
etcd defrag,定期快照备份——集群升级前尤其要备份- 启用自动压缩(
--auto-compaction-mode=periodic),--auto-compaction-retention设一个合理窗口(如 1-2 小时)——防止 etcd 数据库无限增长
etcd 使用 watch 机制(基于 MVCC 的增量通知)支撑 K8s 的事件驱动架构——每个控制器不轮询,而是订阅自己关心的 key 前缀的变化。
4.4 节点的眼睛和手——kubelet
kubelet 是每个 Node 上的代理进程。它不是由 K8s 部署的——它是操作系统级的守护进程(通常由 systemd 管理),在 K8s 集群搭建前就要先装好。
职责清单:
- Pod 管理:watch API Server,当被调度到本节点的 Pod 出现(
spec.nodeName == self),调用容器运行时创建容器 - 探针检查:对每个容器周期性执行 liveness/readiness/startup 探针
- 节点注册:启动时将本节点注册到 API Server(创建 Node 对象),持续更新心跳
- 卷管理:根据 Pod 的 Volume 定义挂载/卸载存储
- 资源监控:通过内置 cAdvisor 收集节点和容器指标
- 镜像垃圾回收:磁盘使用超阈值时按 LRU 清理未使用镜像
kubelet 与容器运行时的交互遵循 CRI(Container Runtime Interface) 标准。你可以选择:
- containerd(Docker 底层也是 containerd——从 v1.24 起 Docker 不再直接支持,改为通过 containerd 的 CRI 接口)
- CRI-O(专为 K8s 设计的轻量运行时)
- 或沙箱容器——gVisor(用户态内核,额外隔离层)、Kata Containers(VM 级隔离)
4.5 流量的搬运工——kube-proxy
回顾第二章:Service 有一个 ClusterIP,请求发到这个虚拟 IP 后,最终要到达某个后端 Pod。谁来负责这个 DNAT 转发?
kube-proxy 在每个节点上运行,watch Service 和 EndpointSlice 变更,在 Linux 内核中写入转发规则:
| 模式 | 转发机制 | 适用规模 | 注意 |
|---|---|---|---|
| iptables(默认) | iptables 规则做随机 DNAT | 中等规模(< 5000 Service) | 规则数量 O(Service × Endpoint),大规模时规则更新慢 |
| ipvs | IPVS 内核模块,支持 rr/lc/dh/sh 等调度算法 | 大规模集群 | 比 iptables 性能更好,功能更多 |
| userspace | 用户态代理 | 已弃用 | 仅历史兼容 |
数据路径(iptables 模式):
Client Pod
│
▼
ClusterIP 10.96.0.10:80
│ (iptables DNAT → 随机选一个后端 Pod IP)
├──→ 10.244.1.5:8080
├──→ 10.244.2.7:8080 (被随机选中)
└──→ 10.244.1.9:8080
eBPF 替代方案:现代 K8s 网络中,Cilium 等方案通过 eBPF 直接在数据路径上完成负载均衡,完全不需要 kube-proxy。这消除了 iptables 在大规模集群中的规则爆炸问题,同时将服务转发、网络策略、可观察性统一到 eBPF 数据平面。
4.6 大脑的各个分区——Controller Manager
kube-controller-manager 是一组独立控制回路的集合进程。每个控制器只关心自己的一亩三分地:
| 控制器 | 关心的问题 | 实现方式 |
|---|---|---|
| Node Controller | 节点失联了? | watch Node 心跳租约,超时则驱逐 Pod |
| Deployment Controller | Deployment 改了? | 创建/更新 ReplicaSet,实现滚动更新 |
| ReplicaSet Controller | 副本数够吗? | 检查 Pod 数量,少了创建 |
| StatefulSet Controller | StatefulSet Pod 顺序对吗? | 按 0→N-1 启动,N-1→0 终止 |
| DaemonSet Controller | 每节点都有吗? | 新节点加 Pod,老节点删 Pod |
| Job Controller | 任务完成了吗? | 跟踪 completion 计数 |
| Service Controller | 需要云 LB? | 调用云厂商 API 创建 LoadBalancer |
每个控制器遵循同一套模式——由 client-go 库的 SharedInformer 框架支撑:
API Server ──watch──→ Informer(本地缓存)──enqueue key──→ Workqueue
│
▼
┌───────────────────────┐
│ Reconcile Loop │
│ 1. 从队列取 key │
│ 2. 读当前状态 │
│ 3. 计算与期望的差异 │
│ 4. 执行操作(create/ │
│ update/delete) │
│ 5. 更新 status │
└───────────────────────┘
几个关键设计原则:
- 本地缓存:控制器不直接查询 API Server——通过 Informer 维护本地缓存(Indexer),避免每次 reconcile 都发起 API 调用。缓存通过 watch 保持同步
- 幂等性:reconcile 函数必须幂等——不管重试多少次,结果一致
- 水平触发(Level-triggered):控制器不关心"发生了什么事件",只关心"当前状态是什么"——即使错过了某个中间事件(比如 Pod 短暂 CrashLoop 又恢复),定时全量同步(resync)也会修复
cloud-controller-manager 是云厂商适配版的 Controller Manager,将云特定逻辑(如管理云 LoadBalancer、云路由表)从核心控制器中分离。如果运行在 bare-metal、minikube 或 kind,此组件不出现。
第五章:Pod 去哪个节点?——调度
5.1 Scheduler 的两阶段决策
你创建了一个 Pod,没有指定节点。谁来给它分配节点?kube-scheduler。
Scheduler 不运行容器——它只做一件事:将 Pod 的 spec.nodeName 填上目标节点名。kubelet 看到 “nodeName 是我” 的 Pod,才真正动手创建容器。
调度过程分两阶段:
第一阶段:过滤(Filtering)——排除不合适的节点
遍历所有节点,排除不满足条件的:
- 剩余 CPU/内存不够 Pod 的
requests nodeSelector/ 节点亲和性不匹配- 节点上的 Taint 不允许此 Pod(Pod 没有对应 Toleration)
- 端口已被占用
- 卷拓扑约束无法满足
第二阶段:打分(Scoring)——在合格者中选最优
给每个通过过滤的节点打分:
LeastRequestedPriority:剩余资源越多,分越高(负载均衡策略)MostRequestedPriority:资源利用率越高,分越高(装箱策略,减少碎片)NodeAffinityPriority:满足亲和性偏好的节点加分ImageLocalityPriority:节点上已有容器镜像的加分(减少镜像拉取时间)
Scheduler 也是可插拔的。你可以写自己的调度器(实现
schedulerName字段),和多调度器并行运行。
5.2 我要控制 Pod 去哪——亲和性
节点亲和性(Node Affinity)——将 Pod 吸引到有特定标签的节点:
| |
required...:硬性——不满足就不调度,Pod 一直 Pendingpreferred...:软性——尽量满足,影响打分但不阻止调度IgnoredDuringExecution:Pod 运行后如果节点标签变了,不会驱逐已运行的 Pod(这是设计选择——K8s 避免无谓的运行时迁移)
Pod 亲和性/反亲和性——基于已运行 Pod 的标签做决策:
- Pod 亲和性:把有关联的 Pod(如 Web + Redis)尽可能放在同一台机器上(降低延迟)
- Pod 反亲和性:把同一服务的 Pod 散开,放在不同机器/可用区(提高容灾能力)
| |
5.3 有些节点是"特殊"的——Taints & Tolerations
与亲和性相反方向:它是节点拒绝 Pod 的机制。
| |
| Taint Effect | 效果 |
|---|---|
NoSchedule | 不容忍的 Pod 不被调度到此节点 |
PreferNoSchedule | 尽量不调度到该节点 |
NoExecute | 不容忍的 Pod 不仅不调度,已在运行的也会被驱逐 |
Pod 要上这种节点,必须声明 Toleration:
| |
典型用例:
- 专用节点池:GPU 节点只跑 GPU 工作负载;数据库节点只跑 StatefulSet
- 节点故障自动驱逐:Node Controller 检测到节点异常后打上
node.kubernetes.io/unreachableTaint,不容忍的 Pod 被驱逐到健康节点
5.4 均匀分布到可用区——拓扑分布约束
反亲和性能让 Pod 散到不同节点,但表达的是"最好不在同一节点"。如果你需要在可用区级别实现均匀分布——比如 9 个 Pod 均分到 3 个可用区(每区 3 个)——用 Topology Spread Constraints:
| |
这比反亲和性更精确地控制分布的均匀程度。
5.5 资源管理——Requests、Limits 与 QoS
每个容器可以声明两个维度的资源:
| |
关键的差异行为:
| 资源 | 可压缩? | 超限行为 |
|---|---|---|
| CPU | 是 | 被限流(throttle),不会被 kill |
| 内存 | 否 | 超过 limit → OOM Kill → 容器重启 |
根据 requests 和 limits 的配置关系,Pod 被分配 QoS 等级,这决定了 OOM 时的"处决顺序":
| QoS 等级 | 条件 | OOM 分 | 驱逐优先级 | 说明 |
|---|---|---|---|---|
| Guaranteed | 所有容器都设了 requests == limits(且 memory/cpu 全设) | 最低 | 最后 | 关键服务应设为此级 |
| Burstable | 至少一个容器有 requests,非 Guaranteed | 中 | 中间 | 最常见的场景 |
| BestEffort | 所有容器都没设 requests 和 limits | 最高 | 最先被杀 | 开发环境或低优批处理 |
从 v1.32 起,
memory.oomGroupGA——某个容器 OOM 时,可以杀死整个 Pod 而不仅那个容器。在 sidecar 模式下此特性特别有用:避免主容器因 sidecar OOM 而成为孤儿。
第六章:不同形状的工作负载
前面我们主要讨论的是 Deployment——无状态、可互换的 Pod。但真实世界的工作负载形态多样。
6.1 数据库需要一个稳定的家——StatefulSet
你用 Deployment 跑了一个 PostgreSQL。它工作了一段时间——直到 Pod 被重新调度到新节点。新 Pod 是一个全新的"干净"实例,之前的数据丢了(即使你挂载了 PVC,PVC 也没有跟着 Pod 迁移到新节点——因为每个 Pod 用的是同一个 PVC)。
更重要的是,PostgreSQL 的主从复制需要每个实例有稳定的网络标识——pg-0 是主库,pg-1 和 pg-2 是从库。它们不能随便改名,启动和终止也必须有先后顺序(先起主库,再起从库;先停从库,再停主库)。
StatefulSet 为这类需求设计:
- 稳定的网络标识:Pod 名固定为
<statefulset-name>-<ordinal>——pg-0,pg-1,pg-2。即使 Pod 被重新调度到其他节点,名字不变。结合 Headless Service,每个 Pod 有独立的 DNS 名 - 有序部署:按
0, 1, 2, ..., N-1顺序启动——前一个 Pod Running & Ready 后,才启动下一个 - 有序终止:按
N-1, ..., 1, 0逆序终止——先停"大的"(从库),最后停"小的"(主库) - 独立的持久存储:每个 Pod 绑定自己的 PVC——
pg-0的数据盘和pg-1的数据盘是两块独立的存储。Pod 重调度后重新挂载同一块存储
| |
Pod pg-0 和它的 PVC data-pg-0 永远绑定——不管 Pod 被调度到哪,卷都跟着。
为什么 StatefulSet 需要 Headless Service?
注意到 YAML 中的 serviceName: pg-headless 了吗?它引用的就是一个 Headless Service(clusterIP: None)。这和第二章学的普通 ClusterIP Service 有什么不同?
回顾一下:普通 Service 分配 ClusterIP,DNS 返回的是这个 VIP——客户端连接 VIP,由 kube-proxy 随机转发到某个后端 Pod。你不需要知道后端是谁。
但 StatefulSet 反过来了——你必须知道每个 Pod 是谁。pg-0 是主库(接受写),pg-1 和 pg-2 是从库(只读)。你的应用需要区分"连主库"还是"连从库",而不是随机连接一个 Pod。
Headless Service 的做法:不分配 ClusterIP,DNS 直接返回每个 Pod 的独立 IP。
普通 Service DNS: pg → 10.96.0.10 (VIP,随机转发)
Headless Service DNS: pg-0.pg-headless → 10.244.1.5 (直连 Pod)
pg-1.pg-headless → 10.244.2.7
pg-2.pg-headless → 10.244.1.9
你的应用可以直接连接 pg-0.pg-headless——这个 DNS 名永远指向 pg-0 这个 Pod,无论它被重新调度到哪个节点、IP 怎么变。这就是"稳定网络标识"的底层机制:StatefulSet 的固定命名 + Headless Service 的独立 DNS = 每个 Pod 的永久地址。
6.2 每个节点都要跑一个——DaemonSet
你想在集群的每个节点上部署一个日志收集器(Fluentd),或者 Prometheus Node Exporter。你不想手动管理副本数——新加节点自动部署,删除节点自动回收。
DaemonSet 保证每个符合条件的节点上恰好运行指定 Pod 的一个副本:
| |
典型用例:日志收集、节点监控、CNI 网络插件、存储守护进程。这些都是基础设施层的工作负载,不是用户业务——每节点一个,随节点生命周期。
6.3 跑完就结束——Job 与 CronJob
不是所有工作负载都需要永久运行。数据迁移脚本、批量图片处理、定时备份——这些是一次性任务或周期性任务。
Job:运行到指定次数成功完成为止。三种模式:
- 非并行:
completions: 1,完成一次即可 - 固定并行:
completions: 5, parallelism: 2——总共完成 5 次,每次并发跑 2 个 Pod - 工作队列:不设
completions,每个 Pod 自己判断是否完成
CronJob:按 Cron 表达式定时创建 Job:
| |
第七章:跨节点网络——Pod 之间怎么互相通信
7.1 K8s 网络的三大铁律
回顾目前的进展:我们有 Pod(带 IP)、有 Service(带 ClusterIP)、有 kube-proxy(做 DNAT)、节点上跑着 kubelet。但一个根本问题还没回答:节点 A 上的 Pod (10.244.1.5) 怎么把 IP 包发给节点 B 上的 Pod (10.244.2.7)?
K8s 对网络模型定了三条不可动摇的规则(所有网络插件必须满足):
- Pod 间通信不需要 NAT:任何节点上的 Pod 可直接与任何其他节点上的 Pod 通信,无需 NAT
- 节点可以与所有 Pod 通信:节点上的进程(kubelet、daemon)可直接访问所有 Pod
- Pod 看到的自身 IP 就是外部看到的 IP:容器内
ip addr看到的 IP 和外部访问的 IP 一致
这三条规则的实质是 IP-per-Pod 模型——每个 Pod 在集群范围内拥有一个唯一的、可路由的 IP,就像虚拟机或物理机一样。不存在端口映射的复杂性。
7.2 CNI——把网络实现外包给插件
K8s 本身不实现网络——它通过 CNI(Container Network Interface) 将网络外包给插件:
kubelet 创建 Pod
│
▼
CRI Runtime (containerd/CRI-O)
│
▼
CNI Plugin ──→ 分配 IP
──→ 创建网络命名空间
──→ 创建 veth pair(连接 Pod 和主机网络)
──→ 添加路由/隧道/封装规则
主流的实现阵营:
| 方案 | 原理 | 性能 | 复杂度 | 适用场景 |
|---|---|---|---|---|
| Overlay(Flannel VXLAN) | IP 包外再封装一层 UDP/VXLAN | 有封装开销 | 低 | 简单环境、快速上手 |
| 路由(Calico BGP) | 直接用 BGP 交换 Pod CIDR 路由 | 接近裸金属 | 中 | 高性能需求、数据平面网络可控 |
| eBPF(Cilium) | 用 eBPF 程序接管数据路径 | 极高 | 高 | 大规模、安全+网络+可观察性一体 |
| 混合(Calico CrossSubnet) | 同子网用路由,跨子网封装 | 兼顾 | 中 | 多可用区但不便全网 BGP |
选择建议:小型集群/学习环境,Flannel 足够。生产环境,Calico 或 Cilium。如果你已经在考虑服务网格(Istio)、网络可观察性和安全策略,直接在 Cilium 上做统一数据平面。
7.3 谁可以访问谁?——NetworkPolicy
默认情况下,K8s 中所有 Pod 间流量都是放行的。这在学习环境没问题,但在生产环境——你的前端 Pod 不应该能直接访问数据库 Pod,数据分析 Pod 不应该能访问支付服务。
NetworkPolicy 定义了 Pod 间和 Pod 与外部端点的流量规则:
| |
关键规则:
- 无策略 = 全放行:没被 NetworkPolicy 选中的 Pod,所有流量放行
- 有策略 = 默认拒绝:一旦 NetworkPolicy 选中 Pod,仅策略中明确允许的流量放行
- 策略是累加的:多条 NetworkPolicy 选中同一 Pod,效果取并集
- 需要 CNI 支持:NetworkPolicy 是 API 资源,需要 CNI 插件实现(Calico、Cilium 全面支持;Flannel 默认不支持,需额外部署组件)
第八章:权限和安全——谁能干什么?
前面的章节都在说"怎么让系统跑起来"。现在换个视角:谁有权限干什么?
8.1 四层防线:认证 → 授权 → 准入 → 容器
每次对 API Server 的请求都经历三道关卡:
请求 ──→ ① 认证(你是谁?)──→ ② 授权(你能做什么?)──→ ③ 准入(这个请求合规吗?)──→ etcd
- 认证:X.509 证书(kubelet 到 API Server 的通信)、Bearer Token(ServiceAccount)、OIDC(用户登录)
- 授权:最常见的是 RBAC——基于角色的访问控制
- 准入:Mutating Admission(可在持久化前修改对象——如注入 sidecar);Validating Admission(可拒绝请求——如禁止使用
latest镜像标签)
8.2 RBAC 四件套
RBAC 通过四个 API 对象实现精细化权限管理:
ServiceAccount Role / ClusterRole
(谁要访问) (能做什么)
│ │
└───────────┬─────────────────────┘
│
RoleBinding / ClusterRoleBinding
(授权关系)
| 对象 | 作用域 | 做什么 |
|---|---|---|
| Role | Namespace | 定义权限集——“可以读/写 pods, services” |
| ClusterRole | 集群 | 同上,但作用范围是集群级别(如访问 nodes、所有 namespace 的资源) |
| RoleBinding | Namespace | 把 Role/ClusterRole 授权给 Subject |
| ClusterRoleBinding | 集群 | 把 ClusterRole 授权给集群范围的 Subject |
ServiceAccount 是 Pod 的"身份证"——Pod 默认挂载所在 Namespace 的默认 ServiceAccount 的 token,并以此身份调用 API Server。
一个精细化的例子:
| |
monitoring-agent 这个身份只能读 frontend 命名空间的 Pod,什么都改不了。
8.3 容器里跑什么用户?——SecurityContext
从"集群权限"往下走一层:容器自身的安全配置。
| |
这些配置不会让你的应用变慢。它们是纯安全的防御层——即使容器内的进程被攻破,攻击者能做的事情也被严格限制。
8.4 Namespace 级别的安全策略——Pod Security Standards
v1.25 起,PodSecurityPolicy(PSP)被移除,取而代之的是 Pod Security Standards(PSS)。它通过给 Namespace 打标签,在 Namespace 级别实施策略:
| 级别 | 含义 | 禁止什么 |
|---|---|---|
| Privileged | 无限制 | 允许一切 |
| Baseline | 防止已知特权提升 | 禁止 hostNetwork、hostPID、特权容器 |
| Restricted | 最佳实践 | Baseline + non-root、不可变 rootfs、seccomp、禁止能力提升 |
| |
三个模式(enforce/audit/warn)可以独立配置——比如 enforce 用 Baseline,warn 用 Restricted,给团队一个过渡期。
第九章:外部流量怎么进来?
截至目前,我们的 Service 都是 ClusterIP——只能从集群内部访问。生产环境还需要从公网接收用户请求。
9.1 从集群外到 Service——NodePort 与 LoadBalancer
NodePort:在每个节点上打开一个端口(30000-32767 范围)。外部请求打到 <任意节点IP>:<NodePort>,被转发到 Service 的后端 Pod。
最简陋的外部访问方式——适合开发测试,不适合生产(依赖节点 IP 稳定性、没有 L7 能力)。
LoadBalancer:调用云厂商 API 创建一个外部的 L4 负载均衡器(AWS NLB、GCP TCP LB),将流量分发到各节点的 NodePort。每个 LoadBalancer 类型的 Service 会创建一个独立的云 LB——成本随 Service 数量线性增长,限制了 Service 数量。
9.2 按域名和路径分流——Ingress
LoadBalancer 的问题是:每个 Service 一个 LB,成本高;而且只有 L4——你没法按 api.example.com/users → user-service、api.example.com/orders → order-service 来路由。
Ingress 定义 L7 路由规则——按域名和路径将请求分发到不同 Service:
| |
Ingress 只是 API 对象,不包含实现。你需要部署一个 Ingress Controller(如 ingress-nginx、Kong、Traefik)来解释和执行 Ingress 规则。K8s 不内置 Ingress Controller——这是有意的设计选择。
9.3 Gateway API——新一代七层入口
Ingress 的功能不够用:金丝雀发布(按权重分流)、Header 路由、请求改写、流量镜像——这些在 Ingress 中只能通过 implementation-specific 的 annotation 表达,缺乏可移植性。
Gateway API(gateway.networking.k8s.io)拆分 Ingress 的角色为三个独立的关注点:
| 角色 | 资源 | 谁来管理 |
|---|---|---|
| 基础设施 | GatewayClass, Gateway | 平台团队(集群运维) |
| 路由规则 | HTTPRoute, GRPCRoute, TCPRoute | 应用开发者 |
| 高级流量 | 权重分流、请求改写、流量镜像 | 应用开发者(通过路由资源表达) |
这种拆分意味着:
- 平台团队定义"集群有一个 nginx Gateway,监听 443 端口,有这些 TLS 证书"
- 应用开发者定义"
/users路径发给 user-service v2 权重 10%,v1 权重 90%" - 两者互不打扰——开发者不需要知道 Gateway 的底层配置,运维不需要知道路由细节
Gateway API 是 Ingress 的继任者——如果你现在开始新项目,应优先评估 Gateway API。
第十章:全景回望与学习路标
10.1 核心关系速览
┌─────────────────────────────────────────────────────────────┐
│ Kubernetes 对象关系全景 │
│ │
│ Namespace ──(逻辑隔离)──→ 所有 Namespace 级资源 │
│ │
│ Deployment ──(管理)──→ ReplicaSet ──(管理)──→ Pod │
│ StatefulSet ──(管理)──→ Pod (+ 稳定标识 + 有序 + 独立存储) │
│ DaemonSet ──(管理)──→ Pod (每节点一个) │
│ Job/CronJob ──(管理)──→ Pod (任务完成后终止) │
│ │
│ ConfigMap/Secret ──(注入)──→ Pod (环境变量 / 卷挂载) │
│ PVC ──(绑定)──→ PV ←──(由 StorageClass 动态供给或手动创建) │
│ PVC ──(挂载)──→ Pod (卷) │
│ │
│ Pod ──(通过 selector 匹配 label)──→ Service │
│ Service ──(暴露方式)──→ ClusterIP / NodePort / LoadBalancer │
│ Ingress/Gateway API ──(L7 路由)──→ Service │
│ │
│ NetworkPolicy ──(限制流量)──→ Pod │
│ ServiceAccount ──(RoleBinding)──→ Role/ClusterRole (权限) │
│ SecurityContext ──(容器安全配置)──→ Pod / Container │
└─────────────────────────────────────────────────────────────┘
10.2 从本文学到的核心原则
- 声明式而非指令式——描述终点,控制回路持续开车
- API Server 是唯一数据总线——组件之间不直接通信,松耦合,可审计
- Pod 是原子调度单元——共享网络和存储的容器组
- 标签是松耦合粘合剂——Service 通过标签发现 Pod,而非硬编码 IP
- 控制回路是核心范式——observe → diff → act,幂等、水平触发
- 一切皆可插拔——CNI、CSI、CRI、Scheduler、DNS、Ingress Controller——都可替换
10.3 下一步:从理解到实战
| 阶段 | 目标 | 做什么 |
|---|---|---|
| 入门 | 内化 Pod/Deployment/Service 三件套 | 用 minikube/kind 部署一个多服务应用(前端 + API + 数据库) |
| 进阶 | 配置与存储分离 | 把配置抽到 ConfigMap/Secret,数据接到 PVC |
| 中级 | 安全与隔离 | 设置 RBAC、NetworkPolicy、SecurityContext |
| 中高 | 多环境与流量管理 | 用 Gateway API 实现金丝雀发布 |
| 高级 | 理解控制器原理 | 阅读 client-go 源码,写一个简单的 Operator(用 Kubebuilder) |
| 深入 | 集群运维 | 拆解 etcd, CNI 内部, 大规模集群调优, 参与 K8s SIG |
参考资源
- Kubernetes 官方文档
- Kubernetes 组件
- Kubernetes API 参考
- Kubernetes the Hard Way — 手动搭建,理解各组件的协作
- Cilium eBPF 网络
- Kubebuilder — Operator 开发框架
本文基于 Kubernetes v1.33 官方文档写成。K8s 演进迅速,架构细节可能随版本变化,请以 kubernetes.io/docs 为最新权威参考。