K8s 系统设计哲学:从第一性原理推导整套架构

这篇博客是一次思想梳理。我没有去读 K8s 文档,而是反过来问:如果今天让我从零设计一个分布式编排系统,我会推导出什么?然后对照 K8s 看,哪些是必然,哪些是巧合,哪些是历史包袱。

我想搞清楚三件事:

  1. K8s 有哪些子系统?为什么是这些,而不是别的?
  2. 为什么有这么多 resource kind?它们怎么分类?
  3. 为什么我们说"K8s 化"某个系统,而不是"部署在 K8s 上"?

下面是我推导出来的框架。


一、起点:两个不可回避的事实

要把"运行很多程序在很多机器上"做对,你只能接受两个现实:

  1. 分布式系统里,部分失败是常态。 机器会挂、网络会断、磁盘会满。
  2. 操作者的意图 ≠ 系统的现实。 你说"我要 3 个副本",但此刻可能只有 2 个在跑。意图稳定,现实漂移。

K8s 所有设计,都是这两个事实的逻辑推论。 不是 Google 拍脑袋设计的,是被现实逼出来的。


二、四个必然推论 → 核心架构

推论 1:必须 declarative,不能 imperative

部分失败下,"执行命令"这个动作本身就没意义——失败了你不知道做了一半还是没做。

解法:不描述"做什么",只描述"应该是什么"。用户写 desired state,系统持续把 observed state 推过去。

这就是 reconcile loop:整个 K8s 只有这一个核心循环。

graph LR A[Desired State<br/>用户写的 YAML] --> R{Reconciler} O[Observed State<br/>etcd 里的真实状态] --> R R -->|diff & act| W[Real World<br/>Pods/Services/...] W -->|observe| O

推论 2:必须有唯一的真相源

意图分散就永远没有收敛目标 → etcd(Raft 共识存储)
但 etcd 只解决"存",还需要:

  • 唯一写入口 = API server(做认证/鉴权/校验)
  • 唯一读出口 = API server(所有 controller 都 watch 它)

推论 3:所有东西都得是"资源"

既然 reconcile 是唯一模式,就把它泛化。所有可管理的东西都建模成 resource,每个 resource 配一个 controller。Controller 之间互不通信,只通过 etcd 交换状态。

这是 level-triggered, edge-blind 设计——controller 可以崩溃、重启、丢消息,不影响正确性。

推论 4:机制与策略必须分离(Unix 哲学)

graph TB subgraph "L4 工作负载" APP[用户的 Pods] end subgraph "L3 机制层 - 怎么做" K[kubelet] CRI[CRI<br/>容器运行时] CNI[CNI<br/>网络插件] CSI[CSI<br/>存储插件] end subgraph "L2 策略层 - 做什么" CTL[Controllers] SCH[Scheduler] end subgraph "L1 内核接口" API[kube-apiserver] end subgraph "L0 真相源" ETCD[(etcd)] end APP --> K K --> CRI K --> CNI K --> CSI K -.watch.-> API CTL -.watch.-> API SCH -.watch.-> API API <--> ETCD

核心洞察:K8s 就是 Unix 哲学在分布式系统的复刻。

  • etcd = 文件系统
  • API server = 内核
  • resources = 文件
  • controllers = daemons
  • reconcile loop = 唯一的 syscall pattern

三、为什么是这些 Resource Kind?

不要去背 K8s 有哪些 resource,反过来问:运行一个程序到底需要什么?

七个原子需求:算力 / 镜像 / 配置 / 存储 / 网络 / 身份 / 生命周期

每一类需求,都因为变化频率、权限边界、生命周期的差异被进一步拆成多个 kind。

核心原则

变化频率不同、生命周期不同、权限边界不同的东西,必须拆成不同 resource。

一锅炖会导致:改一维度被迫动所有维度。这就是耦合的代价。

分类总表

graph TB subgraph "Workload - 跑什么" POD[Pod] --> RS[ReplicaSet] RS --> DEP[Deployment] DEP --> HPA[HPA] STS[StatefulSet] DS[DaemonSet] JOB[Job/CronJob] end subgraph "Config - 带什么参数" CM[ConfigMap] SEC[Secret] end subgraph "Storage - 数据放哪" PVC[PVC<br/>用户视角] --> PV[PV<br/>实现视角] PV --> SC[StorageClass<br/>供给规则] SC --> CSID[CSI Driver] end subgraph "Networking - 怎么通信" SVC[Service<br/>L4] ING[Ingress/Gateway<br/>L7] NP[NetworkPolicy<br/>策略] end subgraph "Identity - 它是谁" SA[ServiceAccount] --> RB[RoleBinding] RB --> ROLE[Role] end subgraph "Policy - 不能干什么" Q[ResourceQuota] LR[LimitRange] PDB[PodDisruptionBudget] end subgraph "Extension - 怎么加新东西" CRD[CustomResourceDefinition] OP[Operator] CRD --> OP end

每类的设计模式

