Rocksdb-Arena源码分析

480 阅读8分钟

内存管理工具Arena

在Rocksdb中提供了两大类内存管理工具,分别是Allocator和MemoryAllocator,在这里不详细分析MemAllocator的实现,因为它仅仅是jemalloc 的wrapper。

Allocator首先是一个虚基类,定义了三类接口

// 这里支持对齐和不对齐两种方式
// 因为我们申请了一个又一个的block然后自己管理内存
// 因此必须自己处理内存对齐的问题
// 分配bytes大小的字节
virtual char* Allocate(size_t bytes) = 0;

// 按照bytes大小对齐过后的字节
virtual char* AllocateAligned(size_t bytes, size_t huge_page_size = 0,
                                Logger* logger = nullptr) = 0;

// 返回当前一个块的大小 
virtual size_t BlockSize() const = 0;

Allocator下有两个实现,一个是Arena另一个是Concurrent_Arena,在这里先讨论非并发版本,因为并发版本是Arena的wrapper

首先我们需要一定的预备知识:hugePage

HugePage 巨页技术

众所周知x86架构下我们默认使用4KB的内存页,通常内存页过大会直接导致对内存利用率的下降,因为我们每次分配内存的时候至少分配一个内存页。而内存页过小会导致页表膨胀,并且TLB的查找速度下降。4KB是一个有历史原因的合理数字,使用4KB未见得就比8KB或者16KB更好

它的优势:

  1. 对于存储同样entries的TLB,如果使用了巨页那就意味着更低的TLB miss。
  2. 对于更大的内存页,它可能会减少页表的级别,那就意味着更少的访存次数。
  3. 减少页表的内存开销。

它的缺点:

  1. 内存操作基本都要求页为单位,例如加载一个动态链接库,可能浪费大量的巨页空间。
  2. OS运行一段时间之后可能不太能分配出大的内存页,需要像DMA一样在启动的时候就预留(这里说的是内核中高端内存的ZONE_DMA16)
  3. 动态hugePage换出会更慢
// Arena的成员变量
class Arena : public Allocator {
public:
    static const size_t kInlineSize = 2048;
    static const size_t kMinBlockSize;
    static const size_t kMaxBlockSize;    
private:
    // 这里使用了__attribute__机制,是GNUC下独有的
    // 使用它可以在后面的括号里设置函数、变量、类的属性
    // 这里设置了inline_block_[kInlineSize]的对齐
    // 通过max_align_t获取当前平台下与至少任何一种标量类型都一样大的对齐
    // 通过alignof获得之后__aligned__设置
    // 这里使用了栈
    char inline_block_[kInlineSize] __attribute__((__aligned__(alignof(max_align_t))));
    // 每次分配的block的字节数量
    const size_t kBlockSize;
    // 使用new[] 分配的block集合
    // 每一个char*代表一个block
    using Blocks = std::vector<char*>;
    Blocks blocks_;
    // 内部结构体,用来记录mmap分配的情况
    // 这里的mmap只使用MAP_PAGETLB来分配大页内存
    struct MmapInfo {
        void* addr_;
        size_t length_;
    
        MmapInfo(void* addr, size_t length) : addr_(addr), length_(length) {}
    };
    // 使用mmap分配的block集合
    std::vector<MmapInfo> huge_blocks_;
    
    // 记录不规则的块数量,当有一次较大分配的时候,就要分配irregular_block
    size_t irregular_block_num = 0;
    
    // 这里有两个指针分别指向一个block的首(低)地址和尾(高)地址
    // 每当分配的时候aligned_alloc_ptr指向的首地址将会分配需要对齐的内存
    // unaligned_alloc_ptr将会分配不需要对齐的内存
    // 如果将不对齐的内存放在一端分配显然会节省空间,而如果和对齐的放在一起分配
    // 那相当于不对齐的内存也被迫对齐了,浪费了空间
    char* unaligned_alloc_ptr_ = nullptr;
    char* aligned_alloc_ptr_ = nullptr;
    // 当前block当中还剩下多少的bytes
    size_t alloc_bytes_remaining_ = 0;
    
