K8s 储存资源

基本定义

Pod 内的进程共享计算资源,但不包括磁盘。Kubernetes 通过定义存储卷来满足磁盘共享的需求,常用于扩展容器的存储空间并为其提供持久存储能力。

存储卷分为临时卷、本地卷和网络卷。临时卷和本地卷都位于工作节点,一旦 Pod 被调度到其他工作节点,这些类型的存储卷将无法访问。因此,临时卷和本地卷通常用于数据缓存,而持久化的数据需要放置于持久卷上。

卷是 Pod 的一个组成部分,因此像容器一样在 Pod 的配置中进行定义。它们不是独立的 Kubernetes 对象,不能单独创建或删除。Pod 中的所有容器都可以使用卷,但必须先将其挂载到容器中。

有多种可用的卷可以使用,单个容器可以同时使用不同类型的多个卷。可用的卷类型如下:

  • emptyDir:用于存储临时数据的简单空目录。

  • hostPath:用于将工作节点文件系统中的目录挂载到 Pod 中。

  • gitRepo:通过检出 Git 仓库的内容来初始化的卷。

  • nfs:挂载到 Pod 中的 NFS 共享卷。

  • gcePersistentDisk、awsElasticBlockStore、azureDisk:用于挂载云服务提供的特定存储类型。

  • ConfigMap、Secret、DownwardAPI:用于将 Kubernetes 部分资源和集群信息公开给 Pod 的特殊类型卷。

  • PersistentVolumeClaim:一种使用预配置或动态配置的持久存储类型。

  • cinder、Cephas、iscsi、flocker、glusterfs、quobyte、rbd、flexVolume、vsphere-Volume、photoPersistentDisk:用于挂载其他类型的网络存储。

emptyDir 卷

emptyDir 卷是一个空目录,Pod 内的程序可以写入所需的任何文件。当删除 Pod 时,卷的内容会丢失。它主要用于在 Pod 中运行的容器之间临时共享文件。

使用 emptyDir 卷

下面使用 Nginx 作为服务器和 fortune 命令来生成 HTML 内容。fortune 命令每次运行都会输出一个随机引用。我们可以创建一个脚本,每 10 秒运行一次,并将其储存在 index.html 文件中:

[root@server4-master ~]$ vi fortune-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: fortune
spec:
  containers:
  - image: luksa/fortune
    name: html-generator
    volumeMounts:
    - name: html
      mountPath: /var/htdocs
  - image: nginx:alpine
    name: web-server
    volumeMounts:
    - name: html
      mountPath: /usr/share/nginx/html
      readOnly: true
    ports:
    - containerPort: 80
      protocol: TCP
  volumes:
  - name: html
    emptyDir: {}                                 
[root@server4-master ~]$ kubectl create -f fortune-pod.yaml 
pod/fortune created

第一个容器 html-generator 用于将生成的 HTML 文件保存到 /var/htdocs 目录中,并将卷 html 挂载到该目录。

第二个容器 web-server 用于运行 Web 服务,并将卷 html 挂载到 /usr/share/nginx/html 目录中,设置为只读权限。

最后,我们定义了一个名为 html 的独立 emptyDir 卷,供上述容器挂载使用。

查看 Pod 状态

为了测试访问,我们可以直接设置端口转发来访问 Pod:

[root@server4-master ~]$ kubectl port-forward fortune 8080:80
Forwarding from 127.0.0.1:8080 -> 80
Forwarding from [::1]:8080 -> 80

在新终端中,通过 curl 命令来访问:

[root@server4-master ~]$ curl 127.0.0.1:8080
Few things are harder to put up with than the annoyance of a good example.
                -- "Mark Twain, Pudd'nhead Wilson's Calendar"

指定储存介质

可以将 emptyDir 卷创建在 tmpfs,也就是内存中。空间受限于内存大小,但性能非常好。需要同时设置 sizeLimit 来限制使用的空间大小。

下面创建一个 Pod 进行测试,包含 Tomcat 和 Busybox 两个容器。Tomcat 向挂载的卷中写入日志,而 Busybox 读取日志内容:

[root@server4-master ~]$ vi empty-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: volume-pod
  namespace: default
spec:
  containers:
  - name: tomcat
    image: tomcat
    ports:
    - containerPort: 8080
    volumeMounts:
    - name: app-logs
      mountPath: /usr/local/tomcat/logs
  - name: busybox
    image: busybox
    command: ["sh", "-c", "tail -f /logs/catalina*.log"]
    volumeMounts:
    - name: app-logs
      mountPath: /logs
  volumes:
  - name: app-logs
    emptyDir:
      medium: Memory
