ConfigMap

ConfigMap作用是存储不加密的数据到etcd中,让Pod以变量或数据卷Volume挂载到容器中

应用场景

配置文件

Nginx 挂载 ConfigMap 配置文件

创建配置文件

创建一个配置文件 nginx.conf

user  nginx;
worker_processes  auto;

error_log  /var/log/nginx/error.log notice;
pid        /var/run/nginx.pid;

events {
    use epoll;
    worker_connections  1024;
    multi_accept on;
}


http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format main
                      '{"@timestamp":"$time_iso8601",'
                      '"host":"$hostname",'
                      '"server_ip":"$server_addr",'
                      '"client_ip":"$remote_addr",'
                      '"xff":"$http_x_forwarded_for",'
                      '"domain":"$host",'
                      '"url":"$uri",'
                      '"referer":"$http_referer",'
                      '"args":"$args",'
                      '"upstreamtime":"$upstream_response_time",'
                      '"responsetime":"$request_time",'
                      '"request_method":"$request_method",'
                      '"status":"$status",'
                      '"size":"$body_bytes_sent",'
                      '"request_body":"$request_body",'
                      '"request_length":"$request_length",'
                      '"protocol":"$server_protocol",'
                      '"upstreamhost":"$upstream_addr",'
                      '"file_dir":"$request_filename",'
                      '"http_user_agent":"$http_user_agent"'
    '}';

    access_log  /var/log/nginx/access.log  main;

    sendfile        on;
    tcp_nopush     on;

    keepalive_timeout  65;

    gzip  on;
  
    upstream halo {
        server 10.96.112.99:8090;
    }

    server {
        listen 443 ssl http2 reuseport;
        server_name  localhost;
        client_max_body_size 100m;

        ssl_certificate /etc/nginx/cert/server.crt;
        ssl_certificate_key /etc/nginx/cert/server.key;
        ssl_session_timeout 5m;
        ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
        ssl_ciphers ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP;
        ssl_prefer_server_ciphers on;

        location / {
            root   /usr/share/nginx/html;
            index  index.html index.htm;
            proxy_pass http://halo;
            proxy_set_header HOST $host;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }

        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   /usr/share/nginx/html;
        }

    }
 
}

创建 ConfigMap

从文件创建

kubectl create configmap halo-nginx-config --from-file=nginx.conf -n halo

注意: ConfigMap 是 namespace 隔离的。

以 yaml 文件方式创建

从 v1.19 开始,你可以添加一个 immutable 字段到 ConfigMap 定义中,创建 不可变更的 ConfigMap。

apiVersion: v1
kind: ConfigMap
metadata:
  name: game-demo
data:
  # 类属性键;每一个键都映射到一个简单的值
  player_initial_lives: "3"
  ui_properties_file_name: "user-interface.properties"

  # 类文件键
  game.properties: |
    enemy.types=aliens,monsters
    player.maximum-lives=5    
  user-interface.properties: |
    color.good=purple
    color.bad=yellow
    allow.textmode=true    

你可以使用四种方式来使用 ConfigMap 配置 Pod 中的容器:

  • 在容器命令和参数内
  • 容器的环境变量
  • 在只读卷里面添加一个文件,让应用来读取
  • 编写代码在 Pod 中运行,使用 Kubernetes API 来读取 ConfigMap

这些不同的方法适用于不同的数据使用方式。对前三个方法,kubelet 使用 ConfigMap 中的数据在 Pod 中启动容器。

第四种方法意味着你必须编写代码才能读取 ConfigMap 和它的数据。然而, 由于你是直接使用 Kubernetes API,因此只要 ConfigMap 发生更改,你的 应用就能够通过订阅来获取更新,并且在这样的情况发生的时候做出反应。 通过直接进入 Kubernetes API,这个技术也可以让你能够获取到不同的名字空间 里的 ConfigMap。

