目录
Ceph
Ceph 是一个分布式存储系统,设计用于提供高性能、可扩展且高度可靠的存储服务。Ceph 可以提供三种主要类型的存储服务:
- RADOS Block Device (RBD):块存储,类似于传统的硬盘,为虚拟机或裸机服务器提供持久化存储。
- Ceph Object Gateway (RGW):对象存储,提供类似 Amazon S3 或 Swift 的 API 接口,适合存储大量非结构化的数据。
- Ceph File System (CephFS):分布式文件系统,提供 POSIX 兼容的文件存储,适合存储文件和目录结构。
在 CephFS 中,MDS(Metadata Server)扮演着至关重要的角色。MDS 的主要职责是管理文件系统的元数据,包括目录结构、文件属性、权限信息等。具体来说,MDS 负责以下工作:
- 目录和文件的元数据管理:MDS 保存和维护文件系统中所有目录和文件的元数据,如文件名、大小、权限、创建时间等。
- 文件系统的命名空间管理:MDS 跟踪文件系统的目录层次结构,处理文件和目录的创建、删除和重命名操作。
- 客户端请求处理:MDS 会接收来自客户端的文件系统操作请求,如打开、关闭、读取、写入文件等,并根据请求执行相应的元数据操作。
- 数据分片和定位:MDS 决定文件数据应存储在哪个 OSD(Object Storage Daemon)上,以及如何将大文件分割成多个数据块。
- 元数据的缓存:为了提高性能,MDS 会缓存元数据,减少对底层存储的访问频率。
MDS 架构设计为高可用和可扩展的,通常包含一个或多个 Active MDS 实例和一个或多个 Standby MDS 实例。Active MDS 负责处理客户端的请求,而 Standby MDS 则监听网络以检测 Active MDS 的故障。如果 Active MDS 发生故障,一个 Standby MDS 会接管其职责,从 RADOS 中重播 journal 日志,以恢复元数据状态并继续提供服务。
目录配额
在Ceph文件系统(CephFS)中,目录配额是一项功能,它允许管理员限制特定目录下可以使用的存储空间总量和/或文件数量。这项功能对于控制资源消耗和防止个别用户或应用程序过度占用共享存储资源特别有用。
CephFS 的目录配额可以通过设置扩展属性(Extended Attributes,xattrs)来实现。特别是 ceph.quota.*
xattrs,这些属性可以被附加到目录的索引节点上,以定义配额限制。
以下是如何使用 getfattr
和 setfattr
命令来查看和设置配额的一些例子:
查看配额
要检查目录是否设置了配额,可以使用 getfattr
命令。例如,要查看目录 /path/to/directory
的配额设置,可以运行:
Sh
getfattr -n user.ceph.quota.max_bytes /path/to/directory getfattr -n user.ceph.quota.max_files /path/to/directory
这里 user.ceph.quota.max_bytes
表示最大字节数限制,user.ceph.quota.max_files
表示最大文件数限制。
设置配额
要设置配额,可以使用 setfattr
命令。例如,要限制目录 /path/to/directory
的存储空间不超过 1GB 和文件数量不超过 1000个,可以运行:
Sh
setfattr -n user.ceph.quota.max_bytes -v 1000000000 /path/to/directory setfattr -n user.ceph.quota.max_files -v 1000 /path/to/directory
需要注意的是,有些情况下,CephFS 的客户端需要挂载文件系统时启用配额支持。例如,在挂载点添加 ceph.conf
文件中指定的 ceph.quota
参数,或者在挂载命令中使用 -o quota
选项。
此外,当目录达到其配额限制时,进一步的写入操作将会被拒绝,直到存储空间或文件数量释放到配额限制以下。这有助于防止个别目录或用户消耗过多的资源,确保存储资源的公平分配。
以上是目录配额的一个简单示例。
流程分析
编辑
考虑到涉及的步骤繁多,上述简化描述着重突出了关键流程。在此流程中,MDS(元数据服务器)扮演着核心协调者的角色,它汇总来自多个节点的信息,统一处理后再分发至各节点。这种设计至关重要,特别是在多节点并发操作同一目录配额的场景下。因为在单一节点环境下,系统仅能准确追踪本机的资源消耗情况,而面对跨节点的目录操作,若无统一的管理机制,很容易导致容量统计的不一致性和错误。因此,MDS的存在不仅解决了这一难题,还确保了整个系统在配额控制上的全局一致性和准确性。
简而言之,MDS通过集中管理的方式,有效避免了因节点间独立操作而导致的统计混乱,为Ceph文件系统中的目录配额实施提供了坚实的后台支撑。
关键函数分析
1. MDCache::predirty_journal_parents
最终容量增加的时候走的其实是 MDCache::predirty_journal_parents 这个函数,这个函数会被很多地方调用,例如说server,locker等地方。也就是我们写入了一个1G文件,那么最终实际添加容量的函数就是这个函数。
void MDCache::predirty_journal_parents(MutationRef mut, EMetaBlob *blob, CInode *in, CDir *parent, int flags, int linkunlink, snapid_t cfollows) { bool primary_dn = flags & PREDIRTY_PRIMARY; // 标记是否为原生目录操作 bool do_parent_mtime = flags & PREDIRTY_DIR; // 标记是否需要更新父目录的修改时间 bool shallow = flags & PREDIRTY_SHALLOW; // 浅层操作标志 ceph_assert(mds->mdlog->entry_is_open()); // 确保日志条目已打开 // 确保时间戳已经设定 if (mut->get_mds_stamp() == utime_t()) mut->set_mds_stamp(ceph_clock_now()); if (in->is_base()) return; // 如果是基本 inode,直接返回 dout(10) << "predirty_journal_parents" << (do_parent_mtime ? " do_parent_mtime" : "") << " linkunlink=" << linkunlink << (primary_dn ? " primary_dn" : " remote_dn") << (shallow ? " SHALLOW" : "") << " follows " << cfollows << " " << *in << dendl; if (!parent) { ceph_assert(primary_dn); // 确保是原生目录操作 parent = in->get_projected_parent_dn()->get_dir(); // 获取父目录 } if (flags == 0 && linkunlink == 0) { dout(10) << " no flags/linkunlink, just adding dir context to blob(s)" << dendl; blob->add_dir_context(parent); // 添加目录上下文到元数据块 return; } // 构建需要写锁、标记脏和更新的 inode 列表 list<CInode *> lsi; CInode *cur = in; CDentry *parentdn = NULL; bool first = true; while (parent) { ceph_assert(parent->is_auth()); // 确保父目录有权限 // 有机会调整父目录的 dirfrag CInode *pin = parent->get_inode(); // inode -> dirfrag mut->auth_pin(parent); // 授权并锁定父目录 auto pf = parent->project_fnode(mut); // 投影父目录的文件节点 pf->version = parent->pre_dirty(); // 设置预脏版本 if (do_parent_mtime || linkunlink) { ceph_assert(mut->is_wrlocked(&pin->filelock)); // 确保写锁 ceph_assert(mut->is_wrlocked(&pin->nestlock)); // 更新过期的 fragstat/rstat parent->resync_accounted_fragstat(); parent->resync_accounted_rstat(); if (do_parent_mtime) { // 更新父目录的修改时间 pf->fragstat.mtime = mut->get_op_stamp(); pf->fragstat.change_attr++; if (pf->fragstat.mtime > pf->rstat.rctime) pf->rstat.rctime = pf->fragstat.mtime; } if (linkunlink) { // 更新链接数 if (in->is_dir()) pf->fragstat.nsubdirs += linkunlink; else pf->fragstat.nfiles += linkunlink; } } // rstat if (!primary_dn || !linkunlink || !(pin->nestlock.can_wrlock(-1) && pin->versionlock.can_wrlock())) { // 标记脏 rstat 或更新父目录的 rstat cur->mark_dirty_rstat(); } else { if (!mut->is_wrlocked(&pin->nestlock)) { mds->locker->wrlock_force(&pin->nestlock, mut); // 强制写锁 } SnapRealm *prealm = pin->find_snaprealm(); snapid_t follows = cfollows; if (follows == CEPH_NOSNAP) follows = prealm->get_newest_seq(); snapid_t first = follows + 1; parent->resync_accounted_rstat(); project_rstat_inode_to_frag(mut, cur, parent, first, linkunlink, prealm); cur->clear_dirty_rstat(); } // 检查是否停止递归 bool stop = false; if (!pin->is_auth() || (!mut->is_auth_pinned(pin) && !pin->can_auth_pin())) { stop = true; } if (!stop && !first && g_conf()->mds_dirstat_min_interval > 0) { double since_last_prop = mut->get_mds_stamp() - pin->last_dirstat_prop; if (since_last_prop < g_conf()->mds_dirstat_min_interval) stop = true; } if (stop) { mds->locker->mark_updated_scatterlock(&pin->nestlock); mut->ls->dirty_dirfrag_nest.push_back(&pin->item_dirty_dirfrag_nest); mut->add_updated_lock(&pin->nestlock); if (do_parent_mtime || linkunlink) { mds->locker->mark_updated_scatterlock(&pin->filelock); mut->ls->dirty_dirfrag_dir.push_back(&pin->item_dirty_dirfrag_dir); mut->add_updated_lock(&pin->filelock); } break; } // 更新 inode 版本和 dirstat mut->auth_pin(pin); lsi.push_front(pin); pin->pre_cow_old_inode(); auto pi = pin->project_inode(mut); pi.inode->version = pin->pre_dirty(); if (do_parent_mtime || linkunlink) { bool touched_mtime = false, touched_chattr = false; pi.inode->dirstat.add_delta(pf->fragstat, pf->accounted_fragstat, &touched_mtime, &touched_chattr); if (touched_mtime) pi.inode->mtime = pi.inode->ctime = pi.inode->dirstat.mtime; if (touched_chattr) pi.inode->change_attr = pi.inode->dirstat.change_attr; } // 更新 rstat parent->resync_accounted_rstat(); if (g_conf()->mds_snap_rstat) { for (auto &p : parent->dirty_old_rstat) { project_rstat_frag_to_inode(p.second.rstat, p.second.accounted_rstat, p.second.first, p.first, pin, true); } } parent->dirty_old_rstat.clear(); project_rstat_frag_to_inode(pf->rstat, pf->accounted_rstat, parent->first, CEPH_NOSNAP, pin, true); // 检查 rstats 的一致性 parent->check_rstats(); broadcast_quota_to_client(pin); if (pin->is_base()) break; // 下一个父目录 cur = pin; parentdn = pin->get_projected_parent_dn(); ceph_assert(parentdn); parent = parentdn->get_dir(); linkunlink = 0; do_parent_mtime = false; primary_dn = true; first = false; } // 将父目录信息添加到 blob 中 ceph_assert(parent); ceph_assert(parent->is_auth()); blob->add_dir_context(parent); blob->add_dir(parent, true); for (const auto &in : lsi) journal_dirty_inode(mut.get(), blob, in); }
上面我们可以看到这里面调用了 broadcast_quota_to_client 和 project_rstat_frag_to_inode 函数,
这两个函数一个是广播函数,一个是容量的增加函数,在这个函数中还包含对父目录的遍历,去进行查找,这一块博主还没有理解,因为此时要开发的功能并不需要去遍历父目录。调用这两个函数也是这个函数关键的原因,我们如果想要开发一个和目录相似的流程那么这个函数的修改是必不可少的。
2. handle_client_setxattr
这个函数也是至关重要的一个函数,server中处理配额的一个函数就是这个函数,这个函数包含了判断各种配额以及处理配额的关键功能。
void Server::handle_client_setxattr(MDRequestRef &mdr) { const cref_t<MClientRequest> &req = mdr->client_request; // 获取MD请求中客户端请求的引用 string name(req->get_path2()); // 从请求中获取属性名 // 检查是否为Ceph虚拟属性 if (is_ceph_vxattr(name)) { CInode *cur = try_get_auth_inode(mdr, req->get_filepath().get_ino()); // 尝试获取inode的授权引用 if (!cur) return; // 如果失败,直接返回 handle_set_vxattr(mdr, cur); // 处理Ceph虚拟属性设置 return; } // 验证属性名是否允许 if (!is_allowed_ceph_xattr(name)) { respond_to_request(mdr, -CEPHFS_EINVAL); // 响应无效请求 return; } CInode *cur = rdlock_path_pin_ref(mdr, true); // 读锁路径并固定引用 if (!cur) return; // 如果获取inode失败,直接返回 // 检查是否为快照,如果是,则拒绝写操作 if (mdr->snapid != CEPH_NOSNAP) { respond_to_request(mdr, -CEPHFS_EROFS); return; } int flags = req->head.args.setxattr.flags; // 获取设置属性的标志 // 准备锁定操作 MutationImpl::LockOpVec lov; lov.add_xlock(&cur->xattrlock); // 添加属性锁 if (!mds->locker->acquire_locks(mdr, lov)) // 尝试获取锁 return; // 如果获取锁失败,直接返回 // 检查访问权限 if (!check_access(mdr, cur, MAY_WRITE)) return; // 计算数据长度和增量大小 size_t len = req->get_data().length(); size_t inc = len + name.length(); auto handler = Server::get_xattr_or_default_handler(name); // 获取属性处理器或默认处理器 const auto &pxattrs = cur->get_projected_xattrs(); // 获取计划的属性列表 if (pxattrs) { // 验证属性键值对的总大小 size_t cur_xattrs_size = 0; for (const auto &p : *pxattrs) { if ((flags & CEPH_XATTR_REPLACE) && name.compare(p.first) == 0) { // 如果是替换模式且名称相同,跳过计算 continue; } cur_xattrs_size += p.first.length() + p.second.length(); // 计算当前属性键值对的总大小 } if (((cur_xattrs_size + inc) > g_conf()->mds_max_xattr_pairs_size)) { // 检查是否超出最大属性键值对大小限制 dout(10) << "xattr kv pairs size too big. cur_xattrs_size " << cur_xattrs_size << ", inc " << inc << dendl; respond_to_request(mdr, -CEPHFS_ENOSPC); // 超出空间限制,响应错误 return; } } // 创建XattrOp对象用于后续操作 XattrOp xattr_op(CEPH_MDS_OP_SETXATTR, name, req->get_data(), flags); int r = std::invoke(handler->validate, this, cur, pxattrs, &xattr_op); // 验证属性操作 if (r < 0) { // 如果验证失败 respond_to_request(mdr, r); // 响应错误码 return; } // 输出日志信息 dout(10) << "setxattr '" << name << "' len " << len << " on " << *cur << dendl; // 执行计划更新 auto pi = cur->project_inode(mdr, true); pi.inode->version = cur->pre_dirty(); // 设置inode版本号 pi.inode->ctime = mdr->get_op_stamp(); // 更新更改时间 if (mdr->get_op_stamp() > pi.inode->rstat.rctime) // 如果操作时间大于最近更改时间 pi.inode->rstat.rctime = mdr->get_op_stamp(); // 更新最近更改时间 if (name == "encryption.ctx") // 如果是加密上下文属性 pi.inode->fscrypt = true; // 标记为使用fscrypt pi.inode->change_attr++; // 增加属性更改计数 pi.inode->xattr_version++; // 增加属性版本计数 // 根据标志执行删除或设置属性操作 if ((flags & CEPH_XATTR_REMOVE)) { std::invoke(handler->removexattr, this, cur, pi.xattrs, xattr_op); // 删除属性 } else { std::invoke(handler->setxattr, this, cur, pi.xattrs, xattr_op); // 设置属性 } // 日志记录与等待 mdr->ls = mdlog->get_current_segment(); // 获取当前日志段 EUpdate *le = new EUpdate(mdlog, "setxattr"); // 创建EUpdate对象 mdlog->start_entry(le); // 开始日志条目 le->metablob.add_client_req(req->get_reqid(), req->get_oldest_client_tid()); // 添加客户端请求ID mdcache->predirty_journal_parents(mdr, &le->metablob, cur, 0, PREDIRTY_PRIMARY); // 记录父级脏日志 mdcache->journal_dirty_inode(mdr.get(), &le->metablob, cur); // 记录脏inode日志 // 日志提交与响应 journal_and_reply(mdr, cur, 0, le, new C_MDS_inode_update_finish(this, mdr, cur)); // 提交日志并响应客户端 }
当我们在使用配额的时候,其实只需要用当上方的 if (is_ceph_vxattr(name)) 里面的代码,因为我们的类型是quota类型的,在这个判断里会进行判断,然后进去执行handle_set_vxattr函数,进行配额的设置
3. project_rstat_frag_to_inode
这个函数的主要功能是在 CephFS 中更新 inode 的 rstat
(资源统计)信息,特别是在涉及快照操作和碎片统计更新的情况下。该函数用于处理从一个片段(或多个连续片段)到 inode 的资源统计更新,这些更新可能源于数据写入、删除或其他引起资源统计变化的操作。
void MDCache::project_rstat_frag_to_inode(const nest_info_t &rstat, const nest_info_t &accounted_rstat, snapid_t ofirst, snapid_t last, CInode *pin, bool cow_head) { dout(10) << "project_rstat_frag_to_inode [" << ofirst << "," << last << "]" << dendl; dout(20) << " frag rstat " << rstat << dendl; dout(20) << " frag accounted_rstat " << accounted_rstat << dendl; nest_info_t delta = rstat; delta.sub(accounted_rstat); dout(20) << " delta " << delta << dendl; // 这部分输出调试日志,记录了快照片段的范围、统计信息和已计入的统计信息,然后计算两者之差delta,即需要应用的实际变化量。 // 初始化一个旧inode映射_old_inodes,用于保存历史inode版本。接下来是一个循环,遍历从ofirst到last的所有快照。 CInode::old_inode_map_ptr _old_inodes; while (last >= ofirst) //判断是否正在处理inode的最新快照版本。 { CInode::mempool_inode *pi; snapid_t first; if (last == pin->last) { pi = pin->_get_projected_inode(); first = std::max(ofirst, pin->first); if (first > pin->first) { auto &old = pin->cow_old_inode(first - 1, cow_head); dout(20) << " cloned old_inode rstat is " << old.inode.rstat << dendl; } } else { if (!_old_inodes) { _old_inodes = CInode::allocate_old_inode_map(); if (pin->is_any_old_inodes()) *_old_inodes = *pin->get_old_inodes(); } if (last >= pin->first) // 如果是最新版本,获取其计划的inode,并确定应用变化的起始快照first。如果first大于inode的起始快照,说明需要克隆一个旧inode版本。 { first = pin->first; pin->cow_old_inode(last, cow_head); } else { // 如果不是最新版本,初始化_old_inodes映射,并复制现有的旧inode版本(如果有)。 // our life is easier here because old_inodes is not sparse // (although it may not begin at snapid 1) auto it = _old_inodes->lower_bound(last); if (it == _old_inodes->end()) { dout(10) << " no old_inode <= " << last << ", done." << dendl; break; } first = it->second.first; if (first > last) { dout(10) << " oldest old_inode is [" << first << "," << it->first << "], done." << dendl; // assert(p == pin->old_inodes.begin()); break; } if (it->first > last) { dout(10) << " splitting right old_inode [" << first << "," << it->first << "] to [" << (last + 1) << "," << it->first << "]" << dendl; (*_old_inodes)[last] = it->second; it->second.first = last + 1; pin->dirty_old_rstats.insert(it->first); } } if (first < ofirst) { dout(10) << " splitting left old_inode [" << first << "," << last << "] to [" << first << "," << ofirst - 1 << "]" << dendl; (*_old_inodes)[ofirst - 1] = (*_old_inodes)[last]; pin->dirty_old_rstats.insert(ofirst - 1); (*_old_inodes)[last].first = first = ofirst; } // // 获取或创建旧inode版本,并标记为脏。 pi = &(*_old_inodes)[last].inode; pin->dirty_old_rstats.insert(last); } // 应用delta到选定的inode版本上,并输出结果。 dout(20) << " projecting to [" << first << "," << last << "] " << pi->rstat << dendl; pi->rstat.add(delta); dout(20) << " result [" << first << "," << last << "] " << pi->rstat << dendl; last = first - 1; } // 更新last,准备处理下一个快照。最后,如果_old_inodes被修改,更新inode的旧inode映射 if (_old_inodes) pin->reset_old_inodes(std::move(_old_inodes)); }
整个函数的核心逻辑在于根据快照片段的范围和统计信息的变化,正确地更新inode及其历史版本的统计信息,确保数据一致性。这在处理快照操作、合并快照或进行其他元数据操作时尤为重要。
同时也是处理数据增加或者减少时的较为关键的流程,如果我们需要记录文件增加的值,无疑这个地方是我们比较好的一个选择。
4. broadcast_quota_to_client
这个函数是用于我们ceph与客户端进行通信的函数,也是我们容量更新传播的一个重要函数。其中包括了容量检查,配额检查,mds的发送(但是暂时一直没被发送,很奇怪),消息的发送。
void MDCache::broadcast_quota_to_client(CInode *in, client_t exclude_ct, bool quota_change) { if (!(mds->is_active() || mds->is_stopping())) // 确保MDServer处于活动或停止状态 return; if (!in->is_auth() || in->is_frozen()) // 检查inode是否为授权状态并且未冻结 return; const auto &pi = in->get_projected_inode(); // 获取inode的计划状态 if (!pi->quota.is_enable() && !quota_change) // 如果配额未启用且没有配额变更,无需广播 return; // 创建配额realm,如果配额在mimic版本之前设置 if (!in->get_projected_srnode()) mds->server->create_quota_realm(in); for (auto &p : in->client_caps) // 遍历所有客户端的能力信息 { Capability *cap = &p.second; // 获取能力对象 if (cap->is_noquota()) // 如果客户端没有配额控制 continue; if (exclude_ct >= 0 && exclude_ct != p.first) // 如果需要排除特定客户端 goto update; // 直接跳转至更新步骤 // 检查inode的资源使用情况是否接近配额限制 if (pi->quota.max_files > 0) // 文件数量配额检查 { if (pi->rstat.rsize() >= pi->quota.max_files) // 达到或超过文件数量上限 goto update; // 比较客户端记录的配额与实际配额差距 if ((abs(cap->last_rsize - pi->quota.max_files) >> 4) < abs(cap->last_rsize - pi->rstat.rsize())) goto update; } if (pi->quota.max_bytes > 0) // 字节大小配额检查 { if (pi->rstat.rbytes > pi->quota.max_bytes - (pi->quota.max_bytes >> 3)) // 接近字节大小上限 goto update; // 同样比较客户端记录的配额与实际配额差距 if ((abs(cap->last_rbytes - pi->quota.max_bytes) >> 4) < abs(cap->last_rbytes - pi->rstat.rbytes)) goto update; } continue; // 如果以上条件都不满足,继续检查下一个客户端 update: // 更新客户端的配额信息 cap->last_rsize = pi->rstat.rsize(); // 更新客户端记录的文件数量 cap->last_rbytes = pi->rstat.rbytes; // 更新客户端记录的字节数量 auto msg = make_message<MClientQuota>(); // 创建MClientQuota消息 msg->ino = in->ino(); // 设置inode编号 msg->rstat = pi->rstat; // 设置资源统计信息 msg->quota = pi->quota; // 设置配额信息 mds->send_message_client_counted(msg, cap->get_session()); // 发送消息给客户端 } // 通知所有副本节点 for (const auto &it : in->get_replicas()) { auto msg = make_message<MGatherCaps>(); // 创建MGatherCaps消息 msg->ino = in->ino(); // 设置inode编号 mds->send_message_mds(msg, it.first); // 发送消息给副本节点 } }
上面的这个函数就是关于配额的一些判断以及要不要去更新一下文件和广播,限制了广播不会乱发,频繁发的这种情况,使得日志中广播的信息更加精准,明确。
目录配额短暂超额的问题:
确实,MDS作为中心节点进行信息汇总与广播的设计,在带来全局一致性的同时,也引入了潜在的延迟问题。这一延迟的不确定性,尤其是当MDS的广播周期与客户端操作的时间点不匹配时,可能导致短暂的配额超限现象。例如,在设定目录配额为1GB的情况下,假设客户端正在进行连续写入操作,当累积写入量达到0.9GB时,MDS恰好进行了信息广播,更新了各节点的配额状态。然而,如果广播间隔较长,等到下一次广播时,客户端可能已经继续写入至1.1GB,此时便出现了超出配额的情况。
值得注意的是,CephFS内部确实存在一种更严格的检查机制,名为ceph_check_caps
,该机制在文件级别的限额控制中表现得更为精准,几乎能即时阻止超出配额的行为。令人好奇的是,为何在目录配额管理上并未采用相同策略。对于有深度探究兴趣的读者,建议直接探索Linux内核中Ceph的相关源码,那里隐藏着更多细节与答案,尽管本文不再深入展开。
总结
CephFs的目录配额功能展现了分布式存储系统在资源管理和优化方面的强大能力。通过深入分析源码,我们不仅理解了其背后的技术细节,也见识了Ceph如何在复杂环境中保持数据的完整性和一致性。对于希望深入了解分布式存储机制或优化存储资源管理的专业人士而言,CephFs的目录配额实现是一个值得研究的典范。