[root@server4-master ~]$ kubectl create -f empty-pod.yaml 
pod/volume-pod created

通过以下命令查看日志:

[root@server4-master ~]$ kubectl logs volume-pod -c busybox
14-Mar-2022 03:49:09.120 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Djava.io.tmpdir=/usr/local/tomcat/temp
14-Mar-2022 03:49:09.128 INFO [main] org.apache.catalina.core.AprLifecycleListener.lifecycleEvent Loaded Apache Tomcat Native library [1.2.31] using APR version [1.7.0].

gitRepo 卷

gitRepo 卷基本上也是一个 emptyDir 卷,通过在 Pod 启动时克隆 Git 仓库来填充数据。创建完成后,gitRepo 卷不会随着远程 Git 仓库的更新而更新。如果使用 ReplicationSet 进行管理,在删除 Pod 后会创建新的 Pod,再次触发 git clone 可以获取最新的文件。

一个典型的应用场景是存放网站的静态 HTML 文件,并创建一个包含 Web 服务器容器和 gitRepo 卷的 Pod。当 Pod 创建时,会拉取网站的最新版本并启动 Web 服务。不过,每次有新版本时需要删除 Pod 才能更新。

使用 gitRepo 卷

下面是一个使用 Nginx 容器和 gitRepo 卷的示例:

[root@k8s-master ~]$ vi gitrepo-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: gitrepo-volume-pod
spec:
  containers:
  - image: nginx:alpine
    name: web-server
    volumeMounts:
    - name: html
      mountPath: /usr/share/nginx/html
      readOnly: true
    ports:
    - containerPort: 80
      protocol: TCP
  volumes:
  - name: html
    gitRepo:
      repository: https://github.com/luksa/kubia-wesite-example.git
      revision: master
      directory: .
[root@k8s-master ~]$ kubectl create -f gitrepo-pod.yaml 
pod/gitrepo-volume-pod created

在上述示例中,gitRepo 卷需要指定 revision 来指定 master 分支,并将仓库克隆到根目录。也可以指定其他文件夹名称。

私有仓库

gitRepo 卷无法直接克隆私有 Git 仓库,例如需要账号密码验证的 GitLab。如果需要添加支持,可以在 Pod 中添加额外的 sidecar 容器,例如 gitsync sidecar,用于对主容器进行操作,以同步仓库的版本等。

需要注意的是,gitRepo 卷现已被官方废弃,官方建议使用初始化容器将仓库中的数据复制到 emptyDir 储存卷上。

ConfigMap 资源

应用配置的关键在于能够在多个环境中区分配置选项,将配置从应用程序源码中分离。Kubernetes 允许将配置选项分离到单独的资源对象 ConfigMap 中。ConfigMap 本质上是一个键值对映射,值可以是字符串,也可以是完整的配置文件。示例图如下所示:

ConfigMap能包含的内容

应用程序无需直接读取 ConfigMap,而是通过环境变量或卷文件的形式将映射的内容传递给容器。在命令行参数的定义中,可以使用变量语法引用环境变量,从而将 ConfigMap 的条目作为命令行参数传递给进程。

Pod 通过名称引用 ConfigMap,因此可以在多个环境中使用相同的 Pod 定义来描述,并根据不同的环境使用不同的配置值。

指定容器环境变量

如果 Docker 镜像有自定义参数可以配置,可以按照下面的 Dockerfile 进行设置:

#!/bin/bash
trap "exit" SIGINT
echo Fortune sleep every $INTERVAL seconds
mkdir -p /bar/htdocs
while :
do
  echo $(date) Writing fortune to /var/htdocs/index.html
  /usr/games/fortune > /var/htdocs/index.html
  sleep $INTERVAL
done

想要在 Kubernetes 中设置自定义参数,可以在 spec.containers.env 中声明:

spec:
  containers:
  - image: fortune
    env:
    - name: INTERVAL
      value: "30"
    name: html-generator

也可以使用 $(VAR) 语法在环境变量值中引用其他环境变量:

spec:
  containers:
  - image: fortune
    env:
    - name: FIRST_VAR
      value: "foobar"
    - name: SECOND_VAR
      value: "$(FIRST_VAR)2000"

创建 ConfigMap

可以直接通过命令行来创建一个最简单的 ConfigMap:

[root@server4-master ~]$ kubectl create configmap fortune-config --from-literal=sleep-interval=5
configmap/fortune-config created

这条指令创建了名为 fortune-config 的 ConfigMap,仅包含单映射条目 sleep-interval=5。可以通过添加多个 --from-literal 参数创建包含多条目的 ConfigMap:

