CubeFS&Kubernetes实践

Kubernetes (K8S) 是目前容器编排领域事实上的标准,在全球范围内有着广泛的应用。

CubeFS 既可以在 K8S 集群中部署客户端,通过 CSI 接口为 K8S 提供持久卷存储能力,也可以直接将 CubeFS 服务端集群整个部署在 K8S 中。

本文将分别介绍一下如何在 K8S 集群中部署 CubeFS 客户端、服务端,以及一些常见的运维操作和注意事项。

服务端

随着容器化的普及,基础设施全部上 K8S 是未来的趋势,这样所有的资源可以统一调度,且有着云原生庞大生态的加持,基础设施的运维成本也将大幅降低。

CubeFS 除了在物理机上通过 Ansible、SaltStack 等工具管理部署,同时也可以部署在 Kubernetes 集群中,享受云原生加持,且我们会直接使用宿主机网络,通过 hostPath 将磁盘映射到容器中的挂载点,理论上是不会有任何性能损耗的。

大致架构

CubeFS 目前由这四部分组成:

StatefulSet Master *资源管理节点*,负责维护整个集群的元信息

DaemonSet DataNode 数据存储节点,需要挂载大量磁盘负责文件数据的实际存储

DaemonSet MetaNode 元数据节点,负责存储所有的文件元信息

Deployment ObjectNode负责提供转换 S3 协议提供对象存储的能力,无状态服务

CubeFS架构(1)

部署

准备机器

在我们开始部署之前,我们需要拥有一个至少有 4 个节点的 Kubernetes 集群,且版本大于 1.14。

然后就可以开始规划准备机器了,需要设置为 DataNode 角色的机器需要先初始化好磁盘,可以参考传统化部署中的准备数据目录章节

然后我们需要给机器打上各自的标签,标明这台机器要在 CubeFS 集群中承担的角色:

# Master 节点,至少三个,建议为奇数个
kubectl label node <nodename> component.cubefs.io/master=enabled
# MetaNode 元数据节点,至少四个,奇偶无所谓
kubectl label node <nodename> component.cubefs.io/metanode=enabled
# Dataode 数据节点,至少四个,奇偶无所谓
kubectl label node <nodename> component.cubefs.io/datanode=enabled
# ObjectNod 对象存储节点,至少一个,可自由水平扩容
kubectl label node <nodename> component.cubefs.io/objectnode=enabled

后续安装时就会根据这些标签通过 nodeSelector进行匹配,然后在机器创建起对应的 Pod

下面我们将使用 Helm 配置并安装 CubeFS,请确保你的操作机器上安装了 Helm3(Helm 安装请参考其官方文档)。

我们都知道 K8S 是接收 yaml 做为输入来应用资源的,然而 YAML 是纯静态的,当我们的 YAML 比较复杂,当需要变量、逻辑判断、循环等高级功能的时候,普通的 YAML 就做不到了,这时候就需要用到 Helm 这种能对 YAML 进行模板化管理的工具了。

拉取 Cubefs Helm 仓库

git clone https://github.com/cubefs/cubefs-helm
cd cubefs-helm

设置配置

我们的项目存在大量的配置,所有的可配置项位于./cubefs/values.yaml中,其中包含有详细的注释,这里我们只单独创建一个配置,覆盖其中常见的关键配置项。

vi ~/cubefs.yaml 

按照你的需要编辑该文件:

# 要安装哪些组件,如果只安装服务端的话保持下方配置即可,如果要安装客户端的话,把 csi 设置为 true
component:
  master: true
  datanode: true
  metanode: true
  objectnode: true
  client: false
  csi: false
  monitor: false
  ingress: true

# path.data: Master、MetaNode 的元数据存储路径,会以 hostPath 的方式存储在宿主机上,建议使用性能较高的底层磁盘
# path.log: 所有组件的日志在宿主机上的存储路径
path:
  data: /var/lib/cubefs
  log: /var/log/cubefs

master:
  # Master 组件实例数量
  replicas: 3
  # Master Ingres 配置使用的域名,记得需要将该域名 DNS 解析到 Ingres Controller 的入口,当然也可以不配置,
  # 在客户端处直接将所有 Master 的 IP + 端口配置上
  host: master.cubefs.com