    #ifdef MAP_HUGETLB
    // 使用mmap给block分配的内存的大小
    size_t hugetlb_size_ = 0;
    #endif  // MAP_HUGETLB
    
    // 目前为止在这一block分配了多少bytes
    size_t blocks_memory_ = 0;
    // AllocTracker跟踪内存分配情况
    AllocTracker* tracker_;
};

通过上述不难发现的是:我们有三种方式来分配一个block的内存。

  1. 通过预设的kInlineSize来分配,我必定分配了2048 Bytes并且保证它们的对齐。
  2. 通过new[]来分配
  3. 通过mmap来分配
// 然后我们看看Arena都提供了什么方法
class Arena : public Allocator {
public:
    // 显然不允许有拷贝操作
    Arena(const Arena&) = delete;
    void operator=(const Arena&) = delete;
    
    
    // 这里涉及了huge_page,我们会先从huge_page中尝试分配,如果分配失败再普通分配
    // 使用huge_page肯定要确保在支持huge_page的OS中
    explicit Arena(size_t block_size = kMinBlockSize,
    AllocTracker* tracker = nullptr, size_t huge_page_size = 0);
    
    ~Arena();
    
    char* Allocate(size_t bytes) override;
    
    // 使用巨页技术的、对齐的内存分配方法
    char* AllocateAligned(size_t bytes, size_t huge_page_size = 0,
    Logger* logger = nullptr) override;
    
    // 返回估计的总的内存使用,不包含那些已经分配但是没有使用的,为了未来分配的内存
    size_t ApproximateMemoryUsage() const {
    return blocks_memory_ + blocks_.capacity() * sizeof(char*) -
    alloc_bytes_remaining_;
    }
    
    // 总共分配了多少字节
    size_t MemoryAllocatedBytes() const { return blocks_memory_; }
    
    // 分配但是还没有使用的字节数量
    size_t AllocatedAndUnused() const { return alloc_bytes_remaining_; }
    
    // 如果一次分配太大了,那会分配一个大小相同的不对齐的块(还是不规则的块)
    // 返回这样的块的数量
    size_t IrregularBlockNum() const { return irregular_block_num; }
    
    // 返回一个block的大小
    size_t BlockSize() const override { return kBlockSize; }
    
    // 是否使用了栈上的内存
    bool IsInInlineBlock() const {
    return blocks_.empty();
    }
    
private:
    char* AllocateFromHugePage(size_t bytes);
    char* AllocateFallback(size_t bytes, bool aligned);
    char* AllocateNewBlock(size_t block_bytes);
};

// 这里有个extern的函数
// 这个函数用来规范kBlockSize,让它处在kMinBlockSize与kMaxBlockSize之间
// 并且保证block_size是align的整数倍

extern size_t OptimizeBlockSize(size_t block_size);

这里主要提供的方法有:

  1. 构造函数
Arena::Arena(size_t block_size, AllocTracker* tracker, size_t huge_page_size)
    : kBlockSize(OptimizeBlockSize(block_size)), tracker_(tracker) {
    // 这里的assert实际上已经通过OptimizeBlockSize函数保证了kBlockSize的正确性了
    // 多此一举,降低效率
  assert(kBlockSize >= kMinBlockSize && kBlockSize <= kMaxBlockSize &&
         kBlockSize % kAlignUnit == 0);
  TEST_SYNC_POINT_CALLBACK("Arena::Arena:0", const_cast<size_t*>(&kBlockSize));
  // 不难发现,此时基本都使用栈上的内存
  alloc_bytes_remaining_ = sizeof(inline_block_);
  blocks_memory_ += alloc_bytes_remaining_;
  aligned_alloc_ptr_ = inline_block_;
  unaligned_alloc_ptr_ = inline_block_ + alloc_bytes_remaining_;
#ifdef MAP_HUGETLB
  hugetlb_size_ = huge_page_size;
  // hugetlb_size向上取整数
  if (hugetlb_size_ && kBlockSize > hugetlb_size_) {
    hugetlb_size_ = ((kBlockSize - 1U) / hugetlb_size_ + 1U) * hugetlb_size_;
  }
#else
  (void)huge_page_size;
#endif
  if (tracker_ != nullptr) {
    tracker_->Allocate(kInlineSize);
  }
}
  1. AllocateNewBlock(size_t block_bytes)