下面是一个 Pod 的示例,它通过使用 game-demo 中的值来配置一个 Pod:

apiVersion: v1
kind: Pod
metadata:
  name: configmap-demo-pod
spec:
  containers:
    - name: demo
      image: alpine
      command: ["sleep", "3600"]
      env:
        # 定义环境变量
        - name: PLAYER_INITIAL_LIVES # 请注意这里和 ConfigMap 中的键名是不一样的
          valueFrom:
            configMapKeyRef:
              name: game-demo           # 这个值来自 ConfigMap
              key: player_initial_lives # 需要取值的键
        - name: UI_PROPERTIES_FILE_NAME
          valueFrom:
            configMapKeyRef:
              name: game-demo
              key: ui_properties_file_name
      volumeMounts:
      - name: config
        mountPath: "/config"
        readOnly: true
  volumes:
    # 你可以在 Pod 级别设置卷,然后将其挂载到 Pod 内的容器中
    - name: config
      configMap:
        # 提供你想要挂载的 ConfigMap 的名字
        name: game-demo
        # 来自 ConfigMap 的一组键,将被创建为文件
        items:
        - key: "game.properties"
          path: "game.properties"
        - key: "user-interface.properties"
          path: "user-interface.properties"

ConfigMap 不会区分单行属性值和多行类似文件的值,重要的是 Pods 和其他对象 如何使用这些值。

上面的例子定义了一个卷并将它作为 /config 文件夹挂载到 demo 容器内, 创建两个文件,/config/game.properties 和 /config/user-interface.properties, 尽管 ConfigMap 中包含了四个键。 这是因为 Pod 定义中在 volumes 节指定了一个 items 数组。 如果你完全忽略 items 数组,则 ConfigMap 中的每个键都会变成一个与 该键同名的文件,因此你会得到四个文件。

在 Deployment 中挂载 config

apiVersion: apps/v1
kind: Deployment
metadata:
  namespace: halo
  name: halo-nginx
spec:
  replicas: 1
  selector:
    matchLabels:
      app: halo-nginx
  template:
    metadata:
      labels:
        app: halo-nginx
    spec:
      containers:
        - name: halo-nginx
          image: nginx:latest
          imagePullPolicy: IfNotPresent
          ports:
          - containerPort: 443 
          volumeMounts:
          - name: nginx-config-volume
            mountPath: /etc/nginx/nginx.conf
            subPath: nginx.conf
          - name: secret-cert-volume
            mountPath: /etc/nginx/cert
      volumes:
        - name: nginx-config-volume
          configMap:
            name: halo-nginx-conf 
            items:
            - key: nginx.conf
              path: nginx.conf
        - name: secret-cert-volume
          secret:
            secretName: halo-nginx-cert

Secret

Nginx 挂载 Secret 证书

创建 Secret

使用 yaml 文件创建

将证书转码

cat server.crt | base64 -w 0
cat server.key | base64 -w 0

使用前面命令的输出来创建yaml文件,如下所示。 base64编码的值应全部放在一行上。

apiVersion: v1
kind: Secret
metadata:
  name: halo-nginx-cert
  namespace: halo
data:
  server.crt : "省略"
  server.key: "省略"

使用命令行创建

使用密钥文件创建secret

kubectl create secret generic halo-nginx-cert \
  --from-file=server.crt=server.crt \
  --from-file=server.key=server.key

Nginx 挂载 Secret 证书

apiVersion: apps/v1
kind: Deployment
metadata:
  namespace: halo
  name: halo-nginx
spec:
  replicas: 1
  selector:
    matchLabels:
      app: halo-nginx
  template:
    metadata:
      labels:
        app: halo-nginx
    spec:
      containers:
        - name: halo-nginx
          image: nginx:latest
          imagePullPolicy: IfNotPresent
          ports:
          - containerPort: 443 
          volumeMounts:
          - name: nginx-config-volume
            mountPath: /etc/nginx/nginx.conf
            subPath: nginx.conf
          - name: secret-cert-volume
            mountPath: /etc/nginx/cert
      volumes:
        - name: nginx-config-volume
          configMap:
            name: halo-nginx-conf 
            items:
            - key: nginx.conf
              path: nginx.conf
        - name: secret-cert-volume
          secret:
            secretName: halo-nginx-cert