objectnode:
  # ObjectNode 组件实例数量
  replicas: 3
  
metanode:
  # Metanode 可以使用的总内存,单位字节,建议设置为机器可以内存的 80%,也可以按需减少
  total_mem: "26843545600"

datanode:
  # DataNode 要使用的磁盘,可以挂载多块
  # 格式: 挂载点:保留的空间
  # 保留的空间: 单位字节,当磁盘剩余空间小于该值时将不会再在该磁盘上写入数据
  disks:
    - /data0:21474836480
    - /data1:21474836480

# CSI 客户端配置
provisioner:
  # Kubelet 的主目录
  kubelet_path: /var/lib/kubelet

安装

编辑好配置文件之后,直接输入下方命令即可开始安装:

helm upgrade --install cubefs ./cubefs -f ~/cubefs.yaml -n cubefs --create-namespace

等待所有组件状态变为 Running 即可:

$ kubectl -n cubefs get pods
NAME                         READY   STATUS    RESTARTS   AGE
datanode-2rcmz                      1/1     Running   0          2m40s
datanode-7c9gv                      1/1     Running   0          2m40s
datanode-s2w8z                      1/1     Running   0          2m40s
master-0                            1/1     Running   0          2m40s
master-1                            1/1     Running   0          2m34s
master-2                            1/1     Running   0          2m27s
metanode-bwr8f                      1/1     Running   0          2m40s
metanode-hdn5b                      1/1     Running   0          2m40s
metanode-w9snq                      1/1     Running   0          2m40s
objectnode-6598bd9c87-8kpvv         1/1     Running   0          2m40s
objectnode-6598bd9c87-ckwsh         1/1     Running   0          2m40s
objectnode-6598bd9c87-pj7fc         1/1     Running   0          2m40s

服务端组件的关键日志会在容器标准输出中输出,运行起来后详细日志会存储在上文提到的 path.log配置地址,如果启动失败可以配合日志查看,常见的启动失败原因可能有:

  • DataNode 数据盘路径配置错误
  • 组件端口被占用
  • 配置的 Metanode 可用总内存大于实际物理内存
  • 等等

运维操作

扩容

DataNode 和 MetaNode 是通过 DaemonSet 类型部署的,只需要给新加的机器打上标签即可扩容:

kubectl label node <nodename> component.cubefs.io/master=enabled
kubectl label node <nodename> component.cubefs.io/metanode=enabled

Objectnode 和 Master 则除了标签之外还需要同步修改配置文件中的副本数,并重新执行 helm 更新命令。

缩容

缩容下线节点之前需要先将节点上面的数据迁移走:

curl -s "http://{{MasterAddr}}/metaNode/decommission?addr={{MetaNodeIP:MetaNodePort}}"
curl -s "http://{{MasterAddr}}/dataNode/decommission?addr={{DataNodeIP:DataNodePort}}"

然后只需要删除掉节点上面的标签即可下线完成:

kubectl label node <nodename> component.cubefs.io/master-
kubectl label node <nodename> component.cubefs.io/metanode-

重启

想要重启哪一个实例,只需要将那个 Pod 直接删除掉即可。

kubectl delete -n cubefs datanode-s2w8z

删除命令下达后,kubelet 会发送 SIGTERM给 CubeFS 应用进程,然后 CubeFS 应用进程在完成了首尾工作之后就会自行结束,实现优雅退出。

升级

CubeFS 所有工作负载的更新策略都是 OnDelete 而不是滚动更新,避免误操作,所以当我们更新了配置或者版本需要让其生效时,一个一个地将 Pod 删除掉等待其他即可完成升级,切忌不要一口气全删了,会导致服务瞬间不可用。

异构机型

当我们的集群中有个别机型比如磁盘配置,或者内存大小和其他机器不一致的时候,我们可以通过编辑 ConfigMap cubefs-config-override 来实现,配置覆盖,在该 ConfigMap 中的配置项将会以更高的优先级合并入原来的配置,对应的 helm 配置为:

