# 持久化存储

容器的 Volume，其实就是将一个宿主机上的目录，跟一个容器里的目录绑定挂载在了一起。

“持久化 Volume”，指的就是这个宿主机上的目录，具备“持久性。即：这个目录里面的内容，既不会因为容器的删除而被清理掉，也不会跟当前的宿主机绑定。这样，当容器被重启或者在其他节点上重建出来之后，它仍然能够通过挂载这个 Volume，访问到这些内容。

大多数情况下，持久化 Volume 的实现，往往依赖于一个远程存储服务，比如：远程文件存储（比如，NFS、GlusterFS）、远程块存储（比如，公有云提供的远程磁盘）等等。

## Persistent Volume（PV）、 Persistent Volume Claim（PVC）和 StorageClass

Kubernetes 中 PVC 和 PV 的设计，**类似于“接口”和“实现”的思想**。开发者只要知道并会使用“接口”，即：PVC；而运维人员则负责给“接口”绑定具体的实现，即：PV。

### PV

PV 定义的是一个持久化存储在宿主机上的目录，比如一个 NFS 的挂载目录。

```yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: nfs-pv
spec:
  storageClassName: manual
  capacity:
    storage: 1Gi
  accessModes:
    - ReadWriteMany
  nfs:
    server: 10.244.1.4
    path: "/"
```

### PV对象是如何变成容器里的一个持久化存储的呢？

显然，hostPath 和 emptyDir 类型的 Volume 并不具备持久性：它们既有可能被 kubelet 清理掉，也不能被“迁移”到其他节点上，所以上述例子是使用了 NFS 类型。

而 Kubernetes 需要做的工作，就是使用这些存储服务，把远程存储目录挂载到容器指定的目录，容器在这个目录里写入的文件，都会保存在远程存储中，从而使得这个目录具备了“持久性”（挂载远程磁盘）。

### PVC

PVC 定义的，则是 Pod 所希望使用的持久化存储的属性。比如，Volume 存储的大小、可读写权限等等。以下定义一个 1 GiB 大小的 PVC：

```yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: nfs-pvc
spec:
  accessModes:
    - ReadWriteMany
  storageClassName: manual
  resources:
    requests:
      storage: 1Gi
```

* PV 和 PVC 的 spec 字段。比如，PV 的存储（storage）大小，就必须满足 PVC 的要求。
* PV 和 PVC 的 storageClassName 字段必须一样（PV 和 PVC是通过 StorageClass 来绑定的）。

### PV 和 PVC 是如何绑定的呢？

Kubernetes 中有一个 PersistentVolumeController，会不断地查看当前每一个 PVC，是不是已经处于 Bound（已绑定）状态。如果不是，那它就会遍历所有的、可用的 PV，并尝试将其与这个“单身”的 PVC 进行绑定。这样，Kubernetes 就可以保证用户提交的每一个 PVC，只要有合适的 PV 出现，它就能够很快进入绑定状态。

而所谓将一个 PV 与 PVC 进行“绑定”，其实就是将这个 PV 对象的名字，填在了 PVC 对象的 spec.volumeName 字段上（可通过`kubectl describe pvc pvc-name`查看）。所以，接下来 Kubernetes 只要获取到这个 PVC 对象，就一定能够找到它所绑定的 PV。

### Pod如何使用它们呢？

在成功地将 PVC 和 PV 进行绑定之后，Pod 就能够像使用 hostPath 等常规类型的 Volume 一样，在自己的 YAML 文件里声明使用这个 PVC 了，如下所示：

```yaml
apiVersion: v1
kind: Pod
metadata:
  labels:
    role: web-frontend
spec:
  containers:
  - name: web
    image: nginx
    ports:
      - name: web
        containerPort: 80
    volumeMounts:
        - name: nfs
          mountPath: "/usr/share/nginx/html"
  volumes:
  - name: nfs
    persistentVolumeClaim:
      claimName: nfs
```

### StorageClass

```yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: block-service
provisioner: kubernetes.io/gce-pd
parameters:
  type: pd-ssd
```

