在文件系统中读写文件时,一般需要先得到操作对象的索引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);
...
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。