ceph bluefs的写操作是由RocksDB的Write之类的操作而触发的,其最终经过层层调用,最后会调用bluefs提供的一些列精简过的接口。
rocksdb::Status BlueRocksEnv::NewWritableFile
BlueFS::FileWriter *h;
fs->open_for_write(dir, file, &h, false);
map::iterator p = dir_map.find(dirname);
dir = p->second;
map::iterator q = dir->file_map.find(filename);
if (q == dir->file_map.end()) //如果file_map中没有该文件,就创建一个
file = new File;
file->fnode.ino = ++ino_last;
file_map[ino_last] = file;
dir->file_map[filename] = file;
++file->refs;
create = true;
else
file = q->second;
file->fnode.mtime = ceph_clock_now();
file->fnode.prefer_bdev = BlueFS::BDEV_DB;
if (boost::algorithm::ends_with(dirname, ".slow")) //根据目录名选择数据存放的块设备
file->fnode.prefer_bdev = BlueFS::BDEV_SLOW;
else if(boost::algorithm::ends_with(dirname, ".wal"))
file->fnode.prefer_bdev = BlueFS::BDEV_WAL;
log_t.op_file_update(file->fnode);
if (create) //如果是创建,就增加一个dir_link操作日志
log_t.op_dir_link(dirname, filename, file->fnode.ino);
*h = _create_writer(file); //创建句柄
if (boost::algorithm::ends_with(filename, ".log")) //写操作的类型
(*h)->writer_type = BlueFS::WRITER_WAL;
else if (boost::algorithm::ends_with(filename, ".sst"))
(*h)->writer_type = BlueFS::WRITER_SST;
result->reset(new BlueRocksWritableFile(fs, h)); //返回一个BlueRocksWritableFile实例,其包含文件句柄和bluefs。RocksDB会使用BlueRocksWritableFile实例来进行写磁盘操作
可以看到创建文件的顺序为
(1)查找对应dir的file_map中是否包含该文件,如果有直接使用,否则创建文件。
(2)增加文件更新日志,如果为创建文件还要增加dir link日志
(3)利用获得的文件来创建写文件句柄
(4)更新写操作的类型(WRITER_WAL或者WRITER_SST)
(5)利用文件句柄和bluefs创建一个BlueRocksWritableFile实例,并返回,rocksdb就会有BlueRocksWritableFile实例来进行写磁盘操作
rocksdb::Status BlueRocksWritableFile::Append(const rocksdb::Slice& data)
h->append(data.data(), data.size());
buffer_appender.append(buf, len);//对于数据文件
或者
buffer.claim_append(bl);//对于日志文件
bluefs只支持追加写,且第一步要先写到缓存中
rocksdb::Status BlueRocksWritableFile::Sync()
fs->fsync(h);
_fsync(h, l);
RocksDB写完缓存后,会自己调用BlueRocksWritableFile::Sync来将缓存刷到磁盘中
int BlueFS::_fsync(FileWriter *h, std::unique_lock& l)
_flush(h, true);
old_dirty_seq = h->file->dirty_seq;
_flush_bdev_safely(h);
if (old_dirty_seq)
_flush_and_sync_log(l, old_dirty_seq);
_fsync实现的内容如下
(1)调用_flush讲FileWriter中的缓存写到磁盘中,其是利用aio_write实现的
(2)_flush只是提交异步写请求,并没有等待请求返回,因此_flush_bdev_safely负责等待请求返回
(3)如果在_flush中文件更改了,则其元数据也应该更新,此时old_dirty_seq就不为0,调用_flush_and_sync_log讲元数据写到磁盘中,同时_flush_and_sync_log还会回收释放的空间
先看看_flush的实现,如下:
h->buffer_appender.flush();//将buffer_appender中buffer的数据追加到pbl中,pbl就是FileWriter中的buffer
if (pos && pos != buffer.c_str()) {
size_t len = pos - buffer.c_str();
pbl->append(buffer, 0, len);
buffer.set_length(buffer.length() - len);
buffer.set_offset(buffer.offset() + len);
}
uint64_t length = h->buffer.length();
uint64_t offset = h->pos;
_flush_range(h, offset, length);
h->buffer_appender.flush();
if (offset + length <= h->pos) //不需要刷了,因为[offset, offset+length]已经刷到磁盘中了
return 0;
if (offset < h->pos) //当一部分已经刷到磁盘中时,计算剩余部分
length -= h->pos - offset;
offset = h->pos;
uint64_t allocated = h->file->fnode.get_allocated();
bool must_dirty = false;
if (allocated < offset + length) //分配剩余的空间
_allocate(h->file->fnode.prefer_bdev, offset + length - allocated, &h->file->fnode);
must_dirty = true; //因为已经扩大文件所占的磁盘空间了,因此需要更新文件的元数据,这里设置must_dirty为true
if (h->file->fnode.size < offset + length)
h->file->fnode.size = offset + length;
if (h->file->fnode.ino > 1) //不需要更新log,_replay会发现???
must_dirty = true;
if (must_dirty)
h->file->fnode.mtime = ceph_clock_now(); //更新修改时间
if (h->file->dirty_seq == 0)
h->file->dirty_seq = log_seq + 1;
dirty_files[h->file->dirty_seq].push_back(*h->file);
else
if (h->file->dirty_seq != log_seq + 1) //先删除,然后再更新log序号,再根据序号重新插入
auto it = dirty_files[h->file->dirty_seq].iterator_to(*h->file);
dirty_files[h->file->dirty_seq].erase(it);
h->file->dirty_seq = log_seq + 1;
dirty_files[h->file->dirty_seq].push_back(*h->file);
auto p = h->file->fnode.seek(offset, &x_off); //寻找offset所在的pextent,并计算出再当前pextent的偏移x_off
unsigned partial = x_off & ~super.block_mask(); //往低地址减小到对齐block_mask,需要减小的字节数, 第一次为0
if (partial)
//因为bluefs只支持追加操作,所以本次写的开始地址一定是上一次写的结束地址的后一位,所以
//本次头部pad的一定和上次尾部多出来的长度一样!!!,所以如果尾部有多出来的,会被写磁盘两次
bl.claim_append_piecewise(h->tail_block);
x_off -= partial;
offset -= partial;
length += partial;
for (auto p : h->iocv)
p->aio_wait(); //等待上次aio结束
bl.claim_append_piecewise(h->buffer);
h->pos = offset + length; //更新FileWriter中属性
while (length > 0)
uint64_t x_len = std::min(p->length - x_off, length); //计算本次所能刷到磁盘中的数据大小
t.substr_of(bl, bloff, x_len);
unsigned tail = x_len & ~super.block_mask(); //长度减小多少,才能使长度对齐,即尾部多出来多少
if (tail)
size_t zlen = super.block_size - tail;
h->tail_block.substr_of(bl, bl.length() - tail, tail); //将尾部因对齐而多出来的部分赋值给tail_block,可以和下一次刷盘时再次刷新到磁盘
if (h->file->fnode.ino > 1) //后面补0操作
const bufferptr &last = t.back();
bufferptr z = last;
z.set_offset(last.offset() + last.length());
z.set_length(zlen);
z.zero();
t.append(z, 0, zlen);
else
t.append_zero(zlen);
bdev[p->bdev]->aio_write(p->offset + x_off, t, h->iocv[p->bdev], buffered);
for (unsigned i = 0; i < MAX_BDEV; ++i)
if (h->iocv[i]->has_pending_aios())
bdev[i]->aio_submit(h->iocv[i]); //提交前面的aio亲求
(1)调用buffer_appender.flush();将buffer_appender的buffer的数据追加到pbl中,pbl就是h(FileWriter)中的buffer,后面数据就是从h的buffer中获取
(2)获取本次写磁盘操作的数据长度和偏移,然后调用_flush_range
(3)因为bluefs为追加操作,如果offset + length <= h->pos,则说明写磁盘已经完成,否则计算要写的长度和偏移。
(4)如果文件当前的空闲空间不够,则需要申请空间,此时也需要设置must_dirty=true,来说明文件的元数据也需要更新了。元数据需要更新就得将该文件插入dirty_files中
(5)因为偏移x_off可能不是磁盘块大小对齐,所以利用x_off & ~super.block_mask()来计算头部需要pad的字节数partial。因为是追加写,当前数据开始位置偏移一定是紧挨着上次写数据的最后一个位置,所以如果partial不为0,说明上次写数据时结尾位置不是磁盘块大小对齐的,而上次写数据时尾部pad的字节数(称为tail_block)必定等于partial,这里需要将上次写的tail_block增加到本次写数据的头部。所以如果末尾位置不是磁盘块对齐的,则多出来的尾部需要写两次
(6)计算此时写数据的尾部因为对齐而多出来的部分,将其插入到tail_block中,配合(5)使用
(7)调用aio_write和aio_submit将异步写请求下发到磁盘
_claim_completed_aios(h, &completed_ios);
wait_for_aio(h);
for (auto p : h->iocv)
p->aio_wait(); //等待aio完成
completed_ios.clear();
flush_bdev();
这里就是等待aio完成
下面分析_flush_and_sync_log函数
_flush_and_sync_log(l, old_dirty_seq);
vector> to_release(pending_release.size());
to_release.swap(pending_release);
uint64_t seq = log_t.seq = ++log_seq; //日志序号
auto lsi = dirty_files.find(seq); //文件更改了,元数据需要更新
if (lsi != dirty_files.end())
for (auto &f : lsi->second)
log_t.op_file_update(f.fnode); //增加更新日志条目
int64_t runway = log_writer->file->fnode.get_allocated() - log_writer->get_effective_write_pos();
if (runway < (int64_t)cct->_conf->bluefs_min_log_runway) //磁盘文件剩余空间小于bluefs_min_log_runway,要再次分配
_allocate(log_writer->file->fnode.prefer_bdev, cct->_conf->bluefs_max_log_runway, &log_writer->file->fnode);
log_t.op_file_update(log_writer->file->fnode); //因为日志文件变大,所以元数据需要更新
encode(log_t, bl);
_pad_bl(bl); //尾部pad 0
log_writer->append(bl);
log_flushing = true;
_flush(log_writer, true); //和flush文件的流程一样
_flush_bdev_safely(log_writer);//等待本次日志aio完成
log_flushing = false;
if (seq > log_seq_stable)
log_seq_stable = seq;
auto p = dirty_files.begin();
while (p != dirty_files.end())
auto l = p->second.begin();
while (l != p->second.end())
File *file = &*l;
file->dirty_seq = 0; //将序号设置为0
p->second.erase(l++);
dirty_files.erase(p++);
for (unsigned i = 0; i < to_release.size(); ++i)
alloc[i]->release(to_release[i]); //将对应的空闲空间插入到btree中
(1)更新dirty_files中对应文件的元数据,这里是利用log_t.op_file_update(f.fnode)实现的,其会将更新操作encode到log_t的op_bl中。注意如果日志文件需要扩充空间,则也需要更新元数据
(2)调用log_writer的append函数,将编码后的操作追加到缓存buffer中
(3)调用_flush函数将log_writer中的缓存写入到磁盘中,其实现和写数据到磁盘相似
(4)调用_flush_bdev_safely等待aio完成
(5)删除dirty_files中该文件的记录
(6)最后是回收要释放的空间,这是通过StupidAllocatr的release实现的,release会把这部分空间重新插入到对应的btree中