OSS存储读写分离最佳实践

OSS存储卷支持多种客户端,不同客户端对写操作的支持程度不同。通常来说,完备的写操作支持会牺牲部分读性能。因此,数据的读写分离能最大程度避免写操作对读性能的影响,显著提升读多写少场景的数据访问性能。本文介绍在读多写少场景下如何通过不同的OSS存储卷客户端,或OSS SDK、ossutil工具等方式实现数据的读写分离。

前提条件

使用场景

OSS存储常见的使用场景包含只读和读写。对于读多写少的场景,建议您将OSS数据的读写操作进行分离:

  • 读:通过选择不同的OSS存储卷客户端,或修改配置参数,以优化数据读取速度。

  • 写:通过ossfs 1.0客户端实现完整写能力,或通过OSS SDK等方式写入数据。

只读

  • 在大数据业务的推断过程、数据分析、数据查询等场景中使用时,为避免数据被误删除和误修改,建议您将OSS存储卷的访问模式配置为ReadOnlyMany。

  • OSS存储卷当前支持ossfs 1.0, ossfs 2.0strmvol三种类型的客户端,均支持只读操作。

    • 建议您升级CSI组件版本至1.32.2及以上,并使用ossfs 2.0替代ossfs 1.0优化只读场景性能。关于ossfs 2.0存储卷的使用方式,请参见使用ossfs 2.0存储卷

    • 若您的业务为数据集读取、量化回测、时序日志分析等需要读取海量小文件数据的场景,可选择使用strmvol存储卷。关于strmvol存储卷的使用方式,请参见使用strmvol存储卷

    更多关于客户端适用场景和选型建议的信息,请参见客户端选型参考

  • 若您的业务需要在只读场景使用ossfs 1.0客户端,可参考以下参数配置提升数据读取性能。

    参数

    说明

    kernel_cache

    开启后,通过内核缓存优化读性能。适用于不需要实时访问最新内容的场景。

    缓存命中时,ossfs重复读取文件时,将通过内核缓冲区高速缓存处理,仅使用未被其他进程使用的可用内存。

    parallel_count

    以分片模式上传或下载大文件时,分片的并发数,默认值为20。

    max_multireq

    列举文件时,访问文件元信息的最大并发数。此处需大于等于parallel_count的值,默认值为20。

    max_stat_cache_size

    用于指定文件元数据的缓存空间可缓存多少个文件的元数据。单位为个,默认值为1000。如需禁止使用元数据缓存,可设置为0。

    在不需要实时访问最新内容的场景下,当目录下文件比较多时,可以根据实例规格增加支持的缓存个数,加快ls的速度。

    direct_read

    ossfs 1.91及以上版本针对只读场景新增直读模式。

读写

  • 在读写场景中,您需要将OSS存储卷的访问模式配置为ReadWriteMany。

  • 目前ossfs 1.0支持完整的写操作,ossfs 2.0仅支持顺序追加写。通过ossfs进行写操作时,注意事项如下:

    • 在并发写场景中,ossfs无法保证数据写入的一致性。

    • 挂载状态下,登录应用Pod或宿主机,在挂载路径下删除或变更文件,都会直接删除或变更OSS Bucket中对应的源文件。您可以开启OSS Bucket的版本控制,避免误删除重要数据,请参见版本控制

  • 在读多写少、尤其是读写路径分离的场景中,例如,在大数据业务的训练过程中,建议您将OSS数据的读写操作进行分离,即将OSS存储卷的访问模式配置为ReadOnlyMany,然后通过配置缓存参数优化数据读取速度,并通过SDK等方式写入数据。具体操作,请参见使用示例

使用示例

本文以手写图像识别训练应用为例,介绍如何实现OSS存储的读写分离。该示例为一个简单的深度学习模型训练,业务通过只读OSS存储卷从OSS Bucket/data-dir目录中读取训练集,并通过读写OSS存储卷或OSS SDKcheckpoint写入OSS Bucket/log-dir目录。

通过ossfs实现读写

