海量小文件挑战下的CephFS:优化策略与实践探索

文章目录

  • 1.背景
  • 2.基本概念
    • 2.1 CephFS IO流程
    • 2.2 Ceph-FUSE
  • 3. 问题
    • 3.1 问题源起
    • 3.2 理论分析
    • 3.3 原因排查
      • 3.3.1 Ceph-FUSE日志分析
      • 3.3.2 提出猜想
      • 3.3.3 代码验证
        • 3.3.3.1 MDS端
        • 3.3.3.2 Ceph-FUSE端
    • 3.4 小结

1.背景

随着大数据、人工智能技术的蓬勃发展,人类对于算力资源的需求也迎来大幅度的增长。在某平台以降本增效为目标,整合了公司的GPU训练卡资源,为算法工程师们提供统一的底层GPU算力服务。借助于虚拟化、算力挖掘等技术,平台服务公司内各BG的AI训练场景,GPU利用率业界领先。同时,通过云原生任务化的方式,对接了内部各大业务,促进了AI技术研究效率的提升和创新研究。
当下,由于AI训练时的高性能计算设备(如NVIDIA GPU)成本高昂,如果任务在训练过程中不能保证数据IO的速度,将会导致计算设备低载甚至空载,这无疑在时间和资源上都是一种极大的浪费。
在某平台内部,用户的训练数据大多存放在平台提供的CephFS中,训练时将对应的CephFS目录挂载至容器内部,从而使用户在训练时能够像使用本地文件系统一样使用CephFS。但在平台运营过程中我们发现,在训练数据集文件数较多时,训练任务使用CephFS会使训练速度变得异常缓慢。基于这个普遍存在的问题,本文剖析其产生的原理,然后介绍相应的优化方案。最后,通过延伸思考来发散思维,简要介绍了不同场景下AI训练加速的技术。

2.基本概念

2.1 CephFS IO流程

CephFS IO流程如下图所示。
在这里插入图片描述
当客户端进行文件系统调用时(如open、read、readdir等),需要先从元数据服务器(Metadata Server, MDS)中获取请求文件的元数据信息,元数据信息主要包括文件的Inode号、权限、uid、gid和访问更改时间等。为了加快元数据的访问效率,MDS将大部分热点元数据都缓存在自己的内存中,从而避免低效地通过访问RADOS(Reliable, Autonomic Distributed Object Store)层来获取元数据。客户端在从MDS中获取元数据后,通过计算的方式(CRUSH算法)得到数据在RADOS中的位置,最后与远程的存储设备进行交互。
从这个架构来看,CephFS是一个元数据和用户数据分离的文件系统。文件的元数据和数据存储在RADOS中的不同Pool中,客户端需要先与MDS进行元数据交互,再与RADOS进行数据交互。

2.2 Ceph-FUSE

Ceph-FUSE是CephFS客户端的一种形式,通过用户空间文件系统(Filesystem in Userspace, FUSE)的方式来实现CephFS客户端的功能。FUSE是一个面向类Unix计算机操作系统的软件接口,它使无特权的用户能够无需编辑内核代码而创建自己的文件系统。目前Linux通过内核模块对此进行支持。通过这种方式,我们可以编写用户态的应用程序,只需要实现Linux定义的一组文件系统接口,即可在用户态实现一个完整的文件系统。

当用户需要与CephFS进行交互时,客户端的整个IO流程如下:

  • 用户程序通过syscall或glibc库进行系统调用。
  • 进程陷入内核态,文件系统操作请求到达Linux虚拟文件系统(Virtual Filesystem, VFS)。
  • VFS根据请求类型,从Dentry Cache、Inode Cache和Page Cache中分别查找dentry、inode和页缓存,若缓存命中可直接返回。
  • 若缓存不命中,则将请求转发至FUSE Driver。
  • Ceph-FUSE进程通过libfuse监听到来自于/dev/fuse的请求,与Ceph集群进行交互并返回结果。

在这里插入图片描述
当用户态程序发起FUSE请求时,Ceph-FUSE在经过处理后会将元数据信息缓存在内存中,提升后续访问的性能。同时,Linux的Dentry Cache、Inode Cache和Page Cache也会分别缓存该文件的dentry、inode和页,提升热点数据的读取性能。

3. 问题

3.1 问题源起

平台上运行的训练任务场景各不相同。在运营过程中我们发现,有用户反映某些任务中CephFS的读取速度较慢,使整个训练的时间拉长,其中属CV(Computer Vision)类的任务较为明显。

平台上CV类的任务数据集,一般都是海量的图片文件。这类数据集的特点是:

  • 文件个数多,小数据集达到十万级别,大数据集达到百万、千万甚至上亿级别。
  • 单个文件占用空间不大,大多是小文件。

3.2 理论分析

AI训练场景与许多复杂的文件操作场景不同,其数据读写的逻辑较为简单。一般来说,用户会在每个epoch训练相同的数据,然后训练多个epoch直至模型达到收敛条件。因此,AI训练场景下,训练文件在训练过程中保持不变,且被读取的频率相对固定,同时写文件的频率较低。
针对这种特点,由于Ceph-FUSE会对访问过的元数据进行缓存,同时Linux的Dentry Cache、Inode Cache和Page Cache也会充分缓存读取过的文件元数据和文件数据。通常来说,在第二个epoch开始时,由于数据集文件在第一个epoch已被访问过,训练时的IO速度应当有非常明显的提升。然而,事与愿违,对于较多数量的文件,我们发现训练速度没有明显提升,且每个epoch的训练速度都很慢。
为了查出其中的原因,接下来我们复制一个一模一样的任务,打开Ceph-FUSE日志进行分析。

3.3 原因排查

3.3.1 Ceph-FUSE日志分析

在训练任务开始时,打开母机上的Ceph-FUSE日志进行查看。
疑点现象:

  • 在第一个epoch接近末尾时,发现出现了日志trim_caps mds.x max xxx caps xxx。
  • 每次trim_caps执行,清除的dentry个数为5000。
  • 该日志每隔5s会打印一次,往后的训练过程中会一直持续。

注:CAPS是指capabilities,MDS用CAPS授予客户端对不同文件进行操作的许可,因此MDS需要实时维护每个客户端文件操作的CAPS。这就意味着,如果客户端持有了某个文件的CAPS并进行了缓存,MDS需要知道每个客户端缓存了哪些文件。

3.3.2 提出猜想

根据疑点现象大概能够提出以下的猜想:

  • 在第一个epoch结束时发生了trim_caps现象,且多次测试结果均是如此,猜测可能是缓存数量到达了某个阈值。
  • 日志每隔5s会打印一次,可能是定时器触发了trim_caps。
  • MDS需要维护每个客户端的CAPS,当客户端读取文件数较多时,MDS的cache总会达到oversize的状态,必定会触发trim_caps。

3.3.3 代码验证

根据上述猜想,可以在茫茫的Ceph源码中直奔主题,分别找出MDS和Ceph-FUSE的关键代码。

3.3.3.1 MDS端

根据现象2,在MDS中的tick函数内找到如下代码:

void MDSRankDispatcher::tick()
{
  ......
  if (is_active() || is_stopping()) {
    server->recall_client_state(nullptr, Server::RecallFlags::ENFORCE_MAX); // 选中该MDS下持有较多caps数量的客户端,执行caps回收
    mdcache->trim();
    mdcache->trim_client_leases();
    mdcache->check_memory_usage(); // 当内存使用量过大时,选中该MDS下所有客户端,执行caps回收(recall_client_state)
    mdlog->trim();
  }
  ......
}

从中可以看出,MDS端定时对客户端的CAPS进行回收,如果回收后内存使用量仍然过高,就对所有客户端再执行一次CAPS回收。在check_memory_usage函数中会根据cache试用情况决定是否再执行recall_client_state。

void MDCache::check_memory_usage()
{
  ......
  if (cache_toofull()) {
    mds->server->recall_client_state(nullptr);
  }
  ......
}

进入关键函数recall_client_state进行查看。

   /**
 * Call this when the MDCache is oversized, to send requests to the clients
 * to trim some caps, and consequently unpin some inodes in the MDCache so
 * that it can trim too.
 */
std::pair<bool, uint64_t> Server::recall_client_state(MDSGatherBuilder* gather, RecallFlags flags)
{
  ......
  const bool enforce_max = flags&RecallFlags::ENFORCE_MAX;
  const auto max_caps_per_client = g_conf->get_val<uint64_t>("mds_max_caps_per_client"); // 默认为1_M
  const auto min_caps_per_client = g_conf->get_val<uint64_t>("mds_min_caps_per_client"); // 默认为100
  const auto recall_max_caps = g_conf->get_val<uint64_t>("mds_recall_max_caps"); // 默认为5000
  ......
  /* trim caps of sessions with the most caps first */
  std::multimap<uint64_t, Session*> caps_session;
  auto f = [&caps_session, enforce_max, max_caps_per_client](Session* s) {
    auto num_caps = s->caps.size(); // 当前caps总量
    // 当flags为RecallFlags::ENFORCE_MAX时,只把caps数量超过max_caps_per_client的客户端找出来,否则找出所有客户端
    if (!enforce_max || num_caps > max_caps_per_client) {
      caps_session.emplace(std::piecewise_construct, std::forward_as_tuple(num_caps), std::forward_as_tuple(s));
    }
  };
  mds->sessionmap.get_client_sessions(std::move(f));
  ......
  for (const auto p : boost::adaptors::reverse(caps_session)) {
    ......
    // 计算每个客户端的最大caps数量
    uint64_t newlim;
    if (num_caps < recall_max_caps || (num_caps-recall_max_caps) < min_caps_per_client) {
      newlim = min_caps_per_client;
    } else {
      newlim = num_caps-recall_max_caps;
    }
    if (num_caps > newlim) {
      /* now limit the number of caps we recall at a time to prevent overloading ourselves */
      uint64_t recall = std::min<uint64_t>(recall_max_caps, num_caps-newlim); // 这里可以看出,每次最多回收mds_recall_max_caps个
      newlim = num_caps-recall;
      ......
      auto m = new MClientSession(CEPH_SESSION_RECALL_STATE); // 新建一个类型为CEPH_SESSION_RECALL_STATE的请求
      m->head.max_caps = newlim; // 设置客户端的最大caps数量
      mds->send_message_client(m, session); // 向客户端发送请求
      ......
    }
    ......
  }
  ......
}

从上述代码基本可以确定CAPS被清除的原因,MDS每隔5s执行了一次recall_client_state。由于mds_max_caps_per_client默认被设置为1_M(也就是1048576),当训练程序读取文件个数达到1_M后该客户端就会被加入caps_session队列发起CAPS回收请求。由于recall_max_caps默认被设置为5000,所以每次CAPS回收的个数为5000。

3.3.3.2 Ceph-FUSE端

首先,根据MDS端发起的类型为CEPH_SESSION_RECALL_STATE的请求,找到客户端接受请求的代码。
void Client::handle_client_session(MClientSession *m)

{
  ......
  switch (m->get_op()) {
    ......
  case CEPH_SESSION_RECALL_STATE:
    trim_caps(session, m->get_max_caps()); // max_caps,值为上述的newlim
    break;
  ......
  }
  ......
}

Ceph-FUSE接收到MDS的请求后,进入trim_caps函数。

void Client::trim_caps(MetaSession *s, uint64_t max)
{
  mds_rank_t mds = s->mds_num;
  size_t caps_size = s->caps.size(); // 客户端caps总量
  ......
  uint64_t trimmed = 0;
  auto p = s->caps.begin();
  std::set<Dentry *> to_trim; // 将需要执行caps回收的Dentry放入其中等待回收
  
  // 以下内容通过迭代器p将caps清理至max以下,将需要清理的Dentry放入to_trim中
  while ((caps_size - trimmed) > max && !p.end()) {
    ......
  }
 
  for (const auto &dn : to_trim) {
    trim_dentry(dn); // 执行Ceph-FUSE内的dentry缓存
  }
  to_trim.clear();
 
  caps_size = s->caps.size();
  if (caps_size > max)
    _invalidate_kernel_dcache(); // 这是关键函数,调用了Linux的remount操作来清理所有的dentries

Ceph-FUSE接收到MDS的请求后,会将CAPS总量清理至max以下(本例中就是清理5000个CAPS)。同时,将这些CAPS对应的dentry缓存全部清除,并调用操作系统命令来清除Dentry Cache、Inode Cache和Page Cache,执行命令为:

static int remount_cb(void *handle)
{
  // used for trimming kernel dcache. when remounting a file system, linux kernel
  // trims all unused dentries in the file system
  char cmd[1024];
  CephFuse::Handle *cfuse = (CephFuse::Handle *)handle;
  snprintf(cmd, sizeof(cmd), "mount -i -o remount %s", cfuse->opts.mountpoint); // 调用remount,清理文件系统的缓存
  int r = system(cmd);
  ......
}

3.4 小结

至此,基本真相大白。整体流程如下图所示:

训练程序启动,开始读取文件。

在第一个epoch训练后期,Ceph-FUSE拥有的CAPS达到1_M。

MDS定时器触发,对持有CAPS超过1_M的客户端执行发起回收CAPS请求,回收个数为5000。

Ceph-FUSE接收到CEPH_SESSION_RECALL_STATE请求,从caps队列中清除5000个CAPS并将这些CAPS对应的dentry从cache中清除。

Ceph-FUSE调用Linux的remount命令来清除Linux文件系统的cache。

MDS检查自身内存使用情况,若超过阈值则重复上述回收操作。

训练程序第二个epoch后,由于文件系统的cache被清除,导致缓存失效。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/913326.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

编写一个脚本实现参数的远程主机网络探测python test_ip.py 192.168.0.10~192.168.0.100(sys模块)

""" 编写一个脚本实现参数的远程主机网络探测python test_ip.py 192.168.0.10~192.168.0.100 """ #导入模块 #读取起始IP&#xff0c;结束IP import sys start_ip sys.argv[1] end_ip sys.argv[2] # print(start_ip,end_ip)##########组装数据…

lvgl: 示例入门

目录 1. A very simple hello world label 2. A button with a label and react on click event 3. Create styles from scratch for buttons 4. Create a slider and write its value on a label 1. A very simple hello world label void _lv_example_get_started_1(void) …

Redis2:Redis数据结构介绍、通用命令、String类型、Key的层级格式

欢迎来到“雪碧聊技术”CSDN博客&#xff01; 在这里&#xff0c;您将踏入一个专注于Java开发技术的知识殿堂。无论您是Java编程的初学者&#xff0c;还是具有一定经验的开发者&#xff0c;相信我的博客都能为您提供宝贵的学习资源和实用技巧。作为您的技术向导&#xff0c;我将…

云计算基础

声明 学习视频来自B站UP主泷羽sec,如涉及侵权马上删除文章 笔记的只是方便各位师傅学习知识,以下网站只涉及学习内容,其他的都与本人无关,切莫逾越法律红线,否则后果自负 目录 一、云架构介绍 二、云服务 三、云分类 四、共享责任模型 五、云架构 六、云架构设计 七、集…

【超级详细】基于Zynq FPGA对雷龙SD NAND的测试

目录 一、SD NAND特征1.1 SD卡简介1.2 SD卡Block图 二、SD卡样片三、Zynq测试平台搭建3.1 测试流程3.2 SOC搭建 一、SD NAND特征 1.1 SD卡简介 雷龙的SD NAND有很多型号&#xff0c;在测试中使用的是CSNP4GCR01-AMW与CSNP32GCR01-AOW。芯片是基于NAND FLASH和 SD控制器实现的…

python中常见的8种数据结构之一列表

列表是Python中最常见的数据结构之一。它是一种有序的集合&#xff0c;可以包含不同类型的数据。 以下是列表的一些特点和常见操作&#xff1a; 1. 定义列表&#xff1a;可以使用方括号&#xff08;[]&#xff09;来定义一个空列表&#xff0c;也可以在方括号中添加元素来初始…

Python 在PDF中绘制形状(线条、矩形、椭圆形等)

在PDF中绘制图形可以增强文档的视觉效果。通过添加不同类型的形状&#xff0c;如实线、虚线、矩形、圆形等&#xff0c;可以使文档更加生动有趣&#xff0c;提高读者的阅读兴趣。这对于制作报告、演示文稿或是教材特别有用。本文将通过以下几个示例介绍如何使用Python 在PDF中绘…

AndroidStudio-滚动视图ScrollView

滚动视图 滚动视图有两种: 1.ScrollView&#xff0c;它是垂直方向的滚动视图;垂直方向滚动时&#xff0c;layout_width属性值设置为match_parent&#xff0c;layout_height属性值设置为wrap_content。 例如&#xff1a; &#xff08;1&#xff09;XML文件中: <?xml ve…

【后端速成Vue】computed计算属性

前言&#xff1a; 本期将会介绍 Vue 中的计算属性&#xff0c;他和 methods 方法又会有什么区别呢&#xff1f;在这里都会给你一一讲解。 篮球哥找工作专属IT岗位内部推荐&#xff1a; 专属内推链接&#xff1a;内推通道 1、computed计算属性 概念&#xff1a; 基于现有的数据…

mysql 配置文件 my.cnf 增加 lower_case_table_names = 1 服务启动不了的原因

原因&#xff1a;在MySQL8.0之后的版本&#xff0c;只允许在数据库初始化时指定&#xff0c;之后不允许修改了 mysql 配置文件 my.cnf 增加 lower_case_table_names 1 服务启动不了 报错信息&#xff1a;Job for mysqld.service failed because the control process exited …

优化时钟网络之时钟偏移

Note&#xff1a;文章内容以Xilinx 7系列FPGA进行讲解 1、基本介绍 所谓时钟偏移&#xff08;Clock Skew&#xff09;&#xff0c;是指在同步时序电路中&#xff0c;同一个时钟信号到达各个寄存器时钟端口的时间不一致的现象。如下图所示&#xff1a; 时钟从源端到达寄存器FF1的…

npm镜像的常用操作

查看当前配置的 npm 镜像 npm config get registry切换官方镜像 npm config set registry https://registry.npmjs.org/切换淘宝镜像(推荐) npm config set registry https://registry.npmmirror.com/切换腾讯云镜像 npm config set registry http://mirrors.cloud.tencent…

Notepad++ 最新官网中文版在线下载 附文本编辑器安装与基础使用教程

Notepad &#xff08;记事本&#xff09;是一个简单的文本编辑器&#xff0c;预装在所有版本的 Microsoft Windows 操作系统中。它的主要功能是创建、编辑和存储纯文本文件&#xff0c;通常以 .txt 格式保存。Notepad 的设计旨在提供一个轻量级的文本处理工具&#xff0c;适合快…

Deprecated Gradle features were used in this build

前言 这是我在这个网站整理的笔记,有错误的地方请指出&#xff0c;关注我&#xff0c;接下来还会持续更新。 作者&#xff1a;神的孩子都在歌唱 今天我使用gradle搭建springboot项目使用&#xff0c;报警告Deprecated Gradle features were used in this build, making it inco…

crond 任务调度 (Linux相关指令:crontab)

相关视频链接 crontab 进行 定时任务 的设置 概述 任务调度&#xff1a;是指系统在某个时间执行的特定的命令或程序 任务调度的分类&#xff1a; 1.系统工作&#xff1a;有些重要的工作必须周而复始地执行。如病毒扫描等。 2.个别用户可能希望执行某些程序&#xff0c;比如…

比流计算资源效率最高提升 1000 倍,“增量计算”新模式能否颠覆数据分析?

作者 | 关涛 云器科技CTO 数据平台领域发展 20 年&#xff0c;逐渐成为每个企业的基础设施。作为一个进入“普惠期”的领域&#xff0c;当下的架构已经完美了吗&#xff0c;主要问题和挑战是什么&#xff1f;在 2023 年 AI 跃变式爆发的大背景下&#xff0c;数据平台又该如何演…

区块链技术在供应链管理中的应用

&#x1f493; 博客主页&#xff1a;瑕疵的CSDN主页 &#x1f4dd; Gitee主页&#xff1a;瑕疵的gitee主页 ⏩ 文章专栏&#xff1a;《热点资讯》 区块链技术在供应链管理中的应用 区块链技术在供应链管理中的应用 区块链技术在供应链管理中的应用 引言 区块链技术概述 定义与…

微搭低代码入门01变量

目录 1 变量的定义2 变量的赋值3 变量的类型4 算术运算符5 字符串的连接6 模板字符串7 检查变量的类型8 解构赋值8.1 数组的解构赋值8.2 对象的解构赋值 9 类型转换9.1 转换为字符串9.2 转换为数字9.3 转换为布尔值 总结 好些零基础的同学&#xff0c;在使用低代码的时候&#…

大数据机器学习算法与计算机视觉应用04:多项式

The Algorithm Magic of Polynomial PolynomialsThe Root of PolynomialA Delete ChannelPolynomials for Finding Maximum Matchings Polynomials 多项式 一个 d d d 次多项式可以用一个 d 1 d1 d1 元组 c i {c_i} ci​ 表达。在这种情况下&#xff0c;两个多项式相加的…

【Vue】Vue3.0(十九)Vue 3.0 中一种组件间通信方式-自定义事件

文章目录 一、自定义事件概念及使用场景二、代码解释三、新的示例 一、自定义事件概念及使用场景 概念 在 Vue 3.0 中&#xff0c;自定义事件是一种组件间通信的机制&#xff0c;允许子组件向父组件传递数据或触发父组件中的操作。子组件通过defineEmits函数定义可以触发的事件…