[root@server4-master ~]$ kubectl create configmap fortune-config --from-literal=one=1 --from-literal=two=11

通过观察 YAML 格式的定义描述,可以自定义配置文件,然后通过 create -f 来创建 ConfigMap。

可以直接从硬盘中读取文件,并将文件内容单独存储为 ConfigMap 中的值。例如,将当前目录下的 my.config 文件内容存为键名为 devconfig 的值:

[root@k8s-master 2]$ kubectl create configmap my-config --from-file=devconfig=my.config

通过多次使用 --from-file 参数可以增加多个文件条目。另外,还可以使用 --from-file 指定目录,kubectl 会为文件夹中的每个文件单独创建条目,键名为文件名:

[root@server4-master ~]$ kubectl create configmap my-config-dir --from-file=k8s
configmap/my-config-dir created

ConfigMap 可以混合使用多种类型的配置,例如一个 my-config 同时包含键值对和文件:

[root@server4-master ~]$ kubectl create configmap my-config --from-file=foo.json --from-file=bar=foobar.conf --from-file=config-opts/ --from-literal=some=thing

传递 ConfigMap

将值传递给 Pod 中的容器有三种方式。如果引用的 ConfigMap 不存在,容器会启动失败。也可以设置 optional: true 对引用进行可选设置。

  • 通过环境变量传递键值

    需要在 spec.containers.env.valueFrom 字段中指定:

    spec:
      containers:
      - image: fortune
        env:
        - name: INTERVAL
          valueFrom:
            configMapKeyRef:
              name: fortune-config
              key: sleep-interval

    这里传递了一个环境变量 INTERVAL,值取自 fortune-config 中键 sleep-interval 的值,然后由容器内的进程读取。

  • 传递整个 ConfigMap 中的键值

    spec.containers 中加入 envFrom 字段来传递整个 ConfigMap 中的键值对:

    spec:
      containers:
      - image: fortune
        envFrom:
        - prefix: CONFIG_
          configMapRef:
            name: my-cofig-map

    上面设置了所有导入的环境变量包含前缀 CONFIG_,若不设置前缀,环境变量的名称与 ConfigMap 中的键名相同。若键名不合法时不会自动转换。

  • 传递 ConfigMap 条目作为命令行参数

    在字段 spec.containers.args 中无法直接引用 ConfigMap 的条目,但可以利用 ConfigMap 条目初始化某个环境变量,然后再在参数字段中引用该变量:

    spec:
      containers:
      - image: fortune
        env:
        - name: INTERVAL
          valueFrom:
            configMapKeyRef:
              name: fortune-config
              key: sleep-interval
        args: ["$(INTERVAL)"]

使用 ConfigMap 卷

由于 ConfigMap 中可以包含完整的配置文件内容,想要将其暴露给容器时可以借助 ConfigMap 卷。ConfigMap 卷会将 ConfigMap 中的每个条目暴露为一个文件,运行在容器中的进程通过读取文件内容获得对应的条目值。

例如,将目录中的 Nginx 配置文件和 interval 文件一起创建名为 fortune-config 的 ConfigMap:

[root@server4-master configmap-files]$ echo "25" > interval
[root@server4-master configmap-files]$ vi advertise-task.iot.com.conf 
server {
    listen 80;
    server_name advertise-task.iot.com;
    location / {
        proxy_pass http://advertise-task.iot.com.dev;
    }
}
[root@server4-master configmap-files]$ kubectl create configmap fortune-config --from-file=../configmap-files/
configmap/fortune-config created
[root@server4-master ~]$ kubectl get configmaps fortune-config -o yaml
apiVersion: v1
data:
  interval: |
    25
  advertise-task.iot.com.conf: |
    server {
        listen 80;
        server_name advertise-task.iot.com;
        location / {
            proxy_pass http://advertise-task.iot.com.dev;
        }
    }
kind: ConfigMap
metadata:
  creationTimestamp: "2022-03-15T14:01:15Z"
  name: fortune-config
  namespace: default
  resourceVersion: "580367"
  uid: 9a4c9cb4-c2d2-424c-9d57-ac3fe1851260

将 ConfigMap 卷内的文件挂载到 /etc/nginx/conf.d/ 目录下:

spec:
  containers:
  - image: nginx:alpine
    name: web-server
	volumeMounts:
	- name: config
	  mountPath: /etc/nginx/conf.d
	  readOnly: true
  volumes:
    - name: config
	  configMap:
	    name: fortune-config

