kubernetes学习笔记八(存储)

kubernetes学习笔记八(存储)

Scroll Down
一、四种存储方案

configMap:在k8s中起到专一存储配置文件的目的

Secret:一种加密的方案,比如密钥、用户密码信息,base64

volume:为pod提供一种共享存储卷的能力,比如nfs共享、本地磁盘下某个目录的共享等

Persistent Volume ( PV ):持久卷

二、ConfigMap

ConfigMap描述信息:

ConfigMap 功能在 Kubernetes1.2 版本中引入,许多应用程序会从配置文件、命令行参数或环境变量中读取配置信息。ConfigMap API 给我们提供了向容器中注入配置信息的机制,ConfigMap 可以被用来保存单个属性,也可以用来保存整个配置文件或者 JSON 二进制大对象

使用场景:

当我们有很多nginx节点时,如果我们需要修改nginx的配置文件的时候,我们可以通过脚本等方案来批量修改nginx节点的配置文件,但是如果我们的nginx节点分属于不同的组( 即不同组需要不同的配置文件 ),这个时候修改不同组的配置文件就比较繁琐了。

nginxregister.jpg

如图,这时候就有了名叫 配置文件注册中心的这样一种解决方案,不同的nginx应用程序会向配置文件注册中心索要配置信息,配置文件注册中心会根据它的主机名、IP等信息向其分配配置文件。当然,不同的应用程序索要到的配置文件是不同的,包括后期需要修改这些配置文件的话,只用修改配置文件注册中心的文件,就会触动更新相应节点中的配置文件。这样就使维护与更新变得更简单一些

nginxconfigMap.jpg

ConfigMap的作用机制就与上面提到的配置文件注册中心几乎一致,当Pod被创建的时候被指定从ConfigMap引入配置文件,当后期ConfigMap中的配置文件被修改之后,Pod中引入的配置文件也会自动更新,pod重新载入配置文件的话,可能需要重启、重载等操作

ConfigMap的创建:

1、使用目录创建

使用目录创建ConfigMap的话,目录下的文件名会作为键值对的key,文件的内容会作为键值对的value

createconfigmapdir.jpg

getconfigmap.PNG

describeconfigmapdir.jpg

# kubectl create configmap game-config --from-file=/root/configMap
# --from-file指定,在目录下的所有文件都会被用在 ConfigMap 里面创建一个键值对,键的名字就是文件名,值就是文件的内容

2、使用文件创建

只要指定为一个文件就可以从单个文件中创建 ConfigMap,--from-file这个参数可以使用多次,指定多个文件

# kubectl create configmap game-config-2 --from-file=/root/configMap/game.properties --from-file=/root/configMap/ui.properties

create_cm_file.png

get_cm_yaml.png

3、使用字面值创建

使用文字值创建,利用--from-literal参数传递配置信息,该参数可以使用多次,格式如下:

# kubectl create configmap 名称 --from-literal=键名称=值名称 --from-literal=键名称=值名称

# kubectl get configmaps config名称  -o yaml

createconfigfromliteral.jpg

在Pod中使用ConfigMap

1、使用ConfigMap来代替环境变量

for_env_create_configmap.png

create_configmap_from_yaml.jpg

create_pod_use_configmap.png

look_pod_env.jpg

2、使用Configmap作为命令行参数

use_configmap_for_command.jpg

3、通过数据卷插件使用ConfigMap

意思就是将configmap挂载到容器中的某一个位置下进行使用,即将文件填入数据卷,在这个文件中,键就是文件名,键值就是文件内容

config_mount_volume_pod.jpg

# 检查哪些api支持当前的kubernetes对象
# kubectl api-resources | grep deployment

ConfigMap的热更新

热更新意为,将ConfigMap挂载到Pod内部后,通过命令kubectl edit configmap cm名称修改configmap的话,Pod内部挂载的Configmap也会被更新,但是默认情况下仅会更新Pod内挂载的ConfigMap值,如果该ConfigMap被nginx等应用使用,其实并不会触发nginx的重载等动作

1、创建ConfigMap、Deployment,并将ConfigMap挂载到Pod中

create_configmappod.png

