K8s 系统设计哲学:从第一性原理推导整套架构
这篇博客是一次思想梳理。我没有去读 K8s 文档,而是反过来问:如果今天让我从零设计一个分布式编排系统,我会推导出什么?然后对照 K8s 看,哪些是必然,哪些是巧合,哪些是历史包袱。
我想搞清楚三件事:
- K8s 有哪些子系统?为什么是这些,而不是别的?
- 为什么有这么多 resource kind?它们怎么分类?
- 为什么我们说"K8s 化"某个系统,而不是"部署在 K8s 上"?
下面是我推导出来的框架。
一、起点:两个不可回避的事实
要把"运行很多程序在很多机器上"做对,你只能接受两个现实:
- 分布式系统里,部分失败是常态。 机器会挂、网络会断、磁盘会满。
- 操作者的意图 ≠ 系统的现实。 你说"我要 3 个副本",但此刻可能只有 2 个在跑。意图稳定,现实漂移。
K8s 所有设计,都是这两个事实的逻辑推论。 不是 Google 拍脑袋设计的,是被现实逼出来的。
二、四个必然推论 → 核心架构
推论 1:必须 declarative,不能 imperative
部分失败下,"执行命令"这个动作本身就没意义——失败了你不知道做了一半还是没做。
解法:不描述"做什么",只描述"应该是什么"。用户写 desired state,系统持续把 observed state 推过去。
这就是 reconcile loop:整个 K8s 只有这一个核心循环。
推论 2:必须有唯一的真相源
意图分散就永远没有收敛目标 → etcd(Raft 共识存储)
但 etcd 只解决"存",还需要:
- 唯一写入口 = API server(做认证/鉴权/校验)
- 唯一读出口 = API server(所有 controller 都 watch 它)
推论 3:所有东西都得是"资源"
既然 reconcile 是唯一模式,就把它泛化。所有可管理的东西都建模成 resource,每个 resource 配一个 controller。Controller 之间互不通信,只通过 etcd 交换状态。
这是 level-triggered, edge-blind 设计——controller 可以崩溃、重启、丢消息,不影响正确性。
推论 4:机制与策略必须分离(Unix 哲学)
核心洞察:K8s 就是 Unix 哲学在分布式系统的复刻。
- etcd = 文件系统
- API server = 内核
- resources = 文件
- controllers = daemons
- reconcile loop = 唯一的 syscall pattern
三、为什么是这些 Resource Kind?
不要去背 K8s 有哪些 resource,反过来问:运行一个程序到底需要什么?
七个原子需求:算力 / 镜像 / 配置 / 存储 / 网络 / 身份 / 生命周期
每一类需求,都因为变化频率、权限边界、生命周期的差异被进一步拆成多个 kind。
核心原则
变化频率不同、生命周期不同、权限边界不同的东西,必须拆成不同 resource。
一锅炖会导致:改一维度被迫动所有维度。这就是耦合的代价。
分类总表
每类的设计模式
| 类别 | 回答什么问题 | 设计模式 |
|---|---|---|
| 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 在三个轴上有坐标:
- 抽象层次:用户视角 ↔ 实现视角(PVC vs PV、Service vs EndpointSlice)
- 变化频率:高频变(Pod、ConfigMap)↔ 低频变(StorageClass、ClusterRole)
- 作用范围:对象级 ↔ namespace 级 ↔ 集群级
这三个轴上每个不同的点,都是一个独立的变化维度,必须独立建模。
四、为什么叫"K8s 化"而不是"部署在 K8s 上"?
"化"是世界观的迁移,不是物理位置的迁移。
- 容器化:不是把程序放进容器,是接受"无状态、不可变"的世界观
- 服务化:不是拆进程,是接受"网络是边界、failure 是一等公民"
- K8s 化:不是 kubectl apply,是让系统采纳 K8s 的运维本体论
K8s 的世界观(五条)
- 一切皆 resource
- 声明而非命令
- Reconcile 是唯一动词
- Level-triggered, eventually consistent
- 通过 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 本身的样子。
Deployment-controller 和 PostgresOperator 在 K8s 看来没有任何区别。
对比其他系统:
- Linux:用户态写不了 syscall——内核机制不向上传递
- 数据库:SQL planner 是封闭的,stored procedure 变不成 query planner
- Hadoop:调度框架是调度框架,用户作业进不去调度逻辑
唯独 K8s,把自己的内核模式完全开放给用户复用。 这就是"终极投射"——设计本身是分形(fractal) 的,从最内层(pod 调度)到最外层(管 ML training),每一层都是同一个图案的复制。
"真的 K8s 化了"的五条判据
- 它有自己的 CRD 吗?
- 删除一个底层 Pod,会被恢复成"该有的样子"吗?
- 运维操作能通过修改 spec 字段触发吗?
- 和 K8s 原生抽象(Service、Secret、RBAC、监控)天然集成吗?
- 运维知识有没有从人脑迁移到 controller 代码?
五条全过,才算真正 K8s 化。否则只是"部署在 K8s 上"。
五、整套思想浓缩成一张图
一句话收尾
K8s 不是被设计出来的,它是"运行很多程序在很多机器上"这件事在两个残酷现实下的必然产物。
理解任何 K8s 的概念,问三个问题:
- 它在 reconcile 什么? Desired state 是谁写的?Observed state 来自哪?
- 它解耦了哪个变化维度? 如果不拆,会跟谁耦合?
- 它在哪个抽象层次? 用户面还是实现面?机制还是策略?
答得出来,就理解了;答不出来,就是还没看到底。