也可以单独指定需要挂载的 ConfigMap 卷内文件。这里将 ConfigMap 卷内的配置文件 advertise-task.iot.com.conf 重命名为 at.conf 并挂载到指定目录下:

spec:
  volumes:
    - name: config
	  configMap:
	    name: fortune-config
	    items:
	    - key: advertise-task.iot.com.conf
	      path: at.conf

使用上述方法挂载卷时,容器内原有的同名目录会被隐藏,被挂载的目录会替代之。如果想要单独挂载文件而不是目录,需要在 spec.containers.volumeMounts 下面加入 subPath 字段:

spec:
  containers:
  - image: nginx:alpine
    name: web-server
	volumeMounts:
	- name: config
	  mountPath: /etc/nginx/conf.d/advertise-task.iot.com.conf
	  subPath: advertise-task.iot.com.conf
	  readOnly: true

ConfigMap 卷中所有文件的默认权限是 644,可以在 spec.volumes.configMap 中加入 defaultMode 来改变默认权限:

spec:
  volumes:
    - name: config
	  configMap:
	    name: fortune-config
	    defaultMod: "6600"

将 ConfigMap 作为卷挂载可以实现配置的热更新效果,无需重启或重建 Pod。在修改了 ConfigMap 的配置后,可以通过 kubectl exec 命令来手动载入更新的配置:

[root@server4-master ~]$ kubectl edit configmap fortune-config
[root@server4-master ~]$ kubectl exec fortune -c web-server -- nginx -s reload

但如果挂载的单个文件,ConfigMap 更新后对应的文件不会被更新。

Secret 资源

Secret 的结构和使用方法与 ConfigMap 相同,也是键值对的映射,主要用来保存敏感信息,例如账号和证书。

Kubernetes 通过将 Secret 分发到 Pod 所在的节点来保障安全性,Secret 只会存储在节点的内存中,从而无法被窃取。

默认令牌

在 Kubernetes 安装完成后,会生成一个以 default-token 开头的默认 Secret,它会挂载到所有容器中:

[root@server4-master ~]$ kubectl get secrets 
NAME                  TYPE                                  DATA   AGE
default-token-vqjsw   kubernetes.io/service-account-token   3      132d
[root@server4-master ~]$ kubectl describe secrets default-token-vqjsw
Name:         default-token-vqjsw
Namespace:    default
Labels:       <none>
Annotations:  kubernetes.io/service-account.name: default
              kubernetes.io/service-account.uid: ae0b90b8-1323-42a2-990b-a18d4bfb7da8

Type:  kubernetes.io/service-account-token

Data
====
ca.crt:     1099 bytes
namespace:  7 bytes
token:      eyJhbGciOiJSUzI1NiIsImtpZCI6IlZDd

默认的 Secret 包含三个条目:ca.crt、namespace 和 token,其中包含了访问 API 服务器所需的安全信息。

可以在 Pod 信息的 Mount 栏中看到 Secret 卷挂载的位置:

[root@server4-master ~]$ kubectl describe po dnsutils 
Containers:
  dnsutils:
    Mounts:
      /var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-5ghsj (ro)

默认的挂载行为可以通过在 Pod 定义中设置 automountServiceAccountToken 字段为 false 来关闭。

创建 Secret

使用命令行创建一个名为 ngx-https 的 Secret,用于存储 Nginx 所需的私钥和证书:

[root@server4-master ~]$ kubectl create secret generic ngx-https --from-file=https.cert --from-file=https.key --from-file=foo

Secret 条目的内容会以 Base64 格式编码,而 ConfigMap 则直接以纯文本形式显示。因此,Secret 还可以用于存储最大为 1MB 的二进制数据。

通过 stringData 字段设置条目的纯文本值,该值不会被编码为 Base64:

[root@server4-master ~]$ vi secret-test.yaml
apiVersion: v1
kind: Secret
stringData:
  foo: ymfyc
data:
  https.cert: Ls-3lcc
  https.key: Lsv9elx
[root@server4-master ~]$ kubectl create -f secret-test.yaml 

注意,stringData 字段只可写入。

使用 Secret

将 Secret 通过 Secret 卷暴露给容器后,Secret 条目的值将解码为实际形式(纯文本或二进制),并写入相应的文件中。同样,通过环境变量暴露 Secret 条目也是如此。在这两种情况下,应用程序无需手动解码,可以直接读取文件内容或查找环境变量:

spec:
  containers:
	volumeMounts:
	- name: certs
	  mountPath: /etc/nginx/certs/
	  readOnly: true
  volumes:
    - name: certs
	  secret:
	    secretame: ngx-https

