概述
在我们前面介绍的几种 Workload 中,每种都各有特色,用于不同的场景,但是,这些都有一个共性,就是创建出来的 Pod 都是一致的。所谓的一致的,就是说,假设我们是使用的 ReplicationSet,创建了 3 个 Pod,那么这 3 个 Pod 除了名字一定不一样之外,其他的属性可能都是一样的,包括运行的参数和模式以及数据存储。
虽然这个在普通的应用中是没问题的,例如是一个 Web Server,只是处理一下逻辑,然后保存到后段的 DB 中。但是,如果通过 ReplicationSet 来部署一个 DB 的多实例,那么可能就会存在一些问题了,通过 Kurbernetes 来运行多实例的 DB,很多时候出于数据持久化考虑,我们是使用 PVC(后续会说),那么当使用 PVC 和 PV 的时候,可能就会变成这个样子:
3 个节点的数据都写到同一个 PV 中去了,这样肯定是不行的。
StatefulSet介绍
为了解决 Pod 的状态性的问题,Kubernetes 引入了 StatefulSet 的概念,通过 StatefulSet,你可以得到这些好处:
- Pod 有单独的存储和固定的网络标识
- 需要配备一个 headless Service,用于 DNS
- 可以通过 DNS 快速发现其他 Pod
- 可以直接通过 Pod DNS 通信
是的,没错,每隔 Pod 都可以通过 DNS 访问到,而这种特性,在其他的 Workload 中是不能实现的。通过 StatefulSet,可以让 Pod 持有状态,即使因为故障 Pod 重建了,那么对应的 Pod 的名字和数据都会保留,和重建之前没有什么区别。
使用 StatefulSet
使用 StatefulSet 有一个硬性要求,那就是必须先建立一个 HeadlessService1,然后绑定这个 HeadlessService 到 StatefulSet。
下面我就以 MongoDB 为示例,演示一下如何创建 StatefulSet,因为我还没有介绍到关于 PVC 和 PV 的内容,所以这个 MongoDB 将使用本地存储卷:
- 创建 Headless Service
apiVersion: v1
kind: Service
metadata:
name: mongo
labels:
app: mongo
spec:
ports:
- name: mongo
port: 27017
targetPort: 27017
clusterIP: None
selector:
app: mongo
- 创建 StatefulSet
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mongo
spec:
selector:
matchLabels:
app: mongo
serviceName: "mongo"
replicas: 3
podManagementPolicy: Parallel
template:
metadata:
labels:
app: mongo
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: "app"
operator: In
values:
- mongo
topologyKey: "kubernetes.io/hostname"
containers:
- name: mongo
image: mongodb:latest
command:
- mongod
- "--bind_ip_all"
- "--replSet"
- rs0
ports:
- containerPort: 27017
volumeMounts:
- name: mongo-data
mountPath: /data/db
volumes:
- name: mongo-data
hostPath:
path: /home/liqiang/mongodb/db
这里因为使用的是 Host 的数据目录,所以需要保证不会有两个 Pod 调度到同一个机器上:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: "app"
operator: In
values:
- mongo
topologyKey: "kubernetes.io/hostname"
然后规定了所有的 Pod 可以同时运行:
podManagementPolicy: Parallel
除了同时运行之外,还有另外一种形式是顺序执行,也就是说假设有 3 个 Pod,那么 StatefulSet 会等第一个 Pod 已经 Running 之后再运行第二个 Pod,依次到所有的 Pod 都运行完成。
通过 DNS 访问 Pod
前面说过了,在 StatefulSet 中,可以通过 DNS 直接访问 Pod,那么我们就试验一下:
[root@liqiang.io]# kubectl exec -it mongo-0 /bin/sh
# mongo --host mongo-1.mongo
MongoDB shell version v4.2.0
connecting to: mongodb://mongo-1.mongo:27017/?compressors=disabled&gssapiServiceName=mongodb
Implicit session: session { "id" : UUID("bfc54171-aa31-4c87-bbf5-e4ddaf5897f8") }
MongoDB server version: 4.2.0
Server has startup warnings:
可以发现,我们可以通过 <pod-name>.<service>
的形式访问 Pod,这其实和 StatefulSet 的设计是有关系的,因为在类似于 Deployment 的 Controller 中,Pod 的名字是不固定的,而在 StatefulSet 中,Pod 的名字都是固定的,具体更多的区别,我们可以看下面一节。
和 ReplicaSet 对比
- 因为有状态的 Pod 彼此不同,通常希望操作的是其中特定的一个,所以一个 StatefulSet 通常要求你创建一个用来记录每个 Pod 网络标记的 headlessService。通过这个 service,每个 pod 都拥有独立的 DNS 记录,而这在 ReplicaSet 中不可行。
- 因为 StatefulSet 缩容任何时候只会操作一个 Pod 实例,所以有状态应用的缩容不会很迅速。
- StatefulSet 在有实例不健康的情况下,是不允许做缩容操作的。
- 持久存储
- 一个 StatefulSet 可以拥有一个或多个卷声明模板,这些声明会在创建 pod 前创建出来,绑定到一个 pod 实例上。
- 扩容 StatefulSet 会创建两个 API 对象,一个 Pod 和 一个卷声明;但是,缩容 StatefulSet 却会删除一个 Pod 对象,而会留下 PVC,因为一旦删除 PVC 则意味着 PV 会被回收。
StatefulSet at-most-one
Kubernetes 必须保证两个拥有相同标记和绑定相同持久卷声明的有状态的 Pod 实例不会同时运行。一个 Statefulset 必须保证有状态的 pod 实例的 at-most-one 语义。也就是说,StatefulSet 必须保证一个 Pod 不再运行后,才会去创建它的替换 pod。这对于处理节点故障有很大影响。