12. Kubernetes - StatefulSet
StatefulSet
Deployment 控制器对于无状态的服务的编排很容易,但是对于有状态的服务就显得无能为力了。
当然,在使用 docker 的时候就有提到过,对于有状态的服务,是不建议放到容器中的。
无状态服务(Stateless Service)
:服务不会在本地存储持久化数据,多个实例对于同一个请求响应的结果完全一致,管理员可以随意的增删副本数。有状态服务(Stateful Service)
:与之相反,服务需要在本地存储持久化数据。类似 MySQL、Redis 这类,多副本运行在集群上数据会不一致。
以开发中遇到的问题为例:
在以前开发以 Tomcat 运行 Java 项目时,用户在登录后,处理登录逻辑的那台后端服务器会在本地缓存目录存放用户登录的 Session,标记用户已经登录成功。如果此时后端是多个节点,用户下次请求进来,经过负载均衡的调度算法可能会被分配到另外的节点上,但那台机器的缓存目录却没该用户的 Session。此时用户就会因为验证失败而退出登录。这在使用中是绝对不允许出现的。此时的 Tomcat 服务就是有状态服务。
为了解决这个问题,一般都会通过修改 Tomcat 的配置文件,通过第三方 Redis 进行节点之间 Session 共享。这样对于各个节点而言,本地就不再存储数据。用户的每次请求,不管被分配到哪个节点,获取 Session 都是去 Redis 中拿取,也就不会出现验证失败的问题。此时的 Tomcat 就变成了无状态服务。
有状态的服务部署往往非常复杂。可能需要关注每个节点的启动顺序,配置文件差异,唯一性保证等。此时如果想要用 Deployment 来管理就会非常困难。此时就需要一种新的控制器专门用于处理有状态服务,这就是 StatefulSet。
StatefulSet 控制器为 Pod 提供唯一的标识。它可以保证部署和 scale 的顺序。具有以下几个特点:
- 稳定的持久化存储,即 Pod 重新调度后还是能访问到相同的持久化数据,基于 PVC 来实现。
- 稳定的网络标志,即 Pod 重新调度后其 PodName 和 HostName 不变,基于 Headless Service 来实现。
- 有序部署,有序扩展,即 Pod 是有顺序的,在部署或扩展的时候依据定义的顺序依次进行(即从 0 到 N-1,在下一个 Pod 运行之前所有排在前面的 Pod 必须都是 Running 和 Ready 状态),基于 init containers 来实现。
- 有序收缩,有序删除(即从 N-1 到 0)
StatefulSet 必须事先拥有拥有以下组件:
- Headless Service,用于负责 Pod 的网络身份,控制网络域。
- 提供给 PersistentVolume Claim(PVC)绑定的 PersistentVolume Provisioner(PV),用于给 Pod 提供稳定的存储。
Headless Service
Service
是应用服务的抽象,后续会专门详细的说明,这里只是简单的了解。
它的作用在于通过 Labels 为应用提供负载均衡和服务发现,每个 Service 都会自动分配一个 Cluster IP
和 DNS
域名解析,在集群内部可以通过它们直接访问后端 Pod。
比如,一个 Deployment 有 3 个 Pod,定义一个 Service 来作为它的访问入口:
- Cluster IP:当访问 Service 分配的 Cluster IP(VIP)地址时,它会把请求转发到该 Service 所代理的 Endpoints 列表中的某一个 Pod 上。
- DNS:当访问
Service名称.命名空间.svc.cluster.local
这条 DNS 记录,就可以访问到指定命名空间下面对应的 Service,然后再代理的某一个 Pod。
对于 DNS 这种方式,不同类型的 Service 访问 Service名称.命名空间.svc.cluster.local
的原理不同:
- 普通的 Service,通过集群中的 DNS 服务解析到的 Service 的 Cluster IP。
- Headless Service,由于没有 Cluster IP,请求直接解析到代理的某一个具体的 Pod 的 IP 地址。
Headless Service 资源清单示例:
apiVersion: v1
kind: Service
metadata:
name: svc-nginx
namespace: default
labels:
app: nginx
spec:
ports:
- name: svc-http
port: 80
# 不设置 Cluster IP 地址
clusterIP: None
selector:
app: nginx
创建完成后可以通过命令查看:
kubectl get svc
可以看到 Cluster IP 没分配,是 None,证明这是一个 Headless Service。
Headless Service 会在集群 DNS 中增加解析:svc-nginx.default.svc.cluster.local
如果后端代理了 Pod,则会为每个 Pod IP 增加解析:pod名称.svc-nginx.default.svc.cluster.local
这样子就实现了在集群中通过 DNS 访问 Service 和 Pod,而不是 IP 地址访问。
PV
由于是测试,所以采用 HostPath 的的 volume 方式提供 PV 存储卷。
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv-nginx-001
spec:
capacity:
storage: 1Gi
accessModes: [ "ReadWriteOnce" ]
hostPath:
path: /data/pv-nginx-001
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv-nginx-002
spec:
capacity:
storage: 1Gi
accessModes: [ "ReadWriteOnce" ]
hostPath:
path: /data/pv-nginx-002
注意,后面使用几个副本就需要多少个 PV 提供绑定,完成后查看:
kubectl get pv
此时可以看到所有的 PV 处于的状态:Available
StatefulSet 资源清单
有了 Headless Service 和 PV 就能创建 StatefulSet 资源清单:
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: sts-nginx
namespace: default
spec:
# 指定 Service
serviceName: svc-nginx
replicas: 2
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx
ports:
- containerPort: 80
volumeMounts:
- name: pvc-nginx
mountPath: /usr/share/nginx/html
# 配置 PVC
volumeClaimTemplates:
- metadata:
name: pvc-nginx
spec:
accessModes:
- "ReadWriteOnce"
resources:
requests:
storage: 1Gi
特殊字段说明:
-
serviceName
:指定管理当前 StatefulSet 的 Service 名称,该服务必须在 StatefulSet 之前存在,并且负责该集合的网络标识。 -
volumeMounts
:这里关联的不是 volume 而是volumeClaimTemplates(PVC)
。 -
volumeClaimTemplates
:该属性会自动创建 PVC 对象,PVC 被创建后会自动去关联当前系统中合适的 PV 进行绑定。
此时查看系统中 PV 和 PVC 状态:
kubectl get pvc
kubectl get pv
可以看到:
- 系统创建了两个 PVC 对象,名称为:
pvc-nginx-sts-nginx-索引
,状态为Bound
,分别绑定到之前创建的 PV 上面。 - 此时 PV 的状态也变更称为了
Bound
。 - 如果此时副本数多余 PV 数量,多出来的副本就会一直处于 Pending 状态,对应的 PVC 也处于 Pending 状态。
关系示意图如下:
解析测试
可以创建一个 busybox 的 Pod 来测试容器内部的解析:
kubectl run -it --image busybox:1.28.3 test --restart=Never --rm /bin/sh
执行 nslookup 操作,看是否能正常解析:
# 解析 Service
# 当前名称空间
nslookup svc-nginx
# 指定名称空间
nslookup svc-nginx.default
# 完整地址
nslookup svc-nginx.default.svc.cluster.local
# 解析 Pod
# 简写地址
nslookup sts-nginx-0.svc-nginx
# 完整地址
nslookup sts-nginx-0.svc-nginx.default.svc.cluster.local
nslookup sts-nginx-1.svc-nginx.default.svc.cluster.local
可以发现由于是 Headless Service,解析 Service 的时候其实际是解析到了后端代理的 Pod。
删除 Pod 测试,测试稳定性和持久化:
kubectl delete pods -l app=nginx
Pod 删除之后会自动重建,可以发现 Pod IP 已经变了,但是 Pod 的名称没变,这意味着使用 DNS 访问的地址也不会变。
但是由于使用的是 hostPath 方式的 volume,下次调度可能就在其它节点上了,之前节点上数据还在,但是新节点上数据就没了,想要数据持久化需要使用其它的 volume。
管理策略
对于某些分布式系统来说,StatefulSet 的顺序性不那么重要,更重要的是唯一性和身份标志。
可以在声明 StatefulSet 的时候设置 spec.podManagementPolicy
修改 Pod 管理策略,目前支持两种策略:
-
OrderedReady
:默认,表示让 StatefulSet 控制器遵循上文的顺序管理 Pod。 -
Parallel
:表示让 StatefulSet 控制器并行的终止所有 Pod,在启动或终止另一个 Pod 前,不必等待这些 Pod 变成 Running 和 Ready 或者完全终止状态。
更新策略
StatefulSet 可以通过设置 spec.updateStrategy.type
指定升级策略:
OnDelete
:当更新 StatefulSet 模板后,只有手动删除旧的 Pod 才会创建新的 Pod。RollingUpdate
:当更新 StatefulSet 模板后,会自动删除旧的 Pod 并创建新的 Pod,如果更新发生了错误,这次滚动更新就会停止。不过需要注意,StatefulSet 的 Pod 在部署时是顺序从 0~n 的,而在滚动更新时,这些 Pod 则是按逆序的方式即 n~0 一次删除并创建。
另外 SatefulSet 的滚动升级还支持部分变更,可以通过 spec.updateStrategy.rollingUpdate.partition
进行设置。在设置 partitions 后,SatefulSet 的 Pod 中序号大于或等于 partitions 的 Pod 会在 StatefulSet 的模板更新后进行滚动升级,而其余的 Pod 保持不变,这个功能类似实现 灰度发布
,新旧版本同时在线。
在实际应用中,一般不会使用 StatefulSet 来部署有状态服务的,出问题容易挂壁。对于一些特定的持久化服务,确实需要放在 Kuberntes 集群中部署的,可能会使用更加高级的 Operator 来部署,比如 etcd-operator、prometheus-operator 等等,这些应用都能够很好的来管理有状态的服务。
永远记得一件事,在生产环境中,数据的安全性,稳定性才是第一位。