1. StatefulSet
StatefulSet 作为 Controller 为 Pod 提供唯一的标识。它可以保证部署和 scale 的顺序。
与 Deploymnet 相比,Statefulset 具有以下显著的特性:
- 稳定的网络标志
- 稳定的持久化存储
- 自动的滚动升级
- 优雅地部署和扩展
- 优雅地删除和终止
使用 Statefulset 也具有一定的限制:
- StatefulSet 是 beta 资源,Kubernetes 1.5 以前版本不支持。对于所有的 alpha/beta 的资源,您都可以通过在 apiserver 中设置
--runtime-config
选项来禁用。 - 给定 Pod 的存储必须由 PersistentVolume Provisioner 根据请求的
storage class
进行配置,或由管理员预先配置。 - 删除或 scale StatefulSet 将不会删除与 StatefulSet 相关联的 volume。 这样做是为了确保数据安全性,这通常比自动清除所有相关 StatefulSet 资源更有价值。
- StatefulSets 目前要求 Headless Service 负责 Pod 的网络身份。
1.1. API 版本对照表
Kubernetes 版本 | Deployment 版本 |
---|---|
v1.5-v1.6 | extensions/v1beta1 |
v1.7-v1.15 | apps/v1beta1 |
v1.8-v1.15 | apps/v1beta2 |
v1.9+ | apps/v1 |
1.2. Statefulset Spec
1.2.1. updateStrategy
updateStrategy:
rollingUpdate:
partition: 0
type: RollingUpdate
.spec.updateStrategy
: 在 kubernetes 1.7 和以上版本中,StatefulSet 的该字段允许您配置和禁用 StatefulSet 中的容器、label、resource request/limit、annotation 的滚动更新。
其中.spec.updateStrategy.type
值可以为 OnDelete
和 RollingUpdate
,默认是 OnDelete
。
删除: OnDelete
OnDelete
更新策略实现了遗留(1.6和以前)的行为。 当 spec.updateStrategy
未指定时,这是默认策略。 当StatefulSet 的 .spec.updateStrategy.type
设置为 OnDelete
时,StatefulSet 控制器将不会自动更新 StatefulSet
中的 Pod。 用户必须手动删除 Pod 以使控制器创建新的 Pod,以反映对StatefulSet的 .spec.template
进行的修改。
滚动更新: RollingUpdate
RollingUpdate
更新策略在 StatefulSet 中实现 Pod 的自动滚动更新。 当StatefulSet的 .spec.updateStrategy.type
设置为 RollingUpdate
时,StatefulSet 控制器将在 StatefulSet 中删除并重新创建每个 Pod。 它将以与 Pod 终止相同的顺序进行(从最大的序数到最小的序数),每次更新一个 Pod。 在更新其前身之前,它将等待正在更新的 Pod 状态变成正在运行并就绪。
分区: partition
可以通过指定 .spec.updateStrategy.rollingUpdate.partition
来对 RollingUpdate
更新策略进行分区。如果指定了分区,则当 StatefulSet 的 .spec.template
更新时,具有大于或等于分区序数的所有 Pod 将被更新。具有小于分区的序数的所有 Pod 将不会被更新,即使删除它们也将被重新创建。如果 StatefulSet 的 .spec.updateStrategy.rollingUpdate.partition
大于其 .spec.replicas
,则其 .spec.template
的更新将不会传播到 Pod。
1.2.2. podManagementPolicy
spec:
podManagementPolicy: OrderedReady
spec.podManagementPolicy
: 在 Kubernetes 1.7 和之后版本,StatefulSet 允许您放开顺序保证,同时通过该字段保证身份的唯一性。
可以值为:OrderedReady
和 Parallel
。
顺序起停 pod: OrderedReady
StatefulSet 中默认使用的是 OrderedReady
pod 管理。它实现了 Statefulset Pod 的 "优雅地部署和扩展" 和 "优雅的缩容和删除" 行为。
并行 Pod 管理: Parallel
Parallel
pod 管理告诉 StatefulSet controller 并行的启动和终止 Pod,在启动和终止其他 Pod 之前不会等待 Pod 变成 运行并就绪或完全终止状态。
1.3. StatefulSet 特性
1.3.1. 稳定的网络标识
StatefulSet 中 “稳定的网络标识”,主要指 Pods 的 hostname 以及对应的 DNS Records 具有稳定的规律。即Pod重新调度后其PodName和HostName不变,基于Headless Service(即没有Cluster IP的Service)来实现。
Hostname:
- 有序部署:部署StatefulSet时,如果有多个Pod副本,它们会被顺序地创建(从0到N-1)并且,在下一个Pod运行之前所有之前的Pod必须都是Running和Ready状态。
- 有序删除:当Pod被删除时,它们被终止的顺序是从N-1到0。
- 有序扩展:当对Pod执行扩展操作时,与部署一样,它前面的Pod必须都处于Running和Ready状态
DNS 记录:
StatefulSet 中每个Pod 的 DNS 格式为 statefulSetName-{0..N-1}.serviceName.namespace.svc.cluster.local
,其中
serviceName
为Headless Service的名字0..N-1
为Pod所在的序号,从0开始到N-1statefulSetName
为StatefulSet的名字namespace
为服务所在的namespace,Headless Servic和StatefulSet必须在相同的namespace.cluster.local
为Cluster Domain
对于 Statefulset 的 名称有一下特性
- 对于有 N 个副本的 StatefulSet,Pod 将按照 {0..N-1} 的顺序被创建和部署。
- 当 删除 Pod 的时候,将按照逆序来终结,从{N-1..0}
- 对 Pod 执行 scale 操作之前,它所有的前任必须处于 Running 和 Ready 状态。
- 在终止 Pod 前,它所有的继任者必须处于完全关闭状态。
不应该将 StatefulSet 的 pod.Spec.TerminationGracePeriodSeconds
设置为 0。这样是不安全的且强烈不建议您这样做。进一步解释,请参阅 强制删除 StatefulSet Pod。
上面的 nginx 示例创建后,3 个 Pod 将按照如下顺序创建 web-0,web-1,web-2。在 web-0 处于 运行并就绪 状态之前,web-1 将不会被部署,同样当 web-1 处于运行并就绪状态之前 web-2也不会被部署。如果在 web-1 运行并就绪后,web-2 启动之前, web-0 失败了,web-2 将不会启动,直到 web-0 成功重启并处于运行并就绪状态。
如果用户通过修补 StatefulSet 来 scale 部署的示例,以使 replicas=1
,则 web-2 将首先被终止。 在 web-2 完全关闭和删除之前,web-1 不会被终止。 如果 web-0 在 web-2 终止并且完全关闭之后,但是在 web-1 终止之前失败,则 web-1 将不会终止,除非 web-0 正在运行并准备就绪。
StatefulSet 中的每个 Pod 从 StatefulSet 的名称和 Pod 的序数派生其主机名。构造的主机名的模式是$(statefulset名称)-$(序数)
。 上面的例子将创建三个名为web-0,web-1,web-2
的 Pod。
StatefulSet 可以使用 Headless Service 来控制其 Pod 的域。此服务管理的域的格式为:$(服务名称).$(namespace).svc.cluster.local
,其中 “cluster.local” 是集群域。
在创建每个Pod时,它将获取一个匹配的 DNS 子域,采用以下形式:$(pod 名称).$(管理服务域)
,其中管理服务由 StatefulSet 上的 serviceName
字段定义。
以下是 Cluster Domain,服务名称,StatefulSet 名称以及如何影响 StatefulSet 的 Pod 的 DNS 名称的一些示例。
Cluster Domain | Service (ns/name) | StatefulSet (ns/name) | StatefulSet Domain | Pod DNS | Pod Hostname |
---|---|---|---|---|---|
cluster.local | default/nginx | default/web | nginx.default.svc.cluster.local | web-{0..N-1}.nginx.default.svc.cluster.local | web-{0..N-1} |
cluster.local | foo/nginx | foo/web | nginx.foo.svc.cluster.local | web-{0..N-1}.nginx.foo.svc.cluster.local | web-{0..N-1} |
kube.local | foo/nginx | foo/web | nginx.foo.svc.kube.local | web-{0..N-1}.nginx.foo.svc.kube.local | web-{0..N-1} |
注意 Cluster Domain 将被设置成 cluster.local
除非进行了其他配置。
1.3.2. 稳定的持久化存储
- 稳定的持久化存储。即Pod重新调度后还是能访问到相同的持久化数据,基于PVC来实现 每个Pod对应一个PVC,PVC的名称是这样组成的:$(volumeClaimTemplates.name)-$(pod's hostname),跟对应的Pod是一一对应的。 当Pod发生re-schedule(其实是recreate)后,它所对应的PVC所Bound的PV仍然会自动的挂载到新的Pod中。 Kubernetes会按照VolumeClaimTemplate创建N(N为期望副本数)个PVC,由PVCs根据指定的StorageClass自动创建PVs。 当通过级联删除StatefulSet时并不会自动删除对应的PVCs,所以PVC需要手动删除。 当通过级联删除StatefulSet或者直接删除对应Pods时,对应的PVs并不会自动删除。需要你手动的去删除PV。
Kubernetes 为每个 VolumeClaimTemplate 创建一个 PersistentVolume。上面的 nginx 的例子中,每个 Pod 将具有一个由 anything
存储类创建的 1 GB 存储的 PersistentVolume。当该 Pod (重新)调度到节点上,volumeMounts
将挂载与 PersistentVolume Claim 相关联的 PersistentVolume。请注意,与 PersistentVolume Claim 相关联的 PersistentVolume 在 产出 Pod 或 StatefulSet 的时候不会被删除。这必须手动完成。
1.3.3. 优雅地部署和扩展
- 有序部署,有序扩展。即Pod是有顺序的,在部署或者扩展的时候要依据定义的顺序依次依次进行(即从0到N-1,在下一个Pod运行之前所有之前的Pod必须都是Running和Ready状态),基于init containers来实现 部署和 当部署有N个副本的StatefulSet应用时,严格按照index从0到N-1的递增顺序创建,下一个Pod创建必须是前一个Pod Ready为前提。 当删除有N个副本的StatefulSet应用时,严格按照index从N-1到0的递减顺序删除,下一个Pod删除必须是前一个Pod shutdown并完全删除为前提。 当扩容StatefulSet应用时,每新增一个Pod必须是前一个Pod Ready为前提。 当缩容StatefulSet应用时,没删除一个Pod必须是前一个Pod shutdown并成功删除为前提。 注意StatefulSet的pod.Spec.TerminationGracePeriodSeconds不要设置为0。
1.3.4. 优雅的缩容和删除
- 有序收缩,有序删除。即从N-1到0的删除
1.3.5. 自动的滚动升级
参考 Statefulset Spec 章节中关于 updatestrategy 的描述
1.4. 示例
1.4.1. nginx 示例
以一个简单的nginx服务web.yaml为例:
---
apiVersion: v1
kind: Service
metadata:
name: nginx
labels:
app: nginx
spec:
ports:
- port: 80
name: web
clusterIP: None
selector:
app: nginx
---
apiVersion: apps/v1beta1
kind: StatefulSet
metadata:
name: web
spec:
serviceName: "nginx"
replicas: 2
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: gcr.io/google_containers/nginx-slim:0.8
ports:
- containerPort: 80
name: web
volumeMounts:
- name: www
mountPath: /usr/share/nginx/html
volumeClaimTemplates:
- metadata:
name: www
annotations:
volume.alpha.kubernetes.io/storage-class: anything
spec:
accessModes: [ "ReadWriteOnce" ]
resources:
requests:
storage: 1Gi
$ kubectl create -f web.yaml
service "nginx" created
statefulset "web" created
$ kubectl get service nginx
NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE
nginx None <none> 80/TCP 1m
$ kubectl get statefulset web
NAME DESIRED CURRENT AGE
web 2 2 2m
$ kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESSMODES AGE
www-web-0 Bound pvc-d064a004-d8d4-11e6-b521-42010a800002 1Gi RWO 16s
www-web-1 Bound pvc-d06a3946-d8d4-11e6-b521-42010a800002 1Gi RWO 16s
$ kubectl get pods -l app=nginx
NAME READY STATUS RESTARTS AGE
web-0 1/1 Running 0 5m
web-1 1/1 Running 0 4m
$ kubectl run -i --tty --image busybox dns-test --restart=Never --rm /bin/sh
/ # nslookup web-0.nginx
Server: 10.0.0.10
Address 1: 10.0.0.10 kube-dns.kube-system.svc.cluster.local
Name: web-0.nginx
Address 1: 10.244.2.10
/ # nslookup web-1.nginx
Server: 10.0.0.10
Address 1: 10.0.0.10 kube-dns.kube-system.svc.cluster.local
Name: web-1.nginx
Address 1: 10.244.3.12
/ # nslookup web-0.nginx.default.svc.cluster.local
Server: 10.0.0.10
Address 1: 10.0.0.10 kube-dns.kube-system.svc.cluster.local
Name: web-0.nginx.default.svc.cluster.local
Address 1: 10.244.2.10
还可以进行其他的操作
$ kubectl scale statefulset web --replicas=5
$ kubectl patch statefulset web -p '{"spec":{"replicas":3}}'
$ kubectl patch statefulset web --type='json' -p='[{"op": "replace", "path": "/spec/template/spec/containers/0/image", "value":"gcr.io/google_containers/nginx-slim:0.7"}]'
$ kubectl delete statefulset web
$ kubectl delete service nginx
$ kubectl delete pvc www-web-0 www-web-1
1.4.2. zookeeper示例
另外一个更能说明StatefulSet强大功能的示例为zookeeper.yaml,这个例子仅为讲解,实际可用的配置请使用 https://github.com/kubernetes/contrib/tree/master/statefulsets 中的配置。
---
apiVersion: v1
kind: Service
metadata:
name: zk-headless
labels:
app: zk-headless
spec:
ports:
- port: 2888
name: server
- port: 3888
name: leader-election
clusterIP: None
selector:
app: zk
---
apiVersion: v1
kind: ConfigMap
metadata:
name: zk-config
data:
ensemble: "zk-0;zk-1;zk-2"
jvm.heap: "2G"
tick: "2000"
init: "10"
sync: "5"
client.cnxns: "60"
snap.retain: "3"
purge.interval: "1"
---
apiVersion: policy/v1beta1
kind: PodDisruptionBudget
metadata:
name: zk-budget
spec:
selector:
matchLabels:
app: zk
minAvailable: 2
---
apiVersion: apps/v1beta1
kind: StatefulSet
metadata:
name: zk
spec:
serviceName: zk-headless
replicas: 3
template:
metadata:
labels:
app: zk
annotations:
pod.alpha.kubernetes.io/initialized: "true"
scheduler.alpha.kubernetes.io/affinity: >
{
"podAntiAffinity": {
"requiredDuringSchedulingRequiredDuringExecution": [{
"labelSelector": {
"matchExpressions": [{
"key": "app",
"operator": "In",
"values": ["zk-headless"]
}]
},
"topologyKey": "kubernetes.io/hostname"
}]
}
}
spec:
containers:
- name: k8szk
imagePullPolicy: Always
image: gcr.io/google_samples/k8szk:v1
resources:
requests:
memory: "4Gi"
cpu: "1"
ports:
- containerPort: 2181
name: client
- containerPort: 2888
name: server
- containerPort: 3888
name: leader-election
env:
- name : ZK_ENSEMBLE
valueFrom:
configMapKeyRef:
name: zk-config
key: ensemble
- name : ZK_HEAP_SIZE
valueFrom:
configMapKeyRef:
name: zk-config
key: jvm.heap
- name : ZK_TICK_TIME
valueFrom:
configMapKeyRef:
name: zk-config
key: tick
- name : ZK_INIT_LIMIT
valueFrom:
configMapKeyRef:
name: zk-config
key: init
- name : ZK_SYNC_LIMIT
valueFrom:
configMapKeyRef:
name: zk-config
key: tick
- name : ZK_MAX_CLIENT_CNXNS
valueFrom:
configMapKeyRef:
name: zk-config
key: client.cnxns
- name: ZK_SNAP_RETAIN_COUNT
valueFrom:
configMapKeyRef:
name: zk-config
key: snap.retain
- name: ZK_PURGE_INTERVAL
valueFrom:
configMapKeyRef:
name: zk-config
key: purge.interval
- name: ZK_CLIENT_PORT
value: "2181"
- name: ZK_SERVER_PORT
value: "2888"
- name: ZK_ELECTION_PORT
value: "3888"
command:
- sh
- -c
- zkGenConfig.sh && zkServer.sh start-foreground
readinessProbe:
exec:
command:
- "zkOk.sh"
initialDelaySeconds: 15
timeoutSeconds: 5
livenessProbe:
exec:
command:
- "zkOk.sh"
initialDelaySeconds: 15
timeoutSeconds: 5
volumeMounts:
- name: datadir
mountPath: /var/lib/zookeeper
securityContext:
runAsUser: 1000
fsGroup: 1000
volumeClaimTemplates:
- metadata:
name: datadir
annotations:
volume.alpha.kubernetes.io/storage-class: anything
spec:
accessModes: [ "ReadWriteOnce" ]
resources:
requests:
storage: 20Gi
kubectl create -f zookeeper.yaml
详细的使用说明见zookeeper stateful application。
关于StatefulSet的更多示例请参阅 github.com/kubernetes/contrib - statefulsets,其中包括了zookeeper和kafka。
1.5. 扩展阅读
1.5.1. Node网络异常等情况下该如何处理
正常情况下,StatefulSet Controller会保证集群内同一namespace下不会出现多个相同network identity的StatefulSet Pods。 如果集群内出现以上情况,那么有可能导致该有状态应用不能正常工作、甚至出现数据丢失等致命问题。 那么什么情况下会导致出现同一namespace下会出现多个相同network identity的StatefulSet Pods呢?我们考虑下Node出现网络Unreachable的情况:
如果你使用Kubernetes 1.5之前的版本,当Node Condition是NetworkUnavailable时,node controller会强制从apiserver中删除这个Node上的这些pods对象,这时StatefulSet Controller就会自动在其他Ready Nodes上recreate同identity的Pods。这样做其实风险是很大的,可能会导致有一段时间有多个相同network identity的StatefulSet Pods,可能会导致该有状态应用不能正常工作。所以尽量不要在Kubernetes 1.5之前的版本中使用StatefulSet,或者你明确知道这个风险并且无视它。 如果你使用Kubernetes 1.5+的版本,当Node Condition是NetworkUnavailable时,node controller不会强制从apiserver中删除这个Node上的这些pods对象,这些pods的state在apiserver中被标记为Terminating或者Unknown,因此StatefulSet Controller并不会在其他Node上再recreate同identity的Pods。当你确定了这个Node上的StatefulSet Pods shutdown或者无法和该StatefulSet的其他Pods网络不同时,接下来就需要强制删除apiserver中这些unreachable pods object,然后StatefulSet Controller就能在其他Ready Nodes上recreate同identity的Pods,使得StatefulSet继续健康工作。 那么在Kubernetes 1.5+中,如何强制从apiserver中删除该StatefulSet pods呢?有如下三种方法:
如果Node永久的无法连接网络或者关机了,意味着能确定这个Node上的Pods无法与其他Pods通信了,不会对StatefulSet应用的可用性造成影响,那么建议手动从apiserver中删除该NetworkUnavailable的Node,Kubernetes会自动从apiserver中删除它上面的Pods object。
如果Node是因为集群网络脑裂导致的,则建议去检查网络问题并成功恢复,因为Pods state已经是Terminating或者Unkown,所以kubelet从apiserver中获取到这个信息后就会自动删除这些Pods。
其他情况才考虑直接手动从apiserver中删除这些Pods,因为这时你无法确定对应的Pods是否已经shutdown或者对StatefulSet应用无影响,强制删除后就可能导致出现同一namespace下有多个相同network identity的StatefulSet Pods,所以尽量不要使用这种方法。
kubectl delete pods
1.5.2. 集群外部访问StatefulSet的Pod
我们设想一下这样的场景:在kubernetes集群外部调试StatefulSet中有序的Pod,那么如何访问这些的pod呢?
方法是为pod设置label,然后用kubectl expose
将其以NodePort的方式暴露到集群外部,以上面的zookeeper的例子来说明,下面使用命令的方式来暴露其中的两个zookeeper节点,也可以写一个serivce配置yaml文件。
kubectl label pod zk-0 zkInst=0
kubectl label pod zk-1 zkInst=1
kubectl expose po zk-0 --port=2181 --target-port=2181 --name=zk-0 --selector=zkInst=0 --type=NodePort
kubectl expose po zk-1 --port=2181 --target-port=2181 --name=zk-1 --selector=zkInst=1 --type=NodePort
这样在kubernetes集群外部就可以根据pod所在的主机所映射的端口来访问了。
查看zk-0
这个service可以看到如下结果:
NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE
zk-0 10.254.98.14 <nodes> 2181:31693/TCP 5m
集群外部就可以使用所有的node中的任何一个IP:31693来访问这个zookeeper实例。