空目录 emptyDir

emptyDir类型的volume在pod分配到node上时被创建,kubernetes会在node上自动分配 一个目录,因此无需指定宿主机node上对应的目录文件。这个目录的初始内容为空,当Pod从node上移除时,emptyDir中的数据会被永久删除。

emptyDir Volume主要用于某些应用程序无需永久保存的临时目录,多个容器的共享目录等。

说明: 容器崩溃并不会导致 Pod 被从节点上移除,因此容器崩溃期间 emptyDir 卷中的数据是安全的。

emptyDir 的一些用途:

  • 缓存空间,例如基于磁盘的归并排序。
  • 为耗时较长的计算任务提供检查点,以便任务能方便地从崩溃前状态恢复执行。
  • 在 Web 服务器容器服务数据时,保存内容管理器容器获取的文件。

取决于你的环境,emptyDir 卷存储在该节点所使用的介质上;这里的介质可以是磁盘或 SSD 或网络存储。但是,你可以将 emptyDir.medium 字段设置为 "Memory",以告诉 Kubernetes 为你挂载 tmpfs(基于 RAM 的文件系统)。 虽然 tmpfs 速度非常快,但是要注意它与磁盘不同。 tmpfs 在节点重启时会被清除,并且你所写入的所有文件都会计入容器的内存消耗,受容器内存限制约束。

说明: 当启用 SizeMemoryBackedVolumes 特性门控时, 你可以为基于内存提供的卷指定大小。 如果未指定大小,则基于内存的卷的大小为 Linux 主机上内存的 50%。

示例

apiVersion: v1
kind: Pod
metadata:
  name: test-pd
spec:
  containers:
  - image: k8s.gcr.io/test-webserver
    name: test-container
    volumeMounts:
    - mountPath: /cache
      name: cache-volume
  volumes:
  - name: cache-volume
    emptyDir: {}

本机目录 hostPath

hostPath Volume为pod挂载宿主机上的目录或文件,使得容器可以使用宿主机的高速文件系统进行存储。

缺点:在k8s中,pod都是动态在各node节点上调度。当一个pod在当前node节点上启动并通过hostPath存储了文件到本地以后,下次调度到另一个节点上启动时,就无法使用在之前节点上存储的文件。

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

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

当使用这种类型的卷时要小心,因为:

  • 具有相同配置(例如基于同一 PodTemplate 创建)的多个 Pod 会由于节点上文件的不同 而在不同节点上有不同的行为。
  • 下层主机上创建的文件或目录只能由 root 用户写入。你需要在 特权容器 中以 root 身份运行进程,或者修改主机上的文件权限以便容器能够写入 hostPath 卷。

示例

apiVersion: v1
kind: Pod
metadata:
  name: test-pd
spec:
  containers:
  - image: k8s.gcr.io/test-webserver
    name: test-container
    volumeMounts:
    - mountPath: /test-pd
      name: test-volume
  volumes:
  - name: test-volume
    hostPath:
      # 宿主上目录位置
      path: /data
      # 此字段为可选
      type: Directory

注意: FileOrCreate 模式不会负责创建文件的父目录。 如果欲挂载的文件的父目录不存在,Pod 启动会失败。 为了确保这种模式能够工作,可以尝试把文件和它对应的目录分开挂载,如 FileOrCreate 配置 所示。

apiVersion: v1
kind: Pod
metadata:
  name: test-webserver