由于写checkpoint是顺序追加写行为,因此可以选择ossfs 1.0ossfs 2.0实现。

  1. 参考以下模板部署手写图像识别训练应用。

    该应用使用简单的Python编写,并挂载使用OSS静态存储卷。关于OSS存储卷配置,请参见使用ossfs 1.0静态存储卷使用ossfs 2.0存储卷

    以下示例中,应用将OSS Bucket的子路径/tf-train挂载至Pod/mnt目录。操作前,请先将MNIST手写图像训练集上传到/tf-train/train/data目录中,供应用读取。image.png

    1. 参考以下内容,创建ossfs 1.0存储卷。

      cat << EOF | kubectl apply -f -
      apiVersion: v1
      kind: Secret
      metadata:
        name: oss-secret
        namespace: default
      stringData:
        akId: "<your-accesskey-id>"
        akSecret: "<your-accesskey-secret>"
      ---
      apiVersion: v1
      kind: PersistentVolume
      metadata:
        name: tf-train-pv
        labels:
          alicloud-pvname: tf-train-pv
      spec:
        capacity:
          storage: 10Gi
        accessModes:
          - ReadWriteMany
        persistentVolumeReclaimPolicy: Retain
        csi:
          driver: ossplugin.csi.alibabacloud.com
          volumeHandle: tf-train-pv
          nodePublishSecretRef:
            name: oss-secret
            namespace: default
          volumeAttributes:
            bucket: "<a-bucket-name>"
            url: "oss-cn-beijing.aliyuncs.com"
            otherOpts: "-o max_stat_cache_size=0 -o allow_other"
            path: "/tf-train"
      ---
      apiVersion: v1
      kind: PersistentVolumeClaim
      metadata:
        name: tf-train-pvc
      spec:
        accessModes:
        - ReadWriteMany
        resources:
          requests:
            storage: 10Gi
        selector:
          matchLabels:
            alicloud-pvname: tf-train-pv
      EOF
    2. 参考以下内容,创建训练容器。

      cat << EOF | kubectl apply -f -
      apiVersion: v1
      kind: Pod
      metadata:
        labels:
          app: tfjob
        name: tf-mnist
        namespace: default
      spec:
        containers:
        - command:
          - sh
          - -c
          - python /app/main.py
          env:
          - name: NVIDIA_VISIBLE_DEVICES
            value: void
          - name: gpus
            value: "0"
          - name: workers
            value: "1"
          - name: TEST_TMPDIR
            value: "/mnt"
          image: registry.cn-beijing.aliyuncs.com/tool-sys/tf-train-demo:rw
          imagePullPolicy: Always
          name: tensorflow
          ports:
          - containerPort: 20000
            name: tfjob-port
            protocol: TCP
          volumeMounts:
            - name: train
              mountPath: "/mnt"
          workingDir: /root
        priority: 0
        restartPolicy: Never
        securityContext: {}
        terminationGracePeriodSeconds: 30
        volumes:
        - name: train
          persistentVolumeClaim:
            claimName: tf-train-pvc
      EOF

      训练开始前,trainning_logs目录为空。在训练过程中,中间文件将写入Pod/mnt/training_logs目录中,由ossfs上传至OSS Bucket/tf-train/trainning_logs目录中。

  2. 验证数据正常读写。

    1. 查看Pod的状态。

      kubectl get pod tf-mnist

      等待Pod状态从Running转换至Completed,约需要数分钟,预期输出为:

      NAME       READY   STATUS      RESTARTS   AGE
      tf-mnist   1/1     Completed   0          2m
    2. 查看Pod运行日志。

      通过Pod运行日志查询数据加载所需的时间,该时间包含从OSS下载文件及TensorFlow加载的时间。

      kubectl logs tf-mnist | grep dataload

      预期输出如下,实际查询的时间与实例的性能和网络状态相关。

      dataload cost time:  1.54191803932
    3. 登录OSS管理控制台查看OSS Bucket/tf-train/trainning_logs目录中已出现相关文件,表明数据可以正常从OSS中读写。image.png