如果没有 StorageClass，那么一个大规模的 Kubernetes 集群里很可能有成千上万个 PVC，这就意味着运维人员必须得事先创建出成千上万个 PV，非常的麻烦。

而 StorageClass 是一套可以自动创建PV的机制。

sc定义绑定的是一类pv和pvc（不是一个）

pv，sc、pvc是一对，bound之后就找不到已经被使用的那个sc了

### subPath

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    name: backend-service
  name: backend-service
spec:
  replicas: 1
  selector:
    matchLabels:
      name: backend-service
  strategy: {}
  template:
    metadata:
      labels:
        name: backend-service
    spec:
      volumes:
        - name: app-data
          persistentVolumeClaim:
            claimName: local-app-claim
      containers:
       - image: registry.cn-qingdao.aliyuncs.com/zw_private/bio-r:v0.2
         name: bio-r
         imagePullPolicy: IfNotPresent
         volumeMounts:
           - name: app-data
             mountPath: "/workspace"
             subPath: "r"  # 子路径,代表存储卷下面的子目录
       - image: registry.cn-qingdao.aliyuncs.com/zw_private/bio-api:v0.01
         name: bio-api
         imagePullPolicy: IfNotPresent
         volumeMounts:
           - name: app-data
             mountPath: "/workspace/r/static"
             subPath: "r/static"  # 存储卷下面的多级子路径
           - name: app-data
             mountPath: "/workspace/config.yaml"
             subPath: "config.yaml"  # 只挂载这一个文件，使这个文件覆盖容器内的配置文件启动！
```

k8s挂载的文件或目录都会直接全覆盖容器内相应的挂载目标，类似于docker的-v

## Local Persistent Volume

### 适用场景

高优先级的系统应用，需要在多个不同节点上存储数据，并且对 I/O 较为敏感。

典型的应用包括：分布式数据存储比如 MongoDB、Cassandra 等，分布式文件系统比如 GlusterFS、Ceph 等，以及需要在本地磁盘上进行大量数据缓存的分布式应用。

相比于正常的 PV，一旦这些节点宕机且不能恢复时，Local Persistent Volume 的数据就可能丢失。这就要求**使用 Local Persistent Volume 的应用必须具备数据备份和恢复的能力**，允许你把这些数据定时备份在其他位置。

### 经典误区：Local PV，不就等同于 hostPath 加 NodeAffinity 吗？

比如，一个 Pod 可以声明使用类型为 Local 的 PV，而这个 PV 其实就是一个 hostPath 类型的 Volume。如果这个 hostPath 对应的目录，已经在节点 A 上被事先创建好了。那么，我只需要再给这个 Pod 加上一个 nodeAffinity=nodeA，不就可以使用这个 Volume 了吗？

事实上，**你绝不应该把一个宿主机上的目录当作 PV 使用**。这是因为，这种本地目录的存储行为完全不可控，它所在的磁盘随时都可能被应用写满，甚至造成整个宿主机宕机。而且，不同的本地目录之间也缺乏哪怕最基础的 I/O 隔离机制。

所以，一个 Local Persistent Volume 对应的存储介质，一定是一块额外挂载在宿主机的磁盘或者块设备（“额外”的意思是，它不应该是宿主机根目录所使用的主硬盘）。这个原则，我们可以称为“**一个 PV 一块盘**”。

### 为什么这么设计？

对于常规的 PV 来说，Kubernetes 都是先调度 Pod 到某个节点上，然后，再通过“两阶段处理”来“持久化”这台机器上的 Volume 目录，进而完成 Volume 目录与容器的绑定挂载。

可是，对于 Local PV 来说，节点上可供使用的磁盘（或者块设备），必须是运维人员提前准备好的。它们在不同节点上的挂载情况可以完全不同，甚至有的节点可以没这种磁盘。

所以，这时候，调度器就必须能够知道所有节点与 Local Persistent Volume 对应的磁盘的关联关系，然后根据这个信息来调度 Pod（把Pod调度到满足条件的node上）。

### 操作实践

* 第一种，当然就是给你的宿主机挂载并格式化一个可用的本地磁盘，这也是最常规的操作；
* 第二种，对于实验环境，你其实可以在宿主机上挂载几个 RAM Disk（内存盘）来模拟本地磁盘。

以下，按第二种操作实践。

#### 准备工作

挂载 RAM Disk（内存盘）来模拟本地磁盘，在Linux下的命令示例：

```bash
# 在 node-1 上执行
$ mkdir /mnt/disks
$ for vol in vol1 vol2 vol3; do
    mkdir /mnt/disks/$vol
    mount -t tmpfs $vol /mnt/disks/$vol  # 在不同的节点定义的磁盘名字要不同
