第二章: Cache Tier
分层存储的原理,就是存储的数据的访问是有热点的,数据并非均匀访问。有个通用法则叫做二八原则,也就是80%的应用只访问20%的数据,这20%的数据成为热点数据,如果把这些热点数据保存性能比较高的SSD磁盘上,就可以提高响应时间。
性能较高的存储,一般由SSD 磁盘组成,称之为Cache 层,hot层,Cache pool 或者 hot pool, 访问性能比较低的存储层就称为 base pool 或者 data pool,cold pool 等。
ceph cache mode
write-back
这种方式比较常见的。读写请求都直接发给 cache pool ,这种模式适合于大量修改的数据应用场景(例如图片视频编辑, 事务处理OLTP类应用)
read-only
这种方式也称为:write-around,或者read cache。读请求直接发送给cache pool,写请求并不经过cache pool, 而是直接发送给base_pool.
这种方式的优点就是,cache pool 设置为单副本就可以了,即使cache pool 层失效,也不会有数据的丢失。这种模式比较适合数据一次写入,多次读取的应用场景。例如图片,视频, 音频等。
read-forward
这种方式类似于write-back 模式,当read时,对象不在cache pool中,就直接返回,client直接向data pool 发送请求
client->cache pool:发送读请求
Note right of cache pool:cache miss
cache pool-->client:redirect
client->base pool:发送读请求
base pool-->>client:返回数据
read-proxy
这种方式类似于read-back,当read是,对象不在cache pool 中, cache pool 层代表client端直接向 data pool 发送请求。
client->cache pool:发送读请求
Note right of cache pool:cache miss
cache pool->base pool:发送读请求
base pool-->cache pool:返回数据
cache pool-->client:返回数据
在代码src/osd/osd_types.h 里定义了上述的几种模式:
typedef enum {
CACHEMODE_NONE = 0, ///< no caching
CACHEMODE_WRITEBACK = 1, ///< write to cache, flush later
CACHEMODE_FORWARD = 2, ///< forward if not in cache
CACHEMODE_READONLY = 3,
///< handle reads, forward writes [not strongly consistent]
CACHEMODE_READFORWARD = 4, ///< forward reads, write to cache flush later
CACHEMODE_READPROXY = 5 ///< proxy reads, write to cache flush later
} cache_mode_t;
Cache Tier 相关数据结构
Pool 层关于Cache Tier 数据结构
ceph的分层存储是基于pool 层的,因此cache tier 相关的配置信息都保存在pg_pool_t 数据结构里, 这些信息最终可持久化保存在monitor的存储中.
Struct pg_pool_t
{
......
set<uint64_t> tiers; //< pools that are tiers of us
int64_t tier_of; //< pool for which we are a tier
//这两个字段用来设置cache pool
//如果本pool是一个cache pool, 那么tier of 记录 base pool
//如果本pool 是 base pool, tiers就记录它的cache pool,可以有多个cache pool
int64_t read_tier; ///< pool/tier for objecter to direct reads to
int64_t write_tier; ///< pool/tier for objecter to direct writes to
cache_mode_t cache_mode; ///< cache pool mode
uint64_t target_max_bytes; ///< tiering: target max pool size
uint64_t target_max_objects; ///< tiering: target max pool size
//这两个字段设置了cache pool 的最大的 bytes 和 对象数量
uint32_t cache_target_dirty_ratio_micro; ///< cache: fraction of target to leave dirty
uint32_t cache_target_dirty_high_ratio_micro; ///<cache: fraction of target to flush with high speed
uint32_t cache_target_full_ratio_micro; ///< cache: fraction of target to fill before we evict in earnest
//这三分别:是dirty 数据的比率,
uint32_t cache_min_flush_age; ///< minimum age (seconds) before we can flush
uint32_t cache_min_evict_age; ///< minimum age (seconds) before we can evict
HitSet::Params hit_set_params; ///< The HitSet params to use on this pool
uint32_t hit_set_period; ///< periodicity of HitSet segments (seconds)
uint32_t hit_set_count; ///< number of periods to retain
bool use_gmt_hitset; ///< use gmt to name the hitset archive object
uint32_t min_read_recency_for_promote; ///< minimum number of HitSet to check before promote on read
uint32_t min_write_recency_for_promote; ///< minimum number of HitSet to check before promote on write
uint32_t hit_set_grade_decay_rate; ///< current hit_set has highest priority on objects
///temperature count,the follow hit_set's priority decay
///by this params than pre hit_set
uint32_t hit_set_search_last_n; ///<accumulate atmost N hit_sets for temperature
}
一些参数的说明
set
当用命令 ceph osd tier add $data_pool $cache_pool 设置cache tier时,就是在修改monitor端保存的相关的pool的上述字段
int64_t read_tier; int64_t write_tier
在这里为什么要再区分出 read tier 和 write tier ?这是由于ceph 实现了不同的cache mode 的。 如果是write-back模式,那么该cache pool 既是read tier ,又是 write tier; 如果只是read only 模式,那么实际上,cache pool 只是 read tier,没有write tier。
当用命令 ceph osd tier set-overlay data_pool cache_pool,就是根据cache mode 设置 read tier 和 write tier 字段
HitSet
类HitSet 定义了查找对象是否在cache 中的接口, 其实现由三种方式,默认的是bloom filter 的方式
- Bloom : 使用bloom filter 算法来实现对象的查找
- ExplicitObjectHitSet:使用hash表来查找,key是对象
- ExplicitHashHitSet :使用hash表来查找, key 使用的是对象的hash值,比直接使用对象,内存占用比较小
Cache Tier 的初始化
Cache Tier 相关的初始化有两个入口
- on_activate:如果该pool已经设置为cache pool,在该cache pool的所有的pg 处于activave后初始化。
- on_pool_change:当该pool的所有的pg 都已经处于active 状态后,才设置该pool 为cache pool,那么就等待monitor的通知osd相关信息的变化,在on_pool_change函数里初始化。
hit_set_setup
void ReplicatedPG::hit_set_setup() 本函数主要初始化hit_set数据结构
- 首先检查pg如果不是处于active 状态,并且不是primary 状态,就调用函数hit_set_clear,直接返回
- 检查pool.info.hit_set_count 参数 pool.info.hit_set_period 不为空,pool.info.hit_set_params的参数不为HitSet::TYPE_NONE, 否则调用hit_set_clear 和函数hit_set_remove_all 去删除hit set 对象
- 调用函数hit_set_create 根据类型创建 相应的hit set
调用函数hit_set_apply_log 来添加上次hit_set 到本次 遗漏的
agent_setup
void ReplicatedPG::agent_setup()
首先检查各种参数
- 设置agent_state的参数
- 调用函数agent_choose_mode
读写流程中Cache Tier的处理
在函数 ReplicatedPG::do_osd_ops 里,有cache tier 相关的处理代码。
maybe_handle_cache_detail
在这个函数里
do_proxy_read
这个函数把请求直接发送给data_pool
- 首先设置oloc.pool 为pool.info.tier_of,也就是是data_pool, 并构建soid 对象
- 设置 flags 设置为CEPH_OSD_FLAG_IGNORE_CACHE | CEPH_OSD_FLAG_IGNORE_OVERLAY,并且把m->get_flags 带的标志也设置上,但是取消掉一些标志:
- CEPH_OSD_FLAG_RWORDERED|CEPH_OSD_FLAG_ORDERSNAP|CEPH_OSD_FLAG_ENFORCE_SNAPC |CEPH_OSD_FLAG_MAP_SNAP_CLONE4)通过调研函数 osd->objecter->read 发送出去
finish_proxy_read
函数finish_proxy_read完成了 prox_read的回调函数,其主要对结果做检查,并清理等待列表complete_read_ctx
给client 端发送返回消息
do_proxy_write
和 do_proxy_read 类似finish_proxy_writedo_proxy_write的回调函数,完成给客户端的reply
promote_object
完成从data_pool 读取相关的对象,并写入cache_pool 里 1)首先检查scrubber.write_blocked_by_scrub 是否被block 住 ####start_copy
_copy_some
process_copy_chunk
Gather 的回调函数
finish_promotevoid
Agent后台的处理flush和evict
在类OSDService中,定义了一个AgentThread的background的线程,来处理dirty 对象从cache pool 层的 flush, 以及evict 掉一些cache pool层不经常访问的对象。其线程函数就是agent_entry 函数
相关数据结构:
Class OSDService{
.....
Mutex agent_lock; //agent thread的 lock,保护以下所有的数据结构
Cond agent_cond; //线程相应的条件变量
map<uint64_t, set<PGRef> > agent_queue;
//所有flush 或者 evict 需要的 pg集合,其根据PG的集合的优先级,保存在不同的map 中
set<PGRef>::iterator agent_queue_pos;
//当前在扫描的PG集合的一个位置,只有agent_valid_iterator 为true时,这个指针在有效,否则从集合的起始处扫描
bool agent_valid_iterator;
int agent_ops; // all pending agent ops include flush and evict //所有正在进行的 flush 和 evict 操作
int flush_mode_high_count; //once have one pg with FLUSH_MODE_HIGH then flush objects with high speed
//如果 flush 模式为HIGH 模式,该该值就增加
set<hobject_t, hobject_t::BitwiseComparator> agent_oids; // //所有正在进行的agent的操作(flush or evict)的对象
bool agent_active;
//agent 是否有效
struct AgentThread : public Thread {
OSDService *osd;
explicit AgentThread(OSDService *o) : osd(o) {}
void *entry() {
osd->agent_entry();
return NULL;
}
} agent_thread;
bool agent_stop_flag; //agent 停止的标志
Mutex agent_timer_lock; //
SafeTimer agent_timer; //agent相关的定时器,该定时器用于:当扫描一个PG的对象时,该既没有evict操作,也没有flush操作,就停止PG的扫描,把该PG加入到定时器,5s后继续
......
}
agent_entry
agent_entry 是agent_thread的 入口函数,其在后台完成flush 和 evict 操作。
void OSDService::agent_entry()
- 首先加agent_lock 锁, 该锁保护OSDService里所有和agent相关的字段
- 如果agent_stop_flag 为true,就解锁agent_lock, 并退出,否则继续以下操作。
- 首先扫描agent_queue队列,如果agent_queue为空,就在条件变量agent_cond上等待
- 从队列agent_queue中取出level 最高的 PG的集合top(始终从level 最高的取),如果top集合为空,就把该集合从agent_queue中删除,并使agent_valid_iterator 集合内的pg 指针设为false,使其无效。
- 检查如果正在进行的agent操作数agent_ops 大于 配置的最大允许的agent 操作的数量g_conf->osd_agent_max_ops ,或者 非agent_active, 就等待
- 如果agent_valid_iterator 有效,就从agent_queue_pos 获取该PG set 中的一个pg,否则从该set的头开始, 获取相应的PG的指针
- 获取max,也就是agent 操作的最大数目,以及agent_flush_quota 的值。
- 调用pg->agent_work, 正常返回true, 如果返回值为false,该PG 处于delay 状态,需要加入agent_timer 定时器,设置的g_conf->osd_agent_delay_time秒后,调用agent_choose_mode 重新设置模式。
agent_work
在ReplicatedPG的内部,变量agent_state 用来保存pg 相关的agent的信息。
struct TierAgentState {
//pg内 当前扫描的的位置
hobject_t position;
/// Count of agent_work since "start" position of object hash space
int started; // pg里所有对象扫描完成后,所发起的所有的agent的操作数目。 当pg 扫描完成所有的对象,如果没有agent 操作,就需要 delay一定时间
hobject_t start; //本次扫描起始位置
bool delaying; //是否delay
/// histogram of ages we've encountered
pow2_hist_t temp_hist;
int hist_age; //
/// past HitSet(s) (not current)
map<time_t,HitSetRef> hit_set_map;
/// a few recent things we've seen that are clean
list<hobject_t> recent_clean;
}
bool ReplicatedPG::agent_work(int start_max, int agent_flush_quota) agent_work 是一个PG内做相关的evict 和 flush 操作的函数。其主要的流程如下:
- 首先调用pg的lock() 加锁, agent_state 数据结构受它保护
- 检查如果agent_state 为空,或者agent_state->is_idle(),就返回true,解锁退出。
- 调用函数加载hit_set, agent_load_hit_sets
- 调用函数 pgbackend->objects_list_partial来扫描本 PG的对象,从agent_state->position 开始位置扫描,结构保存在 ls 向量中,最小扫描1 一个对象,最大扫描osd_pool_default_cache_max_evict_check_size 个对象
- 对扫描到的ls 中的对象,做检查:
- 跳过 hitset 对象
- 跳过 degraded 对象
- 跳过 missing 的对象
- 跳过 object context 不存在的对象
- 跳过对象的obs 不存储
- 跳过正在scrub的对象
- 跳过已经被blocked 的对象
- 跳过有正在读写请求的对象
- 如果base_pool 不支持omap,而该对象有omap,就跳过
- 如果agent_state->evict_mode != TierAgentState::EVICT_MODE_IDLE,调用 agent_maybe_evict,
- 否则如果当前的flush_mode 不是IDLE状态,就调用agent_maybe_flush 函数
- 如果started 大于agent_max, 也就是已经发起的agent 操作数目大于最大允许的agent_max数目,就停止,退出。 并设置next的指针,也就是下次开始扫描的对象的开始位置。 如果ls 没有扫描结束,就设置为当前指针p
- 更新agent_state->hist_age, 如果agent_state->hist_age 大于g_conf->osd_agent_hist_halflife, 就重置为0,agent_state->temp_hist.decay()
- 比较扫描的指针,如果pg的对象扫描了一圈,total_started 为 0, 也就是没有agent 操作,就需要delay, 设置need_delay 为true
- 调用函数 hit_set_in_memory_trim 删除一个hitset
- 如果需要delay,就掉函数agent_delay, 把该pg从agent_queue 中删除。
- 调用 agent_choose_mode 重新计算 agent的mode
agent_maybe_evict
函数agent_maybe_evict 负责一个对象的evict 的操作
- 首先检查对象的状态
- 如果不是after_flush, 就需要clean 状态
- 如果该对象还有watcher,说明还有客户端使用,跳过
- 对象是否blocked
- 跳过cache_pined的对象
- 验证 clone 对象 是否一致
- 如果evict_mode 为 EVICT_MODE_FULL , 就直接发请求删除。
- 如果evict_mode 为 EVICT_MODE_SOME ,就需要:
- 首先确保 该对象 clean的时间大于 pool.info.cache_min_evict_age
- 其次计算effort 大于 agent_state->evict_effort
agent_maybe_flush
start_flush
- 首先调用obc->ssc->snapset.get_filtered 把已经删除的snap 过滤掉
- 检查更老的snap 是否 cleana)首先查找比较当前的snap 更old的 clone 为nextb)如果该clone 处于miss状态,就直接返回 -ENOENT
- 获取该next的 object_context 存储,并且为dirty,直接返回EBUSY, 如果不存在,就默认为clean状态 例如: cloes: 1 4 5 8 当前 snap 为 5, 就必须确保1 4 处于clean 状态
- 如果blocking设置为true,就设置blocked4)在正在flush_ops里查找到,也就是正在flushing5)正式开始flush Flush 必须按照顺序进行
hit_set_persist
void ReplicatedPG::hit_set_persist()hit_set_count hit_set_period 多长时间保存一次 hit set