通过读写分离优化ossfs数据读取速度

  1. 改造应用实现读写分离。

    • 读:参数配置优化后的ossfs 1.0只读存储卷实现读操作

    • 写:分别以ossfs 1.0读写存储卷以及OSS SDK实现写操作

    使用ossfs 1.0读写存储卷实现写操作

    下文以手写图像识别训练应用和ossfs 1.0只读+读写卷为例,介绍如何改造应用实现读写分离。

    1. 参考以下内容,创建ossfs 1.0只读存储卷。

      针对只读场景,对ossfs 1.0存储卷的配置参数进行优化。

      • PVPVCaccessModes均修改为ReadOnlyMany,Bucket的挂载路径可缩小至/tf-train/train/data

      • otherOpts中增加-o max_stat_cache_size=10000 -o kernel_cache -o umask=022选项,使ossfs在读取数据时能使用内存高速缓冲区加速处理,并增加元数据支持的缓存个数(10000个元数据缓存大约占40 M的内存,可根据实例规格及读取的数据量多少进行调整),以及通过umask使容器进程以非root用户运行时也有读权限。更多信息,请参见使用场景

      cat << EOF | kubectl apply -f -
      apiVersion: v1
      kind: Secret
      metadata:
        name: oss-secret
        namespace: default
      stringData:
        akId: "<your-accesskey-id>"
        akSecret: "<your-accesskey-secret>"
      ---
      apiVersion: v1
      kind: PersistentVolume
      metadata:
        name: tf-train-pv
        labels:
          alicloud-pvname: tf-train-pv
      spec:
        capacity:
          storage: 10Gi
        accessModes:
          - ReadOnlyMany
        persistentVolumeReclaimPolicy: Retain
        csi:
          driver: ossplugin.csi.alibabacloud.com
          volumeHandle: tf-train-pv
          nodePublishSecretRef:
            name: oss-secret
            namespace: default
          volumeAttributes:
            bucket: "<a-bucket-name>"
            url: "oss-cn-beijing.aliyuncs.com"
            otherOpts: "-o max_stat_cache_size=10000 -o kernel_cache -o umask=022 -o allow_other"
            path: "/tf-train/train/data"
      ---
      apiVersion: v1
      kind: PersistentVolumeClaim
      metadata:
        name: tf-train-pvc
      spec:
        accessModes:
        - ReadOnlyMany
        resources:
          requests:
            storage: 10Gi
        selector:
          matchLabels:
            alicloud-pvname: tf-train-pv
      EOF
    2. 参考以下内容,创建ossfs 1.0读写存储卷。

      cat << EOF | kubectl apply -f -
      apiVersion: v1
      kind: PersistentVolume
      metadata:
        name: tf-logging-pv
        labels:
          alicloud-pvname: tf-logging-pv
      spec:
        capacity:
          storage: 10Gi
        accessModes:
          - ReadWriteMany
        persistentVolumeReclaimPolicy: Retain
        csi:
          driver: ossplugin.csi.alibabacloud.com
          volumeHandle: tf-logging-pv
          nodePublishSecretRef:
            name: oss-secret
            namespace: default
          volumeAttributes:
            bucket: "<a-bucket-name>"
            url: "oss-cn-beijing.aliyuncs.com"
            otherOpts: "-o max_stat_cache_size=0 -o allow_other"
            path: "/tf-train/trainning_logs"
      ---
      apiVersion: v1
      kind: PersistentVolumeClaim
      metadata:
        name: tf-logging-pvc
      spec:
        accessModes:
        - ReadWriteMany
        resources:
          requests:
            storage: 10Gi
        selector:
          matchLabels:
            alicloud-pvname: tf-logging-pv
      EOF
    3. 参考以下内容,创建训练容器。

      说明

      训练业务的逻辑无需任何改造,只需要部署时同时挂载只读及读写存储卷。

      cat << EOF | kubectl apply -f -
      apiVersion: v1
      kind: Pod
      metadata:
        labels:
          app: tfjob
        name: tf-mnist
        namespace: default
      spec:
        containers:
        - command:
          - sh
          - -c
          - python /app/main.py
          env:
          - name: NVIDIA_VISIBLE_DEVICES
            value: void
          - name: gpus
            value: "0"
          - name: workers
            value: "1"
          - name: TEST_TMPDIR
            value: "/mnt"
          image: registry.cn-beijing.aliyuncs.com/tool-sys/tf-train-demo:rw
          imagePullPolicy: Always
          name: tensorflow
          ports:
          - containerPort: 20000
            name: tfjob-port
            protocol: TCP
          volumeMounts:
            - name: train
              mountPath: "/mnt/train/data"
            - name: logging
              mountPath: "/mnt/trainning_logs"
          workingDir: /root
        priority: 0
        restartPolicy: Never
        securityContext: {}
        terminationGracePeriodSeconds: 30
        volumes:
        - name: train
          persistentVolumeClaim:
            claimName: tf-train-pvc
        - name: logging
          persistentVolumeClaim:
            claimName: tf-logging-pvc
      EOF

    使用OSS SDK实现写操作

    下文以手写图像识别训练应用和OSS SDK为例,介绍如何改造应用实现读写分离。

    1. 在容器环境中安装SDK,可在构建镜像时,增加以下内容。具体操作,请参见安装

      RUN pip install oss2
    2. 参考OSS的官方文档Python SDK demo修改源代码。

      以上述手写图像识别训练应用为例,源镜像的相关源代码如下。

      def train():
          ...
      	saver = tf.train.Saver(max_to_keep=0)
          
          for i in range(FLAGS.max_steps):
              if i % 10 == 0:  # Record summaries and test-set accuracy
                  summary, acc = sess.run([merged, accuracy], feed_dict=feed_dict(False))
                  print('Accuracy at step %s: %s' % (i, acc))
                  if i % 100 == 0:
                      print('Save checkpoint at step %s: %s' % (i, acc))
                      saver.save(sess, FLAGS.log_dir + '/model.ckpt', global_step=i)

      以上代码中,每进行100次迭代,会将中间文件(checkpoint)存入指定的log_dir目录,即Pod/mnt/training_logs目录。由于Savermax_to_keep参数为0,将维护所有的中间文件。如果迭代1000次,则存放10checkpoint文件在OSS端。

      通过修改代码,实现通过OSS SDK上传中间文件,修改要求如下:

      1. 配置访问凭证,从环境变量中读取AccessKeyBucket信息。具体操作,请参见配置访问凭证

      2. 为减少容器内存的使用,可将max_to_keep设置为1,即总是只保存最新一组训练中间文件。每次保存中间文件时,通过put_object_from_file函数上传至对应Bucket目录。

      说明

      在读写目录分离的场景中,使用SDK时,还可以通过异步读写进一步提升训练效率。

      import oss2
      from oss2.credentials import EnvironmentVariableCredentialsProvider
      
      auth = oss2.ProviderAuth(EnvironmentVariableCredentialsProvider())
      url = os.getenv('URL','<default-url>')
      bucketname = os.getenv('BUCKET','<default-bucket-name>')
      bucket = oss2.Bucket(auth, url, bucket)
      
      ...
      def train():
        ...
        saver = tf.train.Saver(max_to_keep=1)
      
       for i in range(FLAGS.max_steps):
          if i % 10 == 0:  # Record summaries and test-set accuracy
            summary, acc = sess.run([merged, accuracy], feed_dict=feed_dict(False))
            print('Accuracy at step %s: %s' % (i, acc))
            if i % 100 == 0:
              print('Save checkpoint at step %s: %s' % (i, acc))
              saver.save(sess, FLAGS.log_dir + '/model.ckpt', global_step=i)
              # FLAGS.log_dir = os.path.join(os.getenv('TEST_TMPDIR', '/mnt'),'training_logs')
              for path,_,file_list in os.walk(FLAGS.log_dir) :  
                for file_name in file_list:  
                  bucket.put_object_from_file(os.path.join('tf-train/training_logs', file_name), os.path.join(path, file_name))

      修改后的容器镜像为registry.cn-beijing.aliyuncs.com/tool-sys/tf-train-demo:ro

    3. 修改部分应用模板,使其通过只读方式访问OSS。

      1. PVPVCaccessModes均修改为ReadOnlyMany,Bucket的挂载路径可缩小至/tf-train/train/data

      2. otherOpts中增加-o kernel_cache -o max_stat_cache_size=10000 -oumask=022选项,使ossfs在读取数据时能使用内存高速缓冲区加速处理,并增加元数据支持的缓存个数(10000个元数据缓存大约占40M的内存,可根据实例规格及读取的数据量多少进行调整),以及通过umask使容器进程以非root用户运行时也有读权限。更多信息,请参见使用场景

      3. Pod模板中增加OSS_ACCESS_KEY_ID、OSS_ACCESS_KEY_SECRET环境变量,其值可从oss-secret中获取,与配置OSS存储卷中的信息保持一致。

      展开查看手写图像识别训练应用示例修改后的YAML文件

      cat << EOF | kubectl apply -f -
      apiVersion: v1
      kind: Secret
      metadata:
        name: oss-secret
        namespace: default
      stringData:
        akId: "<your-accesskey-id>"
        akSecret: "<your-accesskey-secret>"
      ---
      apiVersion: v1
      kind: PersistentVolume
      metadata:
        name: tf-train-pv
        labels:
          alicloud-pvname: tf-train-pv
      spec:
        capacity:
          storage: 10Gi
        accessModes:
          - ReadOnlyMany
        persistentVolumeReclaimPolicy: Retain
        csi:
          driver: ossplugin.csi.alibabacloud.com
          volumeHandle: tf-train-pv
          nodePublishSecretRef:
            name: oss-secret
            namespace: default
          volumeAttributes:
            bucket: "cnfs-oss-csdr-test"
            url: "oss-cn-beijing.aliyuncs.com"
            otherOpts: "-o max_stat_cache_size=10000 -o kernel_cache -o umask=022"
            path: "/tf-train/train/data"
      ---
      apiVersion: v1
      kind: PersistentVolumeClaim
      metadata:
        name: tf-train-pvc
      spec:
        accessModes:
        - ReadOnlyMany
        resources:
          requests:
            storage: 10Gi
        selector:
          matchLabels:
            alicloud-pvname: tf-train-pv
      ---
      apiVersion: v1
      kind: Pod
      metadata:
        labels:
          app: tfjob
        name: tf-mnist
        namespace: default
      spec:
        containers:
        - command:
          - sh
          - -c
          - python /app/main.py
          env:
          - name: NVIDIA_VISIBLE_DEVICES
            value: void
          - name: gpus
            value: "0"
          - name: workers
            value: "1"
          - name: TEST_TMPDIR
            value: "/mnt"
          - name: OSS_ACCESS_KEY_ID      #与pvaksk来源一致
            valueFrom:
              secretKeyRef:
                name: oss-secret
                key: akId
          - name: OSS_ACCESS_KEY_SECRET  #与pvaksk来源一致
            valueFrom:
              secretKeyRef:
                name: oss-secret 
                key: akSecret
          - name: URL                    #若已经配置了default URL,可忽略
            value: "https://oss-cn-beijing.aliyuncs.com"
          - name: BUCKET                 #若已经配置了default BUCKET,可忽略
            value: "<bucket-name>"
          image: registry.cn-beijing.aliyuncs.com/tool-sys/tf-train-demo:ro
          imagePullPolicy: Always
          name: tensorflow
          ports:
          - containerPort: 20000
            name: tfjob-port
            protocol: TCP
          volumeMounts:
            - name: train
              mountPath: "/mnt/train/data"
          workingDir: /root
        priority: 0
        restartPolicy: Never
        securityContext: {}
        terminationGracePeriodSeconds: 30
        volumes:
        - name: train
          persistentVolumeClaim:
            claimName: tf-train-pvc
      EOF
  2. 验证数据正常读写。

    1. 查看Pod状态。

      kubectl get pod tf-mnist

      等待Pod状态从Running转换至Completed,约需要数分钟,预期输出为:

      NAME       READY   STATUS      RESTARTS   AGE
      tf-mnist   1/1     Completed   0          2m
    2. 查看Pod运行日志。

      通过Pod运行日志查询数据加载所需的时间,该时间包含从OSS下载文件及TensorFlow加载的时间。

      kubectl logs tf-mnist | grep dataload

      预期输出:

      dataload cost time:  0.843528985977

      预期输出表明,在只读模式中合理利用缓存,可提升数据读取的速度。在大规模训练或其他持续加载数据的场景中,优化效果更加明显。

    3. 登录OSS管理控制台查看OSS Bucket/tf-train/trainning_logs目录中已出现相关文件,表明数据可以正常从OSS中读写。image.png

相关参考

OSS SDK参考

阿里云官方OSS SDK部分参考代码如下:

更多支持语言PHP、Node.js、Browser.js、.NET、Android、iOS、Ruby,请参见SDK参考

实现OSS读写分离的其他工具

工具

相关文档

OSS管理控制台

控制台快速入门

OpenAPI

PutObject

ossutil命令行工具

cp(上传文件)

ossbrowser图形化管理工具

常用操作