本篇是基于k8s-v1.15.0版本。
RC、Deployment、DaemonSet都是面向无状态的服务,它们所管理的Pod的IP、名字,启停顺序等都是随机的,而StatefulSet是什么?顾名思义,有状态的集合,管理所有有状态的服务,比如MySQL、MongoDB集群等。
StatefulSet本质上是Deployment的一种变体,在v1.9版本中已成为GA版本,它为了解决有状态服务的问题,它所管理的Pod拥有固定的Pod名称,启停顺序,在StatefulSet中,Pod名字称为网络标识(hostname),还必须要用到共享存储。
在Deployment中,与之对应的服务是service,而在StatefulSet中与之对应的headless service,headless service,即无头服务,与service的区别就是它没有Cluster IP,解析它的名称时将返回该Headless Service对应的全部Pod的Endpoint列表。
除此之外,StatefulSet在Headless Service的基础上又为StatefulSet控制的每个Pod副本创建了一个DNS域名,这个域名的格式为:
$(podname).(headless server name)
FQDN:$(podname).(headless server name).namespace.svc.cluster.local
Pod一致性:包含次序(启动、停止次序)、网络一致性。此一致性与Pod相关,与被调度到哪个node节点无关;
稳定的次序:对于N个副本的StatefulSet,每个Pod都在[0,N)的范围内分配一个数字序号,且是唯一的;
稳定的网络:Pod的hostname模式为 ( s t a t e f u l s e t 名 称 ) − (statefulset名称)- (statefulset名称)−(序号);
稳定的存储:通过VolumeClaimTemplate为每个Pod创建一个PV。删除、减少副本,不会删除相关的卷。
Headless Service:用来定义Pod网络标识( DNS domain);
volumeClaimTemplates :存储卷申请模板,创建PVC,指定pvc名称大小,将自动创建pvc,且pvc必须由存储类供应;
StatefulSet :定义具体应用,名为Nginx,有三个Pod副本,并为每个Pod定义了一个域名部署statefulset。
为什么需要 headless service 无头服务?
在用Deployment时,每一个Pod名称是没有顺序的,是随机字符串,因此是Pod名称是无序的,但是在statefulset中要求必须是有序 ,每一个pod不能被随意取代,pod重建后pod名称还是一样的。而pod IP是变化的,所以是以Pod名称来识别。pod名称是pod唯一性的标识符,必须持久稳定有效。这时候要用到无头服务,它可以给每个Pod一个唯一的名称 。
为什么需要volumeClaimTemplate?
对于有状态的副本集都会用到持久存储,对于分布式系统来讲,它的最大特点是数据是不一样的,所以各个节点不能使用同一存储卷,每个节点有自已的专用存储,但是如果在Deployment中的Pod template里定义的存储卷,是所有副本集共用一个存储卷,数据是相同的,因为是基于模板来的 ,而statefulset中每个Pod都要自已的专有存储卷,所以statefulset的存储卷就不能再用Pod模板来创建了,于是statefulSet使用volumeClaimTemplate,称为卷申请模板,它会为每个Pod生成不同的pvc,并绑定pv,从而实现各pod有专用存储。这就是为什么要用volumeClaimTemplate的原因。
kubectl explain sts.spec
:主要字段解释
replicas
:副本数
selector
:那个pod是由自己管理的
serviceName
:必须关联到一个无头服务商
template
:定义pod模板(其中定义关联那个存储卷)
volumeClaimTemplates
:生成PVC
本教程假设你的集群被配置为动态的提供 PersistentVolume,动态PV参考ks8的数据管理—动态配置StorageClass;如果没有这样配置,在开始本教程之前,你需要手动准备存储卷。
如果集群中没有StorageClass的动态供应PVC的机制,也可以提前手动创建多个PV、PVC,手动创建的PVC名称必须符合之后创建的StatefulSet命名规则:(volumeClaimTemplates.name)-(pod_name)
cat << EOF > nginx-ns.yaml
apiVersion: v1
kind: Namespace
metadata:
name: nginx-ss
EOF
cat << EOF > nginx-sc.yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: nginx-nfs-storage
provisioner: fuseim.pri/ifs # or choose another name, must match deployment's env PROVISIONER_NAME'
parameters:
archiveOnDelete: "false" # # When set to "false" your PVs will not be archived
# by the provisioner upon deletion of the PVC.
EOF
kubectl apply -f nginx-sc.yaml
cat << EOF > nginx-ss.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: web
namespace: nginx-ss
spec:
selector:
matchLabels:
app: nginx #必须匹配 .spec.template.metadata.labels
serviceName: "nginx" #声明它属于哪个Headless Service.
replicas: 3 #副本数
template:
metadata:
labels:
app: nginx # 必须配置 .spec.selector.matchLabels
spec:
terminationGracePeriodSeconds: 10
containers:
- name: nginx
image: www.my.com/web/nginx:v1
ports:
- containerPort: 80
name: web
volumeMounts:
- name: nginx-pvc
mountPath: /usr/share/nginx/html
volumeClaimTemplates: #可看作pvc的模板
- metadata:
name: nginx-pvc
spec:
accessModes: [ "ReadWriteOnce" ]
storageClassName: "nginx-nfs-storage" #存储类名,改为集群中已存在的
resources:
requests:
storage: 1Gi
EOF
kubectl apply -f nginx-ss.yaml
kubectl get pod -n nginx-ss
cat << EOF > nginx-svc.yaml
apiVersion: v1
kind: Service
metadata:
name: nginx
namespace: nginx-ss
labels:
app: nginx
spec:
ports:
- port: 80
name: web
clusterIP: None
selector:
app: nginx
EOF
kubectl apply -f nginx-svc.yaml
每个 Pod 都拥有一个基于其顺序索引的稳定的主机名
使用 kubectl run 运行一个提供 nslookup 命令的容器,该命令来自于 dnsutils 包。通过对 Pod 的主机名执行 nslookup,你可以检查他们在集群内部的 DNS 地址
kubectl run -i --tty --image www.my.com/k8s/busybox:1.27 -n nginx-ss dns-test --restart=Never --rm
nslookup web-0.nginx
可以发现headless service 的 CNAME 指向 SRV 记录(记录每个 Running 和 Ready 状态的 Pod)。SRV 记录指向一个包含 Pod IP 地址的记录表项。
重启pod会发现,pod中的ip已经发生变化,但是pod的名称并没有发生变化;这就是为什么不要在其他应用中使用 StatefulSet 中的 Pod 的 IP 地址进行连接,这点很重要
Pod 的序号、主机名、SRV 条目和记录名称没有改变,但和 Pod 相关联的 IP 地址可能发生了改变
如果你需要查找并连接一个 StatefulSet 的活动成员,你应该查询 Headless Service 的 CNAME。和 CNAME 相关联的 SRV 记录只会包含 StatefulSet 中处于 Running 和 Ready 状态的 Pod。
如果你的应用已经实现了用于测试 liveness 和 readiness 的连接逻辑,你可以使用 Pod 的 SRV 记录(web-0.nginx.nginx-ss.svc.cluster.local, web-1.nginx.nginx-ss.svc.cluster.local,web-2.nginx.nginx-ss.svc.cluster.local)。因为他们是稳定的,并且当你的 Pod 的状态变为 Running 和 Ready 时,你的应用就能够发现它们的地址。
将 Pod 的主机名写入它们的index.html文件并验证 NGINX web 服务器使用该主机名提供服务
kubectl exec -it web-0 -n nginx-ss /bin/bash
echo $(hostname) > /usr/share/nginx/html/index.html
kubectl exec -it web-1 -n nginx-ss /bin/bash
echo $(hostname) > /usr/share/nginx/html/index.html
kubectl exec -it web-2 -n nginx-ss /bin/bash
echo $(hostname) > /usr/share/nginx/html/index.html
删除pod后,即使pod的ip发生变化,但是依然不影响访问
虽然 web-0 、web-1 和web-2被重新调度了,但它们仍然继续监听各自的主机名,因为和它们的 PersistentVolumeClaim 相关联的 PersistentVolume 被重新挂载到了各自的 volumeMount 上。不管 web-0、web-1、web-3 被调度到了哪个节点上,它们的 PersistentVolumes 将会被挂载到合适的挂载点上
扩容/缩容StatefulSet 指增加或减少它的副本数。这通过更新replicas
字段完成。你可以使用kubectl scale 或者kubectl patch来扩容/缩容一个 StatefulSet。
kubectl scale sts web --replicas=4 -n nginx-ss #扩容
kubectl scale sts web --replicas=2 -n nginx-ss #缩容
或者
kubectl patch sts web -p '{"spec":{"replicas":4}}' -n nginx-ss #扩容
kubectl patch sts web -p '{"spec":{"replicas":2}}' -n nginx-ss #缩容
会发现,放扩容是,如果pv是动态的sc的话,pvc同样会增加,但当缩容时,pvc并不会自定的删除
cat << EOF > mysql-ns.yaml
apiVersion: v1
kind: Namespace
metadata:
name: mysql-sts
EOF
cat << EOF > mysql-sc.yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: mysql-nfs-storage
provisioner: fuseim.pri/ifs # or choose another name, must match deployment's env PROVISIONER_NAME'
parameters:
archiveOnDelete: "false" # # When set to "false" your PVs will not be archived
# by the provisioner upon deletion of the PVC.
EOF
kubectl apply -f mysql-ns.yaml
kubectl apply -f mysql-sc.yaml
cat << EOF > mysql-ss.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mysql
namespace: mysql-sts
spec:
selector:
matchLabels:
app: mysql #必须匹配 .spec.template.metadata.labels
serviceName: "mysql" #声明它属于哪个Headless Service.
replicas: 3 #副本数
template:
metadata:
labels:
app: mysql # 必须配置 .spec.selector.matchLabels
spec:
terminationGracePeriodSeconds: 10
containers:
- name: mysql
image: www.my.com/sys/mysql:5.7
ports:
- containerPort: 3306
name: mysql
env:
- name: MYSQL_ROOT_PASSWORD
value: "123456"
volumeMounts:
- name: mysql-pvc
mountPath: /var/lib/mysql
volumeClaimTemplates: #可看作pvc的模板
- metadata:
name: mysql-pvc
spec:
accessModes: [ "ReadWriteOnce" ]
storageClassName: "mysql-nfs-storage" #存储类名,改为集群中已存在的
resources:
requests:
storage: 1Gi
EOF
kubectl apply -f mysql-ss.yaml
cat << EOF > mysql-svc.yaml
apiVersion: v1
kind: Service
metadata:
name: mysql
namespace: mysql-sts
labels:
app: mysql
spec:
ports:
- port: 3306
name: mysql
clusterIP: None
selector:
app: mysql
EOF
kubectl apply -f mysql-svc.yaml
cat << EOF > mysql-svc-port.yaml
apiVersion: v1
kind: Service
metadata:
name: mysql-read
namespace: mysql-sts
labels:
app: mysql-2
spec:
ports:
- port: 3306
name: mysql
selector:
app: mysql-2
EOF
kubectl apply -f mysql-svc-port.yaml
让mysql-read可以通过外部访问
使用NodePort作为访问方法
执行命令kubectl edit svc/mysql-read -n mysql-sts
进入修改界面,修改type为NodePort
,同时给spec/ports
中加入nodePort: 31988
配置。
再次执行kubectl get svc -n mysql-sts
在本地电脑通过mysql客户端工具登陆即可