发布于 

Ceph 读写流程:客户端写流程分析

本文对 Ceph 客户端写流程进行了梳理和总结,包括 ll_openll_write 的处理流程。

正文

首先用户需要通过 ll_open 打开文件并拿到 fh,第一步先做权限检查,进入 may_open, 这里会根据用户带回的 flags 确定 wantMAY_WRITE 还是 MAY_READ, 接着通过一次 getattr 拿到文件的 mode, 当然这里 getattr 不是每次都发,只有 client 认为自己的所持有的文件权限不是最新的时候才会发,具体有两种情况:

  1. 开启 acl 并且没有拿过文件的 xattr (acl 设置是外带在 xattr 中的,不在 inode 里)
  2. caps_issued_mask 认为当前 client 的 caps 不足以保证所持有的权限为最新时
Client::_getattr 判断是否需要发送请求
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int Client::_getattr(Inode *in, int mask, const UserPerm& perms, bool force)
{
bool yes = in->caps_issued_mask(mask, true);

if (yes && !force)
return 0;
...
// force 和 mask:
int Client::_getattr_for_perm(Inode *in, const UserPerm& perms)
{
int mask = CEPH_STAT_CAP_MODE;
bool force = false;
if (acl_type != NO_ACL) {
mask |= CEPH_STAT_CAP_XATTR;
force = in->xattr_version == 0;
}
return _getattr(in, mask, perms, force);
}

简单解释一下第二点就是如果 client 没有持有 As (开启 acl 的话还需要 Xs)那么就需要向 mds 发 getattr
拿到 perm 之后做一个 inode_permission 检查一下 mode (如果设了 acl 那就检查 acl)

接着进到 _open, 先把 flags (O_RDONLY, ORDWR, O_CREAT…) 转成 ceph 内部的 cmode (CEPH_FILE_MODE_PIN, CEPH_FILE_MODE_RD, CEPH_FILE_MODE_RDWR, …) 和对应的 want_cap (CEPH_CAP_PIN p, CEPH_CAP_FILE_SHARED Fs, CEPH_CAP_FILE_RD Fr, …)

如果 client 的 caps 满足就直接把 inode 的 ref +1 就可以返回了,否则需要给 mds 发一个 open 消息

Client::_open 判断是否发送 CEPH_MDS_OP_OPEN
1
2
3
4
5
6
if ((flags & O_TRUNC) == 0 && in->caps_issued_mask(want)) {
// update wanted?
check_caps(in, CHECK_CAPS_NODELAY);
} else {
MetaRequest *req = new MetaRequest(CEPH_MDS_OP_OPEN);
...

mds 消息回来之后通过 _create_fh 创建了一个 Fh 加入到 inode 的 fhs 并返回给用户, 同时将这个 fh 加入 client 的 ll_unclosed_fh_set

接着用户拿到 Fh 之后就可以调用 ll_write 并传入刚刚拿到的 Fh,偏移 off, 写入长度 len 和数据 data
首先第一步 _write 先做一些基本检查 (判断一下 offset+len 范围是否合法,pool 有没有 full) ,接着做一下可写性检查(Fh 是不是以可写模式打开的 CEPH_FILE_MODE_WR,会不会超 quota)
另外写之前还需要把 buf 或者 iov 中的数据写到 bl 中 (也就是同时包含 writewritev 的功能),前期的准备就算完了

needwant 的 Caps 传入 Client::get_caps (need 是必须的 Fw 和 As, want 是可选的 Fb 或者 Fl)
Client::get_caps 内的过程比较复杂:

首先通过 Fh 的 open mode 获取 file_wanted (比如以 RD 方式打开的就需要 Fs Fr Fc,和之前 open 时是一样的)
接着通过 Inode::caps_issued 获取当前 inode 所拥有的 caps
如果当前已经拥有 Fw 那么检查一下 max_size 够不够写 wanted_max_size,如果不够就需要通过 check_caps 去向 mds 申请

如果说此时拥有 need 并且没有 want 的 caps 被 revoking 那么直接 get_cap_ref(need) 然后返回,否则就需要 waitfor_caps

caps 问题解决之后就可以开始写了,如果是 O_DIRECT 写的话要把 buffer 和 lazyio 去掉

接着如果是 Direct 写那就直接先 flush_range 然后通过 filer->write_trunc 去写, 否则的话先通过 objectcacher->file_write 写到缓存里再 flush

Client::_write
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 非 Direct 写
if (cct->_conf->client_oc &&
(have & (CEPH_CAP_FILE_BUFFER | CEPH_CAP_FILE_LAZYIO))) {
...
// async, caching, non-blocking.
r = objectcacher->file_write(&in->oset, &in->layout,
in->snaprealm->get_snap_context(),
offset, size, bl, ceph::real_clock::now(),
0);
...

// flush cached write if O_SYNC is set on file fh
// O_DSYNC == O_SYNC on linux < 2.6.33
// O_SYNC = __O_SYNC | O_DSYNC on linux >= 2.6.33
if ((f->flags & O_SYNC) || (f->flags & O_DSYNC)) {
_flush_range(in, offset, size);
}
// Direct 写
} else {
if (f->flags & O_DIRECT)
_flush_range(in, offset, size);
...
filer->write_trunc(in->ino, &in->layout, in->snaprealm->get_snap_context(),
offset, size, bl, ceph::real_clock::now(), 0,
in->truncate_size, in->truncate_seq,
&onfinish);
}

再说一下 filer->write_trunc 写的过程:

首先通过 Striper::file_to_extents 通过 ino 找到实际要写的 objects (对原始请求的拆分), Striper::file_to_extents 的流程见 Ceph 读写流程:file_to_extents 过程分析

接着就是通过 objecter->sg_write_trunc 提交 op 到 OSD 完成写入

写入完成后进入到 success 完成一些收尾工作即可,包括更新文件的 sizemtime,标记 CEPH_CAP_FILE_WR 为 dirty(这样其他客户端要写的话 MDS revoke caps 之前就可以告诉 MDS 这个 Inode 需要同步)以及通过 put_cap_ref 释放刚刚获取的 Fwb 权限等。

以上就是对 Ceph 客户端中写流程的分析和总结,希望能对大家有所帮助!