spec:
  containers:
  - name: test-webserver
    image: k8s.gcr.io/test-webserver:latest
    volumeMounts:
    - mountPath: /var/local/aaa
      name: mydir
    - mountPath: /var/local/aaa/1.txt
      name: myfile
  volumes:
  - name: mydir
    hostPath:
      # 确保文件所在目录成功创建。
      path: /var/local/aaa
      type: DirectoryOrCreate
  - name: myfile
    hostPath:
      path: /var/local/aaa/1.txt
      type: FileOrCreate

看起来支持这么多type还是挺好的,但为什么说不适合在生产环境中使用呢?

  • 由于集群内每个节点的差异化,要使用 hostPath Volume,我们需要通过NodeSelector 等方式进行精确调度,这种事情多了,你就会不耐烦了。
  • 注意 DirectoryOrCreate 和 FileOrCreate 两种类型的 hostPath,当Node上没有对应的 File/Directory 时,你需要保证kubelet有在 Node上Create File/Directory 的权限。
  • 另外,如果 Node 上的文件或目录是由 root 创建的,挂载到容器内之后,你通常还要保证容器内进程有权限对该文件或者目录进行写入,比如你需要以 root 用户启动进程并运行于 privileged 容器,或者你需要事先修改好 Node 上的文件权限配置。
  • Scheduler 并不会考虑 hostPath volume 的大小,hostPath 也不能申明需要的 storage size,这样调度时存储的考虑,就需要人为检查并保证。

Local

Local persistent volume 就是用来解决 hostPath volume 面临的 portability, disk accounting, and scheduling 的缺陷。PV Controller 和 Scheduler 会对 local PV 做特殊的逻辑处理,以实现 Pod 使用本地存储时发生Pod re-schedule 的情况下能再次调度到 local volume 所在的 Node。

local pv 在生产中使用,也是需要谨慎的,毕竟它本质上还是使用的是节点上的本地存储,如果没有相应的存储副本机制,那意味着一旦节点或者磁盘异常,使用该volume的Pod也会异常,甚至出现数据丢失,除非你明确知道这个风险不会对你的应用造成很大影响或者允许数据丢失。

local 卷所代表的是某个被挂载的本地存储设备,例如磁盘、分区或者目录。

local 卷只能用作静态创建的持久卷。尚不支持动态配置。

与 hostPath 卷相比,local 卷能够以持久和可移植的方式使用,而无需手动将 Pod 调度到节点。系统通过查看 PersistentVolume 的节点亲和性配置,就能了解卷的节点约束。

然而,local 卷仍然取决于底层节点的可用性,并不适合所有应用程序。 如果节点变得不健康,那么local 卷也将变得不可被 Pod 访问。使用它的 Pod 将不能运行。 使用 local 卷的应用程序必须能够容忍这种可用性的降低,以及因底层磁盘的耐用性特征 而带来的潜在的数据丢失风险。

下面是一个使用 local 卷和 nodeAffinity 的持久卷示例:

apiVersion: v1
kind: PersistentVolume
metadata:
  name: example-pv
spec:
  capacity:
    storage: 100Gi
  volumeMode: Filesystem
  accessModes:
  - ReadWriteOnce
  persistentVolumeReclaimPolicy: Delete
  storageClassName: local-storage
  local:
    path: /mnt/disks/ssd1
  nodeAffinity:
    required:
      nodeSelectorTerms:
      - matchExpressions:
        - key: kubernetes.io/hostname
          operator: In
          values:
          - example-node

使用 local 卷时,你需要设置 PersistentVolume 对象的 nodeAffinity 字段。 Kubernetes 调度器使用 PersistentVolume 的 nodeAffinity 信息来将使用 local 卷的 Pod 调度到正确的节点。

PersistentVolume 对象的 volumeMode 字段可被设置为 "Block" (而不是默认值 "Filesystem"),以将 local 卷作为原始块设备暴露出来。