与 ConfigMap 卷相同,Secret 卷同样支持使用 defaultModes 属性指定卷中文件的默认权限。

Secret 条目也可以暴露为环境变量:

spec:
  containers:
    env:
    - name: INTERVAL
      valueFrom:
	    secretKeyRef:
		  name: ngx-https
		  key: foo

可以创建一个类型为 docker-registry 的 Secret,名称为 dockerhubsecret,其中包含 Docker 镜像仓库证书,并在拉取镜像时进行引用:

[root@k8s-master 2]$ kubectl create secret docker-registry dockerhubsecret \
 --docker-username=myusername --docker-password=mypasswd \
 --docker-email=my@email.com
[root@k8s-master 2]$ vi dockerhub.yaml
apiVersion: v1
kind: Pod
metadata:
  name: privae-pod
spec:
  imagePullSecrets:
  - name: dockerhubsecret
  containers:
  - image: assassing/av:v1
    name: main

底层持久化储存

在 Kubernetes 中,底层持久化存储是一个重要的组件,用于存储应用程序的持久化数据,并确保数据的安全性和持久性。

hostPath 卷

hostPath 卷用于指向节点文件系统中的特定文件或目录。由于文件存储在特定节点的文件系统中,因此当 Pod 被重新调度到另一个节点时,可能无法访问数据。在 Kubernetes 的系统级服务 Pod 中,通常使用 hostPath 卷来访问节点的日志、配置文件或 CA 证书,但不推荐将其用于存储数据:

volumes:
- name: "hostpath"
  hostPath:
    path: "/data"

此外,还可以使用 type 参数来指定卷的类型:

  • DirectoryOrCreate:如果指定的路径不存在,则自动创建一个权限为 0755 的空目录,所有者为 kubelet。
  • Directory:必须存在的目录路径。
  • FileOrCreate:如果指定的路径不存在,则自动创建一个权限为 0644 的空文件,所有者为 kubelet。
  • File:必须存在的文件路径。
  • Socket:必须存在的 Socket 文件路径。
  • CharDevice:必须存在的字符设备文件路径。
  • BlockDevice:必须存在的块设备文件路径。

根据实际需求选择合适的 type 参数来配置 hostPath 卷。请注意,使用 hostPath 卷时需要谨慎,确保数据的可靠性和安全性。

GCE 持久储存

如果集群运行在 Google Kubernetes Engine 中,你可以选择使用 GCE 持久化磁盘作为底层存储机制。首先,在同一区域的 Kubernetes 集群中创建一个 GCE 持久化磁盘,例如在 “europe-west” 区域,然后创建一个带有 GCE 持久化磁盘卷的 Pod:

apiVersion: v1
kind: Pod
metadata:
  name: mongodb
spec:
  containers:
  - image: mongo
    name: mongodb
    volumeMounts:
    - name: mongodb-data
      mountPath: /data.db
    ports:
    - containerPort: 27017
      protocol: TCP
  volumes:
  - name: mongodb-data
    gcePersistentDisk:
      pdName: mongodb
      fsType: ext4

定义名称和文件系统类型为 ext4,接着向 mongodb 写入数据:

[root@k8s-master ~]kubectl exec -it mongodb mongo
[root@mongodb ~]use mystore
[root@mongodb ~]db.foo.insert({name:'foo'})
[root@mongodb ~]db.foo.find()

重建 Pod 后读取上一个 Pod 保存的数据:

[root@k8s-master ~]kubectl delete pod mongodb
[root@k8s-master ~]kubectl create -f mongodb.yaml
[root@k8s-master ~]kubectl exec -it mongodb mongo
[root@mongodb ~]use mystore
[root@mongodb ~]db.foo.find()

如果数据仍然存在,则说明持久化成功。

其他持久化储存卷

根据不同的基础设施,可以选择使用不同类型的持久化存储卷。例如,在 Amazon 上可以使用 awsElasticBlockStore 卷,在 Microsoft Azure 上可以使用 azureFile 或 azureDisk 卷。下面是一个使用 AWS 的示例:

apiVersion: v1
kind: Pod
metadata:
  name: mongodb
spec:
  volumes:
  - name: mongodb-data
    awsElasticBlockStore:
      volumeID: mongodb
      fsType: ext4
...

对于 NFS 共享,只需要指定 NFS 服务器的 IP 地址和共享路径即可:

...
spec:
  volumes:
  - name: mongodb-data
    nfs:
      server: 1.2.3.4
      path: /some/path
...

要了解每种卷类型所需的属性设置,可以查询 Kubernetes API 文档,或使用 kubectl explain 命令。