// 这里直接使用new分配内存,而没有分配align整数倍大小的内存
char* Arena::AllocateNewBlock(size_t block_bytes) {
    
  // 这一句是一个小trick,通过emplace_back保留一个指针的空间
  // 因为这个blocks_是一个指向block指针的集合
  // emplace_back一个指针来为后面new得到的指针保留空间
  // 这里先emplace_back而后new是因为如果先new而new崩了,那么不需要处理
  // 可是如果new成功了而emplace_back崩了,那就要处理new之后的那段内存
  // 否则就会造成内存泄露,也就是说如果先new然而emplace_back那就要在emplace_back
  // 加上try_catch,而这种写法就不需要!

  
  // 这里不使用reserve而是使用emplace_back是希望vector能自己管理capacity
  // 不要人为插手,因为每次都reserve一下会导致重复的分配内存
  blocks_.emplace_back(nullptr);

  char* block = new char[block_bytes];
  size_t allocated_size;
#ifdef ROCKSDB_MALLOC_USABLE_SIZE
  allocated_size = malloc_usable_size(block);
#ifndef NDEBUG
  // It's hard to predict what malloc_usable_size() returns.
  // A callback can allow users to change the costed size.
  std::pair<size_t*, size_t*> pair(&allocated_size, &block_bytes);
  TEST_SYNC_POINT_CALLBACK("Arena::AllocateNewBlock:0", &pair);
#endif  // NDEBUG
#else
  allocated_size = block_bytes;
#endif  // ROCKSDB_MALLOC_USABLE_SIZE
  blocks_memory_ += allocated_size;
  if (tracker_ != nullptr) {
    tracker_->Allocate(allocated_size);
  }
  blocks_.back() = block;
  return block;
}
  1. AllocateFromHugePage(size_t bytes)
char* Arena::AllocateFromHugePage(size_t bytes) {
#ifdef MAP_HUGETLB
  if (hugetlb_size_ == 0) {
    return nullptr;
  }
  // 这里的trick和上面的哪个是一样的
  huge_blocks_.emplace_back(nullptr /* addr */, 0 /* length */);

  void* addr = mmap(nullptr, bytes, (PROT_READ | PROT_WRITE),
                    (MAP_PRIVATE | MAP_ANONYMOUS | MAP_HUGETLB), -1, 0);

  if (addr == MAP_FAILED) {
    return nullptr;
  }
  huge_blocks_.back() = MmapInfo(addr, bytes);
  blocks_memory_ += bytes;
  if (tracker_ != nullptr) {
    tracker_->Allocate(bytes);
  }
  return reinterpret_cast<char*>(addr);
#else
  (void)bytes;
  return nullptr;
#endif
}
  1. AllocateFallback(size_t bytes, bool aligned)
// 上述两个函数往往被这个函数调用,它们被用来分配内存
// 这个函数是用来当前block内存不足的时候分配一个新的block
// 同时它接收bytes用来分配block的时候就把造成block不足的对象也给他分配了
char* Arena::AllocateFallback(size_t bytes, bool aligned) {
  if (bytes > kBlockSize / 4) {
    ++irregular_block_num;
    // 如果一个对象的大小已经超过了四分之一block
    // 那么将它分开存放,以免浪费过多的block的内存
    // 因为实际上上一个block还有很多内存,我们不应该抛弃它
    // 实际上通过new给他一个新的内存空间
    return AllocateNewBlock(bytes);
  }

  
  size_t size = 0;
  char* block_head = nullptr;
  
#ifdef MAP_HUGETLB
  if (hugetlb_size_) {
    size = hugetlb_size_;
    block_head = AllocateFromHugePage(size);
  }
#endif
// 如果没使用hugetlb那就使用new分配
  if (!block_head) {
    size = kBlockSize;
    block_head = AllocateNewBlock(size);
  }
  // 把造成block不足的对象也给他分配了
  alloc_bytes_remaining_ = size - bytes;
// 这里修改aligned_alloc_ptr和unaligned_alloc_ptr代表开始使用这一个block
  if (aligned) {
    aligned_alloc_ptr_ = block_head + bytes;
    unaligned_alloc_ptr_ = block_head + size;
    return block_head;
  } else {
    aligned_alloc_ptr_ = block_head;
    unaligned_alloc_ptr_ = block_head + size - bytes;
    return unaligned_alloc_ptr_;
  }
}
  1. AllocateAligned(size_t bytes, size_t huge_page_size,Logger* logger)
