目录
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 主要子类
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_lru
由 trim()
来管理. 只有 bh_lru_dirty
中的缓存才会被写入到后端, 而 bh_lru_rest
中的缓存过期后会直接被 remove 掉
class ObjectCacher{
//...
LRU bh_lru_dirty, bh_lru_rest; // LRU 管理的是 BufferHead
LRU ob_lru // LRU 管理的是 Object
//...
}