2.14 网络共享存储

2.14 网络共享存储 #

由于 Kubernetes 里的 Pod 经常会在集群里 “漂移”,要想让存储卷真正能被 Pod 任意挂载,就不能限定在本地磁盘,而是要改成网络存储,这样 Pod 无论在哪里运行,只要知道 IP 地址或者域名,就可以通过网络通信访问存储设备。

在网络存储中有比较简单的 NFS 系统(Network File System),可以通过 NFS 理解在 Kubernetes 里使用网络存储,以及静态存储卷和动态存储卷的概念。

2.14.1 安装 NFS 服务器 #

NFS 采用的是 Client/Server 架构,需要选定一台主机作为 Server,安装 NFS 服务端;其他要使用存储的主机作为 Client,安装 NFS 客户端工具。

可以在 Kubernetes 集群里增添一台名字叫 Storage 的服务器,在上面安装 NFS,实现网络存储、共享网盘的功能。这台 Storage 只是一个逻辑概念,在实际安装部署的时候完全可以把它合并到集群里的某台主机里。

在 Ubuntu/Debian 系统里安装 NFS 服务端很容易,使用 apt 即可:

sudo apt -y install nfs-kernel-server

安装好之后,需要给 NFS 指定一个存储位置,也就是网络共享目录。一般来说,应该建立一个专门的 /data 目录,这里使用了临时目录 /tmp/nfs:

mkdir -p /tmp/nfs

接下来需要配置 NFS 访问共享目录,修改 /etc/exports,指定目录名、允许访问的网段,还有权限等参数。把下面这行加上就行,注意目录名和 IP 地址要改成和自己的环境一致:

/tmp/nfs 192.168.14.0/24(rw,sync,no_subtree_check,no_root_squash,insecure)

改好之后,需要用 exportfs -ra 通知 NFS,让配置生效,再用 exportfs -v 验证效果:

sudo exportfs -ra
sudo exportfs -v

以上的步骤完成之后,就可以使用 systemctl 来启动 NFS 服务器了:

sudo systemctl start  nfs-server
sudo systemctl enable nfs-server
sudo systemctl status nfs-server

可以使用命令 showmount 来检查 NFS 的网络挂载情况:

showmount -e 127.0.0.1

2.14.2 安装 NFS 客户端 #

NFS 服务器安装完成之后,为了让 Kubernetes 集群能够访问 NFS 存储服务,还需要在每个节点上都安装 NFS 客户端。

sudo apt -y install nfs-common

可以在节点上可以用 showmount 检查 NFS 能否正常挂载,这里的 IP 地址要写成 NFS 服务器的地址:

挂载测试 #

现在可以尝试手动挂载一下 NFS 网络存储,可以在 worker 节点创建一个目录 /tmp/test 作为挂载点:

mkdir -p /tmp/test

然后用命令 mount 把 NFS 服务器的共享目录挂载到刚才创建的本地目录上:

sudo mount -t nfs 192.168.14.142:/tmp/nfs /tmp/test

在 worker 节点 /tmp/test 目录里随便创建一个文件,比如 x.yml:

touch /tmp/test/x.yml

回到 NFS 服务器,检查共享目录 /tmp/nfs,应该会看到也出现了一个同样的文件 x.yml,这就说明 NFS 安装成功了。之后集群里的任意节点,只要通过 NFS 客户端,就能把数据写入 NFS 服务器,实现网络存储。

2.14.3 使用 NFS 存储卷 #

在配置好 NFS 存储系统后,就可以使用它来创建 PV 存储对象了。可以先手工分配一个存储卷,需要指定 storageClassName 是 nfs,而 accessModes 可以设置成 ReadWriteMany,这是由 NFS 的特性决定的,因为它支持多个节点同时访问一个共享目录

因为存储卷是 NFS 系统,所以需要在 YAML 里添加 nfs 字段,指定 NFS 服务器的 IP 地址和共享目录名。

在 NFS 服务器的 /tmp/nfs 目录里创建了一个新的目录 1g-pv,表示分配了 1GB 的可用存储空间,相应的,PV 里的 capacity 也要设置成同样的数值,也就是 1Gi。

以下是一个使用 NFS 网络存储的 YAML 描述文件:

apiVersion: v1
kind: PersistentVolume
metadata:
  name: nfs-1g-pv

spec:
  storageClassName: nfs
  accessModes:
    - ReadWriteMany
  capacity:
    storage: 1Gi

  nfs:
    path: /tmp/nfs/1g-pv
    server: 192.168.14.142

spec.nfs 里的 IP 地址一定要正确,路径也要事先建好目录,不然在 Pod 使用 NFS 时会报 No such file or directory 的错误。使用命令 kubectl apply 创建 PV 对象,可以使用 kubectl get pv 查看它的状态:

有了 PV,就可以定义申请存储的 PVC 对象了,内容和 PV 差不多,但不涉及 NFS 存储的细节,只需要用 resources.request 来表示希望要有多大的容量,这里写成 1GB,和 PV 的容量相同:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: nfs-static-pvc

spec:
  storageClassName: nfs
  accessModes:
    - ReadWriteMany

  resources:
    requests:
      storage: 1Gi

创建 PVC 对象之后,Kubernetes 就会根据 PVC 的描述,找到最合适的 PV,把它们 “绑定” 在一起,也就是存储分配成功:

此时可以再创建一个 Pod,把 PVC 挂载成它的一个 volume,用 persistentVolumeClaim 指定 PVC 的名字就可以了:

apiVersion: v1
kind: Pod
metadata:
  name: nfs-static-pod

spec:
  volumes:
    - name: nfs-pvc-vol
      persistentVolumeClaim:
        claimName: nfs-static-pvc

  containers:
    - name: nfs-pvc-test
      image: nginx:alpine
      ports:
        - containerPort: 80

      volumeMounts:
        - name: nfs-pvc-vol
          mountPath: /tmp

Pod、PVC、PV 和 NFS 存储的关系可以用下图来表示:

因为在 PV/PVC 里指定了 storageClassName 是 nfs,节点上也安装了 NFS 客户端,所以 Kubernetes 就会自动执行 NFS 挂载动作,把 NFS 的共享目录 /tmp/nfs/1g-pv 挂载到 Pod 里的 /tmp,完全不需要去手动管理。

spec.nfs 里的路径一定要事先存在,不然在创建 Pod 时会报错

Output: mount.nfs: mounting 192.168.14.142:/tmp/nfs/1g-pv failed, reason given by server: No such file or directory

在用 kubectl apply 创建 Pod 之后,可以使用 kubectl exec 进入 Pod,再试着操作 NFS 共享目录:

可以看到,NFS 服务器的 /tmp/nfs/1g-pv 目录里有在 Pod 里创建的文件,说明文件确实写入了共享目录。

NFS 是一个网络服务,不会受 Pod 调度位置的影响,所以只要网络通畅,这个 PV 对象就会一直可用,数据也就实现了真正的持久化存储。

2.14.4 部署 NFS Provisoner #

网络存储系统确实能够让集群里的 Pod 任意访问,数据在 Pod 销毁后仍然存在,新创建的 Pod 可以再次挂载,然后读取之前写入的数据,整个过程完全是自动化的。但因为 PV 还是需要人工管理,必须要由系统管理员手动维护各种存储设备,再根据开发需求逐个创建 PV,而且 PV 的大小也很难精确控制,容易出现空间不足或者空间浪费的情况。

在一个大集群里,每天可能会有几百几千个应用需要 PV 存储,如果仍然用人力来管理分配存储,管理员很可能会忙得焦头烂额,导致分配存储的工作大量积压。

在 Kubernetes 里有一个 “动态存储卷” 的概念,它可以用 StorageClass 绑定一个 Provisioner /prəˈvɪʒənə(r)/ 对象,而这个 Provisioner 就是一个能够自动管理存储、创建 PV 的应用,代替了原来系统管理员的手工劳动。有了 “动态存储卷” 的概念,手工创建的 PV 可以称为 “静态存储卷”。

Kubernetes 里每类存储设备都有相应的 Provisioner 对象,对于 NFS 来说,它的 Provisioner 就是 “NFS subdir external provisioner”,可以在 GitHub 上找到这个项目 https://github.com/kubernetes-sigs/nfs-subdir-external-provisioner

NFS Provisioner 也是以 Pod 的形式运行在 Kubernetes 里的,在 GitHub 的 deploy 目录里是部署它所需的 YAML 文件,一共有三个,分别是 rbac.yaml、class.yaml 和 deployment.yaml。但是这三个文件只是示例,想要在集群里真正运行起来还要修改其中的两个文件。

第一个要修改的是 rbac.yaml,它使用的是默认的 default 名字空间,应该把它改成其他的名字空间,避免与普通应用混在一起,可以用 “查找替换” 的方式把它统一改成 kube-system。

第二个要修改的是 deployment.yaml,首先要把名字空间改成和 rbac.yaml 一样,比如是 kube-system,然后要修改 volumes 和 env 里的 IP 地址和共享目录名,必须和集群里的 NFS 服务器配置一样。

按照现行环境修改后的 rbac.yaml、class.yaml 和 deployment.yaml 分别如下:

rbac.yaml