char* Arena::AllocateAligned(size_t bytes, size_t huge_page_size,
                             Logger* logger) {
    // 位运算我总是搞不好,看看人家的操作
    // 这里保证AlignUnit是2的k次幂
  assert((kAlignUnit & (kAlignUnit - 1)) == 0);

#ifdef MAP_HUGETLB
  // 这里的huge_page_size是自定义的内存对齐值
  // 如果是0就表示使用默认对齐值
  if (huge_page_size > 0 && bytes > 0) {
    // Allocate from a huge page TLB table.
    assert(logger != nullptr);  // logger need to be passed in.
    // 将需要的上调到huge_page_size对齐值的整数倍
    size_t reserved_size =
        ((bytes - 1U) / huge_page_size + 1U) * huge_page_size;
    assert(reserved_size >= bytes);

    char* addr = AllocateFromHugePage(reserved_size);
    if (addr == nullptr) {
      ROCKS_LOG_WARN(logger,
                     "AllocateAligned fail to allocate huge TLB pages: %s",
                     errnoStr(errno).c_str());
      // fail back to malloc
    } else {
      return addr;
    }
  }
#else
  (void)huge_page_size;
  (void)logger;
#endif
  // 先计算对齐获得needed
  size_t current_mod =
      reinterpret_cast<uintptr_t>(aligned_alloc_ptr_) & (kAlignUnit - 1);
  size_t slop = (current_mod == 0 ? 0 : kAlignUnit - current_mod);
  size_t needed = bytes + slop;
  char* result;
  // 如果所需的没超过alloc_bytes_remaining那就分配
  if (needed <= alloc_bytes_remaining_) {
    result = aligned_alloc_ptr_ + slop;
    aligned_alloc_ptr_ += needed;
    alloc_bytes_remaining_ -= needed;
  } else {
      // 超过了就使用AllocateFallback分配一个block
    result = AllocateFallback(bytes, true /* aligned */);
  }
  assert((reinterpret_cast<uintptr_t>(result) & (kAlignUnit - 1)) == 0);
  return result;
}

到此为止可以说一下allocateAligned的分配逻辑了:

  1. 通过Arena的构造函数它会有一个初始的block在栈上,然后通过allocateAligned分配的时候
  2. 检查一下是否使用了hugePageTLB,如果使用了一定通过mmap来分配对象
  3. 如果没有使用就先分配栈上空间,当栈上面空间不够的时候进入AllocateFallback
  4. 然后看是不是这个对象太大了,如果是通过new分配一个irregularBlock
  5. 如果不是这个时候又检查是否开启了hugePageTLB,如果开了就通过mmap分配对象(这里又检查了一遍是为了下面实现allocate方便)
  6. 然后通过new分配一个新的Block并且将那个对象分配,转而使用新的block
  7. Allocate(size_t bytes)
inline char* Arena::Allocate(size_t bytes) {
  // The semantics of what to return are a bit messy if we allow
  // 0-byte allocations, so we disallow them here (we don't need
  // them for our internal use).
  // 不允许0字节分配
  // 不难看出就是先使用栈上的
  // 上面不够了进入Fallback
  assert(bytes > 0);
  if (bytes <= alloc_bytes_remaining_) {
    unaligned_alloc_ptr_ -= bytes;
    alloc_bytes_remaining_ -= bytes;
    return unaligned_alloc_ptr_;
  }
  return AllocateFallback(bytes, false /* unaligned */);
}

两头处理对齐与非对齐,防止内存泄露的小trick,确保是2的k次幂的方法