本文面向会用 Docker/容器、但尚未系统学习过 Kubernetes 的读者。每一节从一个你已经在头疼的问题出发,引出 K8s 对应的概念与解法。所有技术断言以 Kubernetes v1.33 官方文档为基准。


第一章:从一台机器说起——容器不只是 docker run

1.1 我会用 Docker,还有什么问题?

你已经能熟练地 docker builddocker rundocker 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-composenetwork_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)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
apiVersion: v1
kind: Pod
metadata:
  name: web-app
  labels:              # 标签——后面会反复用到
    app: web
spec:
  containers:
  - name: web
    image: nginx:1.25
    ports:
    - containerPort: 80
  - name: log-agent
    image: fluentd
    volumeMounts:
    - name: shared-logs
      mountPath: /var/log/app
  volumes:
  - name: shared-logs
    emptyDir: {}

要点

  • Pod 是 K8s 中可创建和调度的最小单位——你不能直接调度一个容器,必须通过 Pod
  • 同 Pod 容器共享网络和存储,但各自有独立的文件系统和进程空间
  • Pod 是易逝的(ephemeral)——它被删除后不会复活,IP 地址也会变

这个"易逝性"正是下一节要解决的问题。

1.3 用标签(Labels)在混乱中建立秩序

Pod 多了之后,你需要一种灵活的方式来组织和选择它们。K8s 使用标签(Labels)

1
2
3
4
5
6
metadata:
  labels:
    app: nginx
    env: production
    version: v1.2
    tier: frontend

标签是任意的 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.510.244.2.710.244.1.9)。你的后端应用需要访问 nginx——它该用哪个 IP?

更致命的是:Pod 挂了被重建,新 Pod 的 IP 就变了(比如变成 10.244.3.12)。所有依赖方都得跟着改配置,这在几十个微服务之间根本不可行。

需求很明确:不管后端 Pod 怎么变,前端始终用一个固定的地址访问。K8s 的答案是 Service

Service 做三件事:

  1. 给自己申请一个固定的集群内部 IP(叫 ClusterIP)——这个 IP 在 Service 存在期间永远不变
  2. 通过你在 1.3 节学到的标签选择器,自动找到属于它的 Pod
  3. 当请求到达 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 很简短:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
apiVersion: v1
kind: Service
metadata:
  name: nginx
spec:
  selector:
    app: nginx       # 这个标签决定了哪些 Pod 是后端
  ports:
  - port: 80         # Service 监听的端口
    targetPort: 80   # 转发到 Pod 的哪个端口

创建这个 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 副本在任何时刻都在运行

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: nginx-rs
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:             # Pod 模板——创建新 Pod 时照此生成
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.25

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 个版本),一个命令即可回退:

1
kubectl rollout undo deployment/nginx --to-revision=2

暂停/恢复

1
2
kubectl rollout pause deployment/nginx   # 暂停,排查滚动中的问题
kubectl rollout resume deployment/nginx  # 继续

Deployment 的滚动更新机制是 K8s 声明式 API 的教科书案例:你只改了一个字段(image: nginx:2.0),K8s 自动创建新 ReplicaSet、逐步替换、保留旧版本、记录变更历史。一个字段修改,背后的控制回路完成了所有编排工作。


第三章:应用要配置、要存数据——将状态从容器中分离

Docker 的哲学是"容器是无状态的"。但现实中的应用怎么可能没有状态——配置文件、数据库密码、上传的文件、数据库数据本身——这些都是状态。只不过,状态应该由平台管理,而非硬编码在容器镜像中

3.1 开发/生产环境配置不同——ConfigMap

你把数据库连接字符串写死在镜像里,每次切换环境都要重新构建镜像——不应该是这样。

ConfigMap 将配置从镜像中解耦出来,以键值对存储:

1
2
3
4
5
6
7
8
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
data:
  database-url: "postgres://db-prod:5432/mydb"
  log-level: "debug"
  max-connections: "100"

注入 Pod 的三种方式:

方式配置内容热更新适用场景
环境变量envFrom[].configMapRef❌ 需重启简单的一对一键值
命令行参数$(ENV_VAR) 语法❌ 需重启传递启动参数
卷挂载每个 key 投影为文件✅(需应用重载)配置文件、证书
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 卷挂载方式——推荐,支持热更新
spec:
  containers:
  - name: app
    volumeMounts:
    - name: config
      mountPath: /etc/app/config
  volumes:
  - name: config
    configMap:
      name: app-config

