ceph bluefs 写操作 源码解析

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中

你可能感兴趣的:(ceph,ceph,bluefs)