done
```

mac 不支持 tmpfs，可用 diskutil。

RAM disk 把内存当硬盘使，推出该 Ramdisk 即可释放内存，然后绑定在本地磁盘地址的数据也会全部丢失。

格式如下：

```bash
diskutil erasevolume HFS+ "<名称>" `hdiutil attach -nomount ram://$((<容量（GB）>*2097152))`
```

例如：

```bash
$ diskutil erasevolume HFS+ "k8s-disk" `hdiutil attach -nomount ram://$((1*2097152))`
Started erase on disk2
Unmounting disk
Erasing
Initialized /dev/rdisk2 as a 1024 MB case-insensitive HFS Plus volume
Mounting disk
Finished erase on disk2 (k8s-disk)
```

操作成功后的挂载路径：/Volumes/k8s-disk

创建 k8s node 标签，指令格式：

```bash
$ kubectl label nodes <node-name> <label-key>=<label-value> 
```

例如：

```bash
$ kubectl get nodes
NAME             STATUS   ROLES    AGE    VERSION
docker-desktop   Ready    master   153d   v1.19.7
# 可以看到有多组标签，可以找到kubernetes.io/hostname=docker-desktop
# 可以找到InternalIP:192.168.65.4
$ kubectl describe node docker-desktop 
$ kubectl label node 192.168.65.4 custom_label=test # 给指定node添加自定义标签
```

#### 创建pv、pvc、storageClass

```bash
$ cat local_pv.yaml 
apiVersion: v1
kind: PersistentVolume
metadata:
  name: example-pv
spec:
  capacity:
    storage: 0.5Gi
  volumeMode: Filesystem
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Delete
  storageClassName: local-storage
  local:
    path: /Volumes/k8s-disk
  nodeAffinity:
    required:
      nodeSelectorTerms:
        - matchExpressions:
            - key: kubernetes.io/hostname
              operator: In
              values:
                - docker-desktop

$ kubectl create -f local_pv.yaml 
persistentvolume/example-pv created
 
$ kubectl get pv
NAME         CAPACITY   ACCESS MODES   RECLAIM POLICY  STATUS      CLAIM             STORAGECLASS    REASON    AGE
example-pv   5Gi        RWO            Delete           Available  

# 因为 Local Persistent Volume 不支持 Dynamic Provisioning
# provisioner 指定的是 no-provisioner
# 所以它没办法在用户创建 PVC 的时候，就自动创建出对应的 PV
# 也就是说，前面创建 PV 的操作，是不可以省略的
# volumeBindingMode=WaitForFirstConsumer 延迟绑定
$ cat local_sc.yaml 
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: local-storage
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer

$ kubectl create -f local_sc.yaml 
storageclass.storage.k8s.io/local-storage created

# storageClassName: local-storage
# 所以，local_pvc 创建之后 k8s 的 Volume Controller 不会为它进行绑定操作
$ catlocal_pvc.yaml 
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: example-local-claim
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 0.5Gi
  storageClassName: local-storage

$ kubectl create -f local_pvc.yaml 
persistentvolumeclaim/example-local-claim created

$ kubectl get pvc
NAME                  STATUS    VOLUME    CAPACITY   ACCESS MODES   STORAGECLASS    AGE
example-local-claim   Pending 
```

#### 创建Pod

```bash
$ cat local_pod.yaml 
kind: Pod
apiVersion: v1
metadata:
  name: example-pv-pod
spec:
  volumes:
    - name: example-pv-storage
      persistentVolumeClaim:
        claimName: example-local-claim
  containers:
    - name: example-pv-container
      image: nginx
      ports:
        - containerPort: 80
          name: "http-server"
      volumeMounts:
        - mountPath: "/usr/share/nginx/html"
          name: example-pv-storage