挂载后 /etc/app/config/database-url 文件中就写着 postgres://db-prod:5432/mydb

ConfigMap 支持标记为不可变(immutable: true),适合大规模集群中数千 Pod 共用同一份不变配置的场景——但对于现在的你,这个优化还不需要关心。

3.2 密码不能明文写在 YAML 里——Secret

数据库密码、API Key、TLS 私钥——这些不能和 ConfigMap 一样以明文管理。

Secret 专为敏感数据设计:

1
2
3
4
5
6
7
8
apiVersion: v1
kind: Secret
metadata:
  name: db-credentials
type: kubernetes.io/basic-auth
stringData:               # 直接写明文,创建时自动编码
  username: admin
  password: s3cret!

使用时和 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 绑定,但独立于容器重启:

1
2
3
4
5
6
7
8
9
spec:
  containers:
  - name: app
    volumeMounts:
    - name: cache
      mountPath: /tmp/cache
  volumes:
  - name: cache
    emptyDir: {}          # 随 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 定义了存储的"类型"和自动供给方式:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: fast-ssd
provisioner: kubernetes.io/aws-ebs
parameters:
  type: gp3
  encrypted: "true"
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer

现在开发者只需要创建 PVC 并指定 storageClassName: fast-ssd——CSI Provisioner 自动创建 PV 和底层存储。

volumeBindingMode 的两种模式值得理解:

模式何时分配 PV何时创建后端存储适用场景
ImmediatePVC 创建时立即绑定后立即不考虑拓扑,简单场景
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 问题:多节点集群需要什么?

当你从单机走到多节点,一系列新的本质性问题浮出水面:

  1. 状态存哪里? “有 3 个 nginx Pod 在运行”——这句话本身是状态,必须可靠地持久化,所有节点都要能读到
  2. 谁来决定? 新的 Pod 应该放在哪台机器上?这不是某个节点自己能决定的
  3. 怎么感知故障? 节点宕机了,谁负责把上面的 Pod 搬到健康节点?
  4. 节点之间怎么通信? 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 控制平面的前端,所有内部和外部请求都要经过它。它做了三件事:

  1. 认证(Authentication):你是谁?(X.509 证书、Bearer Token、OIDC 等)
  2. 授权(Authorization):你能做什么?(RBAC、Webhook 等模式)
  3. 准入控制(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 集群搭建前就要先装好。

职责清单:

  1. Pod 管理:watch API Server,当被调度到本节点的 Pod 出现(spec.nodeName == self),调用容器运行时创建容器
  2. 探针检查:对每个容器周期性执行 liveness/readiness/startup 探针
  3. 节点注册:启动时将本节点注册到 API Server(创建 Node 对象),持续更新心跳
  4. 卷管理:根据 Pod 的 Volume 定义挂载/卸载存储
  5. 资源监控:通过内置 cAdvisor 收集节点和容器指标
  6. 镜像垃圾回收:磁盘使用超阈值时按 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),大规模时规则更新慢
ipvsIPVS 内核模块,支持 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 ControllerDeployment 改了?创建/更新 ReplicaSet,实现滚动更新
ReplicaSet Controller副本数够吗?检查 Pod 数量,少了创建
StatefulSet ControllerStatefulSet 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 吸引到有特定标签的节点:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
affinity:
  nodeAffinity:
    requiredDuringSchedulingIgnoredDuringExecution:   # 硬性
      nodeSelectorTerms:
      - matchExpressions:
        - key: gpu
          operator: In
          values: ["nvidia-a100"]
    preferredDuringSchedulingIgnoredDuringExecution:   # 软性
    - weight: 100
      preference:
        matchExpressions:
        - key: disk-type
          operator: In
          values: ["ssd"]
  • required...:硬性——不满足就不调度,Pod 一直 Pending
  • preferred...:软性——尽量满足,影响打分但不阻止调度
  • IgnoredDuringExecution:Pod 运行后如果节点标签变了,不会驱逐已运行的 Pod(这是设计选择——K8s 避免无谓的运行时迁移)

Pod 亲和性/反亲和性——基于已运行 Pod 的标签做决策:

  • Pod 亲和性:把有关联的 Pod(如 Web + Redis)尽可能放在同一台机器上(降低延迟)
  • Pod 反亲和性:把同一服务的 Pod 散开,放在不同机器/可用区(提高容灾能力)
1
2
3
4
5
6
7
affinity:
  podAntiAffinity:
    requiredDuringSchedulingIgnoredDuringExecution:
    - labelSelector:
        matchLabels:
          app: nginx
      topologyKey: kubernetes.io/hostname   # 以主机为拓扑域:同主机不共存