apiVersion: v1
kind: ServiceAccount
metadata:
  name: nfs-client-provisioner
  # replace with namespace where provisioner is deployed
  namespace: kube-system
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: nfs-client-provisioner-runner
rules:
  - apiGroups: [ "" ]
    resources: [ "nodes" ]
    verbs: [ "get", "list", "watch" ]
  - 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: kube-system
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: kube-system
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
  # replace with namespace where provisioner is deployed
  namespace: kube-system
subjects:
  - kind: ServiceAccount
    name: nfs-client-provisioner
    # replace with namespace where provisioner is deployed
    namespace: kube-system
roleRef:
  kind: Role
  name: leader-locking-nfs-client-provisioner
  apiGroup: rbac.authorization.k8s.io

class.yaml

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: nfs-client
provisioner: k8s-sigs.io/nfs-subdir-external-provisioner # or choose another name, must match deployment's env PROVISIONER_NAME'
parameters:
  archiveOnDelete: "false"

deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nfs-client-provisioner
  labels:
    app: nfs-client-provisioner
  # replace with namespace where provisioner is deployed
  namespace: kube-system
spec:
  replicas: 1
  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: k8s.gcr.io/sig-storage/nfs-subdir-external-provisioner:v4.0.2
          # image: xiaobinqt/nfs-subdir-external-provisioner:v4.0.2
          volumeMounts:
            - name: nfs-client-root
              mountPath: /persistentvolumes
          env:
            - name: PROVISIONER_NAME
              value: k8s-sigs.io/nfs-subdir-external-provisioner
            - name: NFS_SERVER
              value: 192.168.14.142 #改IP地址
            - name: NFS_PATH
              value: /tmp/nfs  #改共享目录名
      volumes:
        - name: nfs-client-root
          nfs:
            server: 192.168.14.142 #改IP地址
            path: /tmp/nfs  #改共享目录名

deployment.yaml 的镜像仓库用的是 gcr.io,拉取很困难,可以使用 Docker Hub 的镜像 xiaobinqt/nfs-subdir-external-provisioner:v4.0.2

或是直接把镜像打成 tar 包上传到服务器上再 load 解包。

YAML 修改好之后,就可以在 Kubernetes 里创建 NFS Provisioner 了:

kubectl apply -f rbac.yaml
kubectl apply -f class.yaml
kubectl apply -f deployment.yaml

使用命令 kubectl get,再加上名字空间限定 -n kube-system,就可以看到 NFS Provisioner 在 Kubernetes 里运行起来了。

2.14.5 使用 NFS 动态存储卷 #

因为有了 Provisioner,就不再需要手工定义 PV 对象了,只需要在 PVC 里指定 StorageClass 对象,它再关联到 Provisioner。

NFS 默认的 StorageClass 定义:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: nfs-client

provisioner: k8s-sigs.io/nfs-subdir-external-provisioner
parameters:
  archiveOnDelete: "false"

YAML 里的关键字段是 provisioner,它指定了应该使用哪个 Provisioner。另一个字段 parameters 是调节 Provisioner 运行的参数,需要参考文档来确定具体值,在这里的 archiveOnDelete: "false" 就是自动回收存储空间。

定义一个 PVC,向系统申请 10MB 的存储空间,使用的 StorageClass 是默认的 nfs-client:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: nfs-dyn-10m-pvc

spec:
  storageClassName: nfs-client
  accessModes:
    - ReadWriteMany

  resources:
    requests:
      storage: 10Mi

有了 PVC,还需要在 Pod 里用 volumes 和 volumeMounts 挂载,然后 Kubernetes 就会自动找到 NFS Provisioner,在 NFS 的共享目录上创建出合适的 PV 对象:

apiVersion: v1
kind: Pod
metadata:
  name: nfs-dyn-pod

spec:
  volumes:
    - name: nfs-dyn-10m-vol
      persistentVolumeClaim:
        claimName: nfs-dyn-10m-pvc

  containers:
    - name: nfs-dyn-test
      image: nginx:alpine
      ports:
        - containerPort: 80

      volumeMounts:
        - name: nfs-dyn-10m-vol
          mountPath: /tmp

使用 kubectl apply 创建好 PVC 和 Pod,可以查看一下集群里的 PV 状态:

从上图可以看到,虽然没有直接定义 PV 对象,但由于有 NFS Provisioner,它就自动创建一个 PV,大小刚好是在 PVC 里申请的 10MB。

这个时候如果去 NFS 服务器上查看共享目录,会发现多出了一个目录,名字与这个自动创建的 PV 一样,但加上了名字空间和 PVC 的前缀:

Pod、PVC、StorageClass 和 Provisioner 的关联关系,可以通过下图有一个大致的了解: