librbd 缓存管理 ObjectCacher 概述

librbd 缓存管理

概述

缓存时 librdb 的一大特性, 开启缓存后, 可以有效地提高客户端数据读写的性能 ceph 中 class ObjectCacher 是缓存管理的核心类. 实现了 librbd 缓存读写的绝大部分功能, 本文主要围绕着 ObjectCacher 来讲述 librdb 缓存的主要实现, 包括缓存的创建, 下刷, 淘汰机制等, 不过不包括缓存的读写, 缓存的读写将会在其他文章中讲述.

关于 librbd 缓存可见:

Config Settings — Ceph Documentation

LRU

ObjectCacher 中对缓存管理采用了 LRU 算法, LRU 是一种常见的缓存管理算法, ceph 有自己的 LRU 链表的实现, 想要了解ObjectCacher缓存的管理过程, 就需要对这个基础的算法有一定的了解.

ceph list 的 LRU 算法的基础实现主要位于 src/include/lru.h, 该 LRU 在 librdb , cephfs client 中都有所应用, lru.h 中主要是两个 class LRU 和 class LRUObject 两个类

LRUObject

首先先看一下LRUObject

LRUObject 是 LRU 算法中的抽象类型, 其核心为 xlist<LRUObject *>::item lru_link 是一个双向不循环链表, 通常情况下LRUObject不会被直接使用, 一般都是通过继承 LRUObject 然后在利用 class LRU 的接口实现格子的 LRU 算法.

class LRUObject {
public:
  LRUObject() : lru(), lru_link(this), lru_pinned(false) { }
  ~LRUObject();
  void lru_pin();
  void lru_unpin();
  bool lru_is_expireable() const { return !lru_pinned; }
  friend class LRU;
private:
  class LRU *lru; // 指向所属的 LRU 
  xlist<LRUObject *>::item lru_link;
  bool lru_pinned;
};

LRUObject 主要子类

LRUObject
Dentry
Item
CDentry
BufferHead
Object

LRU

class LRU 为 LRU 算法实现, 其管理的对象为class LRUObject.

class LRU 其内部主要有 3 个 list, top, bottom, pintail , 并且实现了对应的 insert, touch, expire 等基本 LRU 能力的接口.

看一下 LRU 中的主要 3 个 list

top, bottom 可以理解为活跃, 不活跃的两组元素的 list, 当获取过期的元素的时候会优先从 bottom 底部获取.

插入元素是, 可以直接向 top, bottom 两个 list insert, insert 之后会调用 adjust() 方法 top, bottom 中元素比例, 例如如果 middle == 0.5 没有 pin 住的元素, insert 10 个元素之后 top, bottom 都会存在 5 个元素

pintail 则是属于不会被过期的元素, 不可直接插入元素, 但可以调用相应接口将元素 pin 住.被 pin 的元素在被淘汰时如果是 pin 住的, 就会被放入 pintail

class LRU {
private:
  typedef xlist<LRUObject *> LRUList;
  LRUList top, bottom, pintail;
public:
  LRU() : num_pinned(0), midpoint(0.6) {}
    
  void lru_insert_top(LRUObject *o) {
    //...
    o->lru = this; // 指向 LRU 自己
    top.push_front(&o->lru_link); // 插入 top 的头部, 变更成当前最晚被淘汰的元素
    adjust();
  }
    

  LRUObject *lru_remove(LRUObject *o) {
      //...
    o->lru_link.remove_myself();
	//...
    adjust();
    return o;
  }

  bool lru_touch(LRUObject *o) { // 提升元素
    lru_insert_top(o);
	//...
    return true;
  }

  // expire -- expire a single item
  LRUObject *lru_get_next_expire() {
    adjust();
    // 先从 bottom 找, 并且排除被 pin 的元素
    while (bottom.size()) {
      LRUObject *p = bottom.back();
      if (!p->lru_pinned) return p;
      pintail.push_front(&p->lru_link); // 被 pin 的元素此时才会被插入 pintail
    }

    //  再从 bottom 找, 同样要排除被 pin 的元素
    while (top.size()) {
      LRUObject *p = top.back();
      if (!p->lru_pinned) return p;
      pintail.push_front(&p->lru_link);
    }
    
    // no luck!
    return NULL;
  }
  
  // 返回一个过期的元素
  LRUObject *lru_expire() {
    LRUObject *p = lru_get_next_expire();
    if (p) 
      return lru_remove(p);
    return NULL;
  }

protected:
  void adjust() {
    uint64_t toplen = top.size();
    // 根据 midpoint (默认 0.6) 调整 top 和 bottom
    uint64_t topwant = (midpoint * (double)(lru_get_size() - num_pinned));
    for (uint64_t i = toplen; i < topwant; i++) {
      top.push_back(&bottom.front()->lru_link);
    }
    for (uint64_t i = toplen; i > topwant; i--) {
      bottom.push_front(&top.back()->lru_link);
    }
  }

};

ObjectCacher

ObjectCacher 是 librbd 缓存主要的实现类, 首先要了解的是的核心成员 objects, 它保存着所有缓存的对象, 从它可以找到所有的缓存数据

objects 数据结构

定义:

class ObjectCacher{
    //...
    vector<ceph::unordered_map<sobject_t, Object*> > objects;
    //...
}

看一下获取缓存的函数, 再结合后面的数据结构图, 就可以理解 objects 的数据结构了

ObjectCacher::Object *ObjectCacher::get_object(sobject_t oid, uint64_t object_no, ObjectSet *oset,
                                          object_locator_t &l,
                                          uint64_t truncate_size,
                                          uint64_t truncate_seq)
{

  if ((uint32_t)l.pool < objects.size()) { //找到了这个 pool 的缓存
    if (objects[l.pool].count(oid)) { // oid 的缓存存在, 直接找到返回即可
      Object *o = objects[l.pool][oid]; // 先根据 pool id 进行索引, 然后在使用 oid 索引
      o->object_no = object_no;
      o->truncate_size = truncate_size;
      o->truncate_seq = truncate_seq;
      return o; // 找到的缓存
    }
  } else {
    objects.resize(l.pool+1); 
  }

  // 没找到, 直至 new 一个
  Object *o = new Object(this, oid, object_no, oset, l, truncate_size,
                      truncate_seq);
  objects[l.pool][oid] = o;
  ob_lru.lru_insert_top(o);
  return o;
}

数据结构图:

如图 ObjectCacher 中存储缓存的变量为 ObjectCacher::objects 类型为 vector<ceph::unordered_map<sobject_t, Object*> > objects; 它保存了 librbd 的所有缓存数据, 包括不同 pool 的缓存. 想要找到 id 为 i 的 pool 的缓存数据, 可以通过 objects[i] 索引, 例如: objects[0],objects[1], ... 代表 id 为 0, 1, … 的 pool 的 缓存.

每个 pool 的缓存又是以一个 map 结构保存的并且 map 的 key 为 sobject_t (可以把它理解为对象的 oid) value 就是该 oid 的对应的缓存数据 Object.

一个对象的数据不一定全部都会被缓存, 可能只有其中一部分被缓存. 所以 Object 中的缓存也时一段一段的, 每一段就是一个 BufferHead. Object 中存储缓存变量为Object::data 类型 map<loff_t, BufferHead*> data , 其中 loff_t 代表 BufferHead 这段缓存在该对象中的起始位置偏移量. BufferHead 是存储缓存的最小单位

BufferHead 就是真正存储缓存的地方了, BufferHead 的缓存存储在变量 BufferHead::bl, 类型为 ceph 标准类型 bufferlist .

BufferHead :

BufferHead 存储缓存对象 bufferlist bl 的类, 来看一下他的定义

  class BufferHead : public LRUObject {
  public:
    // states
    static const int STATE_MISSING = 0;  // 没命中的读缓存
    static const int STATE_CLEAN = 1;  
    static const int STATE_ZERO = 2;   // NOTE: these are *clean* zeros
    static const int STATE_DIRTY = 3;  
    static const int STATE_RX = 4; // 接收中
    static const int STATE_TX = 5; // 发送中
    static const int STATE_ERROR = 6; // a read error occurred
// ...
      
    int state;  // 该段缓存的状态
    int ref; // 该段缓存引用计数 todo 什么时候加减
    struct {
      loff_t start, length;  // 该段缓存在 Object 中的位置
    } ex;
    Object *ob; // 指向所在的 Object
    bufferlist  bl; // 最终存储缓存的数据类型, 缓存的最小单位
//...
    ceph::real_time last_write; // 缓存最后写的时间, flush 可以用来计算缓存是不是过期了
//...
    map<loff_t, list<Context*> > waitfor_read;

//...
  };

缓存的创建

当开启缓存(配置 rbd_cache=true) 后, ObjectCacher 将会在 Open Image 时候创建

int librbd::RBD::open(IoCtx& io_ctx, Image& image, const char *name);

open 的过程中会触发两个线程工作 fn-radosclient 和 tp_librbd

fn-radosclient 线程中会去去读配置, 在 ImageCtx::apply_metadata 中会将配置 rbd_cache 的值赋给 ImageCtx::cache

void ImageCtx::apply_metadata(xxx) {
    //...
    #define ASSIGN_OPTION(param, type)              \
    param = config.get_val<type>("rbd_"#param)

    bool skip_partial_discard = true;
    ASSIGN_OPTION(non_blocking_aio, bool);
    ASSIGN_OPTION(cache, bool); // ImageCtx::cache
    ASSIGN_OPTION(cache_writethrough_until_flush, bool);
    ASSIGN_OPTION(cache_max_dirty, Option::size_t);
    //...
    
#undef ASSIGN_OPTION
}

tp_librbd 线程中会根据 ImageCtx::cache 的值决定是否创建缓存,即是否创建

template <typename I>
Context *OpenRequest<I>::send_init_cache(int *result) {
  // cache is disabled or parent image context
  if (!m_image_ctx->cache || m_image_ctx->child != nullptr ||
      !m_image_ctx->data_ctx.is_valid()) {
    return send_register_watch(result); // 不开启缓存, 直接返回
  }
//...
  auto cache = cache::ObjectCacherObjectDispatch<I>::create(m_image_ctx);
  cache->init(); // 创建缓存的地方,后文会展开讲述
//...

  return send_register_watch(result);
}

可以看一下 ObjectCacherObjectDispatch::init() 都干了什么, ObjectCacher 缓存核心类 ObjectCacher 就是在这里创建的

template <typename I>
void ObjectCacherObjectDispatch<I>::init() {
  // 读取相关配置
  auto cache_size =
    m_image_ctx->config.template get_val<Option::size_t>("rbd_cache_size");
  auto target_dirty =
    m_image_ctx->config.template get_val<Option::size_t>("rbd_cache_target_dirty");
  auto max_dirty_age =
    m_image_ctx->config.template get_val<double>("rbd_cache_max_dirty_age");
  auto block_writes_upfront =
    m_image_ctx->config.template get_val<bool>("rbd_cache_block_writes_upfront");
  auto max_dirty_object = 
      m_image_ctx->config.template get_val<uint64_t("rbd_cache_max_dirty_object");
//...
  m_object_cacher = new ObjectCacher(cct, m_image_ctx->perfcounter->get_name(),
                                     *m_writeback_handler, m_cache_lock,
                                     nullptr, nullptr, cache_size,
                                     10,  /* reset this in init */
                                     init_max_dirty, target_dirty,
                                     max_dirty_age, block_writes_upfront););  // 创建 ObjectCacher 对象
  m_object_set = new ObjectCacher::ObjectSet(nullptr,m_image_ctx->data_ctx.get_id(), 0);
  m_object_cacher->start(); // 这里会创建 flusher 线程, 其主要负责缓存的淘汰
			// start vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
             void start() {
                flusher_thread.create("flusher");
              }
			// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 
//...
  // add ourself to the IO object dispatcher chain
  m_image_ctx->io_object_dispatcher->register_object_dispatch(this);
			// register_object_dispatch vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
			void ObjectDispatcher<I>::register_object_dispatch(xxx) {
				auto cct = m_image_ctx->cct;
				auto type = object_dispatch->get_object_dispatch_layer();  
							// get_object_dispatch_layer vvvvvvvvvvvvvvvvvvv
							get_object_dispatch_layer() const override {
								return io::OBJECT_DISPATCH_LAYER_CACHE;
							}
							// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
			// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
               // 将 cache layer 插入 m_object_dispatches 中, 包括 cache layer 的 type 以及 object_dispatch , m_object_dispatches 是后续进行读写流程的一个核心对象 
               auto result = m_object_dispatches.insert(
                 {type, {object_dispatch, new AsyncOpTracker()}});
               ceph_assert(result.second);
           }
}

