【ceph】cephfs内核客户端到MDS的Lookup流程分析--未消化

在文件系统中读写文件时,一般需要先得到操作对象的索引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

posted on 2022-10-04 01:21  bdy  阅读(67)  评论(0编辑  收藏  举报

导航