2、exec进入Pod查看挂载的ConfigMap值

get_pod_configmap_data.png

3、edit修改ConfigMap的值

kubectl_edit_configmap.png

edit_configmap_data.png

5、等待大概10s左右时间,查看Pod中的ConfigMap值是否发生了改变

get_changed_configmap.png

更新 ConfigMap 目前并不会触发相关 Pod 的滚动更新,可以通过修改 pod annotations 的方式强制触发滚动更新

# kubectl patch deployment my-nginx --patch'{"spec": {"template": {"metadata": {"annotations":{"version/config": "20201209" }}}}}'
# 这个例子里我们在.spec.template.metadata.annotations中添加version/config
# 每次通过修改version/config来触发滚动更新

更新 ConfigMap 后:

使用该 ConfigMap 挂载的 Env 不会同步更新

使用该 ConfigMap 挂载的 Volume 中的数据需要一段时间(实测大概10秒)才能同步更新

三、Secret

Secret在yaml资源清单中是加密的,但是如果以卷的形式挂载到pod中。在pod中是自动解密的

1、Secret

Secret 解决了密码、token、密钥等敏感数据的配置问题,而不需要把这些敏感数据暴露到镜像或者 Pod Spec中。Secret 可以以 Volume 或者环境变量的方式使用

Secret有三种类型:

**Service Account:**用来访问 Kubernetes API,由 Kubernetes 自动创建,并且会自动挂载到 Pod 的/run/secrets/kubernetes.io/serviceaccount目录中

**Opaque:**base64编码格式的Secret,用来存储密码、密钥等(因为base64加密解密都是固定的,其实他认识可以根据加密后的敏感数据得到原数据的)

**kubernetes.io/dockerconfigjson:**用来存储私有 docker registry 的认证信息

Service Account

Service Account 用来访问 Kubernetes API,由 Kubernetes 自动创建,并且会自动挂载到 Pod的/run/secrets/kubernetes.io/serviceaccount目录中

describe_serviceaccount_secret.png

2、Opaque Secret

Opaque 类型的数据是一个 map 类型,要求 value 是 base64 编码格式( base64对同一字符加密解密得到的结果都是固定的,这就使解密很容易 )

① 创建一个base64编码的账户密码

# echo -n "admin" | base64
YWRtaW4=
# echo -n "1f2d1e2e67df" | base64
MWYyZDFlMmU2N2Rm

base64.png

使用方式:

② 将Secret挂载到Volume中

apply_opaque_secret.png

查看挂载到Pod中的Secret

exec_cat_opaque_secret.png

③ 将 Secret 导出到Pod环境变量中

这个测试中我遇到了一个奇怪的问题,将Opaque Secret作为Env挂载到Pod中时,Pod也无报错,也无异常,但是就是Opaque Secret没有生效成为Pod的Env。然后经我多方测试之后,最终确定了containers.env.name 用了横杠"-"就导致该Secret在Pod中未生效

wrong_envname.png

当我修改了containers.env.name 字段无横杠"-"时,就成功得到了该Env值,就很迷

describe_pod_get_info.png

no_opaque_env.png

edit_env_name_success.png

3、kubernetes.io/dockerconfigjson

此种类型的Secret主要是为了应用在镜像仓库认证时使用,用户从镜像仓库拉取镜像需要进行登陆验证,否则无法拉取、推送镜像

如果镜像仓库的镜像是私有的,那么你在拉取镜像时如果不加以认证就会无法拉取该镜像,所以docker-registry的方式就是:先创建一个docker-registry 类型的Secret,然后创建Pod、Deployment的时候通过imagePullSecrets字段来引用创建的docker-registry 类型的Secret,便可以与镜像仓库进行认证并下载镜像

① 使用kubectl创建docker registry认证的secret

# kubectl create secret docker-registry myregistrykey --docker-server=DOCKER_REGISTRY_SERVER --docker-username=DOCKER_USER --docker-password=DOCKER_PASSWORD --docker-email=DOCKER_EMAI

docker-registry 是Secret的类型,名称是myregistrykey

② 在创建Pod的时候,通过imagePullSecrets来引用刚创建的myregistrykey

apiVersion: v1
kind: Pod
metadata: 
name: foo
spec:
containers:
    - name: foo
      image: nginx
  imagePullSecrets:
    - name: myregistrykey

四、Volume

Volume

容器磁盘上的文件的生命周期是短暂的,这就使得在容器中运行重要应用时会出现一些问题。首先,当容器崩溃时,kubelet 会重启它,但是容器中的文件将丢失——容器以干净的状态(镜像最初的状态)重新启动。其次,在Pod中同时运行多个容器时,这些容器之间通常需要共享文件。Kubernetes 中的Volume抽象就很好的解决了这些问题

docker与k8s的模式还不太一样,在docker中,如果容器意外崩溃且重启策略为Always的话,容器重启时并不会清除原有的数据,而在k8s中,如果Pod意外崩溃,那么Pod重启时就会以最干净的状态(即镜像最初的状态)重新启动。

背景

Kubernetes 中的卷有明确的寿命 —— 与封装它的 Pod 相同。所f以,卷的生命比 Pod 中的所有容器都长,当这个容器重启时数据仍然得以保存。当然,当 Pod 不再存在时,卷也将不复存在。也许更重要的是,Kubernetes支持多种类型的卷,Pod 可以同时使用任意数量的卷

那为什么以卷volume的形式挂载到Pod中之后,容器的崩溃不会造成数据的丢失呢?

pod_volume.gif

在上图中,volume卷挂载到了Pod的pause容器上,容器1、2共享pause容器的存储卷,当容器1、2意外崩溃的时候,实际环境的 Deployment 等控制器会重建新的容器1、2,但是挂载卷volume是始终没有发生变化,变化被重启的只是容器本身,这样就避免了数据的丢失。

Kubernetes 支持以下类型的卷:

awsElasticBlockStore、azureDisk、azureFile、cephfs、csi、downwardAPI、emptyDir、

fc、flocker、gcePersistentDisk、gitRepo、glusterfs、hostPath、iscsi、local、nfs、

persistentVolumeClaim、projected、portworxVolume、quobyte、rbd、scaleIO、secret

storageos、vsphereVolume

官方示例用法

1、EmptyDir

当 Pod 被分配给节点时,首先创建emptyDir卷,并且只要该 Pod 在该节点上运行,该卷就会存在。正如卷的名字所述,它最初是空的。Pod 中的容器可以读取和写入emptyDir卷中的相同文件,尽管该卷可以挂载到每个容器中的相同或不同路径上。当出于任何原因从节点中删除 Pod 时,emptyDir中的数据将被永久删除

EmptyDir卷的原理与上面的 gif 类似,相当于将node节点的EmptyDir空卷挂载到了Pod的pause容器,当容器意外崩溃时重建的只是容器本身,EmptyDir还是依然挂载在Pause容器上的并没有改变

EmptyDir的用法

暂存空间,例如用于基于磁盘的合并排序

用作长时间计算崩溃恢复时的检查点

Web服务器容器提供数据时,保存内容管理器容器提取的文件

EmptyDir测试:

可以将该EmptyDir卷挂载到Pod的多个容器的不同路径下,使多个容器共享该EmptyDir卷

在使用yaml创建Pod多容器的时候,我忘记了一件事情:Pod内一个容器为nginx,一个镜像为centos,我在创建Pod的时候一直在纳闷为什么centos那个容器为什么没有错误日志却一直不是Ready,测试了好大一会才想起来,容器在创建成功之后如果没有后台进程存在的话,那么它就会直接退出!

所以如果我们想在一个Pod内创建多个容器挂载emptyDir卷,那么就要给无后台进程的就要给他加一个sleep避免其自动退出

apiVersion: v1
kind: Pod
metadata:
  name: emptydirpod
spec:
  containers:
  - image: nginx
    name: container1
    volumeMounts:
    - mountPath: /emptydir1
      name: emptydir-volume
  - image: centos
    name: container2
    command: ['/bin/sh','-c','sleep 3600s']
    volumeMounts:
    - mountPath: /emptydir2
      name: emptydir-volume
  volumes:
  - name: emptydir-volume
    emptyDir: {}

emptydir_volume.gif

2、hostPath

hostPath卷能将主机节点的文件系统中的文件或目录挂载到集群的Pod中

hostpath的一些用法如下:

运行一个需要访问Docker内部的容器时,可使用hostPath 挂载 宿主机的/var/lib/docker 路径。

允许指定所需要的`hostPath在Pod运行前是否应该存在,是否应该创建等

除了必需的 path 属性之外,用户可以选择性地为 hostPath 卷指定 type

行为
空字符串(默认)用于向后兼容,这意味着在挂载 hostPath 卷之前不会执行任何检查
DirectoryOrCreate如果在给定的路径上没有任何东西存在,那么将根据需要在那里创建一个空目录,权限设置为 0755,与 Kubelet 具有相同的组和所有权
Directory给定的路径下必须存在目录
FileOrCreate如果在给定的路径上没有任何东西存在,那么会根据需要创建一个空文件,权限设置为 0644,与 Kubelet 具有相同的组和所有权
File给定的路径下必须存在文件
Socket给定的路径下必须存在 UNIX 套接字
CharDevice给定的路径下必须存在字符设备
BlockDevice给定的路径下必须存在块设备

使用hostPath卷类型的时候请注意:

因为hostPath是将宿主机的特定文件或目录挂载到Pod中,比如将宿主机的nfs目录挂载到Pod的test目录下,结果有一天Pod被调度到了另一个node节点,然而这个节点(即宿主机)上并没有nfs目录,这种情况下就会发生问题。所以如果想使用hostpath这种方案的话,就要确定每个node节点上都有一样的挂载目录等设置。

当 Kubernetes 按照计划添加资源感知调度时,将无法考虑hostPath使用的资源。比如hostPath设置将宿主机/data目录挂载到容器的/test目录下,不同node节点下的文件数据可能不一样,这就会导致hostPath会使用到不同的资源

在底层主机上创建的文件或目录只能由 root 写入。所以需要在特权容器中以 root 身份运行进程,或修改主机上的文件权限以便写入hostPath卷

hostPath测试:

apiVersion: v1
kind: Pod
metadata:
  name: hostpath-test-pod
spec:
  containers:
  - image: nginx
    name: container1
    volumeMounts:
    - mountPath: /hostpath1
      name: test-hostpath-volume
  volumes:
  - name: test-hostpath-volume
    hostPath:
      path: /data
      type: Directory

hostpath_volume.gif

五、PV、PVC

PV (Persistent Volumes):

在k8s中,可以将外部nfs、mfs(一个具有容错性的网络分布式文件系统)等集群外部的存储挂载到K8s集群内部的PV( Persistent Volumes )上,集群内部的Pod可以通过连接PV来使用该集群外部的存储

PV官方概念

是由管理员设置的存储,它是集群的一部分。就像节点是集群中的资源一样,PV 也是集群中的资源。 PV 是Volume 之类的卷插件,但PV 独立于 Pod 的生命周期之外( 即Pod被删除了,PV依然存在 )。此 API 对象包含存储实现的细节,即 NFS、iSCSI 或特定于云供应商的存储系统

PV 供应分类

静态供应PV

集群管理员自身创建的PV卷,它们带有集群用户使用的实际存储的细节,比如该PV卷的实际IP,连接方式等。它们存在于Kubernetes API中

动态供应PV

当管理员创建的静态 PV 都不匹配用户的PVC时,集群可能会尝试动态地为 PVC 创建卷。此配置基于StorageClasses:PVC 必须请求 [存储类],并且管理员必须创建并配置该类才能进行动态创建。声明该类为""可以有效地禁用其动态配置

要启用基于存储级别的动态存储配置,集群管理员需要启用 API server 上的DefaultStorageClass[准入控制器]。例如,通过确保DefaultStorageClass位于 API server 组件的--admission-control标志,使用逗号分隔的有序值列表中,可以完成此操作

pv.jpg

PV卷类型

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、VMware Photon、Portworx Volumes、ScaleIO Volumes、StorageOS

PV卷演示代码:

apiVersion: v1
kind: PersistentVolume  # PV卷kind
metadata:
  name: pv0003
spec:
  capacity:
    storage: 5Gi
  volumeMode: Filesystem  # 卷类型
  accessModes:    # PV卷访问模式
    - ReadWriteOnce  # ReadWriteOnce为该PV仅允许一个单位(即仅允许一个PVC、一个Pod等进行绑定)进行读、写
  persistentVolumeReclaimPolicy: Recycle  # persistentVolumeReclaimPolicy为PV卷的回收策略
  storageClassName: slow  # 存储类的名称,比如PV卷挂载的外部存储速度有快有慢,这时候我们就可以根据存储速度进行分类,而每一个类就是通过这个storageClassName进行区分。Pod的PVC请求中,就可以通过这个storageClassName来表明所想要使用的PV卷的类型,而该PVC就会去该类下寻找符合其他条件的PV卷
  mountOptions:  # 挂载的一些额外选项
    - hard
    - nfsvers=4.1
  nfs:  # 挂载的存储的服务类型,这里是nfs,指定了哪一个服务器下的哪一个目录作为PV卷
    path: /tmp
    server: 172.17.0.2
    
# 以上资源清单就实现了将nfs外部存储挂载成为集群内部的PV卷pv0003,大小为5GB
# 资源清单中涉及到的 accessModes访问模式、persistentVolumeReclaimPolicy回收策略、storageClassName存储类的含义等将会在下面详细介绍

PV 访问模式

PV卷可以,以资源提供者支持的任何方式挂载到主机上。如下表所示,供应商具有不同的功能,每个PV 的访问模式都将被设置为该卷支持的特定模式。例如,NFS 可以支持多个读/写客户端,但特定的 NFS PV 可能是以只读的方式导出到服务器上。每个 PV 都有一套自己的用来描述特定功能的访问模式

ReadWriteOnce——该卷可以被单个节点以读/写模式挂载

ReadOnlyMany——该卷可以被多个节点以只读模式挂载

ReadWriteMany——该卷可以被多个节点以读/写模式挂载

在命令行中,访问模式的缩写方式为:

RWO - ReadWriteOnce

ROX - ReadOnlyMany

RWX - ReadWriteMany

注意:一个卷一次只能使用一种访问模式挂载,即使它支持很多访问模式。例如,GCEPersistentDisk 可以由单个节点作为ReadWriteOnce模式挂在,或由多个节点以ReadOnlyMany模式挂载,但不能同时挂载。

pvaccessmode.jpg

PV卷回收策略:

Retain(保留) -- 手动回收。当 PVC 被删除时并不会一并删除其所使用的PV,该PV会一直保留到管理员手动删除,如果管理员一直无操作,则该卷会被一直保留。

Recycle(回收)-- 如果下层的卷插件支持,回收策略 Recycle 会在卷上执行一些基本的 擦除(rm -rf /thevolume/*)操作,相当于擦除卷中的数据,之后允许该卷用于新的 PVC 申领。( Recycle 已被废弃。取而代之的建议方案是使用动态供应 )

Delete(删除)-- 对于支持 Delete 回收策略的卷插件,删除动作会将 PersistentVolume 对象从 Kubernetes 中移除,同时也会从外部基础设施(如 AWS EBS、GCE PD、Azure Disk 或 Cinder 卷)中移除所关联的存储资产。动态供应的卷会继承其 StorageClass 中设置的回收策略,该策略默认 为 Delete。 管理员需要根据用户的期望来配置 StorageClass;否则 PV 卷被创建之后必须要被 编辑或者修补。

PV 卷状态

Available ( 可用 ) -- 此状态下的PV卷没有被任何声明所绑定,处于可用的状态

Bound( 已绑定 )-- 此状态下的PV卷已经被声明所绑定,如果该PV卷的模式为 ReadOnlyMany 、ReadWriteMany

Released( 已释放 )-- 该PV卷绑定的声明已经被删除了,但是资源还没有被集群重新声明,等待一些时间,等待k8s集群重新将其声明化

Failed( 失败 ) -- 回收策略执行失败,产生了这么一个状态

命令行会显示绑定到PV的PVC名称

PVC (Persistent Volumes Claim):

当集群达到一定规模,PV的数量达到一级级别,比如集群中挂载有上万的PV卷,当我们要使用具体的PV卷时,我们还要去考虑该PV卷的速度如何,容量如何,是否能够达到当前的需求?这就需要我们自己去匹配到某一个具体的PV卷,这个步骤想想就觉得繁琐的不行,要从那么大的一个量级中找出适合自己需求的PV卷。

所以为了解决这个问题,就引出了PVC的概念:

在创建Pod的时候,会给其附加一个PVC的请求,该PVC请求会描述用户所需要使用的PV持久卷的具体要求(比如需要多大的容量、稳定性、速度、级别等),PVC会自动在所有的PV卷中匹配最适合的PV持久卷( PVC会匹配到满足需求的前提下选择资源量最小的一个PV,比如容量要求是20GB,在满足了其他要求后,一个21GB,一个25GB,PVC就会选择这个21GB ),并与之绑定,这样该Pod就可以使用到PVC所绑定的PV持久卷

PVC官方概念

是用户存储的请求。它与 Pod 相似。Pod 消耗节点资源,PVC 消耗 PV 资源。Pod 可以请求特定级别的资源(CPU 和内存)。PVC声明可以请求特定的大小和访问模式(例如,可以以读/写一次或只读多次模式挂载)

PV与PVC绑定

master 中的控制环路监视者新的 PVC 请求,寻找匹配的 PV(如果可能),并将它们绑定在一起。如果为新的 PVC 动态调配 PV,则该环路将始终将该 PV 绑定到 PVC。否则,用户总会得到他们所请求的存储,但是容量可能超出要求的数量( 满足需求前提下选择最小资源量 )。一旦 PV 和 PVC 绑定后,PVC的绑定是排他性的,不管它们是如何绑定的。 PVC 跟PV 绑定是一对一的映射。即 PV 与 PVC对应绑定之后,该PV卷将不能被其他PVC所请求到。

PVC(持久化卷声明)的保护:

PVC 保护的目的是确保由 pod 正在使用的 PVC 不会从系统中移除,因为如果PVC被移除的话可能会导致数据丢失

( 意即为当Pod被删除的时候,其PVC不会被一并删除,因为如果该PVC被一并删除了,可能将会导致PV数据的丢失 )

需要注意的是: 当Pod的状态为Pending并且Pod已经分配给节点,或Pod的状态为Running时,PVC仍处于活跃状态

当启用PVC 保护 ,alpha 功能时,如果用户删除了一个 pod 正在使用的 PVC,则该 PVC 不会被立即删除。PVC 的删除将被推迟,直到 PVC 不再被任何 pod 使用

PV 持久化存储测试: 结合NFS

# nfs server 安装配置

# yum install -y nfs-common nfs-utils  rpcbind
# mkdir /nfsdata
# chmod 666 /nfsdata
# chown nfsnobody /nfsdata
# cat /etc/exports
/nfsdata *(rw,no_root_squash,no_all_squash,sync)
# systemctl start rpcbind
# systemctl start nfs
# nfs 客户端(k8s每个节点都安装nfs客户端)

# yum -y install nfs-utils rpcbind
# 测试挂载并查看是否生效
#showmount -e 192.168.52.100
Export list for 192.168.52.100:
/nfsdata *
# mkdir /mount_nfs
# mount -t nfs 192.168.52.100:/nfsdata /mount_nfs/
# cd /mount_nfs/
# cat << EOF >> test.html
> hello world
> EOF

nfsserver.jpg

nfsserverconfig.jpg

k8s每个节点都要安装:
k8seachnodeinstallnfs.jpg

测试nfs使用
nfs_get_data.jpg

check_mount.jpg

测试没问题之后进行umount,如果nfs进程仍然打开有文件的话,umount执行可能会失败,用lsof查询一下nfs打开的文件,并将该进程杀掉即可
check_mount.jpg

创建4个pv

apiVersion: v1
kind: PersistentVolume
metadata: 
  name: nfspv1
spec:
  capacity:
    storage: 1Gi
  accessModes:    # 访问模式
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain  # 回收策略指定为保留Retain
  storageClassName: nfs
  nfs:
    path: /1nfsdata              # nfs服务器的共享路径
    server: 192.168.52.100       # nfs服务器地址
---
apiVersion: v1
kind: PersistentVolume
metadata: 
  name: nfspv2
spec:
  capacity:
    storage: 5Gi
  accessModes:
    - ReadOnlyMany
  persistentVolumeReclaimPolicy: Retain
  storageClassName: nfs
  nfs:
    path: /2nfsdata
    server: 192.168.52.100
---
apiVersion: v1
kind: PersistentVolume
metadata: 
  name: nfspv3
spec:
  capacity:
    storage: 5Gi
  accessModes:
    - ReadWriteMany
  persistentVolumeReclaimPolicy: Retain
  storageClassName: slow
  nfs:
    path: /3nfsdata
    server: 192.168.52.100
---
apiVersion: v1
kind: PersistentVolume
metadata: 
  name: nfspv4
spec:
  capacity:
    storage: 1Gi
  accessModes:
    - ReadOnlyMany
  persistentVolumeReclaimPolicy: Retain
  storageClassName: nfs
  nfs:
    path: /4nfsdata
    server: 192.168.52.100

apply_4pv.jpg

pv创建出来之后状态为Available的情况下就已经可以被Pod挂载使用了,但是正常情况下我们都是通过PVC请求来绑定PV进行使用的。

创建StatefulSet、无头Service、PVC去使用上面创建PV:

apiVersion: v1      
kind: Service 
metadata:
  name: nginx
  labels:
    app: nginx
spec: 
  ports:
  - port: 80
    name: web
  clusterIP: None    # Service类型为无头的ClusterIP
  selector:
    app: nginx
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: web
spec: 
  selector:
    matchLabels: 
      app: nginx
  serviceName: "nginx"   # 这里StatefulSet所使用的Service要与上面创建的无头Service的Name一致,该StatefulSet就是使用上面的无头Service
  replicas: 3  
  template:
    metadata:
      labels:
        app: nginx
    spec: 
      containers:
      - name: nginx
        image: nginx
        ports:
        - containerPort: 80
          name: web
        volumeMounts:
        - name: www
          mountPath: /usr/share/nginx/html
  volumeClaimTemplates:     # 卷声明模板,就是PVC,这一部分来描写所申请PV的一些特定条件,比如访问模式、存储类名、请求的大小
  - metadata:
      name: www
    spec: 
      accessModes: ["ReadWriteOnce"]
      storageClassName: "nfs"
      resources:
        requests:
          storage: 1Gi

下面的gif图详细记录了创建的过程,创建无头Service、StatefulSet、PVC的yaml资源清单的介绍在上面的代码中有相关介绍

由上面创建PV的步骤可以得知,集群现有的PV总共有四个,分别是nfs类的RWO模式nfs类的ROXslow类的RWXnfs类的ROX,但是我们在创建StatefulSet的3个Pod副本时,PVC都要求PV是nfs类,且访问模式为RWO,这样的话符合条件PV就只有一个了。那么虽然StatefulSet定义了3个副本数目,但是第二个pod因为没有PV可挂载就无法正常启动,因为StatefulSet是按序创建的,,第二个不能创建成功,那第三个pod副本就更无法被创建了。

apply_svc_statefulset_pvc_pv.gif

statefulSet创建的Pod副本通过PVC绑定外部nfs挂载到k8s集群内部的pv卷:
为了测试效果,这里选择删除上面创建的svc、StatefulSet、pvc、然后删除pv,并将PV的访问模式都修改为RWO,且存储类名为nfs,这样就可以匹配pvc所申请PV的要求了。

# pv
apiVersion: v1
kind: PersistentVolume
metadata: 
  name: nfspv1
spec:
  capacity:
    storage: 1Gi
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  storageClassName: nfs
  nfs:
    path: /1nfsdata
    server: 192.168.52.100
---
apiVersion: v1
kind: PersistentVolume
metadata: 
  name: nfspv2
spec:
  capacity:
    storage: 5Gi
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  storageClassName: nfs
  nfs:
    path: /2nfsdata
    server: 192.168.52.100
---
apiVersion: v1
kind: PersistentVolume
metadata: 
  name: nfspv3
spec:
  capacity:
    storage: 5Gi
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  storageClassName: nfs
  nfs:
    path: /3nfsdata
    server: 192.168.52.100
---
apiVersion: v1
kind: PersistentVolume
metadata: 
  name: nfspv4
spec:
  capacity:
    storage: 1Gi
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  storageClassName: nfs
  nfs:
    path: /4nfsdata
    server: 192.168.52.100

这样,再去创建新的pv,新的svc、新的statefulSet、新的pvc(该资源清单并未修改),再来查看PVC与PV是否已绑定:

statefulset_bounding_pv.jpg

pod_bound_success.jpg

确认一下pod挂载的nfs路径:

get_pod_nfs_location.png

因为我们是将nfs路径挂载到了pod下nginx的index目录,所以我们可以做一个测试index文件

注意:这里/usr/share/nginx/html的html目录是需要至少755的权限,如果你在访问的时候出现了403权限的问题,请检查html目录权限和index.html的权限

nfs_index_html.png

curl 访问测试,结果显示挂载pv的index文件已成功应用:
curl_pod_nfs.png

并且,我们可以通过删除pod来模拟pod意外终止的情况来查看pv的数据是否会丢失:

delete_pv_no_change.png

因为我们设置PV卷的回收策略是Retain,当 PVC 被删除时并不会一并删除其所使用的PV,该PV会一直保留到管理员手动删除,如果管理员一直无操作,则该卷会被一直保留。

PV回收策略测试:

delete_pvc_test.png

delete_parm.png

由图我们可以看到,因为PV的回收策略是Retain,删除statefuleSet创建的 pod 并 删除相应的 pvc 之后,pv 由Bound的状态变为了Released的状态,如果要使PV重新可利用,就需要管理员手工删除 pv 中的 claimRef字段

五、StatefulSet知识点回顾

StatefulSet 创建 Pod 时,Pod Name 的模式为:$(statefulset名称)-$(序号)

比如上面的测试的示例:web-0,web-1,web-2,其中web是 StatefulSet 的名称,后面则是序号

statefulSet_pod_name.png

StatefulSet 会为每个 Pod 副本创建了一个 DNS 域名,这个域名的格式为: $(podname).(headless ServiceName)。所以StatefulSetPod副本创建的DNS域名格式即为 pod名称.无头Service名称 。也就意味着服务间是通过Pod域名来通信而非 Pod IP,因为当Pod所在Node发生故障时, Pod 会被飘移到其它 Node 上,Pod IP 会发生变化,但是 Pod 域名不会有变化

exec 进入到 pod 内部,即可通过 ping pod名称.无头Service名称解析到对应的IP,我这里镜像内部没有 ping 命令等工具,就不再演示了,贴一张网图

statefulset_DNS

StatefulSet 使用 Headless 服务来控制 Pod 的域名,这个域名的 FQDN 为:$(servicename).$(namespace).svc.cluster.local,其中,“cluster.local” 指的是集群的域名

根据 volumeClaimTemplates,为每个 Pod 创建一个 pvc,pvc 的命名规则匹配模式:(volumeClaimTemplates.name)-(pod_name),比如上面的 volumeMounts.name=www, Podname=web-[0-2],因此创建出来的 PVC 是 www-web-0、www-web-1、www-web-2

删除 Pod 不会删除其 pvc,手动删除 pvc 将自动释放 pv

Statefulset的启停顺序:

有序部署:部署StatefulSet时,如果有多个Pod副本,它们会被顺序地创建(从0到N-1)并且,在下一个Pod运行之前所有之前的Pod必须都是Running和Ready状态

有序删除:当Pod被删除时,它们被终止的顺序是从N-1到0

有序扩展:当对Pod执行扩展操作时,与部署一样,它前面的Pod必须都处于Running和Ready状态

StatefulSet使用场景:

稳定的持久化存储,即Pod重新调度后还是能访问到相同的持久化数据,基于 PVC 来实现

稳定的网络标识符,即 Pod 重新调度后其 PodName 和 HostName 不变

有序部署,有序扩展,基于 init containers 来实现

有序收缩