类别 回答什么问题 设计模式
Workload 跑什么、几个、怎么重启 controller 层级:Pod → RS → Deployment → HPA
Config 带什么参数 与 Pod 解耦 + RBAC 隔离(Secret 单独存在为了权限边界)
Storage 数据放哪 声明(PVC) / 实现(PV) / 供给(SC) / 驱动(CSI)四层分离
Networking 怎么通信 L4 / L7 分层 + 连通性与策略正交
Identity 它是谁 能力(Role) 与 授予(Binding) 分离
Policy 不能干什么 全部是"约束"而非"实体"
Extension 怎么加新东西 元抽象——让 K8s 自己可扩展

三个正交维度

每个 resource 在三个轴上有坐标:

  1. 抽象层次:用户视角 ↔ 实现视角(PVC vs PV、Service vs EndpointSlice)
  2. 变化频率:高频变(Pod、ConfigMap)↔ 低频变(StorageClass、ClusterRole)
  3. 作用范围:对象级 ↔ namespace 级 ↔ 集群级

这三个轴上每个不同的点,都是一个独立的变化维度,必须独立建模。


四、为什么叫"K8s 化"而不是"部署在 K8s 上"?

"化"是世界观的迁移,不是物理位置的迁移。

  • 容器化:不是把程序放进容器,是接受"无状态、不可变"的世界观
  • 服务化:不是拆进程,是接受"网络是边界、failure 是一等公民"
  • K8s 化:不是 kubectl apply,是让系统采纳 K8s 的运维本体论

K8s 的世界观(五条)

  1. 一切皆 resource
  2. 声明而非命令
  3. Reconcile 是唯一动词
  4. Level-triggered, eventually consistent
  5. 通过 label/ref 组合

一个 Postgres 被"K8s 化"前后

维度 化之前 化之后
接口 psql、SSH、配置文件 kind: PostgresCluster
谁懂运维 DBA 脑子 + Confluence Controller 代码
扩容 手动开机器 + basebackup spec.replicas: 5
故障切换 告警 → 人 → 改 DNS Controller watch → 自动 promote
升级 周末停机 + 祈祷 spec.version,rolling upgrade
错误模型 Pager → 人介入 Reconcile 失败 → 自动重试
审计 history + Slack 截图 所有变更过 API server

注意:Postgres 本身没变。变的是"谁在操作它"——从人的肌肉记忆,迁移到一段持续运行的代码。

这就是"化"的本质:运维知识的载体从 tribal knowledge 变成 executable code。

Operator 是 K8s 哲学的"终极投射"

绝大多数人没意识到的点:Operator 模式不是 K8s 新增的特性,它就是 K8s 本身的样子。

graph LR subgraph "K8s 内置" D1[Deployment CRD] --> C1[deployment-controller] C1 --> RS1[创建 ReplicaSet] end subgraph "你写的 Operator" D2[PostgresCluster CRD] --> C2[postgres-operator] C2 --> RS2[创建 StatefulSet+Service+Secret] end D1 -.形状一模一样.-> D2

Deployment-controller 和 PostgresOperator 在 K8s 看来没有任何区别。

对比其他系统:

  • Linux:用户态写不了 syscall——内核机制不向上传递
  • 数据库:SQL planner 是封闭的,stored procedure 变不成 query planner
  • Hadoop:调度框架是调度框架,用户作业进不去调度逻辑

唯独 K8s,把自己的内核模式完全开放给用户复用。 这就是"终极投射"——设计本身是分形(fractal) 的,从最内层(pod 调度)到最外层(管 ML training),每一层都是同一个图案的复制。

"真的 K8s 化了"的五条判据

  1. 它有自己的 CRD 吗?
  2. 删除一个底层 Pod,会被恢复成"该有的样子"吗?
  3. 运维操作能通过修改 spec 字段触发吗?
  4. 和 K8s 原生抽象(Service、Secret、RBAC、监控)天然集成吗?
  5. 运维知识有没有从人脑迁移到 controller 代码?

五条全过,才算真正 K8s 化。否则只是"部署在 K8s 上"。


五、整套思想浓缩成一张图

graph TB F1[事实 1<br/>部分失败是常态] F2[事实 2<br/>意图 ≠ 现实] F1 --> R1[推论:Declarative] F2 --> R1 R1 --> RL[Reconcile Loop<br/>唯一核心模式] RL --> N1[需要唯一真相源] N1 --> ETCD[(etcd + API server)] RL --> N2[需要泛化模式] N2 --> RES[一切皆 Resource] RES --> KIND[Resource Kinds<br/>按变化频率/权限/生命周期拆分] KIND --> CAT[七类:Workload/Config/Storage<br/>Network/Identity/Policy/Extension] RES --> EXT[CRD + Custom Controller] EXT --> OP[Operator 模式] OP --> K8SIFY[K8s 化<br/>运维知识 → executable code] RL --> SEP[机制/策略分离] SEP --> LAYER[L0 etcd → L1 API → L2 策略 → L3 机制 → L4 负载]

一句话收尾

K8s 不是被设计出来的,它是"运行很多程序在很多机器上"这件事在两个残酷现实下的必然产物。

理解任何 K8s 的概念,问三个问题:

  1. 它在 reconcile 什么? Desired state 是谁写的?Observed state 来自哪?
  2. 它解耦了哪个变化维度? 如果不拆,会跟谁耦合?
  3. 它在哪个抽象层次? 用户面还是实现面?机制还是策略?

答得出来,就理解了;答不出来,就是还没看到底。