flusher 线程

flusher 线程负责缓存的淘汰, flusher 线程跟随缓存的创建一起创建, 上文在缓存的创建章节中有说明.

flusher 线程主要代码在calss FlusherThread 中,

可以注意到 FlusherThread 同上文讲述的 BufferHead, Object 等一样, 也是 ObjectCacher 的一个内部类, librbd 缓存中大部分类都在 ObjectCacher

FlusherThread 代码

class ObjectCacher {
    //....
    class FlusherThread : public Thread {
        ObjectCacher *oc;
        public:
        explicit FlusherThread(ObjectCacher *o) : oc(o) {}
        void *entry() override {
            oc->flusher_entry();
            return 0;
        }
    } flusher_thread;
    //... 

flusher 线程的启停

FlusherThread 直接继承自 Thread.所以通过 Thread::create 便可以直接启动线程.

  void start() {
    flusher_thread.create("flusher");
  }
  // flusher 线程关闭函数
  void stop() {
    ceph_assert(flusher_thread.is_started());
    lock.Lock();  // hmm.. watch out for deadlock!
    flusher_stop = true;
    flusher_cond.Signal();
    lock.Unlock();
    flusher_thread.join();
  }
}

flusher_entry

flusher 线程的主要业务逻辑函数是 ObjectCacher::flusher_entry(), 可以一起看一下.

void ObjectCacher::flusher_entry()
{
  lock.Lock();
  while (!flusher_stop) { // 循环执行
	//...
    loff_t actual = get_stat_dirty() + get_stat_dirty_waiting();
	//...
    if (actual > 0 && (uint64_t) actual > target_dirty) { // target_dirty 对应配置文件中 rbd_cache_target_dirty , 在 init() 中获取到的(上文有讲), 后面很多的参数也都类似
      // 下刷超过指定量 ( actual - target_dirty ) 的缓存
      flush(&trace, actual - target_dirty); 
    } else { 
      // 下刷超过指定时间的缓存
      ceph::real_time cutoff = ceph::real_clock::now();
      cutoff -= max_dirty_age;
      BufferHead *bh = 0;
      int max = MAX_FLUSH_UNDER_LOCK; // 拿锁之后最多下刷多少快缓存, 默认 20
      // 从 bh_lru_dirty 这个链表中获取过期的缓存
      while ((bh = static_cast<BufferHead*>(bh_lru_dirty.lru_get_next_expire())) != 0 &&
	     	bh->last_write <= cutoff &&
	     	max > 0) {
       ldout(cct, 10) << "flusher flushing aged dirty bh " << *bh << dendl;
       if (scattered_write) {
         bh_write_adjacencies(bh, cutoff, NULL, &max);
           } else {
         bh_write(bh, trace);  
         --max;
       }
       }
     if (!max) { // 防止饿死其其它线程
        trace.event("backoff");
        lock.Unlock();
        lock.Lock();
        continue;
      }
    }

    trace.event("finish");
    if (flusher_stop)
      break;

    flusher_cond.WaitInterval(lock, seconds(1));  // 每秒循环执行一次
  }
//...
}

可以注意到 flusher_entry 函数下刷的都是 dirty 的缓存, 也就是需要写到后端的缓存, 那么那些 clean 状态的缓存(比如单纯读并且没有被修改过的缓存) 是如何管理的呢.

主要就是通过 ObjectCacher::trim() 函数进行的, 在每次缓存读写完毕后都会调用 trim() 函数来清理这些缓存

trim()

一起来看下 trim() 干了啥
trim() 中会首先删除 bh_lru_rest 中的缓存, 在删除 ob_lru 中的缓存, 如果缓存太多了的话

void ObjectCacher::trim()
{
  ceph_assert(lock.is_locked());

  uint64_t max_clean_bh = max_size >> BUFFER_MEMORY_WEIGHT; // max_size 就是 rbd_cache_size 
  uint64_t nr_clean_bh = bh_lru_rest.lru_get_size() - bh_lru_rest.lru_get_num_pinned();
  while (get_stat_clean() > 0 && ((uint64_t)get_stat_clean() > max_size || nr_clean_bh > max_clean_bh)) {

    BufferHead *bh = static_cast<BufferHead*>(bh_lru_rest.lru_expire()); // 从 bh_lru_rest 拿出缓存
    if (!bh)
      break; // 删完了

    Object *ob = bh->ob;
    bh_remove(ob, bh);  // 直接移除缓存就可以了, 因为这里的都是 clean 的数据
    delete bh;
    --nr_clean_bh;

    if (ob->complete) {
      ldout(cct, 10) << "trim clearing complete on " << *ob << dendl;
      ob->complete = false;
    }
  }

  while (ob_lru.lru_get_size() > max_objects) { // 按照 Object 来管理, 前面的是按照 BufferHead 管理的
    Object *ob = static_cast<Object*>(ob_lru.lru_expire());
    if (!ob)
      break;
    close_object(ob);
  }
//...
}

ObjectCacher:: LRU 类型

class ObjectCacher 中有三个主要的 lru 链表, bh_lru_dirty, bh_lru_rest, ob_lru. bh_lru_dirty 由 flusher 线程负责管理, bh_lru_rest , ob_lrutrim() 来管理. 只有 bh_lru_dirty 中的缓存才会被写入到后端, 而 bh_lru_rest 中的缓存过期后会直接被 remove 掉

class ObjectCacher{
//...
LRU   bh_lru_dirty, bh_lru_rest;  // LRU 管理的是 BufferHead
LRU   ob_lru                      // LRU 管理的是 Object 
//...
}

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
Spring Batch是一个用于批处理处理大量数据的开源框架。在Spring Batch中,可以使用缓存来提高性能和效率。 Spring Batch提供了两种主要的缓存管理方式:读取缓存和写入缓存。 读取缓存主要用于减少从源数据源读取数据的次数。当批处理作业需要读取大量数据时,可以将部分数据读取到内存中的缓存中,然后逐步从缓存中读取数据,而不是每次都从数据源中读取。这样可以减少数据库等数据源的访问次数,提高读取数据的性能。 写入缓存主要用于减少向目标数据源写入数据的次数。当批处理作业需要将大量数据写入目标数据源时,可以将数据暂时存储在内存中的缓存中,然后按照一定的批量大小进行写入。这样可以减少与目标数据源的交互次数,提高写入数据的性能。 在Spring Batch中,可以通过配置来启用缓存管理。可以使用各种缓存技术,如内存缓存、分布式缓存等。可以设置缓存的大小、存储方式、有效期等参数,以满足具体业务需求。 需要注意的是,缓存管理在某些情况下可能会带来一定的风险和副作用。例如,如果缓存中的数据与数据源中的数据不一致,可能导致错误的结果。因此,在使用缓存管理时,需要谨慎考虑业务的一致性和完整性,以及缓存的更新策略和机制。 综上所述,Spring Batch提供了缓存管理的功能,可以通过读取缓存和写入缓存来提高性能和效率。但在使用缓存管理时需要注意业务的一致性和完整性,以及缓存的更新策略和机制。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值