在文件系统中读写文件时,一般需要先得到操作对象的索引inode信息,在客户端未缓存的情况下,除了调用open外,客户端也会下发lookup或者getattr命令到服务端去获取操作对象的inode。Lookup和getattr IO的mds端流程几乎一致,所以以lookup流程为例进行介绍。
客户端以linux 4.18内核源码(cephfs的内核客户端在linux内核中实现)进行分析,服务端使用单活冷备的3节点集群。
1. 内核客户端处理发送
假设lookup的路径是/mnt/cephfs/testdir/1 (/mnt/cephfs是内核挂载根目录)
函数入口:
static struct dentry *ceph_lookup(struct inode *dir, struct dentry *dentry, unsigned int flags) { ... 入参分析:dir包含了父目录的inode索引号; dentry里面包含了d_name表示文件名’1’,可以获得 len;flag变量cephfs没有用到 ... /* 判断lookup的是否是快照来决定opcode,这里为opcode当然为CEPH_MDS_OP_LOOKUP*/ op = ceph_snap(dir) == CEPH_SNAPDIR ?CEPH_MDS_OP_LOOKUPSNAP : CEPH_MDS_OP_LOOKUP; /* 新建一个req请求。*/ req = ceph_mdsc_create_request(mdsc, op, USE_ANY_MDS); if (IS_ERR(req)) return ERR_CAST(req); /* 赋予dentry到请求r_dentry */ req->r_dentry = dget(dentry); req->r_num_caps = 2; /* 需要去mds端获取的inode cap和auth shard cap */ mask = CEPH_STAT_CAP_INODE | CEPH_CAP_AUTH_SHARED; ... /* 赋父目录的inode号给请求的r_parent */ req->r_parent = dir; ... /* 发送请求处理,等待请求完成 */ err = ceph_mdsc_do_request(mdsc, NULL, req); ... /* 获取dentry (这时dentry已经拼接好了对应inode了)*/ dentry = ceph_finish_lookup(req, dentry, err); /* 减请求计数,释放请求 */ ceph_mdsc_put_request(req); /* will dput(dentry) */ dout("lookup result=%p\n", dentry); return dentry; }
大体流程为创建输入父目录testdir的inode和文件1的名字,创建请求发送到mds处理,等待mds处理完成后回请求返回文件1的inode和dentry结构,返回带有inode链接的dentry给vfs。
ceph_mdsc_do_request处理了较为复杂的逻辑,涉及到消息发送的框架,下面重点分析下入参相关的转换。
调用关系如下:
|__ ceph_mdsc_do_request
|__ __do_request
|__ ____prepare_send_request
|__ ______create_request_message
由于mds端处理的函数是直接使用了filepath结构体承接客户端传来的参数,所以在创建请求消息create_request_message这里客户端转换之前填充req的入参为filepath结构体。
Mds端的filepath结构体:
Class filepath { inodeno_t ino; string path; }
create_request_message函数里使用了set_request_path_attr,如下图所示,由于本次是使用了lookup,所以只用了第一次的set_request_path_attr,rename的情况才需要记录old的dentry和新的dentry所在。
set_request_path_attr中由于lookup只填了目标的dentry,所以走图中标记的红框流程,build_dentry_path主要是将之前填入req中的父目录testdir的inode和文件1的dentry的名字和长度转填到filepath结构体中。
2. MDS服务端
服务端通过消息通信框架机制Messager来处理请求,由于消息的收发都是异步的,所以需要单独的模块来处理,这块本文就简单略说,调用堆栈如下:
|___ Server::handle_client_request
|__ __Server::dispatch
|__ ____MDSRank::handle_deferrable_message
|__ ______MDSRank::_dispatch
|__ ________MDSRank::retry_dispatch
|__ __________MDSContext::complete
|__ ____________MDSRank::_advance_queues
|__ ______________MDSRank::ProgressThread::entry
总体而言,MDSRank::ProgressThread::entry到MDSRank::_dispatch是消息入等待队列,出队后由当前线程处理,调用MDSRank::handle_deferrable_message和Server::dispatch完成消息分发,由于是client发的消息所以是CEPH_MSG_CLIENT_REQUEST消息类型,所以由Server::dispatch来进行分发,分发到Server::handle_client_request函数中进行处理。
Server::handle_client_request处理函数中使用dispatch_client_request对不同的opcode的io进行分发。当前的opcode是CEPH_MDS_OP_LOOKUP,所以调用handle_client_getattr函数来处理,lookup和getattr都是调用的handle_client_getattr函数来处理,不同的是lookup调用的时候第二个入参is_lookup标志位传的是true。
Lookup的mds端主要处理逻辑便是在void Server::handle_client_getattr(MDRequestRef& mdr, bool is_lookup)函数,处理逻辑如下:
void Server::handle_client_getattr(MDRequestRef& mdr, bool is_lookup) { //req为请求消息结构体 const MClientRequest::const_ref &req = mdr->client_request; //lov 锁变量 MutationImpl::LockOpVec lov; //确保path非空 if (req->get_filepath().depth() == 0 && is_lookup) { // refpath can't be empty for lookup but it can for // getattr (we do getattr with empty refpath for mount of '/') respond_to_request(mdr, -EINVAL); return; } ... // 核心函数后详细讲!查找到文件1的dentry和inode对象,lov添加1的dentry的读锁和1的inode的snap 读锁 CInode *ref = rdlock_path_pin_ref(mdr, 0, rdlocks, want_auth, false, NULL, !is_lookup); //若文件1的inode未找到,直接返回,在rdlock_path_pin_ref中会完成向客户端的response if (!ref) return; //inode找到的情况,由于客户端的mask的cap申请,需要对1的inode加authlock加读锁 if ((mask & CEPH_CAP_AUTH_SHARED) && !(issued & CEPH_CAP_AUTH_EXCL)) lov.add_rdlock(&ref->authlock); ... // 获取到需要加锁的请求列表lov后,实际的加锁函数 if (!mds->locker->acquire_locks(mdr, lov)) return; // 查看当前mdr是否有权限获取该inode if (!check_access(mdr, ref, MAY_READ)) return; ... // 返回文件1的inode mdr->tracei = ref; if (is_lookup) // 返回文件1的dentry,lookup才需要 mdr->tracedn = mdr->dn[0].back(); respond_to_request(mdr, 0); }
函数主要流程是通过rdlock_path_pin_ref找到/testdir/1的inode和dentry,并加对应读锁进行读取,返回给客户端。
这里可以看到getattr和lookup的主要区别之一就是回客户端消息前getattr无需返回dentry信息,而lookup需要,由于不涉及到修改,所以不用记录mdlog后先early_reply,直接使用respond_to_request回复客户端即可。
核心函数rdlock_path_pin_ref的逻辑如下:
CInode* Server::rdlock_path_pin_ref(MDRequestRef& mdr, int n, MutationImpl::LockOpVec& lov, bool want_auth, bool no_want_auth, file_layout_t **layout, bool no_lookup) { // n为0,是使用的mdr->get_filepath(),获取到在客户端填入的filepath参数(refpath>ino父目录inode号(假设为0x02),refpath->path为1的文件名,此时为字符串”1”) const filepath& refpath = n ? mdr->get_filepath2() : mdr->get_filepath(); ... // 遍历mdcache获得inode和dentry,一次不一定找得到 int r = mdcache->path_traverse(mdr, cf, refpath, &mdr->dn[n], &mdr->in[n], MDS_TRAVERSE_FORWARD); if (r > 0) return NULL; // delayed if (r < 0) { // error if (r == -ENOENT && n == 0 && !mdr->dn[n].empty()) { if (!no_lookup) { mdr->tracedn = mdr->dn[n].back(); } respond_to_request(mdr, r); } else if (r == -ESTALE) { dout(10) << "FAIL on ESTALE but attempting recovery" << dendl; // C_MDS_TryFindInode回调,若仍然返回-ESTALE则直接向客户端返回错误码; // 若find_ino_peers成功,则重新分发请求至handle_client_getattr处理 MDSInternalContextBase *c = new C_MDS_TryFindInode(this, mdr); // 循环所有MDS RANK,_do_find_ino_peer mdcache->find_ino_peers(refpath.get_ino(), c); } else { dout(10) << "FAIL on error " << r << dendl; respond_to_request(mdr, r); } return 0; } CInode *ref = mdr->in[n]; dout(10) << "ref is " << *ref << dendl; // fw to inode auth? if (mdr->snapid != CEPH_NOSNAP && !no_want_auth) want_auth = true; if (want_auth) { // lookup请求的want_auth为false ... // 找到了1的dentry后增加一个读锁的加锁请求 for (int i=0; i<(int)mdr->dn[n].size(); i++) lov.add_rdlock(&mdr->dn[n][i]->lock); // getattr的layout为NULL,给1的inode加snap 读锁 if (layout) mds->locker->include_snap_rdlocks_wlayout(ref, lov, layout); else mds->locker->include_snap_rdlocks(ref, lov); // set and pin ref,这个mds增加pin mdr->pin(ref); return ref; }
综上,该函数主要通过客户端传来的父目录inode号和文件名,使用mdcache->path_traverse在mdcache进行查找,获得1的dentry(出参mdr->dn)和1的inode(出参mdr->in),最后新增1的dentry加读锁和1的inode加snap读锁的请求加入到请求列表lov当中。
mdcache->path_traverse的函数流程主要是通过遍历给的入参filepath进行加锁,但实际上lookup的命令传入的都是父目录的inode号和目录下的文件名dentry(这里为字符串‘1’),所以只有一层,也就是一个文件名‘1’和depth为1,许是其他ops会有多层次的path传入遍历,depth不为1的情况下该函数会层层遍历(如果多活的话,涉及到多个mds之间的消息通信),获取每一级的dentry压栈pdnvec->push_back(dn),最后都加上读锁,然后获取到目标的inode返回,但lookup只有一层。流程细节如下:
int MDCache::path_traverse(MDRequestRef& mdr, MDSContextFactory& cf, // who const filepath& path, // what vector<CDentry*> *pdnvec, // result CInode **pin, int onfail) { bool discover = (onfail == MDS_TRAVERSE_DISCOVER); // false bool null_okay = (onfail == MDS_TRAVERSE_DISCOVERXLOCK); //false bool forward = (onfail == MDS_TRAVERSE_FORWARD); //true ... //通过父目录的inode号获取inode对象索引. CInode *cur = get_inode(path.get_ino()); if (cur == NULL) { if (MDS_INO_IS_MDSDIR(path.get_ino())) open_foreign_mdsdir(path.get_ino(), _get_waiter(mdr, req, fin)); else { /*对于一般的目录,若本地cache中找不到其inode,会返回-ESTALE, 这样在上层函数`rdlock_path_pin_ref`中就会向其它MDS进行inode查询*/ return -ESTALE; } return 1; } if (cur->state_test(CInode::STATE_PURGING)) return -ESTALE; // 清空pdnvec,设置返回的inode,接下来要组装祖先dentry列表(lookup只有一个),通过pdnvec返回。 if (pdnvec) pdnvec->clear(); if (pin) *pin = cur; //开始path逐级遍历,但我们这是lookup所以就一个... unsigned depth = 0; while (depth < path.depth()) { ... // open dir,通过path的名字进行hash得到分片号fg,通过fg在父目录inode中知道对应的分片对象CDir,找到了就好办了,找不到curdir便为空。 frag_t fg = cur->pick_dirfrag(path[depth]); CDir *curdir = cur->get_dirfrag(fg); if (!curdir) { if (cur->is_auth()) { // parent dir frozen_dir? ... // 没找到的情况用get_or_open_dirfrag新建一个 curdir = cur->get_or_open_dirfrag(this, fg); } else { // discover? // dir不属于本rank,向inode的auth_rank发起discover请求 discover_path(cur, snapid, path.postfixpath(depth), cf.build(), null_okay); // 触发异步重试,然后回到这里继续后续流程 return 1; } } // 至此,CDir不管是不是找到了还是只是新建的,肯定要是存在的。 assert(curdir); ... // 获取dentry。如果上面是真在mdcache中找到了对应分片的,那么获取dentry问题不大dentry和dentry的链接linkage_t都获取到了;如果是get_or_open_dirfrag新建了一个,这里肯定都是找不到,为NULL CDentry *dn = curdir->lookup(path[depth], snapid); CDentry::linkage_t *dnl = dn ? dn->get_projected_linkage() : 0; ... //dn不为空的情况,等待别的mds释放dn独占锁并重试 ... // dn还未与inode关联的情况 if (dnl && dnl->is_null() && null_okay) ) { ... } // 可能是别的客户端已经锁定正在操作,则等待后重试 if (dnl && dn->lock.is_xlocked() && dn->lock.get_xlock_by() != mdr && !dn->lock.can_read(client) && (dnl->is_null() || fodout(10) << "traverse: xlocked dentry at " << *dn << dendl; dn->lock.add_waiter(SimpleLock::WAIT_RD, cf.build()); if (mds->logger) mds->logger->inc(l_mds_traverse_lock); mds->mdlog->flush(); return 1; rward)) { } } // dn已经与inode关联的情况 if (dnl && !dnl->is_null()) { CInode *in = dnl->get_inode(); if (!in) { // linkage中只有remote_ino没有inode的情况 assert(dnl->is_remote()); in = get_inode(dnl->get_remote_ino()); //先从本地缓存加载remote_ino对应的inode,若没有,走open_ino流程加载inode ... } // 至此找到dn对应的inode cur = in; ... touch_inode(cur); // dentry找到了压栈,inode找到了赋值完成。 if (pdnvec) pdnvec->push_back(dn); if (pin) *pin = cur; depth++; continue; } // dentry都不存在的情况 if (curdir->is_auth()) { // dir属于本rank的情况 if (curdir->is_complete() || (snapid == CEPH_NOSNAP && curdir->has_bloom() && !curdir->is_in_bloom(path[depth]))) { // dir处于complete状态,但使用bloom确认dn不存在,报错 ... return -ENOENT; } else { ... // 接前文说道,如果这个分片没找到dentry,那就说明分片dir处于非complete状态,没有完全缓存,需要重新从metapool里头fetch加载,然后重试,这里返1,重试由C_MDS_RetryRequest此函数外层完成。 touch_inode(cur); curdir->fetch(cf.build(), path[depth]); if (mds->logger) mds->logger->inc(l_mds_traverse_dir_fetch); return 1; } } else { // dir不属于本rank的情况,到其他rank上去拿,单活暂不研究。 mds_authority_t dauth = curdir->authority(); ... } } } ... return 0; }
如果mdcache已经缓存了,那么这里就已经找到了1的dentry对象pdnvec,和inode对象pin返回了,如果没找到说明要么文件1所在的目录分片没有缓存到mdcache中,要么目录分片缓存了,但是没有缓存文件1的dentry。这两种情况都需要curdir->fetch(cf.build(), path[depth])去元数据池中读出数据,再重试读取。
这里就要提到cephfs使用的元数据预读策略,本质是利用了缓存的局部性原理中的空间局部性(被用过的存储器位置附近的数据很可能将被再次被引用),即使用curdir->fetch读取目录分片下的某个文件/目录的inode时,会先将整个分片curdir的数据都读上来,之后MDS去读该目录分片下的其他文件/目录的inode时,就直接去缓存中拿,这样性能就上来了,也叫inode预取。
void CDir::fetch(MDSContext *c, const std::set<dentry_key_t>& keys) { dout(10) << "fetch " << keys.size() << " keys on " << *this << dendl; ceph_assert(is_auth()); ceph_assert(!is_complete()); if (!can_auth_pin()) { dout(7) << "fetch keys waiting for authpinnable" << dendl; add_waiter(WAIT_UNFREEZE, c); return; } if (state_test(CDir::STATE_FETCHING)) { dout(7) << "fetch keys waiting for full fetch" << dendl; add_waiter(WAIT_COMPLETE, c); return; } auth_pin(this); if (cache->mds->logger) cache->mds->logger->inc(l_mds_dir_fetch); _omap_fetch(c, keys); }
如上所示,CDir::fetch函数逻辑简单,先判断是否该目录分片dir当前mds是否能pin,然后设置CDir的状态为STATE_FETCHING,pin了这个目录后,在CDir::_omap_fetch中调用mds的objecter模块与OSD交互,去读取目录分片的内容。"testdir"目录分片的内容读上来后,将内容解析出来,添加到缓存中。重试来读1的dentry和inode信息就可以直接在缓存中得到。
3. 内核客户端处理回复
服务端使用respond_to_request回请求时新建MClientReply对象来发送回复消息给client端,这个对象对应的消息op为CEPH_MSG_CLIENT_REPLY。客户端接收消息分发函数fs\ceph\mds_client.c的dispatch函数会调用handle_reply处理回复消息。
static void handle_reply(struct ceph_mds_session *session, struct ceph_msg *msg) { ... //将服务端返回的trace信息转成rinfo结构,详情见parse_reply_info的arse_reply_info_trace rinfo = &req->r_reply_info; err = parse_reply_info(msg, rinfo, session->s_con.peer_features); ... //内存中填充inode和dentry的信息。 err = ceph_fill_trace(mdsc->fsc->sb, req); ... }
摘抄自:cephfs内核客户端到MDS的Lookup流程分析 - https://segmentfault.com/a/1190000041597873
cephfs: 用户态客户端lookup - https://zhuanlan.zhihu.com/p/88753967