讲讲 StatefulSet vs Deployment
这两个都是管理一组 Pod 副本的 Controller,但解决的是完全不同性质的问题:Deployment 假设每个副本都是无差别、可互换的;StatefulSet 假设每个副本都有独一无二的身份,不能被随便替换。
最直观的区别:Pod 命名
你的 fleet-api 就是典型的 Deployment,之前你贴过的 Pod 名字长这样:
fleet-api-6958d5465d-nmg4g fleet-api-6958d5465d-shntt
6958d5465d 是 ReplicaSet 的哈希,nmg4g/shntt 是随机后缀——这两个 Pod 之间没有任何区别,谁死了 Deployment 就随便造一个新的随机名字补上去,调用方压根不关心自己连的是哪一个副本。
换成 StatefulSet 管理同样两个副本,名字会是:
mysql-0 mysql-1
固定的序号,不是随机哈希。mysql-0 这个身份是持久的——它重启、被重新调度到别的节点,名字还是 mysql-0,不会变成别的东西。
网络身份:要不要能单独找到某一个副本
Deployment 前面配一个普通 Service,你只能说”随便转发给这几个副本里的一个”(就是你现在 fleet-api 这个 Service 干的事)。
StatefulSet 通常搭配一个 Headless Service(clusterIP: None),这样每个 Pod 会额外获得一个可以单独寻址的 DNS 名字:
mysql-0.mysql.default.svc.cluster.local mysql-1.mysql.default.svc.cluster.local
这在数据库集群里是刚需——比如”写请求必须发给 mysql-0(主库),读请求可以发给 mysql-1/2(从库)”,这种”必须精确指定某一个成员”的场景,普通 Service 的负载均衡语义完全做不到。
存储:磁盘跟着身份走,不是跟着 Pod 走
这是最关键的差异。StatefulSet 用 volumeClaimTemplates,会给每个序号单独创建一个 PVC(data-mysql-0、data-mysql-1……),并且这个绑定关系是长期固定的:mysql-0 不管重启多少次、被调度到哪个节点,重新起来后永远拿回它自己那块盘,数据不丢。
普通 Deployment 没有这个机制——如果你给 Deployment 挂载持久卷,通常所有副本要么共享同一个 PVC(会有并发写冲突问题),要么干脆就不该用 Deployment 装有状态应用,这正是 StatefulSet 存在的意义。
创建/更新顺序:StatefulSet 讲究先后顺序,Deployment 不讲究
Deployment 滚动更新时,多个新 Pod 可以并行起来,谁先 Ready 无所谓。StatefulSet 默认严格按序号顺序操作:扩容时 0 号必须 Running+Ready 之后才会创建 1 号;缩容/更新时反过来,从最大序号开始处理。这对”集群成员需要按顺序加入”的场景(比如数据库主从初始化、需要先选出 leader 再让 follower 加入)很重要。
一句话版本 + 例子
| Deployment | StatefulSet | |
|---|---|---|
| 副本关系 | 完全对等、可互换 | 每个副本有独立身份 |
| Pod 命名 | 随机哈希后缀 | 固定序号(-0, -1, -2) |
| 存储 | 通常无状态或共享 | 每个副本独立 PVC,重启后原样带回 |
| 单独寻址 | 不支持 | 支持(Headless Service + DNS) |
| 创建/更新顺序 | 无序、可并行 | 严格按序号 |
| 典型场景 | Web/API 服务(你的 fleet-api) | MySQL/Postgres 集群、Kafka、ZooKeeper、etcd |
顺带一提——我们之前聊了很多的 etcd,如果你要在 K8s 上跑一个独立的 etcd 集群(而不是像你 kind 集群里这种 kubeadm 管理的静态 Pod),几乎总是用 StatefulSet 部署,原因就是上面这几条全都用得上:每个 etcd 成员需要固定的身份用于 Raft 里的 peer 寻址(etcd-0、etcd-1、etcd-2 各自持久化的 WAL/snapshot 数据不能跟别的成员搞混),这也是为什么”有状态”和”StatefulSet”这两个词是绑在一起的。
