【Ceph】Ceph中Bufferlist的设计与使用

原文:

1、http://www.voidcn.com/article/p-kmnnilin-kd.html

2、https://zhuanlan.zhihu.com/p/96659509

3、https://www.jianshu.com/p/01e1f4e398df

如果非要在整个Ceph中,找出一个类最重要,我觉得非Bufferlist莫属了,原因很简单,因为Bufferlist负责管理Ceph中所有的内存。整个Ceph中所有涉及到内存的操作,无论是msg分配内存接收消息,还是OSD构造各类数据结构的持久化表示(encode/decode),再到实际磁盘操作,都将bufferlist作为基础。

ceph::buffer是ceph非常底层的实现,负责管理ceph的内存。ceph::buffer的设计较为复杂,但本身没有任何内容,主要包含buffer::list、buffer::ptr、buffer::raw、buffer::hash。这三个类都定义在src/include/buffer.h和src/common/buffer.cc中。

buffer::raw:负责维护物理内存的引用计数nref和释放操作。
buffer::ptr:指向buffer::raw的指针。
buffer::list:表示一个ptr的列表(std::list<bufferptr>),相当于将N个ptr构成一个更大的虚拟的连续内存。

buffer::hash:一个或多个bufferlist的有效哈希。

buffer这三个类的相互关系可以用下面这个图来表示:

图中蓝色的表示bufferlist,橙色表示bufferptr,绿色表示bufferraw。

在这个图中,实际占用的系统内存一共就三段,分别是raw0,raw1和raw2代表的三段内存。其中:
raw0被ptr0,ptr1,ptr2使用
raw1被ptr3,ptr4,ptr6使用
raw2被ptr5,ptr7使用
而list0是由ptr0-5组成的,list1是由ptr6和ptr7组成的。

从这张图上我们就可以看出bufferlist的设计思路了: 对于bufferlist来说,仅关心一个个ptr。bufferlist将ptr连在一起,当做是一段连续的内存使用。因此,可以通过bufferlist::iterator一个字节一个字节的迭代整个bufferlist中的所有内容,而不需要关心到底有几个ptr,更不用关心这些ptr到底和系统内存是怎么对应的;也可以通过bufferlist::write_file方法直接将bufferlist中的内容出到一个文件中;或者通过bufferlist::write_fd方法将bufferlist中的内容写入到某个fd中。

与bufferlist相对的是负责管理系统内存的bufferraw。bufferraw只关心一件事:维护其所管理的系统内存的引用计数,并且在引用计数减为0时——即没有ptr再使用这块内存时,释放这块内存。

连接bufferlist和bufferraw的是bufferptr。bufferptr关心的是如何使用内存。每一个bufferptr一定有一个bufferraw为其提供系统内存,然后ptr决定使用这块内存的哪一部分。bufferlist只用通过ptr才能对应到系统内存中,而bufferptr而可以独立存在,只是大部分ptr还是为bufferlist服务的,独立的ptr使用的场景并不是很多。

通过引入ptr这样一个中间层次,bufferlist使用内存的方式可以非常灵活,这里可以举两个场景:

1. 快速encode/decode
在Ceph中经常需要将一个bufferlist编码(encode)到另一个bufferlist中,例如在msg发送消息的时候,通常msg拿到的osd等逻辑层传递给它的bufferlist,然后msg还需要给这个bufferlist加上消息头和消息尾,而消息头和消息尾也是用bufferlist表示的。这时候,msg通常会构造一个空的bufferlist,然后将消息头、消息尾、内容都encode到这个空的bufferlist。而bufferlist之间的encode实际只需要做ptr的copy,而不涉及到系统内存的申请和Copy,效率较高。

2. 一次分配,多次使用
我们都知道,调用malloc之类的函数申请内存是非常重量级的操作。利用ptr这个中间层可以缓解这个问题,即我们可以一次性申请一块较大的内存,也就是一个较大的bufferraw,然后每次需要内存的时候,构造一个bufferptr,指向这个bufferraw的不同部分。这样就不再需要向系统申请内存了。最后将这些ptr都加入到一个bufferlist中,就可以形成一个虚拟的连续内存。

关于作者1:袁冬博士,UnitedStack产品副总裁,负责UnitedStack产品、售前和对外合作工作;云计算专家,在云计算、虚拟化、分布式系统和企业级应用等方面有丰富的经验;对分布式存储、非结构数据存储和存储虚拟化有深刻地理解,在云存储和企业级存储领域有丰富的研发与实践经验;Ceph等开源存储项目的核心代码贡献者。