使用 local 卷时,建议创建一个 StorageClass 并将其 volumeBindingMode 设置为 WaitForFirstConsumer。要了解更多详细信息,请参考 local StorageClass 示例。 延迟卷绑定的操作可以确保 Kubernetes 在为 PersistentVolumeClaim 作出绑定决策时, 会评估 Pod 可能具有的其他节点约束,例如:如节点资源需求、节点选择器、Pod 亲和性和 Pod 反亲和性。

kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: local-storage
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer

本地卷还不支持动态制备,然而还是需要创建 StorageClass 以延迟卷绑定, 直到完成 Pod 的调度。这是由 WaitForFirstConsumer 卷绑定模式指定的。

延迟卷绑定使得调度器在为 PersistentVolumeClaim 选择一个合适的 PersistentVolume 时能考虑到所有 Pod 的调度限制。

你可以在 Kubernetes 之外单独运行静态驱动以改进对 local 卷的生命周期管理。 请注意,此驱动尚不支持动态配置。 有关如何运行外部 local 卷驱动,请参考 local 卷驱动用户指南。

说明: 如果不使用外部静态驱动来管理卷的生命周期,用户需要手动清理和删除 local 类型的持久卷.

使用local persistent volume注意事项

  • 使用 local pv 时必须定义 nodeAffinity,Kubernetes Scheduler 需要使用PV 的 nodeAffinity 描述信息来保证 Pod 能够调度到有对应 local volume 的Node 上。
  • volumeMode 可以是 FileSystem(Default)和 Block,并且需要 enable BlockVolume Alpha feature gate。
  • 节点上 local volume 的初始化需要我们人为去完成(比如 local disk 需要 pre-partitioned, formatted, and mounted. 共享存储对应的 Directories 也需要 pre-created),并且人工创建这个 local PV,当 Pod 结束,我们还需要手动的清理 local volume,然后手动删除该 local PV 对象。因此,persistentVolumeReclaimPolicy 只能是 Retain。
  • 创建 local PV 之前,你需要先保证有对应的 storageClass 已经创建。并且该storageClass 的 volumeBindingMode 必须是 WaitForFirstConsumer 以标识延迟 Volume Binding 。WaitForFirstConsumer 可以保证正常的 Pod 调度要求(resource requirements, node selectors, Pod affinity, and Pod anti-affinity等),又能保证 Pod 需要的 Local PV 的 nodeAffinity 得到满足,实际上,一共有以下两种 volumeBindingMode:
// VolumeBindingImmediate indicates that PersistentVolumeClaims should be
// immediately provisioned and bound.
VolumeBindingImmediate VolumeBindingMode = "Immediate"

// VolumeBindingWaitForFirstConsumer indicates that PersistentVolumeClaims
// should not be provisioned and bound until the first Pod is created that
// references the PeristentVolumeClaim.  The volume provisioning and
// binding will occur during Pod scheduing.
VolumeBindingWaitForFirstConsumer VolumeBindingMode = "WaitForFirstConsumer"

local volume manager

上面这么多事情需要人为的去做预处理的工作,我们必须要有解决方案帮我们自动完成 local volume 的 create 和 cleanup 的工作。官方给出了一个简单的 local volume manager ,注意它仍然只是一个 static provisioner,目前主要帮我们做两件事:

  • local volume manager 监控配置好的 discovery directory 的新的挂载点,并为每个挂载点根据对应的 storageClassName, path, nodeAffinity, and capacity 创建PersistentVolume object。
  • 当Pod结束并删除了使用 local volume 的 PVC,local volume manager 将自动清理该 local mount 上的所有文件, 然后删除对应的 PersistentVolume object.

因此,除了需要人为的完成 local volume 的 mount 操作,local PV 的生命周期管理就全部交给 local volume manager 了。

示例

创建 PV

apiVersion: v1
kind: PersistentVolume
metadata:
  name: example-local-pv
  labels
    release: "stable"  # 为了后续的 PVC 绑定到此 PV 上,加了 labels