datanode:
  config_override: |
    {
      "1.1.1.1": {
      "logDir": "/data/cfs/logx",
      "disks": [
        "/disk/nvme0n1:59000000000",
        "/disk/nvme1n1:59000000000",
        "/disk/nvme2n1:59000000000"
      ]
      }
    }

不过这需要我们 DataNode 和 MetaNode 的配置项有所了解,可以在这两篇文档中找到详细描述:MetaNode DataNode

磁盘损坏

对于高吞吐分布式存储系统而言,底层物理磁盘损坏会是家常便饭,在 CubeFS on Kubernetes 的部署方式中,更换磁盘步骤和传统部署的方式差不多:

  1. 下线磁盘:curl -v "http://{{MasterAddr}}/dataNode/migrate?srcAddr=src&targetAddr=dst&count=3"

  2. 将磁盘报修,在磁盘维修期间,在 cubefs-config-override 中覆盖该节点的磁盘配置,将损坏的判断那一行删掉

  3. 删除 Pod 重启节点,这样在磁盘维修期间,该节点也能正常提供服务

  4. 磁盘维修或更换完成后,删掉配置覆盖项,再重启节点,这样就修复完成了

客户端

CubeFS 实现了 CSI 的接口,可以动态的为 K8S 集群提供 PersistentVolume。

大致架构

CSI 由负责控制逻辑的 Controller 和负责实际提供挂载服务的 CSI Node Daemonset 两部分组成,逻辑相对来说比较简单。

Controller 由如下四个容器组成:

  • cfs-driver 核心逻辑容器
  • csi-provisioner官方提供的 Sidecar 容器,主要负责监听 PVC 资源的创建和删除,然后调用 cfs-dirver 上对应的接口
  • csi-attacher官方提供的 Sidecar 容器,主要负责监听 VolumeAttachment 对象,要挂载卷时,调用 cfs-driver
  • csi-resizer官方提供的 Sidecar 容器,顾名思义主要负责 PVC 扩容相关的动作转换

CSI Node 是一个 Daemonset,其中的每一个 Pod 由如下两个容器组成:

  • cfs-driver 核心逻辑容器,所有的 cfs-client 客户端都将会在这个容器中启动,并挂载卷然后映射到业务容器中
  • csi-node-driver-registrar 官方提供的 Sidecar 容器,负责将 CSI 注册到 Kubelet 上,告知 Kubelet 这台机器上能提供 CubeFS 的存储服务

CubeFS CSI架构 (1)

部署

CSI 支持通过 Helm 部署,同时由于逻辑比较简单,CSI 也可以直接 apply yaml 文件进行部署。

直接部署

直接部署步骤如下:

  1. 拉取 CSI 源代码:git clone https://github.com/cubefs/cubefs-csi

  2. 给需要部署的节点打标签:kubectl label node <nodename> component.cubefs.io/csi=enabled

  3. apply 相关资源文件:

    $ kubectl apply -f deploy/csi-rbac.yaml
    $ kubectl apply -f deploy/csi-controller-deployment.yaml
    $ kubectl apply -f deploy/csi-node-daemonset.yaml	
       
    不过如果 kubelet 路径不是默认的 /var/lib/kubelet 的话需要全局替换成新的路径,例如替换成 /data1/k8s/lib/kubelet:
    sed -i 's#/var/lib/kubelet#/data1/k8s/lib/kubelet#g'  deploy/csi-controller-deployment.yaml
    sed -i 's#/var/lib/kubelet#/data1/k8s/lib/kubelet#g'  deploy/csi-node-daemonset.yaml
    
  4. 编辑 deploy/storageclass.yaml 中设置对应的 Master 地址,然后将其 apply 即可完成部署

Helm 部署

