在基于k8s容器云平台上,对存储资源的使用需求通常包括以下几方面:
1.应用配置文件、密钥的管理; 2.应用的数据持久化存储; 3.在不同的应用间共享数据存储;
k8s的Volume抽象概念就是针对这些问题提供的解决方案,k8s的volume类型非常丰富,从临时目录、宿主机目录、ConfigMap、Secret、共享存储(PV和PVC)。
k8s支持Volume类型包括以下几类:
1.临时空目录(随着Pod的销毁而销毁) emptyDir: ? 2.配置类(将配置文件以Volume的形式挂载到容器内) ConfigMap:将保存在ConfigMap资源对象中的配置文件信息挂载到容器内的某个目录下 ? Secret:将保存在Secret资源对象中的密码密钥等信息挂载到容器内的某个文件中。 ? downwardAPI:将downwardAPI的数据以环境变量或文件的形式注入容器中。 ? gitErpo:将某Git代码库挂载到容器内的某个目录下 ? 3.本地存储类 hostpath:将宿主机的目录或文件挂载到容器内进行使用。 ? local:Kubernetes从v1.9版本引入,将本地存储以PV的形式提供给容器使用,并能够实现存储空间的管理。 ? 4.共享存储 PV(Persistent Volume):将共享存储定义为一种“持久存储卷”,可以被多个容器应用共享使用。 ? PVC(Persistent Volume Claim):用户对存储资源的一次“申请”,PVC申请的对象是PV,一旦申请成功,应用就能够像使用本地目录一样使用共享存储了。
共享存储主要用于多个应用都能够使用存储资源,例如NFS存储、光纤存储Glusterfs共享文件系统等,在k8s系统中通过PV/StorageClass和PVC来完成定义,并通过volumeMount挂载到容器的目录或文件进行使用。
ConfigMap、Secret、emptyDir、hostPath等属于临时性存储,当pod被调度到某个节点上时,它们随pod的创建而创建,临时占用节点存储资源,当pod离开节点时,存储资源被交还给节点,pod一旦离开这个节点,存储就失效,不具备持久化存储数据的能力。与此相反,持久化存储拥有独立的生命周期,具备持久化存储能力,其后端一般是独立的存储系统如NFS、iSCSI、cephfs、glusterfs等。
pv与pvc
Kubernetes中的node代表计算资源,而PersistentVolume(PV)则代表存储资源,它是对各种诸如NFS、iSCSI、云存储等各种存储后端所提供存储块的统一抽象从而提供网络存储,通过它屏蔽低层实现细节。与普通volume不同,PV拥有完全独立的生命周期。 ? 因为PV表示的是集群能力,它是一种集群资源,所以用户(通常是开发人员)不能直接使用PV,就像不能直接使用内存一样,需要先向系统申请,而 PVC 就是请求存储资源的。 Kubernetes通过PersistentVolumeClaim(PVC)代理用户行为,用户通过对PVC的操作实现对PV申请、使用、释放等操作,PVC是用户层面的资源。 ? PV 和 PVC 可以将 pod 和数据卷解耦,pod 不需要知道确切的文件系统或者支持它的持久化引擎。
Kubernetes的共享存储供应模式包括静态和动态两种模式
静态模式 静态PV由系统管理员负责创建、提供、维护,系统管理员为用户屏蔽真正提供存储的后端及其实现细节,普通用户作为消费者,只需通过PVC申请、使用此类资源。 ? 动态模式: 集群管理员无须手工创建PV,而是通过对Storage Class的设置对后端存储进行描述,“storage class”可以理解成某种具体的后端存储,标记为某种“类型(Class)”,此时要求PVC对存储的类型进行声明,系统将自动完成PV的创建与PVC的绑定。如果用户的PVC中“storage class”的值为"",则表示不能为此PVC动态创建PV。
PV与PVC的绑定
用户创建包含容量、访问模式等信息的PVC,向系统请求存储资源。系统查找已存在PV或者监控新创建PV,如果与PVC匹配则将两者绑定。如果PVC创建动态PV,则系统将一直将两者绑定。PV与PVC的绑定是一一对应关系,不能重复绑定。如果系统一直没有为PVC找到匹配PV,则PVC无限期维持在"unbound"状态,直到系统找到匹配PV。实际绑定的PV容量可能大于PVC中申请的容量。
PV 持久卷是用插件的形式来实现的。Kubernetes 目前支持以下插件
- GCEPersistentDisk
- AWSElasticBlockStore
- AzureFile
- AzureDisk
- FC (Fibre Channel)
- Flexvolume
- Flocker
- NFS
- iSCSI
- RBD (Ceph Block Device)
- CephFS
- Cinder (OpenStack block storage)
- Glusterfs
- VsphereVolume
- Quobyte Volumes
- HostPath (Single node testing only – local storage is not supported in any way and WILL NOT WORK in a multi-node cluster)
- Portworx Volumes
- ScaleIO Volumes
- StorageOS
机器准备
k8s-master 制作nfs服务端作为共享文件系统 [root@k8s-master ~]# yum install -y nfs-utils rpcbind [root@k8s-master ~]# mkdir /mnt/data #制作共享目录 [root@k8s-master ~]# vim /etc/exports /mnt/data *(rw,no_root_squash) [root@k8s-master ~]# systemctl start rpcbind #启动服务 [root@k8s-master ~]# systemctl start nfs # 使配置生效 [root@k8s-master ~]# exportfs -r ? # 检查配置是否生效 [root@k8s-master ~]# exportfs ? 集群中的工作节点都需要安装客户端工具,主要作用是节点能够驱动 nfs 文件系统 只安装,不启动服务 # yum install -y nfs-utils # mkdir /mnt/data # showmount -e 192.168.58.149(master_ip) # mount -t nfs 192.168.58.149:/mnt/data /mnt/data ? ? master节点 制作pv.yaml(一般这个动作由 kubernetes 管理员完成,也就是我们运维人员) [root@k8s-master ~]# mkdir /k8s/mysql -p [root@k8s-master ~]# cd /k8s/mysql/ [root@k8s-master mysql]# vim pv.yaml apiVersion: v1 kind: PersistentVolume ? #类型定义为pv metadata: name: my-pv #pv的名字 labels: #定义标签 ? type: nfs #类型为nfs spec: nfs: # 存储类型,需要与底层实际的存储一致,这里采用 nfs ? server: 192.168.122.24 # NFS 服务器的 IP ? path: "/mnt/data" # NFS 上共享的目录 capacity: ? #定义存储能力 ? storage: 3Gi #指定存储空间 accessModes: #定义访问模式 ? - ReadWriteMany ? #读写权限,允许被多个Node挂载 persistentVolumeReclaimPolicy: Retain #定义数据回收策略,这里是保留
1.存储能力(Capacity)
描述存储设备的能力,目前仅支持对存储空间的设置(storage=xx)。
2.访问模式(Access Modes)
对PV进行访问模式的设置,用于描述用户应用对存储资源的访问权限。访问模式如下:
ReadWriteOnce:读写权限,并且只能被单个pod挂载。 ReadOnlyMany:只读权限,允许被多个pod挂载。 ReadWriteMany:读写权限,允许被多个pod挂载。 ? 某些PV可能支持多种访问模式,但PV在挂载时只能使用一种访问模式,多种访问模式不能同时生效。
3.persistentVolumeReclaimPolicy定义数据回收策略
目前支持如下三种回收策略:
保留(Retain):保留数据,需要手工处理。 ? 回收空间(Recycle):警告: 回收策略 Recycle 已被废弃。取而代之的建议方案是使用动态供应。如果下层的卷插件支持,回收策略 Recycle 会在卷上执行一些基本的 擦除(rm -rf /thevolume/*)操作,之后允许该卷用于新的 PVC 申领 ? 删除(Delete):会将 PersistentVolume 对象从 Kubernetes 中移除,同时也会从外部基础设施(如 AWS EBS、GCE PD、Azure Disk 或 Cinder 卷)中移除所关联的存储资产。 就是把云存储一起删了。
目前,只有 NFS 和 HostPath 两种类型的存储设备支持 “Recycle” 策略; AWS EBS、 GCE PD、Azure Disk 和 Cinder volumes 支持 “Delete” 策略。
4.storageClassName存储类别(Class)
PV可以设定其存储的类型(Class),通过 storageClassName参数指定一个 StorageClass 资源对象的名称。 具有特定“类别”的 PV 只能与请求了该“类别”的 PVC 进行绑定。未设定 “类别” 的 PV 则只能与不请求任何 “类别” 的 PVC 进行绑定。 相当于一个标签。
[root@k8s-master mysql]# kubectl apply -f pv.yaml persistentvolume/my-pv created [root@k8s-master mysql]# kubectl get pv
PV 生命周期的各个阶段(Phase)
某个 PV 在生命周期中,可以处于以下4个阶段之一:
- Available:可用状态,还未与某个 PVC 绑定。 - Bound:已与某个 PVC 绑定。 - Released:释放,绑定的 PVC 已经删除,但没有被集群回收存储空间 。 - Failed:自动资源回收失败。
pv创建成功目前属于可用状态,还没有与pvc绑定,那么现在创建pvc
创建pvc
[root@k8s-master mysql]# vim mysql-pvc.yaml apiVersion: v1 kind: PersistentVolumeClaim ? #定义类型为PVC metadata: name: mypvc ? #声明pvc的名称,当做pod的卷使用时会用到 spec: accessModes: #定义访问pvc的模式,与pv拥有一样的模式 ? - ReadWriteMany ? #读写权限,允许被多个pod挂载 resources: #声明可以请求特定数量的资源,目前仅支持 request.storage 的设置,即存储空间大小。 ? requests: ? ? storage: 3Gi #定义空间大小 selector: #PV选择条件,标签选择器,通过标签选择 ? ? matchLabels: ? ? ? type: "nfs" #选择pv类型的nfs ? ? ? ? 注意:当我们申请pvc的容量大于pv的容量是无法进行绑定的。 ? 创建pvc [root@k8s-master mysql]# kubectl apply -f mysql-pvc.yaml persistentvolumeclaim/mypvc created [root@k8s-master mysql]# kubectl get pvc NAME ? STATUS ? VOLUME ? CAPACITY ? ACCESS MODES ? STORAGECLASS ? AGE mypvc ? Bound ? my-pv ? 3Gi ? ? ? RWX ? ? ? ? ? pv-nfs ? ? ? ? 37s
status状态
- Available (可用): 表示可用状态,还未被任何PVC绑定 - Bound (已绑定):已经绑定到某个PVC - Released (已释放):对应的PVC已经删除,但资源还没有被集群收回 - Failed:PV自动回收失败
Kubernetes中会自动帮我们查看pv状态为Available并且根据声明pvc容量storage的大小进行筛选匹配,同时还会根据AccessMode进行匹配。如果pvc匹配不到pv会一直处于pending状态。
创建secret
[root@k8s-master mysql]# echo -n 'QianFeng@123!' | base64
UWlhbkZlbmdAMTIzIQ==
[root@k8s-master mysql]# vim mysql-secret.yaml
apiVersion: v1
data:
password: UWlhbkZlbmdAMTIzIQ==
kind: Secret
metadata:
annotations:
name: my-pass
type: Opaque
[root@k8s-master mysql]# kubectl apply -f mysql-secret.yaml
secret/my-pass created
[root@k8s-master mysql]# kubectl get secret
NAME ? ? ? ? ? ? ? ? TYPE ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? DATA ? AGE
default-token-24c52 ? kubernetes.io/service-account-token ? 3 ? ? 6d22h
my-pass ? ? ? ? ? ? ? Opaque ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 1 ? ? 69s
?
创建myslq-pod文件
[root@k8s-master mysql]# cat mysql-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-mysql
spec:
replicas: 1
selector:
? matchLabels:
? ? app: mysql
template:
? metadata:
? ? labels:
? ? ? app: mysql
? spec:
? ? containers:
? ? - name: my-mysql
? ? ? image: daocloud.io/library/mysql:5.7
# ? ? ? imagePullPolicy: IfNotPresent
? ? ? ports:
? ? ? - containerPort: 3306
? ? ? env:
? ? ? - name: MYSQL_ROOT_PASSWORD
? ? ? ? valueFrom:
? ? ? ? ? secretKeyRef:
? ? ? ? ? ? name: my-pass
? ? ? ? ? ? key: password
? ? ? volumeMounts:
? ? ? - name: mysql-data
? ? ? ? mountPath: /var/lib/mysql
? ? volumes:
? ? - name: mysql-data
? ? ? persistentVolumeClaim: #绑定pvc
? ? ? ? claimName: mypvc #指定对应的pvc名字
[root@k8s-master mysql]# kubectl apply -f mysql-deployment.yaml ? ? ? ? ?
[root@k8s-master mysql]# kubectl get pod
NAME ? ? ? ? ? ? ? ? ? ? ? ? READY ? STATUS ? RESTARTS ? AGE
my-mysql-5474b6885f-c5dmp ? 1/1 ? ? Running ? 0 ? ? ? ? 8s
?
[root@k8s-master mysql]# kubectl get pod -o wide
NAME ? ? ? ? ? ? ? ? ? ? ? ? READY ? STATUS ? RESTARTS ? AGE ? IP ? ? ? ? ? NODE ? ? ? NOMINATED NODE ? READINESS GATES
my-mysql-5474b6885f-c5dmp ? 1/1 ? ? Running ? 0 ? ? ? ? 40s ? 10.244.2.5 ? k8s-node2 ? <none> ? ? ? ? ? <none>
?
测试
[root@k8s-master ~]# cd /mnt/data/
[root@k8s-master data]# ll
总用量 188484
-rw-r----- 1 polkitd ssh_keys ? ? ? 56 11月 8 21:49 auto.cnf
-rw------- 1 polkitd ssh_keys ? ? 1680 11月 8 21:49 ca-key.pem
-rw-r--r-- 1 polkitd ssh_keys ? ? 1112 11月 8 21:49 ca.pem
-rw-r--r-- 1 polkitd ssh_keys ? ? 1112 11月 8 21:49 client-cert.pem
-rw------- 1 polkitd ssh_keys ? ? 1680 11月 8 21:49 client-key.pem
-rw-r----- 1 polkitd ssh_keys ? ? 688 11月 8 21:57 ib_buffer_pool
-rw-r----- 1 polkitd ssh_keys 79691776 11月 8 21:59 ibdata1
-rw-r----- 1 polkitd ssh_keys 50331648 11月 8 21:59 ib_logfile0
-rw-r----- 1 polkitd ssh_keys 50331648 11月 8 21:49 ib_logfile1
-rw-r----- 1 polkitd ssh_keys 12582912 11月 8 22:00 ibtmp1
drwxr-x--- 2 polkitd ssh_keys ? ? 4096 11月 8 21:49 mysql
drwxr-x--- 2 polkitd ssh_keys ? ? 8192 11月 8 21:49 performance_schema
-rw------- 1 polkitd ssh_keys ? ? 1680 11月 8 21:49 private_key.pem
-rw-r--r-- 1 polkitd ssh_keys ? ? 452 11月 8 21:49 public_key.pem
-rw-r--r-- 1 polkitd ssh_keys ? ? 1112 11月 8 21:49 server-cert.pem
-rw------- 1 polkitd ssh_keys ? ? 1676 11月 8 21:49 server-key.pem
drwxr-x--- 2 polkitd ssh_keys ? ? 8192 11月 8 21:49 sys
StorageClass 相当于一个创建 PV 的模板,用户通过 PVC 申请存储卷,StorageClass 通过模板自动创建 PV,然后和 PVC 进行绑定。
StorageClass创建动态存储卷流程
集群管理员预先创建存储类(StorageClass);
用户创建使用存储类的持久化存储声明(PVC:PersistentVolumeClaim);
存储持久化声明通知系统,它需要一个持久化存储(PV: PersistentVolume);
系统读取存储类的信息;
系统基于存储类的信息,在后台自动创建PVC需要的PV;
用户创建一个使用PVC的Pod;
Pod中的应用通过PVC进行数据的持久化;
而PVC使用PV进行数据的最终持久化处理。
NFS Provisioner 是一个自动配置卷程序,它使用现有的和已配置的 NFS 服务器来支持通过持久卷声明动态配置 Kubernetes 持久卷。
注意:k8s 1.21版本中创建pvc时nfs-provisioner会报错
E0903 08:00:24.858523 1 controller.go:1004] provision “default/test-claim” class “managed-nfs-storage”: unexpected error getting claim reference: selfLink was empty, can’t make reference
解决方法: 修改 /etc/kubernetes/manifests/kube-apiserver.yaml文件 增加 - --feature-gates=RemoveSelfLink=false
spec:
containers:
- command:
? - kube-apiserver
? - --feature-gates=RemoveSelfLink=false ? # 增加这行
? - --advertise-address=172.24.0.5
? - --allow-privileged=true
? - --authorization-mode=Node,RBAC
? - --client-ca-file=/etc/kubernetes/pki/ca.crt
创建ServiceAccount、ClusterRole、ClusterRoleBinding等,为nfs-client-provisioner授权
# vim rbac.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: nfs-client-provisioner
# replace with namespace where provisioner is deployed
namespace: default
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: nfs-client-provisioner-runner
rules:
- apiGroups: [""]
resources: ["persistentvolumes"]
verbs: ["get", "list", "watch", "create", "delete"]
- apiGroups: [""]
resources: ["persistentvolumeclaims"]
verbs: ["get", "list", "watch", "update"]
- apiGroups: ["storage.k8s.io"]
resources: ["storageclasses"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["events"]
verbs: ["create", "update", "patch"]
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: run-nfs-client-provisioner
subjects:
- kind: ServiceAccount
name: nfs-client-provisioner
# replace with namespace where provisioner is deployed
namespace: default
roleRef:
kind: ClusterRole
name: nfs-client-provisioner-runner
apiGroup: rbac.authorization.k8s.io
---
kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: leader-locking-nfs-client-provisioner
# replace with namespace where provisioner is deployed
namespace: default
rules:
- apiGroups: [""]
resources: ["endpoints"]
verbs: ["get", "list", "watch", "create", "update", "patch"]
---
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: leader-locking-nfs-client-provisioner
subjects:
- kind: ServiceAccount
name: nfs-client-provisioner
# replace with namespace where provisioner is deployed
namespace: default
roleRef:
kind: Role
name: leader-locking-nfs-client-provisioner
apiGroup: rbac.authorization.k8s.io
# vim nfs-provisioner.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nfs-client-provisioner
labels:
app: nfs-client-provisioner
# replace with namespace where provisioner is deployed
namespace: default #与RBAC文件中的namespace保持一致
spec:
replicas: 1
selector:
matchLabels:
app: nfs-client-provisioner
strategy:
type: Recreate
selector:
matchLabels:
app: nfs-client-provisioner
template:
metadata:
labels:
app: nfs-client-provisioner
spec:
serviceAccountName: nfs-client-provisioner
containers:
- name: nfs-client-provisioner
image: quay.io/external_storage/nfs-client-provisioner:latest
volumeMounts:
- name: nfs-client-root
mountPath: /persistentvolumes
env:
- name: PROVISIONER_NAME
value: gxf-nfs-storage #provisioner名称,请确保该名称与 nfs-StorageClass.yaml文件中的provisioner名称保持一致
- name: NFS_SERVER
value: 10.24.X.X #NFS Server IP地址
- name: NFS_PATH
value: /home/nfs/1 #NFS挂载卷
volumes:
- name: nfs-client-root
nfs:
server: 10.24.X.X #NFS Server IP地址
path: /home/nfs/1 #NFS 挂载卷
# 部署
[root@kube-master ~]# kubectl apply -f rbac.yaml
[root@kube-master ~]# kubectl apply -f nfs-provisioner.yaml
[root@kube-master ~]# kubectl get pod
NAME READY STATUS RESTARTS AGE
nfs-client-provisioner-75bfdbdcd8-5mqbv 1/1 Running 0 4m24s
?
# vim nfs-StorageClass.yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: managed-nfs-storage
provisioner: gxf-nfs-storage #这里的名称要和provisioner配置文件中的环境变量PROVISIONER_NAME保持一致
reclaimPolicy: Retain # 默认为delete
parameters:
archiveOnDelete: "true" # false表示pv被删除时,在nfs下面对应的文件夹也会被删除,true正相反
部署一个有2个副本的deployment,挂载共享目录
# 创建pvc,“storageClassName"为上面创建的"managed-nfs-storage”,即指定动态创建PV的模板文件的名字。
# test-pvclaim.yaml
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: test-claim
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 100Mi
storageClassName: managed-nfs-storage
[root@kube-master ~]# kubectl apply -f test-pvclaim.yaml
# deployment部署
# test-deploy.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: test-deploy
labels:
app: test-deploy
namespace: default #与RBAC文件中的namespace保持一致
spec:
replicas: 2
selector:
matchLabels:
app: test-deploy
strategy:
type: Recreate
selector:
matchLabels:
app: test-deploy
template:
metadata:
labels:
app: test-deploy
spec:
containers:
- name: test-pod
image: busybox:1.24
command:
- "/bin/sh"
args:
- "-c"
# - "touch /mnt/SUCCESS3 && exit 0 || exit 1" #创建一个SUCCESS文件后退出
- touch /mnt/SUCCESS5; sleep 50000
volumeMounts:
- name: nfs-pvc
mountPath: "/mnt"
# subPath: test-pod-3 # 子路径 (这路基代表存储卷下面的test-pod子目录)
volumes:
- name: nfs-pvc
persistentVolumeClaim:
claimName: test-claim #与PVC名称保持一致
# vim test-sts-1.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: test-sts
labels:
k8s-app: test-sts
spec:
serviceName: test-sts-svc
replicas: 3
selector:
matchLabels:
k8s-app: test-sts
template:
metadata:
labels:
k8s-app: test-sts
spec:
containers:
- image: busybox:1.24
name: test-pod
command:
- "/bin/sh"
args:
- "-c"
# - "touch /mnt/SUCCESS3 && exit 0 || exit 1" #创建一个SUCCESS文件后退出
- touch /mnt/SUCCESS5; sleep 50000
imagePullPolicy: IfNotPresent
volumeMounts:
- name: nfs-pvc
mountPath: "/mnt"
volumeClaimTemplates:
- metadata:
name: nfs-pvc
spec:
accessModes: ["ReadWriteMany"]
storageClassName: managed-nfs-storage
resources:
requests:
storage: 20Mi
[root@kube-master ~]# kubectl apply -f test-sts-1.yaml
[root@kube-master ~]# kubectl get sts
NAME READY AGE
test-sts 3/3 4m46s
在Kubernetes系统中,Pod的管理对象RC、Deployment、DaemonSet都是面向无状态的服务,它们所管理的Pod的IP、名字,启停顺序等都是随机的。但现实中有很多服务是有状态的,特别是一些复杂的中间件集群。例如Mysql集群、MongoDB集群、Kafka集群、Zookeeper集群等,这些都有一个共同的特点:
1.每个节点都有固定的ID,通过这个ID,集群中的成员可以相互发现并且通信。 2.集群的规模是比较固定的,集群规模不能随意改动。 3.集群里的每个节点都是有状态的,通常会持久化数据到永久存储中。 4.如果磁盘损坏,集群里的某个节点就无法正常运行,集群功能受损。
StatefulSet本质上是Deployment的一种变体,在v1.9版本中已成为GA版本,它为了解决有状态服务的问题,它所管理的Pod拥有固定的Pod名称,启停顺序的,如果使用RC/Deployment控制副本的方式实现有状态的集群那么pod的名字是没有办法控制的因为是随机产生,同时也无法为每一个pod确定唯一不变的ID号,为了解决这个问题,在k8s中引用了新的资源对象即StatefulSet。在StatefulSet中,Pod名字称为网络标识(hostname),还必须要用到共享存储。 ? 1.StatefulSet里的每个Pod都有稳定、唯一的网络标识,可以用来发现集群中的其他成员,比如说StatefulSet的名字是N,那么第一个pod的名字就是N-0,第二个就是N-1,以此类推 2.StatefulSet控制的pod副本的启停顺序是受控制的,操作第n个pod,前面的n-1的pod已经是运行且准备好的状态。 3.StatefulSet里的pod采用稳定的持久化存储卷,通过PV/PVC来实现,删除Pod时默认不会删除与StatefulSet相关的存储卷(目的是为了保证数据的安全性) ? 所以,StatefulSet的核心功能,就是通过某种方式,记录这些状态,然后在Pod被重新创建时,能够为新Pod恢复这些状态.
在Deployment中,与之对应的服务是service,而在StatefulSet中与之对应的headless service,headless service,即无头服务,与service的区别就是它没有Cluster IP,解析它的名称时将返回该Headless Service对应的全部Pod的Endpoint列表。
除此之外,StatefulSet在Headless Service的基础上又为StatefulSet控制的每个Pod副本创建了一个DNS域名,这个域名的格式为:$(podname).(headless server name)
通过PVC机制来实现存储状态管理
在StatefulSet对象中除了定义PodTemplate还会定义一个volumeClaimTemplates凡是被这个StatefulSet管理的Pod都会声明一个对应的PVC,这个PVC的定义就来自于 volumeClaimTemplates这个模板字段,这个PVC的名字,会被分配一个与这个Pod完全一致的编号。 ? 把一个Pod比如N-0删除之后,这个Pod对应的PVC和PV并不会被删除,而这个Volume 里已经写入的数据,也依然会保存在远程存储服务里 ? StatefulSet在重新创建N-0这个pod的时候.它声明使用的PVC的名字还是叫作:N-0 这个PVC的定义,还是来自于PVC模板(volumeClaimTemplates)这是StatefulSet创建 Pod的标准流程 ? Kubernetes为它查找名叫N-0的PVC时,就会直接找到旧Pod遗留下来的同名的 PVC进而找到跟这个PVC绑定在一起的PV.这样新的Pod就可以挂载到旧Pod对应的那个Volume并且获取到保存在Volume里的数据.通过这种方式Kubernetes的StatefulSet就实现了对应用存储状态的管理
StatefulSet的核心功能就是,通过某种方式记录应用状态,在Pod被重建的时候,通过这种方式还可以恢复原来的状态。
StatefulSet由以下几个部分组成:
Headless Service 用于定义网络标识(DNS)
volumeClaimTemplates ?用于创建PV
StatefulSet ?用于定义具体应用
稳定且有唯一的网络标识符 当节点挂掉,既pod重新调度后其PodName和HostName不变,基于Headless Service来实现
稳定且持久的存储 ?当节点挂掉,既pod重新调度能访问到相同的持久化存储,基于PVC实现
有序、平滑的扩展、部署 即Pod是有顺序的,在部署或者扩展的时候要依据定义的顺序依次进行(即从0到N-1,在下一个Pod运行之前所有之前的Pod必须都是Running和Ready状态),基于init containers来实现。
有序、平滑的收缩、删除 既Pod是有顺序的,在收缩或者删除的时候要依据定义的顺序依次进行(既从N-1到0,既倒序)。
拓扑状态。是应用多个实例之间的不完全对等的关系,这些应用实例是必须按照定义的顺序启动的。例如主应用A先于从应用B启动,如果把A和B删掉,应用还要按照先启动主应用A再启动从应用B,且创建的应用必须和原来的应用的网络标识一样(既PodName和HostName)。这样他们就可以按照原来的顺序创建了。
存储状态。应用实例分别绑定了不同的数据存储,Pod A第一次读到的数据要和10分钟后读到的数据,是同一份。哪怕这期间Pod A被重建。这种典型的例子就是数据库应用的多个存储实例。
由于statefulSet在创建的时候使用的是动态绑定pvc,而动态绑定pvc是需要插件支持的,一下实验通过手动创建pv在通过StatefulSet自动实现pvc申请持久卷。
?
1.创建基于NFS的pv持久卷
[root@k8s-master ~]# mkdir /mnt/data-1 #创建共享目录
[root@k8s-master ~]# mkdir /mnt/data-2
[root@k8s-master ~]# vim /etc/exports
/mnt/data-1 172.16.229.*/24 (rw,sync,insecure,no_subtree_check,no_root_squash)
/mnt/data-2 172.16.229.*/24 (rw,sync,insecure,no_subtree_check,no_root_squash)
[root@k8s-master ~]# systemctl restart nfs
[root@k8s-master ~]# mkdir /k8s/nginx #创建工作目录
[root@k8s-master ~]# cd /k8s/nginx/
[root@k8s-master nginx]# vim nginx-pv.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv-0
labels:
type: pv-0
spec:
capacity:
storage: 5Gi
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Recycle
nfs:
server: 172.16.229.143
path: "/mnt/data-1"
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv-1
labels:
type: pv-1
spec:
capacity:
storage: 5Gi
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Recycle
nfs:
server: 172.16.229.143
path: "/mnt/data-2"
2.创建pv
[root@k8s-master nginx]# kubectl apply -f nginx-pv.yaml
persistentvolume/nginx-pv created
[root@k8s-master nginx]# kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
my-pv 3Gi RWX Recycle Bound default/mypvc pv-nfs 6d16h
pv-0 5Gi RWX Recycle Available 43s
pv-1 5Gi RWX Recycle Available 43s
3.创建 service的Headless Service无头服务
[root@k8s-master nginx]# vim nginx-service.yaml
apiVersion: v1
kind: Service
metadata:
name: nginx #service的名字
labels:
app: nginx
spec:
ports:
- port: 80
name: web
clusterIP: None #采用什么类型的service,这里采用的是Headless Service
selector:
app: nginx
[root@k8s-master nginx]# kubectl apply -f nginx-service.yaml
service/nginx created
[root@k8s-master nginx]# kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 12d
nginx ClusterIP None <none> 80/TCP 5s
4.创建StatefulSet,通过StatefulSet创建pvc实现自动绑定已经创建好的pv
[root@k8s-master nginx]# cat nginx-web.yaml
apiVersion: apps/v1 #选择apiserver的版本
kind: StatefulSet #定义pod类型
metadata:
name: nginx-web
spec:
serviceName: "nginx" #定义属于哪个Headless Service
replicas: 2 #定义pod的副本
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: daocloud.io/library/nginx
ports:
- containerPort: 80
name: web
volumeMounts: #定义卷的挂载到pod的路径
- name: nginx-data
mountPath: /usr/share/nginx/html
volumeClaimTemplates: #pvc的申请模版,会自动创建pvc并与pv绑定。从而实现各pod有专用存储(我们使用的是静态创建pv)
- metadata:
name: nginx-data #pvc的名字
spec:
accessModes: ["ReadWriteMany"]
resources:
requests:
storage: 5Gi #指定每一个持久卷的大小
[root@k8s-master nginx]# kubectl apply -f nginx-web.yaml
查看statefulset
[root@k8s-master nginx]# kubectl get statefulset nginx-web
NAME READY AGE
nginx-web 2/2 19m
查看service
[root@k8s-master nginx]# kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
nginx ClusterIP None <none> 80/TCP 18m
为什么需要volumeClaimTemplate?
对于有状态的副本集都会用到持久存储,对于分布式系统来讲,它的最大特点是数据是不一样的,所以各个节点不能使用同一存储卷,每个节点有自已的专用存储,但是如果在Deployment中的Pod template里定义的存储卷,是所有副本集共用一个存储卷,数据是相同的,因为是基于模板来的 ,而statefulset中每个Pod都要自已的专有存储卷,所以statefulset的存储卷就不能再用Pod模板来创建了,于是statefulSet使用volumeClaimTemplate,称为卷申请模板,它会为每个Pod生成不同的pvc,并绑定pv, 从而实现各pod有专用存储。这就是为什么要用volumeClaimTemplate的原因。
对于一个拥有 N 个副本的 StatefulSet,Pod 被部署时是按照 {0 …… N-1} 的序号顺序创建的。在终端中使用 kubectl get
检查输出。这个输出最终将看起来像下面的样子
[root@k8s-master nginx]# kubectl get -l app='nginx' pods
NAME READY STATUS RESTARTS AGE
nginx-web-0 1/1 Running 0 18m
nginx-web-1 1/1 Running 0 6m34s
请注意在 nginx-web-0
Pod 处于 Running和Ready 状态后 nginx-web-1
Pod 才会被启动
如同 StatefulSets 概念中所提到的,StatefulSet 中的 Pod 拥有一个具有黏性的、独一无二的身份标志。这个标志基于 StatefulSet 控制器分配给每个 Pod 的唯一顺序索引。Pod 的名称的形式为<statefulset name>-<ordinal index>
。web
StatefulSet 拥有两个副本,所以它创建了两个 Pod:nginx-web-0
和nginx-web-1
。
每个 Pod 都拥有一个基于其顺序索引的稳定的主机名。使用[kubectl exec](https://kubernetes.io/zh/docs/reference/generated/kubectl/kubectl-commands/#exec)
在每个 Pod 中执行hostname
[root@k8s-master nginx]# for i in 0 1; do kubectl exec nginx-web-$i -- /bin/bash -c 'hostname'; done
nginx-web-0
nginx-web-1
获取 nginx-web-0
和 nginx-web-1
的 PersistentVolumeClaims。
[root@k8s-master nginx]# kubectl get pvc -l app='nginx'
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
nginx-data-nginx-web-0 Bound pv-0 5Gi RWX 53m
nginx-data-nginx-web-1 Bound pv-1 5Gi RWX 53m
StatefulSet 控制器创建了两个 PersistentVolumeClaims,绑定到两个 PersistentVolumes。由于这里我们使用的是手动创建pv,所有的 PersistentVolume 都是手动创建自动的绑定。
NGINX web 服务器默认会加载位于 /usr/share/nginx/html/index.html
的 index 文件。StatefulSets spec
中的 volumeMounts
字段保证了 /usr/share/nginx/html
文件夹由一个 PersistentVolume 支持。
将 Pod 的主机名写入它们的index.html
文件并验证 NGINX web 服务器使用该主机名提供服务。
[root@k8s-master nginx]# kubectl exec -it nginx-web-0 /bin/bash
root@nginx-web-0:/# echo nginx-web-0 >> /usr/share/nginx/html/index.html
root@nginx-web-0:/# exit
exit
[root@k8s-master nginx]# kubectl exec -it nginx-web-1 /bin/bash
root@nginx-web-1:/# echo nginx-web-1 >> /usr/share/nginx/html/index.html
root@nginx-web-1:/# exit
exit
[root@k8s-master nginx]# kubectl get pods -l app='nginx' -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
nginx-web-0 1/1 Running 0 84m 10.244.2.24 k8s-node2 <none> <none>
nginx-web-1 1/1 Running 0 84m 10.244.1.26 k8s-node1 <none> <none>
[root@k8s-master nginx]# curl -s http://10.244.2.24
nginx-web-0
[root@k8s-master nginx]# curl -s http://10.244.1.26
nginx-web-1
请注意,如果你看见上面的 curl 命令返回了 403 Forbidden 的响应,你需要像这样修复使用 `volumeMounts`挂载的目录的权限:
# for i in 0 1; do kubectl exec web-$i -- chmod 755 /usr/share/nginx/html; done
将StatefulSet创建的 所有 Pod删除
[root@k8s-master nginx]# kubectl delete -f nginx-web.yaml
statefulset.apps "nginx-web" deleted
在使用StatefulSet重新创建的所有Pod
[root@k8s-master nginx]# kubectl apply -f nginx-web.yaml
statefulset.apps/nginx-web created
[root@k8s-master nginx]# kubectl get pods -l app='nginx' -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
nginx-web-0 1/1 Running 0 14s 10.244.2.25 k8s-node2 <none> <none>
nginx-web-1 1/1 Running 0 12s 10.244.1.27 k8s-node1 <none> <none>
我们发现pod的IP地址发生了变化,接下来再次访问
[root@k8s-master nginx]# curl -s http://10.244.2.25
nginx-web-0
[root@k8s-master nginx]# curl -s http://10.244.1.27
nginx-web-1
虽然 nginx-web-0
和 nginx-web-1
被重新调度了,但它们仍然继续监听各自的主机名,因为和它们的 PersistentVolumeClaim 相关联的 PersistentVolume 被重新挂载到了各自的 volumeMount
上。不管 nginx-web-0
和 nginx-web-1
被调度到了哪个节点上,它们的 PersistentVolumes 将会被挂载到合适的挂载点上。
扩容/缩容 StatefulSet 指增加或减少它的副本数。这通过更新 replicas
字段完成.如果是手动创建的pv,那么需要提前将pv创建好了。
[root@k8s-master ~]# mkdir /mnt/data-3
[root@k8s-master ~]# vim /etc/exports
/mnt/data-3 172.16.229.*/24 (rw,sync,insecure,no_subtree_check,no_root_squash)
[root@k8s-master ~]# systemctl restart nfs
[root@k8s-master ~]# mkdir /k8s/application/mysql
# 如果mysql-server.yaml在apply时出错,请将mysql.pv.yaml delete即可
[root@k8s-master mysql]# cat mysql.pv.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
name: mysql-pv
labels:
type: mysql-pv
spec:
capacity:
storage: 5Gi
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Recycle
nfs:
server: 172.16.229.143
path: "/mnt/data-3"
[root@k8s-master mysql]# cat mysql-svc.yaml
apiVersion: v1
kind: Service
metadata:
name: mysql
labels:
app: mysql
spec:
ports:
- port: 3306
name: mysql
clusterIP: None
selector:
app: mysql
[root@k8s-master mysql]# cat mysql-server.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mysql
spec:
serviceName: "mysql"
replicas: 1
selector:
matchLabels:
app: mysql
template:
metadata:
labels:
app: mysql
spec:
containers:
- name: mysql
image: daocloud.io/library/mysql:5.7
ports:
- containerPort: 3306
env:
- name: MYSQL_ROOT_PASSWORD
value: "Qfedu@123"
volumeMounts:
- name: mysql-data
mountPath: /var/lib/mysql
volumeClaimTemplates:
- metadata:
name: mysql-data
spec:
accessModes: ["ReadWriteMany"]
storageClassName: managed-nfs-storage
resources:
requests:
storage: 5Gi
[root@k8s-master mysql]# kubectl apply -f mysql.pv.yaml
persistentvolume/mysql-pv created
[root@k8s-master mysql]# kubectl apply -f mysql-svc.yaml
service/mysql created
[root@k8s-master mysql]# kubectl apply -f mysql-server.yaml
statefulset.apps/mysql created
[root@k8s-master mysql]# kubectl get pv | grep mysql
mysql-pv 5Gi RWX Recycle Bound default/mysql-data-mysql-0 7m28s
[root@k8s-master mysql]# kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
mysql-data-mysql-0 Bound mysql-pv 5Gi RWX 7m16s
[root@k8s-master mysql]# kubectl get pods -l app='mysql' -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
mysql-0 1/1 Running 0 6m22s 10.244.1.28 k8s-node1 <none> <none>
[root@k8s-master mysql]# kubectl exec -it mysql-0 /bin/bash
root@mysql-0:/# mysql -uroot -p'Qfedu@123'
mysql: [Warning] Using a password on the command line interface can be insecure.
Welcome to the MySQL monitor. Commands end with ; or \g.
...
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| mysql |
| performance_schema |
| sys |
+--------------------+
4 rows in set (0.01 sec)
mysql> create database test1;
Query OK, 1 row affected (0.00 sec)
mysql> exit
Bye
root@mysql-0:/# exit
exit
[root@k8s-master mysql]# cd /mnt/data-3/
[root@k8s-master data-3]# ll
总用量 188484
-rw-r----- 1 polkitd ssh_keys 56 11月 15 16:24 auto.cnf
-rw------- 1 polkitd ssh_keys 1676 11月 15 16:24 ca-key.pem
-rw-r--r-- 1 polkitd ssh_keys 1112 11月 15 16:24 ca.pem
-rw-r--r-- 1 polkitd ssh_keys 1112 11月 15 16:24 client-cert.pem
-rw------- 1 polkitd ssh_keys 1680 11月 15 16:24 client-key.pem
-rw-r----- 1 polkitd ssh_keys 1353 11月 15 16:25 ib_buffer_pool
-rw-r----- 1 polkitd ssh_keys 79691776 11月 15 16:25 ibdata1
-rw-r----- 1 polkitd ssh_keys 50331648 11月 15 16:25 ib_logfile0
-rw-r----- 1 polkitd ssh_keys 50331648 11月 15 16:24 ib_logfile1
-rw-r----- 1 polkitd ssh_keys 12582912 11月 15 16:25 ibtmp1
drwxr-x--- 2 polkitd ssh_keys 4096 11月 15 16:25 mysql
drwxr-x--- 2 polkitd ssh_keys 8192 11月 15 16:25 performance_schema
-rw------- 1 polkitd ssh_keys 1680 11月 15 16:24 private_key.pem
-rw-r--r-- 1 polkitd ssh_keys 452 11月 15 16:24 public_key.pem
-rw-r--r-- 1 polkitd ssh_keys 1112 11月 15 16:24 server-cert.pem
-rw------- 1 polkitd ssh_keys 1680 11月 15 16:24 server-key.pem
drwxr-x--- 2 polkitd ssh_keys 8192 11月 15 16:25 sys
drwxr-x--- 2 polkitd ssh_keys 20 11月 15 16:29 test1
[root@k8s-master mysql]# kubectl delete -f mysql-server.yaml
statefulset.apps "mysql" deleted
[root@k8s-master mysql]# cd /mnt/data-3/
[root@k8s-master data-3]# ll
总用量 176196
-rw-r----- 1 polkitd ssh_keys 56 11月 15 16:24 auto.cnf
-rw------- 1 polkitd ssh_keys 1676 11月 15 16:24 ca-key.pem
-rw-r--r-- 1 polkitd ssh_keys 1112 11月 15 16:24 ca.pem
-rw-r--r-- 1 polkitd ssh_keys 1112 11月 15 16:24 client-cert.pem
-rw------- 1 polkitd ssh_keys 1680 11月 15 16:24 client-key.pem
-rw-r----- 1 polkitd ssh_keys 688 11月 15 16:32 ib_buffer_pool
-rw-r----- 1 polkitd ssh_keys 79691776 11月 15 16:32 ibdata1
-rw-r----- 1 polkitd ssh_keys 50331648 11月 15 16:32 ib_logfile0
-rw-r----- 1 polkitd ssh_keys 50331648 11月 15 16:24 ib_logfile1
drwxr-x--- 2 polkitd ssh_keys 4096 11月 15 16:25 mysql
drwxr-x--- 2 polkitd ssh_keys 8192 11月 15 16:25 performance_schema
-rw------- 1 polkitd ssh_keys 1680 11月 15 16:24 private_key.pem
-rw-r--r-- 1 polkitd ssh_keys 452 11月 15 16:24 public_key.pem
-rw-r--r-- 1 polkitd ssh_keys 1112 11月 15 16:24 server-cert.pem
-rw------- 1 polkitd ssh_keys 1680 11月 15 16:24 server-key.pem
drwxr-x--- 2 polkitd ssh_keys 8192 11月 15 16:25 sys
drwxr-x--- 2 polkitd ssh_keys 20 11月 15 16:29 test1
[root@k8s-master data-3]# cd -
/k8s/application/mysql
[root@k8s-master mysql]# kubectl apply -f mysql-server.yaml
statefulset.apps/mysql created
[root@k8s-master mysql]# kubectl exec -it mysql-0 /bin/bash
root@mysql-0:/# mysql -uroot -p'Qfedu@123'
...
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| mysql |
| performance_schema |
| sys |
| test1 |
+--------------------+
5 rows in set (0.01 sec)
mysql>
无状态:
1. pod命名:pod名由资源名+随机的字符串组成; 2. 数据存储:多个实例pod可以共享相同的持久化数据,存储不是必要条件; 3. 扩缩容:可以随意扩缩容某个pod,不会指定某个pod进行扩缩容; 4. 启停顺序:因为pod名的序号是随机串,无启停顺序之分; 5. 无状态k8s资源:ReplicaSet、ReplicationController、Deployment、DaemonSet、Job等资源; 6. 无状态服务:tomcat、nginx等;
有状态 这里假设有N个pod;
1. pod命名:pod名由statefulset资源名+有序的数字组成(0,1,2...N-1),且pod有特定的网络标识; 2. 数据存储:有状态的服务对应实例需要有自己的独立持久卷存储; 3. 扩缩容:扩缩容不可随意,缩容是从数字最大的开始递减,扩容是在原有pod序号基础上递增1。 4. 启停顺序:pod启停是有顺序的,启动时,先启动pod序号为0的,然后依次递增至N-1;停止时,先停止pod序号为最大的N-1,然后依次递减至0; 5. 有状态k8s资源:StatefulSet资源; 6. 有状态服务:Kafka、ZooKeeper、MySql、MongoDB以及一些需要保存日志的应用等服务;