1. NeoSet 是什么
为什么要造这个轮子
原生 StatefulSet 有四个生产痛点:
痛点 1:发版必须重建 Pod
原生 STS 滚动更新时,每个 Pod 被删除重建——IP 变、Volume 重新挂载、启动慢。MySQL 主从集群每次发版主库都要重启切主,业务感知明显。
痛点 2:没有金丝雀和分批
原生 STS 的 RollingUpdate 从最大序号逐个滚动。不能"先发 1 个观察 10 分钟,没问题再发 30%,最后全量"。要做金丝雀得自己造轮子。
痛点 3:发版途中扩容会混版本
发版到一半(5 个 Pod 里前 2 个已是新版本),业务量突增要扩容到 8 个。原生 STS 新建的 3 个 Pod 用新版本 image——但新版本还没验证完,这 3 个新 Pod 跟那 3 个旧版本 Pod 混在一起,行为不可预测。
痛点 4:回滚不方便
原生 STS 的回滚靠 partition——把 partition 设回 0 让旧版本 Pod "更新"回旧版本。但这是反向滚动,又要逐个重建。没有"快速回到某个历史版本"的能力。
NeoSet 的六个核心能力
| 能力 | 做什么 | 解决什么痛点 |
|---|---|---|
| 原地升级 | 不删 Pod,PATCH 改 container image | 痛点 1:IP 不变、Volume 不动 |
| 金丝雀发布 | partition: replicas-1 只发最后 1 个 | 痛点 2:先小范围验证 |
| 分批发布 | 逐步调 partition + MaxUnavailable 控制 | 痛点 2:控制爆炸半径 |
| 暂停/恢复 | pause: true 任意时刻停住 | 痛点 2:人工介入窗口 |
| 用旧镜像扩容 | 扩容时用"当前稳定版本"image | 痛点 3:不混版本 |
| 历史版本回滚 | ControllerRevision 存历史,一键回滚 | 痛点 4:快速回退 |
跟原生 StatefulSet 的对比
原生 STS 滚动更新(5 副本):
mysql-4 删除 → 重建(新IP, 重挂Volume)→ ready
mysql-3 删除 → 重建 → ready
...逐个进行
NeoSet 原地升级 + 金丝雀:
阶段1 (金丝雀): partition=4
→ 只 PATCH mysql-4 的 image,等 ready
→ pause=true,停住观察
阶段2 (分批): pause=false, partition=2
→ PATCH mysql-3, mysql-2 的 image
→ 等两个都 ready
阶段3 (全量): partition=0
→ PATCH mysql-1, mysql-0 的 image
→ 完成
全程:IP 不变、Volume 不动、秒级完成
"用旧镜像扩容"是什么意思
这是生产环境高频场景:
当前状态: replicas=5, 正在发版 v1→v2
mysql-0~1: v2(已更新)
mysql-2~4: v1(待更新)
业务突增,扩容到 8:
❌ 原生 STS: mysql-5~7 用 v2(新版本没验证完就扩散了)
✅ NeoSet: mysql-5~7 用 v1(当前稳定版本)
发版继续:
mysql-2~7 逐批原地升级到 v2
全程不混版本
NeoSet 怎么做到的?在 status 里记一个 currentImage(当前稳定版本镜像)。扩容时新 Pod 用 currentImage,不用 spec.template 里的新 image。等发版完成后,currentImage 更新为新版本。
"历史版本回滚"怎么实现
NeoSet 用 ControllerRevision 存历史版本(跟 StatefulSet 一样,但机制不同):
NeoSet mysql (当前 template: v2)
│
├── ControllerRevision mysql-v1 (template v1 快照)
└── ControllerRevision mysql-v2 (template v2 快照) ← 当前
回滚到 v1:
kubectl rollout undo neoset mysql --to-revision=1
→ Controller 把 v1 的 template 应用回 NeoSet
→ 原地升级把所有 Pod 改回 v1 image
→ 不重建,秒级回滚
原地升级让回滚也变快——原生 STS 回滚是反向重建,NeoSet 回滚是反向 PATCH image,速度快一个数量级。
整体架构
用户提交 NeoSet CR
│
▼
┌─────────────────────────────────────────┐
│ Admission Webhook │
│ - 配额校验(副本数、资源上限) │
│ - 默认值填充(maxUnavailable 默认 1) │
│ - 金丝雀参数校验 │
└──────────────┬──────────────────────────┘
│ 通过
▼
┌─────────────────────────────────────────┐
│ NeoSet Controller │
│ │
│ Reconcile: │
│ 1. 读 spec(期望状态 + 更新策略) │
│ 2. 读 status(当前进度 + currentImage) │
│ 3. 判断该干什么: │
│ ├─ 扩容?→ 用 currentImage 建新 Pod │
│ ├─ 发版?→ 算下一批,PATCH image │
│ ├─ 暂停?→ 停住不动 │
│ └─ 回滚?→ 找历史版本,PATCH 回去 │
│ 4. 等就绪 │
│ 5. 更新 status(进度 + currentImage) │
│ 6. 存 ControllerRevision(版本变了时) │
└───────────────────────────────────────────┘
系列规划
| 章 | 主题 | 核心代码 |
|---|---|---|
| 1 | NeoSet 是什么 | 项目背景(本章) |
| 2 | 设计 CRD | 完整 spec/status Go 类型 |
| 3 | ControllerRevision 版本管理 | 历史版本存储、回滚逻辑 |
| 4 | Admission Webhook | 配额校验、默认值填充 |
| 5 | 实现原地升级 | PATCH image、优雅摘除、资源热更新 |
| 6 | 金丝雀与分批发布 | Partition 状态机、OnDelete、更新 Hooks |
| 7 | 用旧镜像扩容 | currentImage 追踪、Parallel/PVC 保留策略 |
| 8 | 就绪判断与进度追踪 | ReadinessGate、minReadySeconds、Conditions |
| 9 | 生产化部署 | Leader Election、指标、e2e 测试 |
每章都有可运行的 Go 代码,不是伪代码。到最后能 make install && make run 跑起来一个真实的 NeoSet Controller。
前置阅读
- StatefulSet控制循环 — Controller 系列第 4 章,原生 STS 的 Reconcile
- 编写CRD — Controller 系列第 7 章,CRD 设计基础
- 原地升级 — Starter 第 15 章,原地升级概念
- 使用Kubebuilder — Controller 系列第 9 章,Kubebuilder 用法
思考题
问题 1:原地升级时 Pod IP 不变。如果新版本代码有 bug 导致容器 CrashLoopBackOff,下游还连着这个 IP,会发生什么?
问题 2:用旧镜像扩容时,新 Pod 用 v1 镜像。但 spec.template 已经是 v2。如果此时 Controller 重启了,它怎么知道该用 v1 还是 v2 扩容?
问题 3:原地升级回滚比重建回滚快。但有什么场景下原地回滚反而不如重建回滚?
解答
问题 1:CrashLoopBackOff 的 Pod 会被 EndpointSlice Controller 从端点列表摘掉(ready=false)。下游通过 Service 访问不会路由到它。但如果是 Headless Service + 客户端直连 IP,客户端会连上一个不断重启的容器——这就是原地升级的风险:IP 不变是优势也是风险。解决方案:原地升级后必须配 readinessProbe,让 EndpointSlice 及时摘除不健康的 Pod。
问题 2:靠 status.currentImage。Controller 每次发版时,先更新 spec.template(新 image),但不立即更新 status.currentImage——等所有 Pod 都更新完才更新 currentImage。扩容时读 status.currentImage 而不是 spec.template.spec.containers[0].image。Controller 重启后从 etcd 读 status,currentImage 还在,逻辑一致。
问题 3:当新版本修改了 Volume 挂载路径或数据格式时。原地回滚只改 image,Volume 里的数据还是新版本写的——旧版本代码读不懂新格式的数据,会出错。这种场景下重建回滚更安全:新 Pod 用旧 image + 全新 Volume(从快照恢复)。原地升级的前提是"新旧版本的数据格式兼容"。
下一章
下一章我们设计 NeoSet 的 CRD——完整的 spec/status Go 类型定义,包含更新策略、版本管理、进度追踪字段。