需要注意的是,将这些基础设施类型放在 Pod 的配置中意味着该 Pod 的设置与特定集群强耦合,这样无法在另一个 Pod 中重复使用相同的设置。因此,在设计和配置持久化存储时需要谨慎考虑。

持久卷和持久卷声明

在集群中为了使应用正常请求储存资源,同时避免处理基础设施细节,引入了两个新资源,分别是持久卷(PersistentVolume,PV)和持久卷声明(PersistentVolumeClaim,PVC)。

当集群用户需要在其 pod 中使用持久化储存时,首先创建持久卷声明清单,指定所需最低容量要求和访问模式,由 API 服务器分配持久卷并绑定到持久卷声明中。

持久卷声明可以当做 pod 中的一个卷来使用,其他用户不能使用相同的持久卷,除非先通过删除持久卷声明释放。

下图展示了 PV 和 PVC 的关系:

PV和PVC

下面用 NFS 文件系统做示例。

安装 NFS 文件系统

在所有节点上进行安装 NFS 套件:

[root@server4-master ~]$ yum install nfs-utils rpcbind -y
[root@server4-master ~]$ systemctl enable --now nfs
Created symlink from /etc/systemd/system/multi-user.target.wants/nfs-server.service to /usr/lib/systemd/system/nfs-server.service.

在 NFS 服务器上启动服务:

[root@server4-master ~]$ systemctl enable --now rpcbind

配置服务器上的共享目录:

[root@server4-master ~]$ mkdir /srv/pv
[root@server4-master ~]$ chown nfsnobody:nfsnobody /srv/pv
[root@server4-master ~]$ chmod 755 /srv/pv
[root@server4-master ~]$ echo -e "/srv/pv *(rw,no_root_squash,sync)">/etc/exports
[root@server4-master ~]$ exportfs -r
[root@server4-master ~]$ exportfs
/srv/pv         <world>
[root@server4-master ~]$ showmount -e 192.168.2.204
Export list for 192.168.2.204:
/srv/pv *

修改最大同时连接用户数:

[root@server4-master ~]$ echo "options sunrpc tcp_slot_table_entries=128" >> /etc/modprobe.d/sunrpc.conf
[root@server4-master ~]$ echo "options sunrpc tcp_max_slot_table_entries=128" >>  /etc/modprobe.d/sunrpc.conf
[root@server4-master ~]$ sysctl -w sunrpc.tcp_slot_table_entries=128
sunrpc.tcp_slot_table_entries = 128

创建新的挂载点:

[root@server4-master ~]$ mkdir -p /srv/pv/pv001 /srv/pv/pv002
[root@server4-master ~]$ echo -e "/srv/pv/pv001 *(rw,no_root_squash,sync)">>/etc/exports
[root@server4-master ~]$ echo -e "/srv/pv/pv002 *(rw,no_root_squash,sync)">>/etc/exports
[root@server4-master ~]$ exportfs -r
[root@server4-master ~]$ systemctl restart rpcbind && systemctl restart nfs
[root@server4-master ~]$ showmount -e 192.168.2.204
Export list for 192.168.2.204:
/srv/pv/pv002 *
/srv/pv/pv001 *
/srv/pv       *

创建持久卷

配置一个 1 GB 大小的持久卷,供 MongoDB 使用:

[root@server4-master ~]$ vi mongodb-pv.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: mongodb-pv
spec:
  capacity:
    storage: 1Gi
  accessModes:
  - ReadWriteOnce
  - ReadOnlyMany
  persistentVolumeReclaimPolicy: Retain
  nfs:
    path: /srv/pv
    server: 192.168.2.204
[root@server4-master ~]$ kubectl create -f mongodb-pv.yaml 
persistentvolume/mongodb-pv created
[root@server4-master ~]$ kubectl get pv
NAME         CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM   STORAGECLASS   REASON   AGE
mongodb-pv   1Gi        RWO,ROX        Retain           Available                                   4s

持久卷不属于任何命名空间,它和节点一样是集群层面的资源。

创建持久卷声明

如果 Pod 需要使用之前创建的持久卷,需要创建一个持久卷声明:

[root@server4-master ~]$ vi mongodb-pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: mongodb-pvc
spec:
  resources:
    requests:
      storage: 1Gi
  accessModes:
  - ReadWriteOnce
  storageClassName: ""
[root@server4-master ~]$ kubectl create -f mongodb-pvc.yaml
persistentvolumeclaim/mongodb-pvc created
[root@server4-master ~]$ kubectl get pvc
NAME          STATUS   VOLUME       CAPACITY   ACCESS MODES   STORAGECLASS   AGE
mongodb-pvc   Bound    mongodb-pv   1Gi        RWO,ROX                       4s