5.3 有些节点是"特殊"的——Taints & Tolerations

与亲和性相反方向:它是节点拒绝 Pod 的机制。

1
2
# 给 GPU 节点打上 Taint
kubectl taint nodes gpu-node-1 gpu=true:NoSchedule
Taint Effect效果
NoSchedule不容忍的 Pod 不被调度到此节点
PreferNoSchedule尽量不调度到该节点
NoExecute不容忍的 Pod 不仅不调度,已在运行的也会被驱逐

Pod 要上这种节点,必须声明 Toleration

1
2
3
4
5
tolerations:
- key: "gpu"
  operator: "Equal"
  value: "true"
  effect: "NoSchedule"

典型用例:

  • 专用节点池:GPU 节点只跑 GPU 工作负载;数据库节点只跑 StatefulSet
  • 节点故障自动驱逐:Node Controller 检测到节点异常后打上 node.kubernetes.io/unreachable Taint,不容忍的 Pod 被驱逐到健康节点

5.4 均匀分布到可用区——拓扑分布约束

反亲和性能让 Pod 散到不同节点,但表达的是"最好不在同一节点"。如果你需要在可用区级别实现均匀分布——比如 9 个 Pod 均分到 3 个可用区(每区 3 个)——用 Topology Spread Constraints

1
2
3
4
5
6
7
topologySpreadConstraints:
- maxSkew: 1                          # 任意两个拓扑域 Pod 数量差不超过 1
  topologyKey: topology.kubernetes.io/zone
  whenUnsatisfiable: DoNotSchedule    # ScheduleAnyway 则仅作偏好
  labelSelector:
    matchLabels:
      app: nginx

这比反亲和性更精确地控制分布的均匀程度。

5.5 资源管理——Requests、Limits 与 QoS

每个容器可以声明两个维度的资源:

1
2
3
4
5
6
7
resources:
  requests:           # 调度时的保证量——Scheduler 据此判断节点有无空位
    cpu: "500m"
    memory: "256Mi"
  limits:             # 运行时上限——超过则限流(CPU) 或 kill(内存)
    cpu: "1000m"
    memory: "512Mi"

关键的差异行为:

资源可压缩?超限行为
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.oomGroup GA——某个容器 OOM 时,可以杀死整个 Pod 而不仅那个容器。在 sidecar 模式下此特性特别有用:避免主容器因 sidecar OOM 而成为孤儿。


第六章:不同形状的工作负载

前面我们主要讨论的是 Deployment——无状态、可互换的 Pod。但真实世界的工作负载形态多样。

6.1 数据库需要一个稳定的家——StatefulSet

你用 Deployment 跑了一个 PostgreSQL。它工作了一段时间——直到 Pod 被重新调度到新节点。新 Pod 是一个全新的"干净"实例,之前的数据丢了(即使你挂载了 PVC,PVC 也没有跟着 Pod 迁移到新节点——因为每个 Pod 用的是同一个 PVC)。

更重要的是,PostgreSQL 的主从复制需要每个实例有稳定的网络标识——pg-0 是主库,pg-1pg-2 是从库。它们不能随便改名,启动和终止也必须有先后顺序(先起主库,再起从库;先停从库,再停主库)。

StatefulSet 为这类需求设计:

  1. 稳定的网络标识:Pod 名固定为 <statefulset-name>-<ordinal>——pg-0, pg-1, pg-2。即使 Pod 被重新调度到其他节点,名字不变。结合 Headless Service,每个 Pod 有独立的 DNS 名
  2. 有序部署:按 0, 1, 2, ..., N-1 顺序启动——前一个 Pod Running & Ready 后,才启动下一个
  3. 有序终止:按 N-1, ..., 1, 0 逆序终止——先停"大的"(从库),最后停"小的"(主库)
  4. 独立的持久存储:每个 Pod 绑定自己的 PVC——pg-0 的数据盘和 pg-1 的数据盘是两块独立的存储。Pod 重调度后重新挂载同一块存储
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: pg
spec:
  serviceName: pg-headless    # 必须——提供稳定 DNS
  replicas: 3
  selector:
    matchLabels:
      app: pg
  template:
    metadata:
      labels:
        app: pg
    spec:
      containers:
      - name: postgres
        image: postgres:16
        volumeMounts:
        - name: data
          mountPath: /var/lib/postgresql/data
  volumeClaimTemplates:       # 每个 Pod 自动创建独立的 PVC
  - metadata:
      name: data
    spec:
      accessModes: ["ReadWriteOnce"]
      storageClassName: fast-ssd
      resources:
        requests:
          storage: 100Gi