和通过 Helm 部署服务端一样,CSI 的部署只是参数不同而已:

  1. git clone https://github.com/cubefs/cubefs-helm && cd cubefs-helm

  2. vi ~/cubefs.yaml 如果只安装 CSI 的话,只需要填写以下参数即可:

    component:
     master: false
     datanode: false
     metanode: false
     objectnode: false
     client: false
     csi: true
     monitor: false
     ingress: false
       
    image:
     # CSI related images
     csi_driver: ghcr.io/cubefs/cfs-csi-driver:3.2.0.150.0
     csi_provisioner: k8s.gcr.io/sig-storage/csi-provisioner:v2.2.2
     csi_attacher: k8s.gcr.io/sig-storage/csi-attacher:v3.4.0
     csi_resizer: k8s.gcr.io/sig-storage/csi-resizer:v1.3.0
     driver_registrar: k8s.gcr.io/sig-storage/csi-node-driver-registrar:v2.5.0
       
    csi:
     driverName: csi.cubefs.com
     logLevel: error
     # If you changed the default kubelet home path, this
     # value needs to be modified accordingly
     kubeletPath: /var/lib/kubelet
     controller:
       tolerations: [ ]
       nodeSelector:
         "component.cubefs.io/csi": "enabled"
     node:
       tolerations: [ ]
       nodeSelector:
         "component.cubefs.io/csi": "enabled"
       resources:
         enabled: false
         requests:
           memory: "4048Mi"
           cpu: "2000m"
         limits:
           memory: "4048Mi"
           cpu: "2000m"
     storageClass:
       # Whether automatically set this StorageClass to default volume provisioner
       setToDefault: true
       # StorageClass reclaim policy, 'Delete' or 'Retain' is supported
       reclaimPolicy: "Delete"
       # Override the master address parameter to connect to external cluster
       # 如果是要连接外部 CubeFS 集群的设置该参数,否则可以忽略
       masterAddr: ""
       otherParameters:
    

验证

部署完成之后,我们可以创建一个测试的卷并进行挂载验证一下流程:

kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: pvc-for-verify
  namespace: default
spec:
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 2Gi
  storageClassName: cfs-sc
  volumeMode: Filesystem
---
kind: Deployment
apiVersion: apps/v1
metadata:
  name: pod-for-verify-cubefs
  namespace: default
  labels:
    app: pod-for-verify-cubefs
spec:
  replicas: 2
  selector:
    matchLabels:
      app: pod-for-verify-cubefs
  template:
    metadata:
      name: pod-for-verify-cubefs
      namespace: cubefs
      labels:
        app: pod-for-verify-cubefs
    spec:
      volumes:
        - name: pvc-for-verify
          persistentVolumeClaim:
            claimName: pvc-for-verify
      containers:
        - name: nginx
          image: nginx:alpine
          volumeMounts:
            - name: pvc-for-verify
              mountPath: /data/pv

Pod 创建出来后,我们可以进入容器,df -Th 看一下容器中挂载的文件系统,我们可以注意到其中有一行:

Filesystem                                       Type Size Used Avail Use% Mounted on
cubefs-pvc-606dd219-617d-45d8-a43d-725a568310c5  fuse  2G   0    2G    0%   /data/pv

这就代表 CubeFS 文件系统在 /data/pv 这个挂载点挂载的好好的,然后我们可以进一步在该目录下创建、修改文件,在另外一个 Pod 看是否能同步观察到。

日志

CSI 的日志均位于 cfs-driver 容器的 /cfs/logs 目录下,除了 CSI Driver 本身的日志,cfs-client 挂载后的日志也在该目录下。

风险

FUSE 类的 CSI 都会面临一个问题,客户端一旦崩溃之后就无法恢复,所以我们需要尽可能的避免 CSI 容器的崩溃:

  1. 尽量先预先分配好足够的内存和 CPU
  2. 尽量将 Pod 的 QOS 级别设置为 Guaranteed

同时 CubeFS CSI 支持在容器重启后对异常退出的卷进行重挂载,给 csi-node 注入环境变量:REMOUNT_DAMAGED: true ,同时在 Pod 挂载时将卷的挂载传播级别设置为 HostToContainer,例如:

      containers:
        - name: nginx
          image: nginx:alpine
          volumeMounts:
            - name: pvc-for-verify8
              mountPath: /data/pv
              mountPropagation: HostToContainer

但是只能保证新打开的文件能正常读取,所以需要上层业务能处理重试逻辑。

image-20221229180012981