相关文章:https://www.jianshu.com/p/6c8b361cc665

源码分析 (http://bean-li.github.io/bufferlist-in-ceph/

buffer::raw

介绍buffer list之前,我们必须先介绍buffer::raw和buffer::ptr。相对于buffer list,这两个数据结构相对比较容易理解。

class buffer::raw {
    public:
        char *data;
        unsigned len;
        atomic_t nref;

        mutable simple_spinlock_t crc_spinlock;
        map<pair<size_t, size_t>, pair<uint32_t, uint32_t> > crc_map;
        
        ...
}

注意,这个数据结构,data是个指针,指向真正的数据,而len记录了该buffer::raw数据区数据的长度,nref表示引用计数。

注意,data指针指向的数据可能有不同的来源,最容易想到的当然是malloc,其次我们以可以使用mmap通过创建匿名内存映射来分配空间,甚至我们可以通过pipe管道+splice实现零拷贝获取空间。有些时候,分配的空间时,会提出对齐的要求,比如按页对齐。

这是因为这些来源不同,要求不同,buffer::raw也就有了一些变体:

buffer:raw_malloc

这个变体数据来源源自malloc,因此,创建的时候,需要通过malloc分配长度为len的空间,,而不意外,析构的时候,会掉用free释放空间。

    class buffer::raw_malloc : public buffer::raw {
        public:
            explicit raw_malloc(unsigned l) : raw(l) {
                if (len) {
                    data = (char *)malloc(len);
                    if (!data)
                        throw bad_alloc();
                } else {
                    data = 0;
                }
                inc_total_alloc(len);
                inc_history_alloc(len);
                bdout << "raw_malloc " << this << " alloc " << (void *)data << " " << l << " " << buffer::get_total_alloc() << bendl;
            }
            raw_malloc(unsigned l, char *b) : raw(b, l) {
                inc_total_alloc(len);
                bdout << "raw_malloc " << this << " alloc " << (void *)data << " " << l << " " << buffer::get_total_alloc() << bendl;
            }
            ~raw_malloc() {
                free(data);
                dec_total_alloc(len);
                bdout << "raw_malloc " << this << " free " << (void *)data << " " << buffer::get_total_alloc() << bendl;
            }
            raw* clone_empty() {
                return new raw_malloc(len);
            }
    };

buffer::raw_mmap_pages

顾名思义,也能够猜到,这个数据的来源是通过mmap分配的匿名内存映射。因此析构的时候,毫不意外,掉用munmap解除映射,归还空间给系统。

    class buffer::raw_mmap_pages : public buffer::raw {
        public:
            explicit raw_mmap_pages(unsigned l) : raw(l) {
                data = (char*)::mmap(NULL, len, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANON, -1, 0);
                if (!data)
                    throw bad_alloc();
                inc_total_alloc(len);
                inc_history_alloc(len);
                bdout << "raw_mmap " << this << " alloc " << (void *)data << " " << l << " " << buffer::get_total_alloc() << bendl;
            }
            ~raw_mmap_pages() {
                ::munmap(data, len);
                dec_total_alloc(len);
                bdout << "raw_mmap " << this << " free " << (void *)data << " " << buffer::get_total_alloc() << bendl;
            }
            raw* clone_empty() {
                return new raw_mmap_pages(len);
            }
    };

buffer::raw_posix_aligned

看名字也看出来了,对空间有对齐的要求。Linux下posix_memalign函数用来分配有对齐要求的内存空间。这种分配方式分配的空间,也是用free函数来释放,将空间归还给系统。

class buffer::raw_posix_aligned : public buffer::raw {
        unsigned align;
        public:
        raw_posix_aligned(unsigned l, unsigned _align) : raw(l) {
            align = _align;
            assert((align >= sizeof(void *)) && (align & (align - 1)) == 0);
#ifdef DARWIN
            data = (char *) valloc (len);
#else
            data = 0;
            int r = ::posix_memalign((void**)(void*)&data, align, len);
            if (r)
                throw bad_alloc();
#endif /* DARWIN */
            if (!data)
                throw bad_alloc();
            inc_total_alloc(len);
            inc_history_alloc(len);
            bdout << "raw_posix_aligned " << this << " alloc " << (void *)data << " l=" << l << ", align=" << align << " total_alloc=" << buffer::get_total_alloc() << bendl;
        }
        ~raw_posix_aligned() {
            ::free((void*)data);
            dec_total_alloc(len);
            bdout << "raw_posix_aligned " << this << " free " << (void *)data << " " << buffer::get_total_alloc() << bendl;
        }
        raw* clone_empty() {
            return new raw_posix_aligned(len, align);
        }
    };

后面还有基于pipe和splice的零拷贝方式,我们不赘述。从上面的函数不难看出,buffer::raw系列,就像他的名字一样,真的是很原生,并没有太多的弯弯绕,就是利用系统提供的API来达到分配空间的目的。

buffer::ptr

buffer::ptr是在buffer::raw系列的基础上,这个类也别名bufferptr。

src/include/buffer_fwd.h


#ifndef BUFFER_FWD_H
#define BUFFER_FWD_H

namespace ceph {
  namespace buffer {
    class ptr;
    class list;
    class hash;
  }

  using bufferptr = buffer::ptr;
  using bufferlist = buffer::list;
  using bufferhash = buffer::hash;
}

#endif

这个类的成员变量如下,这个类是raw这个类的包装升级版本,它的_raw就是指向buffer::raw类型的变量。

        class CEPH_BUFFER_API ptr {
            raw *_raw;
            unsigned _off, _len;
          ......    
      }

很多操作都是很容易想到的:


   buffer::ptr& buffer::ptr::operator= (const ptr& p)
    {
        if (p._raw) {
            p._raw->nref.inc();
            bdout << "ptr " << this << " get " << _raw << bendl;
        }
        buffer::raw *raw = p._raw; 
        release();
        if (raw) {
            _raw = raw;
            _off = p._off;
            _len = p._len;
        } else {
            _off = _len = 0;
        }
        return *this;
    }
    
    buffer::raw *buffer::ptr::clone()
    {
        return _raw->clone();
    }
    
    void buffer::ptr::swap(ptr& other)
    {
        raw *r = _raw;
        unsigned o = _off;
        unsigned l = _len;
        _raw = other._raw;
        _off = other._off;
        _len = other._len;
        other._raw = r;
        other._off = o;
        other._len = l;
    }
    
   const char& buffer::ptr::operator[](unsigned n) const
    {
        assert(_raw);
        assert(n < _len);
        return _raw->get_data()[_off + n];
    }
    char& buffer::ptr::operator[](unsigned n)
    {
        assert(_raw);
        assert(n < _len);
        return _raw->get_data()[_off + n];
    }
    
    int buffer::ptr::cmp(const ptr& o) const
    {
        int l = _len < o._len ? _len : o._len;
        if (l) {
            int r = memcmp(c_str(), o.c_str(), l);
            if (r)
                return r;
        }
        if (_len < o._len)
            return -1;
        if (_len > o._len)
            return 1;
        return 0;
    }

bufferlist

bufferlist才是我们的目的地,前两个类其实是比较容易理解的,但是bufferlist相对复杂一点。

bufferlist是buffer::list的别名:

#ifndef BUFFER_FWD_H
#define BUFFER_FWD_H

namespace ceph {
  namespace buffer {
    class ptr;
    class list;
    class hash;
  }

  using bufferptr = buffer::ptr;
  using bufferlist = buffer::list;
  using bufferhash = buffer::hash;
}

#endif


class CEPH_BUFFER_API list {
            // my private bits
            std::list<ptr> _buffers;
            unsigned _len;
            unsigned _memcopy_count; //the total of memcopy using rebuild().
            ptr append_buffer;  // where i put small appends

多个bufferptr形成一个list,这就是bufferlist。成员变量并无太多难以理解的地方,比较绕的是bufferlist的迭代器 ,理解迭代器,就不难理解bufferlist各个操作函数。

要理解bufferlist 迭代器,,首先需要理解迭代器成员变量的含义。

_buffers是一个ptr的链表,_len是整个_buffers中所有的ptr的数据的总长度,_memcopy_count用于统计memcopy的字节数,append_buffer是用于优化append操作的缓冲区,可以看出bufferlist将数据以不连续链表的方式存储。

                        bl_t* bl;
                        list_t* ls;  // meh.. just here to avoid an extra pointer dereference..
                        unsigned off; // in bl
                        list_iter_t p;
                        unsigned p_off;   // in *p
  • bl:指针,指向bufferlist
  • ls:指针,指向bufferlist的成员 _buffers
  • p: 类型是std::list::iterator,用来迭代遍历bufferlist中的bufferptr
  • p_off : 当前位置在对应的bufferptr的偏移量
  • off: 如果将整个bufferlist看成一个buffer::raw,当前位置在整个bufferlist的偏移量

这个递进关系比较明显,从宏观的bufferlist,递进到内部的某个bufferptr,再递进到bufferptr内部raw数据区的某个偏移位置。 此外还包含了当前位置在整个bufferlist的偏移量off。

注意p_off和off容易产生误解,请阅读seek函数仔细揣摩

seek(unsigned o),顾名思义就是将位置移到o处,当然o指的是整个bufferlist的o处。ceph实现了一个更通用的advance,接受一个int型的入参。

如果o>0,表示向后移动,如果o小于0,表示想前移动。移动的过程中可能越过当前的bufferptr之指向的数据区。

    template<bool is_const>
        void buffer::list::iterator_impl<is_const>::advance(int o)
        {
            //cout << this << " advance " << o << " from " << off << " (p_off " << p_off << " in " << p->length() << ")" << std::endl;
            if (o > 0) {
                p_off += o;
                while (p_off > 0) {
                    if (p == ls->end())
                        throw end_of_buffer();
                    if (p_off >= p->length()) {
                        // skip this buffer
                        p_off -= p->length();
                        p++;
                    } else {
                        // somewhere in this buffer!
                        break;
                    }
                }
                off += o;
                return;
            }
            while (o < 0) {
                if (p_off) {
                    unsigned d = -o;
                    if (d > p_off)
                        d = p_off;
                    p_off -= d;
                    off -= d;
                    o += d;
                } else if (off > 0) {
                    assert(p != ls->begin());
                    p--;
                    p_off = p->length();
                } else {
                    throw end_of_buffer();
                }
            }
        }

    template<bool is_const>
        void buffer::list::iterator_impl<is_const>::seek(unsigned o)
        {
            p = ls->begin();
            off = p_off = 0;
            advance(o);
        }

除此以外,获取当前位置的ptr也很有意思,理解该函数也有帮助理解迭代器五个成员的含义。

template<bool is_const>
    buffer::ptr buffer::list::iterator_impl<is_const>::get_current_ptr() const
    {
        if (p == ls->end())
            throw end_of_buffer();
        return ptr(*p, p_off, p->length() - p_off);
    }

相当于多个bufferptr对应的buffer::raw组成了一个可能不连续的buffer列表,因此使用起来可能不方便,ceph出于这种考虑,提供了rebuild的函数。该函数的作用是,干脆创建一个buffer::raw,来提供同样的空间和内容。

    void buffer::list::rebuild()
    {
        if (_len == 0) {
            _buffers.clear();
            return;
        }
        ptr nb;
        if ((_len & ~CEPH_PAGE_MASK) == 0)
            nb = buffer::create_page_aligned(_len);
        else
            nb = buffer::create(_len);
        rebuild(nb);
    }

    void buffer::list::rebuild(ptr& nb)
    {
        unsigned pos = 0;
        for (std::list<ptr>::iterator it = _buffers.begin();
                it != _buffers.end();
                ++it) {
            nb.copy_in(pos, it->length(), it->c_str(), false);
            pos += it->length();
        }
        _memcopy_count += pos;
        _buffers.clear();
        if (nb.length())
            _buffers.push_back(nb);
        invalidate_crc();
        last_p = begin();
    }

从下面测试代码中不难看出rebuild的含义,就是划零为整,重建一个buffer::raw来提供空间

  {
    bufferlist bl;
    const std::string str(CEPH_PAGE_SIZE, 'X');
    bl.append(str.c_str(), str.size());
    bl.append(str.c_str(), str.size());
    EXPECT_EQ((unsigned)2, bl.buffers().size());
    bl.rebuild();
    EXPECT_EQ((unsigned)1, bl.buffers().size());
  }

理解了上述的内容,bufferlist剩余的上千行代码,基本也就变成了流水账了,不难理解了,在此就不再赘述了。

更多文章:

《ceph源码阅读 buffer》https://zhuanlan.zhihu.com/p/96659509

《ceph:bufferlist实现》https://www.it610.com/article/1231080926906257408.htm

ceph 博客:http://bean-li.github.io/

bufferlist是buffer::list的别名,其由来在 http://bean-li.github.io/bufferlist-in-ceph/ 中有非常详细的介绍

其p、p_off、off字段的含义可以通过advance(int o)函数来理解

简单来说,p就说指向_buffer的迭代器,p_off则是在当前buffer::ptr的_raw中的偏移,而off则是把_buffer中的_raw视为一个整体时的当前偏移。如下图中,o为1000时,则p为ptr3,p_off为200(由1000-500-300得来),而off则为1000.

猜你喜欢

转载自blog.csdn.net/bandaoyu/article/details/113250328