Pod pg-0 和它的 PVC data-pg-0 永远绑定——不管 Pod 被调度到哪,卷都跟着。

为什么 StatefulSet 需要 Headless Service?

注意到 YAML 中的 serviceName: pg-headless 了吗?它引用的就是一个 Headless ServiceclusterIP: None)。这和第二章学的普通 ClusterIP Service 有什么不同?

回顾一下:普通 Service 分配 ClusterIP,DNS 返回的是这个 VIP——客户端连接 VIP,由 kube-proxy 随机转发到某个后端 Pod。你不需要知道后端是谁。

但 StatefulSet 反过来了——你必须知道每个 Pod 是谁pg-0 是主库(接受写),pg-1pg-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 的一个副本:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: fluentd
spec:
  selector:
    matchLabels:
      name: fluentd
  template:
    metadata:
      labels:
        name: fluentd
    spec:
      containers:
      - name: fluentd
        image: fluentd
        volumeMounts:
        - name: varlog
          mountPath: /var/log
      volumes:
      - name: varlog
        hostPath:             # 挂载宿主机的日志目录
          path: /var/log

典型用例:日志收集、节点监控、CNI 网络插件、存储守护进程。这些都是基础设施层的工作负载,不是用户业务——每节点一个,随节点生命周期。

6.3 跑完就结束——Job 与 CronJob

不是所有工作负载都需要永久运行。数据迁移脚本、批量图片处理、定时备份——这些是一次性任务或周期性任务。

Job:运行到指定次数成功完成为止。三种模式:

  • 非并行:completions: 1,完成一次即可
  • 固定并行:completions: 5, parallelism: 2——总共完成 5 次,每次并发跑 2 个 Pod
  • 工作队列:不设 completions,每个 Pod 自己判断是否完成

CronJob:按 Cron 表达式定时创建 Job:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
apiVersion: batch/v1
kind: CronJob
metadata:
  name: daily-backup
spec:
  schedule: "0 2 * * *"        # 每天凌晨 2 点
  concurrencyPolicy: Forbid    # 不允许并发(上次没跑完,这次跳过)
  startingDeadlineSeconds: 300 # 超过 5 分钟没启动则跳过
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: backup
            image: backup-tool
          restartPolicy: OnFailure

第七章:跨节点网络——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 对网络模型定了三条不可动摇的规则(所有网络插件必须满足):

  1. Pod 间通信不需要 NAT:任何节点上的 Pod 可直接与任何其他节点上的 Pod 通信,无需 NAT
  2. 节点可以与所有 Pod 通信:节点上的进程(kubelet、daemon)可直接访问所有 Pod
  3. 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 与外部端点的流量规则:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: api-policy
spec:
  podSelector:
    matchLabels:
      app: api
  policyTypes:
  - Ingress
  - Egress
  ingress:
  - from:
    - namespaceSelector:         # 仅允许 frontend 命名空间
        matchLabels:
          kubernetes.io/metadata.name: frontend
    ports:
    - port: 8080
      protocol: TCP
  egress:
  - to:
    - podSelector:               # 仅允许访问 database Pod
        matchLabels:
          app: database
    ports:
    - port: 5432
      protocol: TCP

关键规则:

  • 无策略 = 全放行:没被 NetworkPolicy 选中的 Pod,所有流量放行
  • 有策略 = 默认拒绝:一旦 NetworkPolicy 选中 Pod,仅策略中明确允许的流量放行
  • 策略是累加的:多条 NetworkPolicy 选中同一 Pod,效果取并集
  • 需要 CNI 支持:NetworkPolicy 是 API 资源,需要 CNI 插件实现(Calico、Cilium 全面支持;Flannel 默认不支持,需额外部署组件)

第八章:权限和安全——谁能干什么?

前面的章节都在说"怎么让系统跑起来"。现在换个视角:谁有权限干什么?

8.1 四层防线:认证 → 授权 → 准入 → 容器

每次对 API Server 的请求都经历三道关卡:

请求 ──→ ① 认证(你是谁?)──→ ② 授权(你能做什么?)──→ ③ 准入(这个请求合规吗?)──→ etcd
  1. 认证:X.509 证书(kubelet 到 API Server 的通信)、Bearer Token(ServiceAccount)、OIDC(用户登录)
  2. 授权:最常见的是 RBAC——基于角色的访问控制
  3. 准入:Mutating Admission(可在持久化前修改对象——如注入 sidecar);Validating Admission(可拒绝请求——如禁止使用 latest 镜像标签)