spec:
  capacity:
    storage: 5Gi
  accessModes:
  - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  storageClassName: local-volume
  local:
    path: /data/local/vol1  # 本地存放目录
  nodeAffinity:             # 调度选择
    required:
      nodeSelectorTerms:
      - matchExpressions:
        - key: kubernetes.io/hostname
          operator: In
          values:
          - worker1

创建一个storage class

kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: local-volume
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer

sc的provisioner是 kubernetes.io/no-provisioner。
WaitForFirstConsumer表示PV不要立即绑定PVC,而是直到有Pod需要用PVC的时候才绑定。调度器会在调度时综合考虑选择合适的local PV,这样就不会导致跟Pod资源设置,selectors,affinity and anti-affinity策略等产生冲突。很明显:如果PVC先跟local PV绑定了,由于local PV是跟node绑定的,这样selectors,affinity等等就基本没用了,所以更好的做法是先根据调度策略选择node,然后再绑定local PV。

静态创建PV

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: myclaim
spec:
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi
  storageClassName: local-volume
  selector:
    matchLabels:
      release: "stable" # 为了与上面的 PVC 绑定

挂载

kind: Pod
apiVersion: v1
metadata:
  name: mypod
spec:
  containers:
    - name: myfrontend
      image: nginx
      volumeMounts:
      - mountPath: "/usr/share/nginx/html"
        name: mypd
  volumes:
    - name: mypd
      persistentVolumeClaim:
        claimName: myclaim

NFS

插件

https://github.com/kubernetes-sigs/nfs-subdir-external-provisioner/tree/master/deploy

下载

https://github.com/kubernetes-sigs/nfs-subdir-external-provisioner/releases

在上述连接下载

tree deploy/
deploy/
├── class.yaml
├── deployment.yaml
├── objects
│   ├── class.yaml
│   ├── clusterrolebinding.yaml
│   ├── clusterrole.yaml
│   ├── deployment.yaml
│   ├── README.md
│   ├── rolebinding.yaml
│   ├── role.yaml
│   └── serviceaccount.yaml
├── rbac.yaml
├── test-claim.yaml
└── test-pod.yaml

objects 目录中的对象与父目录中的对象相同,只是为了某些用户的方便将每个对象分成一个文件。

配置

修改 nfs 服务器地址和挂载地址

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nfs-client-provisioner
  labels:
    app: nfs-client-provisioner
  # replace with namespace where provisioner is deployed
  namespace: default
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
          volumeMounts:
            - name: nfs-client-root
              mountPath: /persistentvolumes
          env:
            - name: PROVISIONER_NAME
              value: k8s-sigs.io/nfs-subdir-external-provisioner
            - name: NFS_SERVER
              value: 10.3.243.101
            - name: NFS_PATH
              value: /ifs/kubernetes
      volumes:
        - name: nfs-client-root
          nfs:
            server: 10.3.243.101
            path: /ifs/kubernetes

安装

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

测试

PVC

cat test-claim.yaml

kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: test-claim
spec:
  storageClassName: managed-nfs-storage
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 1Mi

挂载

cat test-pod.yaml 
kind: Pod
apiVersion: v1
metadata:
  name: test-pod
spec:
  containers:
  - name: test-pod
    image: gcr.io/google_containers/busybox:1.24
    command:
      - "/bin/sh"
    args:
      - "-c"
      - "touch /mnt/SUCCESS && exit 0 || exit 1"
    volumeMounts:
      - name: nfs-pvc
        mountPath: "/mnt"
  restartPolicy: "Never"
  volumes:
    - name: nfs-pvc
      persistentVolumeClaim:
        claimName: test-claim

部署

kubectl apply -f test-claim.yaml
kubectl apply -f test-pod.yaml

RBD

参考 https://www.lsx.cool/archives/kubernetesshi-yong-cephrbdcun-chu