$ kubectl create -f local_pod.yaml 
pod/example-pv-pod created
 
$ kubectl get pvc # 创建pod后STATUS就变成了Bound
NAME                  STATUS    VOLUME       CAPACITY   ACCESS MODES   STORAGECLASS    AGE
example-local-claim   Bound     example-pv   5Gi        RWO            local-storage   6h
```

#### 测试localPV的功能

进入容器创建文件

```bash
$ kubectl exec -it example-pv-pod -- /bin/sh
# cd /usr/share/nginx/html  # Pod 把volume挂载到了 /usr/share/nginx/html 
# touch test.txt
```

然后可以检查宿主机目录，尝试重建该Pod，发现该创建的文件还在。

### 延迟绑定

当提交了 PV 和 PVC 的 YAML 文件之后，Kubernetes 就会根据它们俩的属性，以及它们指定的 StorageClass 来进行绑定。只有绑定成功后，Pod 才能通过声明这个 PVC 来使用对应的 PV。

可是，如果你使用的是 Local Persistent Volume 的话，就会发现，这个流程根本行不通。

比如，现在你有一个 Pod，它声明使用的 PVC 叫作 pvc-1。并且，我们规定，这个 Pod 只能运行在 node-2 上。

而在 Kubernetes 集群中，有两个属性（比如：大小、读写权限）相同的 Local 类型的 PV。

其中，第一个 PV 的名字叫作 pv-1，它对应的磁盘所在的节点是 node-1。而第二个 PV 的名字叫作 pv-2，它对应的磁盘所在的节点是 node-2。

假设现在，Kubernetes 的 Volume 控制循环里，首先检查到了 pvc-1 和 pv-1 的属性是匹配的，于是就将它们俩绑定在一起。

然后，你用 kubectl create 创建了这个 Pod。

这时候，问题就出现了。

调度器看到，这个 Pod 所声明的 pvc-1 已经绑定了 pv-1，而 pv-1 所在的节点是 node-1，根据“调度器必须在调度的时候考虑 Volume 分布”的原则，这个 Pod 自然会被调度到 node-1 上。

可是，我们前面已经规定过，这个 Pod 根本不允许运行在 node-1 上。所以。最后的结果就是，这个 Pod 的调度必然会失败。

这就是为什么，在使用 Local Persistent Volume 的时候，我们必须想办法推迟这个“绑定”操作。

那么，具体推迟到什么时候呢？

答案是：推迟到调度的时候。

所以说，StorageClass 里的 volumeBindingMode=WaitForFirstConsumer 的含义，就是告诉 Kubernetes 里的 Volume 控制循环（“红娘”）：虽然你已经发现这个 StorageClass 关联的 PVC 与 PV 可以绑定在一起，但请不要现在就执行绑定操作（即：设置 PVC 的 VolumeName 字段）。

而要等到第一个声明使用该 PVC 的 Pod 出现在调度器之后，调度器再综合考虑所有的调度规则，当然也包括每个 PV 所在的节点位置，来统一决定，这个 Pod 声明的 PVC，到底应该跟哪个 PV 进行绑定。

这样，在上面的例子里，由于这个 Pod 不允许运行在 pv-1 所在的节点 node-1，所以它的 PVC 最后会跟 pv-2 绑定，并且 Pod 也会被调度到 node-2 上。

所以，通过这个延迟绑定机制，原本实时发生的 PVC 和 PV 的绑定过程，就被延迟到了 Pod 第一次调度的时候在调度器中进行，从而保证了这个绑定结果不会影响 Pod 的正常调度。

## 存储插件Rook

### 安装

Rook 项目巧妙地依赖了 Kubernetes 提供的编排能力，合理的使用了很多诸如 Operator、CRD 等重要的扩展特性，属于“云原生”项目（也就是“Kubernetes 原生”）。Rook 具备水平扩展、迁移、灾难备份、监控等大量的企业级功能，是一个完整的、生产级别可用的容器存储插件。

[在此目录获取Rook-v1.7.4版本的yaml](https://github.com/rook/rook/tree/master/cluster/examples/kubernetes/ceph)，逐个安装 crds.yaml、common.yaml、operator.yaml、cluster.yaml，然后检查部署状态，是正常的 Running 状态即可。

很遗憾，有的镜像拉不下来，比如 k8s.gcr.io 下的镜像，用如下方法替换之：

```bash
# 拉取国内镜像
$ docker pull registry.aliyuncs.com/google_containers/csi-node-driver-registrar:v2.2.0
$ docker pull registry.aliyuncs.com/google_containers/csi-attacher:v3.2.1
$ docker pull registry.aliyuncs.com/google_containers/csi-provisioner:v2.2.2
$ docker pull registry.aliyuncs.com/google_containers/csi-snapshotter:v4.1.1
$ docker pull registry.aliyuncs.com/google_containers/csi-resizer:v1.2.0
# 手动将镜像做tag，否则依然会去远端下载
$ docker tag registry.aliyuncs.com/google_containers/csi-node-driver-registrar:v2.2.0 k8s.gcr.io/sig-storage/csi-node-driver-registrar:v2.2.0
$ docker tag registry.aliyuncs.com/google_containers/csi-attacher:v3.2.1 k8s.gcr.io/sig-storage/csi-attacher:v3.2.1
$ docker tag registry.aliyuncs.com/google_containers/csi-provisioner:v2.2.2 k8s.gcr.io/sig-storage/csi-provisioner:v2.2.2
$ docker tag registry.aliyuncs.com/google_containers/csi-snapshotter:v4.1.1 k8s.gcr.io/sig-storage/csi-snapshotter:v4.1.1
$ docker tag registry.aliyuncs.com/google_containers/csi-resizer:v1.2.0 k8s.gcr.io/sig-storage/csi-resizer:v1.2.0
```

依赖的 k8s.gcr.io 镜像都类似处理，前缀换成 registry.aliyuncs.com/google\_containers，然后打 tag。

```bash
$ kubectl get pods -n rook-ceph
NAME                                            READY   STATUS    RESTARTS   AGE
csi-cephfsplugin-fj452                          3/3     Running   0          9m23s
csi-cephfsplugin-provisioner-789d69957d-64rf5   6/6     Running   0          9m22s
csi-rbdplugin-6ft6j                             3/3     Running   0          9m24s
csi-rbdplugin-provisioner-d8bcc5fc4-qc2r2       6/6     Running   0          9m24s
rook-ceph-operator-9bf8b5959-lvns9              1/1     Running   0          10m
$ kubectl patch storageclass rook-ceph-block -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}' # 设置成默认的storageclass
$ kubectl get storageclass
NAME                        PROVISIONER          RECLAIMPOLICY   VOLUMEBINDINGMODE   ALLOWVOLUMEEXPANSION   AGE
hostpath                    docker.io/hostpath   Delete          Immediate           false                  130d
rook-ceph-block (default)   ceph.rook.io/block   Delete          Immediate           false                  2m15s
```

这样，一个基于 Rook 持久化存储集群就以容器的方式运行起来了，而接下来在 Kubernetes 项目上创建的所有 Pod 就能够通过 Persistent Volume（PV）和 Persistent Volume Claim（PVC）的方式，在容器里挂载由 Ceph 提供的数据卷了。

### 使用

```bash
$ kubectl create -f rook-storage.yaml
$ cat rook-storage.yaml
apiVersion: ceph.rook.io/v1beta1
kind: Pool
metadata:
  name: replicapool
  namespace: rook-ceph
spec:
  replicated:
    size: 3
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: rook-ceph-block
provisioner: ceph.rook.io/block
parameters:
  pool: replicapool
  clusterNamespace: rook-ceph
```

StorageClass 的作用，是自动地为集群里存在的每一个 PVC，调用存储插件（Rook）创建对应的 PV，从而省去了手动创建 PV 的机械劳动。

注：在使用 Rook 的情况下，statefulset.yaml 里的 volumeClaimTemplates.spec字段下需要加上声明 storageClassName:rook-ceph-block，才能使用到这个 Rook 提供的持久化存储。


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://www.1024cx.top/architecture/kubernetes/store.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