8.2 RBAC 四件套

RBAC 通过四个 API 对象实现精细化权限管理:

        ServiceAccount                    Role / ClusterRole
         (谁要访问)                        (能做什么)
              │                                 │
              └───────────┬─────────────────────┘
                          │
                 RoleBinding / ClusterRoleBinding
                        (授权关系)
对象作用域做什么
RoleNamespace定义权限集——“可以读/写 pods, services”
ClusterRole集群同上,但作用范围是集群级别(如访问 nodes、所有 namespace 的资源)
RoleBindingNamespace把 Role/ClusterRole 授权给 Subject
ClusterRoleBinding集群把 ClusterRole 授权给集群范围的 Subject

ServiceAccount 是 Pod 的"身份证"——Pod 默认挂载所在 Namespace 的默认 ServiceAccount 的 token,并以此身份调用 API Server。

一个精细化的例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  namespace: frontend
  name: pod-reader
rules:
- apiGroups: [""]
  resources: ["pods"]
  verbs: ["get", "list", "watch"]   # 只能读,不能写、不能删
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  namespace: frontend
  name: reader-binding
subjects:
- kind: ServiceAccount
  name: monitoring-agent        # 仅此 ServiceAccount
  namespace: frontend
roleRef:
  kind: Role
  name: pod-reader

monitoring-agent 这个身份只能读 frontend 命名空间的 Pod,什么都改不了。

8.3 容器里跑什么用户?——SecurityContext

从"集群权限"往下走一层:容器自身的安全配置。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
securityContext:
  runAsNonRoot: true              # 必须以非 root 用户运行
  runAsUser: 1000                 # 指定 UID
  fsGroup: 2000                   # 挂载卷的文件系统属主
  seccompProfile:
    type: RuntimeDefault          # 使用默认 seccomp 过滤系统调用
  capabilities:
    drop:
    - ALL                         # 移除所有 Linux capabilities
    add:
    - NET_BIND_SERVICE            # 只加需要的能力(如绑定低端口)
  readOnlyRootFilesystem: true    # 根文件系统只读
  allowPrivilegeEscalation: false # 禁止任何方式提权(包括 setuid 二进制)

这些配置不会让你的应用变慢。它们是纯安全的防御层——即使容器内的进程被攻破,攻击者能做的事情也被严格限制。

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、禁止能力提升
1
2
3
4
5
6
7
8
9
# 在 Namespace 上实施
apiVersion: v1
kind: Namespace
metadata:
  name: production
  labels:
    pod-security.kubernetes.io/enforce: restricted  # 违规则拒绝创建
    pod-security.kubernetes.io/audit: restricted    # 违规则记录审计
    pod-security.kubernetes.io/warn: restricted     # 违规则显示警告

三个模式(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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: api-ingress
spec:
  ingressClassName: nginx
  rules:
  - host: api.example.com
    http:
      paths:
      - path: /users
        pathType: Prefix
        backend:
          service:
            name: user-service
            port:
              number: 80
      - path: /orders
        pathType: Prefix
        backend:
          service:
            name: order-service
            port:
              number: 80
  tls:                          # TLS 终结
  - hosts:
    - api.example.com
    secretName: api-tls-cert    # 证书和私钥来自此 Secret

Ingress 只是 API 对象,不包含实现。你需要部署一个 Ingress Controller(如 ingress-nginx、Kong、Traefik)来解释和执行 Ingress 规则。K8s 不内置 Ingress Controller——这是有意的设计选择。

9.3 Gateway API——新一代七层入口

Ingress 的功能不够用:金丝雀发布(按权重分流)、Header 路由、请求改写、流量镜像——这些在 Ingress 中只能通过 implementation-specific 的 annotation 表达,缺乏可移植性。

Gateway APIgateway.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 从本文学到的核心原则

  1. 声明式而非指令式——描述终点,控制回路持续开车
  2. API Server 是唯一数据总线——组件之间不直接通信,松耦合,可审计
  3. Pod 是原子调度单元——共享网络和存储的容器组
  4. 标签是松耦合粘合剂——Service 通过标签发现 Pod,而非硬编码 IP
  5. 控制回路是核心范式——observe → diff → act,幂等、水平触发
  6. 一切皆可插拔——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 v1.33 官方文档写成。K8s 演进迅速,架构细节可能随版本变化,请以 kubernetes.io/docs 为最新权威参考。