发布于 

Ceph 读写流程:file_to_extents 过程分析

本文介绍 Ceph 中条带化的相关概念并对 Striper::file_to_extents 的流程进行了分析和总结。

Client::_write 中客户端通过 filer->write_trunc 对文件进行写入,write_trunc 其中分为两步:

  1. 通过 Striper::file_to_extents 对请求进行拆分,也就是将对文件的读写请求转换为对对象的读写请求
  2. 通过 objecter->sg_write_trunc 把数据写到 object 中

本文主要对其中的第一步,也就是 Striper::file_to_extents 的过程进行分析。

Ceph 中的 Stripping

在正式开始先简单介绍一下 Ceph 中条带化(Stripping)读写的相关概念:

为什么要使用 Stripping

最主要的原因就是由于单个存储设备存在吞吐量限制,例如一块机械硬盘的写入能力最大也不过 200MB/s, 在与内存、固态缓存甚至是万兆网络的吞吐量对比下无疑就是整个系统中最大的短板,那么条带化就是用来解决存储设备性能瓶颈,提高存储系统吞吐量的东西。

那么什么是条带化,说的准确一点就是把信息的连续部分存储在多个存储设备上,说人话就是把一个文件切成多份放在不同的磁盘上,每次对文件中一段内容的读写会同时发给多个盘,这样的话我们实际的写入速度就变成了多个磁盘的写入速度总和。

Ceph 中的条带化的概念大致和其他存储没有区别,只不过在 Ceph 中所有的存储最后都会落到 object 也就是对象上,而对象的存储是没有条带化的,因此文件的条带化实际上是在客户端上进行的,也就是我们本文的主要内容 file_to_extents 的过程

以下图为例,对文件数据的写入首先会在 ObjectSet1Object0stripe unit0 中进行, 当第一个条带被写满之后,后续的写会移动到 Object1stripe unit1 上进行,直到 Object3stripe unit3 被写满之后 Ceph 会根据 ObjectSet 中可容纳的对象数量判断是否写满,并将后续的写转回到 Object0 上的 stripe unit4 中进行。而这个过程在一次包含比较多数据的写中当然是可以并行执行的,只要我们提前计算好写入涉及的 objects 即可。

file_to_extents 流程

概念理解以后我们就正式来看 file_to_extents 的过程,首先看一下在 Filer::write_trunc 是如何调用 file_to_extents 的:

1
2
3
// osdc/Filer.cc
// void write_trunc
Striper::file_to_extents(cct, ino, layout, offset, len, truncate_size, extents);

这里传入的 ino offsetlen 比较好理解,就是我们要写入的文件的 Inode 号,然后从哪里开始写,要写多长,注意这里因为只是算映射所以是不需要传 bl 也就是真正要写入的数据的。

接着 truncate_sizetruncate 写有关所以我们先不过多关心,那么 layout 就是用来辅助我们切分请求的,而 extents 就是我们稍后真正要写入的 ObjectExtent 集合了,定义如下:

1
std::vector<ObjectExtent> extents;

ObjectExtent 就是我们对于某一个 object 要写入的条带的集合,定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class ObjectExtent
{
public:
object_t oid; // object id
uint64_t objectno;
uint64_t offset; // in object
uint64_t length; // in object
uint64_t truncate_size; // in object

object_locator_t oloc; // object locator (pool etc)

std::vector<std::pair<uint64_t,uint64_t> > buffer_extents; // off -> len. extents in buffer being mapped (may be fragmented bc of striping!)

ObjectExtent() : objectno(0), offset(0), length(0), truncate_size(0) {}
ObjectExtent(object_t o, uint64_t ono, uint64_t off, uint64_t l, uint64_t ts) :
oid(o), objectno(ono), offset(off), length(l), truncate_size(ts) { }
};

其中我们通过 oidobjectno 找到一个 object,而 oid 就是文件的 inode 号。

offsetlength 则是要写入 object 的这段数据在 object 中的偏移和长度(从上图我们可以看到一段连续的数据按条带写到 object 中实际也是连续的)

buffer_extents 则是要写入 object 的数据在 buffer 中的偏移和长度,这里的 buffer 指的是实际的数据,比如一个刚好写满上图 5 个条带的数据必然会横跨三个条带,这时我们就需要在 buffer_extents 中记录下 buffer 中要写入 object 的两段数据的偏移和各自的长度。

理解了上述部分以后,我们就实际看一下 file_to_extents 是如何填充 extents 的:

1
2
3
4
5
6
7
8
9
10
11
static void file_to_extents(CephContext *cct, inodeno_t ino,
const file_layout_t *layout,
uint64_t offset, uint64_t len,
uint64_t trunc_size,
std::vector<ObjectExtent>& extents) {
// generate prefix/format
char buf[32];
snprintf(buf, sizeof(buf), "%llx.%%08llx", (long long unsigned)ino);

file_to_extents(cct, buf, layout, offset, len, trunc_size, extents);
}

第一步就是将 ino 写入 buf 然后进入第二个版本的重载:

1
2
3
4
5
6
7
8
9
10
11
void Striper::file_to_extents(CephContext *cct, const char *object_format,
const file_layout_t *layout,
uint64_t offset, uint64_t len,
uint64_t trunc_size,
std::vector<ObjectExtent>& extents,
uint64_t buffer_offset)
{
striper::LightweightObjectExtents lightweight_object_extents;
file_to_extents(cct, layout, offset, len, trunc_size, buffer_offset,
&lightweight_object_extents);
...

这里可以看到又多了一个 LightweightObjectExtents,实际上这个和 ObjectExtent 的区别就是没有 object_locator_t oloc,进入第三个版本的重载:

1
2
3
4
5
void Striper::file_to_extents(
CephContext *cct, const file_layout_t *layout, uint64_t offset,
uint64_t len, uint64_t trunc_size, uint64_t buffer_offset,
striper::LightweightObjectExtents* object_extents) {
...

首先读一下 layoutobject_sizestripe_unit 我们刚刚都提到了,stripe_count 实际就是每个 ObjectSet 中包含的 object 数量:

1
2
3
4
// void Striper::file_to_extents
__u32 object_size = layout->object_size;
__u32 su = layout->stripe_unit;
__u32 stripe_count = layout->stripe_count;

如果 stripe_count 为 1 的话切条带就没有意义了,因此我们直接让一个 object 中只包含一个条带即可:

1
2
3
4
if (stripe_count == 1) {
ldout(cct, 20) << " sc is one, reset su to os" << dendl;
su = object_size;
}

接下来就进入到切分的过程,这个过程稍微有点长,其中涉及的变量如下:

Striper::file_to_extents
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
uint64_t stripes_per_object = object_size / su;
uint64_t cur = offset;
uint64_t left = len;

while (left > 0) {
// layout into objects
uint64_t blockno = cur / su; // which block
// which horizontal stripe (Y)
uint64_t stripeno = blockno / stripe_count;
// which object in the object set (X)
uint64_t stripepos = blockno % stripe_count;
// which object set
uint64_t objectsetno = stripeno / stripes_per_object;
// object id
uint64_t objectno = objectsetno * stripe_count + stripepos;

// map range into object
uint64_t block_start = (stripeno % stripes_per_object) * su;
uint64_t block_off = cur % su;
uint64_t max = su - block_off;

uint64_t x_offset = block_start + block_off;
uint64_t x_len;

插入 object_extents 的过程分两种情况,其中 if 分支是插入一个新 LightweightObjectExtentelse 分支则是写满一组条带之后更新 LightweightObjectExtent 的过程

首先看 if 分支,满足下列条件其中之一的进入此分支:

  1. object_extents 为空
  2. 当前 object_extent 不存在于 object_extents
  3. 写入的数据在 object 中不是连续的

进入分支以后直接插入一个 LightweightObjectExtentobject_extents 即可:

1
2
3
4
5
6
7
8
9
10
11
12
striper::LightweightObjectExtent* ex = nullptr;
auto it = std::upper_bound(object_extents->begin(), object_extents->end(),
objectno, OrderByObject());
striper::LightweightObjectExtents::reverse_iterator rev_it(it);
if (rev_it == object_extents->rend() ||
rev_it->object_no != objectno ||
rev_it->offset + rev_it->length != x_offset) {
// expect up to "stripe-width - 1" vector shifts in the worst-case
ex = &(*object_extents->emplace(
it, objectno, x_offset, x_len,
object_truncate_size(cct, layout, objectno, trunc_size)));
ldout(cct, 20) << " added new " << *ex << dendl;

接着 else 分支则是对 LightweightObjectExtent 中的 length 进行更新:

1
2
3
4
5
6
7
} else {
ex = &(*rev_it);
ceph_assert(ex->offset + ex->length == x_offset);

ldout(cct, 20) << " adding in to " << *ex << dendl;
ex->length += x_len;
}

以上两种情况我们都需要对 buffer_extents 进行更新,插入这段数据在 buffer 中的偏移和长度,并更新下一轮我们待切分的数据长度 left 和已经切分的偏移位置 cur

1
2
3
ex->buffer_extents.emplace_back(cur - offset + buffer_offset, x_len);
left -= x_len;
cur += x_len;

全部切分完成并加入 object_extents 之后我们回到第二个版本的 Striper::file_to_extents 中,在这里遍历 lightweight_object_extents 并填入 extents,这里基本上就是原样填充,只多了一步就是算了一下 oloc

将 LightweightObjectExtent 转换为 ObjectExtent
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// convert lightweight object extents to heavyweight version
extents.reserve(lightweight_object_extents.size());
for (auto& lightweight_object_extent : lightweight_object_extents) {
auto& object_extent = extents.emplace_back(
object_t(format_oid(object_format, lightweight_object_extent.object_no)),
lightweight_object_extent.object_no,
lightweight_object_extent.offset, lightweight_object_extent.length,
lightweight_object_extent.truncate_size);

object_extent.oloc = OSDMap::file_to_object_locator(*layout);
object_extent.buffer_extents.reserve(
lightweight_object_extent.buffer_extents.size());
object_extent.buffer_extents.insert(
object_extent.buffer_extents.end(),
lightweight_object_extent.buffer_extents.begin(),
lightweight_object_extent.buffer_extents.end());
}

以上就是关于 Ceph 条带化的介绍和 Striper::file_to_extents 的全部过程。

References