通过查看 PVC(持久卷声明)状态,可以确认 PVC 已经与相应的 PV(持久卷)绑定。其中访问模式的简写含义如下:

  • RWO(ReadWriteOnce):仅允许单个节点挂载读写。

  • ROX(ReadOnlyMany):允许多个节点挂载只读。

  • RWX(ReadWriteMany):允许多个节点挂载读写。

这里的节点指的是 Kubernetes 节点,而不是 Pod 的数量。

再次查看 PV 的状态:

[root@server4-master ~]$ kubectl get pv
NAME         CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                 STORAGECLASS   REASON   AGE
mongodb-pv   1Gi        RWO,ROX        Retain           Bound    default/mongodb-pvc                           12m

可以看到它已经被绑定到 PVC 的声明中。其中 CLAIM 中的 default 表示默认命名空间。虽然持久卷属于整个集群,但持久卷声明只能在特定的命名空间内创建。因此,持久卷和持久卷声明只能由同一命名空间内的 Pod 创建和使用。还可以使用选择器来为 PV 应用标签选择器。

使用持久卷声明

在 Pod 中使用持久卷时,需要在 Pod 的卷中引用 PVC 的名称:

[root@server4-master ~]$ vi mongodb-pod.yaml
apiVersion: v1
kind: Service
metadata:
  name: mongodb
spec:
  type: NodePort
  ports:
  - port: 27017
    targetPort: 27017
    nodePort: 30000
  selector:
    app: kubia
---
apiVersion: v1
kind: Pod
metadata:
  name: mongodb
  labels:
    app: mongo
spec:
  containers:
  - image: mongo
    name: mongodb
    volumeMounts:
    - name: mongodb-data
      mountPath: /data/db
    ports:
    - containerPort: 27017
      protocol: TCP
  volumes:
  - name: mongodb-data
    persistentVolumeClaim:
      claimName: mongodb-pvc
[root@server4-master ~]$ kubectl create -f mongodb-pod.yaml 
pod/mongodb created

请注意,虽然 PV 是全局资源,但 PVC 属于特定命名空间,只有同一命名空间内的 Pod 才能调用。

删除持久卷

在持久卷正在被使用时,不能直接删除它,需要先删除使用该持久卷的 Pod 和 PVC。持久卷的空间释放处理机制有三种:

  • Retain(保留)

    删除 PV 后,根据设置的释放规则(Retain),硬盘中的文件仍然存在。重新创建 PV、PVC 和 Pod 后,文件内容和上一次运行时一样。使用 Retain 手动回收策略只能通过删除和重建持久卷来恢复可用状态。

  • Recycle(回收)

    删除 PVC 后,会删除卷的内容,并使卷可用于再次声明。

  • Delete(删除)

    删除底层存储。

动态化持久卷

在 Kubernetes 中,可以通过创建持久卷配置并定义一个或多个 StorageClass 对象,实现每次通过持久卷声明请求时自动创建一个新的持久卷。获取动态持久卷的步骤如下图所示:

获取动态持久卷

不同的后端存储需要不同的置备程序(provisioner)。以 NFS 文件系统为例,首先需要部署 nfs-client:

[root@k8s-master html]$ echo "/root/3/html *(rw,sync,no_root_squash)" >> /etc/exports
[root@k8s-master ~]$ vi nfs-dp.yaml
kind: Deployment
apiVersion: extensions/v1beta1
metadata:
  name: nfs-client-provisioner
spec:
  replicas: 1
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: nfs-client-provisioner
    spec:
      serviceAccount: nfs-client-provisioner
      containers:
        - name: nfs-client-provisioner
          image: jmgao1983/nfs-client-provisioner
          volumeMounts:
            - name: nfs-client-root
              mountPath: /persistentvolumes
          env:
            - name: PROVISIONER_NAME
              value: mynfs
            - name: NFS_SERVER
              value: 192.168.2.113
            - name: NFS_PATH
              value: /srv/pv
      volumes:
        - name: nfs-client-root
          nfs:
            server: 192.168.2.113
            path: /srv/pv
[root@k8s-master ~]$ kubectl create -f nfs-dp.yaml
deployment.extensions/nfs-provisioner created
[root@k8s-master ~]$ kubectl get deployment
NAME              READY   UP-TO-DATE   AVAILABLE   AGE
nfs-provisioner   1/1     1            1           29s

然后创建 StorageClass 资源:

[root@k8s-master ~]$ vi mongodb-pv-sc.yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: fast
provisioner: mynfs
[root@k8s-master ~]$ kubectl create -f mongodb-pv-sc.yaml
storageclass.storage.k8s.io/fast created
[root@k8s-master ~]$ kubectl get sc
NAME   PROVISIONER   AGE
fast   mynfs         9s

创建一个 Pod 引用 StorageClass:

[root@k8s-master ~]$ vi nginx.yaml
apiVersion: apps/v1beta1
kind: StatefulSet
metadata:
  name: web
spec:
  serviceName: "nginx1"
  replicas: 2
  template:
    metadata:
      labels:
        app: nginx1
    spec:
      containers:
      - name: nginx1
        image: nginx:latest
        volumeMounts:
        - mountPath: "/mnt"
          name: test
  volumeClaimTemplates:
  - metadata:
      name: test
      annotations:
        volume.beta.kubernetes.io/storage-class: "fast"
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 1Gi
"nginx.yaml" [New] 28L, 556C written
[root@k8s-master ~]$ kubectl create -f nginx.yaml
statefulset.apps/web created

创建 ServiceAccount 和角色:

[root@k8s-master ~]$ vi serviceaccount.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: nfs-client-provisioner
[root@k8s-master ~]$ kubectl create -f serviceaccount.yaml
serviceaccount/nfs-client-provisioner created
[root@k8s-master ~]$ kubectl get sa
NAME                     SECRETS   AGE
default                  1         3d14h
nfs-client-provisioner   1         16s
[root@k8s-master ~]$ vi clusterrole.yaml
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: nfs-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: ["watch", "create", "update", "patch"]
  - apiGroups: [""]
    resources: ["services", "endpoints"]
    verbs: ["get"]
  - apiGroups: ["extensions"]
    resources: ["podsecuritypolicies"]
    resourceNames: ["nfs-provisioner"]
    verbs: ["use"]
"clusterrole.yaml" [New] 24L, 735C written
[root@k8s-master ~]$ vi clusterrolebinding.yaml
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: run-nfs-provisioner
subjects:
  - kind: ServiceAccount
    name: nfs-client-provisioner
    namespace: default
roleRef:
  kind: ClusterRole
  name: nfs-provisioner-runner
  apiGroup: rbac.authorization.k8s.io
[root@k8s-master ~]$ kubectl create -f clusterrole.yaml -f clusterrolebinding.yaml
clusterrole.rbac.authorization.k8s.io/nfs-provisioner-runner created
clusterrolebinding.rbac.authorization.k8s.io/run-nfs-provisioner created

最后,验证是否会自动创建新的 PV:

[root@k8s-master ~]$ kubectl get pv |grep web
pvc-d2423554-6b6e-4c65-9209-e739abe7c653   1Gi        RWO            Delete           Bound       default/test-web-0   fast                    2m28s
pvc-d430fa25-d4e4-4971-961f-80be40b8d9dc   1Gi        RWO            Delete           Bound       default/test-web-1   fast                    2m12s
[root@k8s-master ~]$ kubectl get pvc |grep web
test-web-0   Bound    pvc-d2423554-6b6e-4c65-9209-e739abe7c653   1Gi        RWO            fast           13m
test-web-1   Bound    pvc-d430fa25-d4e4-4971-961f-80be40b8d9dc   1Gi        RWO            fast           2m36s
[root@k8s-master ~]$ kubectl get storageclass
NAME   PROVISIONER   AGE
fast   mynfs         19m
[root@k8s-master ~]$ kubectl get pod |grep web
web-0                                    1/1     Running   0          8m43s
web-1                                    1/1     Running   0          3m29s

[root@k8s-master ~]$ kubectl scale statefulset web --replicas=3
statefulset.apps/web scaled
[root@k8s-master ~]$ kubectl get pod |grep web
web-0                                    1/1     Running             0          9m32s
web-1                                    1/1     Running             0          4m18s
web-2                                    0/1     ContainerCreating   0          4s
[root@k8s-master ~]$ ll /srv/pv/
total 0
drwxrwxrwx 2 root root 6 Jul 22 17:34 default-test-web-0-pvc-d2423554-6b6e-4c65-9209-e739abe7c653
drwxrwxrwx 2 root root 6 Jul 22 17:35 default-test-web-1-pvc-d430fa25-d4e4-4971-961f-80be40b8d9dc
drwxrwxrwx 2 root root 6 Jul 22 17:39 default-test-web-2-pvc-ffc2b763-2dd8-4a5b-a565-86dd416eba36