第二章: 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 tiers; 如果本pool 是 data pool, tiers 集合保存了该pool的所有的cache pool int64_t tier_of 如果本pool 是cache pool,该字段就保存了相应的data pool

当用命令 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数据结构

  1. 首先检查pg如果不是处于active 状态,并且不是primary 状态,就调用函数hit_set_clear,直接返回
  2. 检查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 对象
  3. 调用函数hit_set_create 根据类型创建 相应的hit set
  4. 调用函数hit_set_apply_log 来添加上次hit_set 到本次 遗漏的

    agent_setup

    void ReplicatedPG::agent_setup()

  5. 首先检查各种参数

  6. 设置agent_state的参数
  7. 调用函数agent_choose_mode

读写流程中Cache Tier的处理

在函数 ReplicatedPG::do_osd_ops 里,有cache tier 相关的处理代码。

maybe_handle_cache_detail

在这个函数里

do_proxy_read

这个函数把请求直接发送给data_pool

  1. 首先设置oloc.pool 为pool.info.tier_of,也就是是data_pool, 并构建soid 对象
  2. 设置 flags 设置为CEPH_OSD_FLAG_IGNORE_CACHE | CEPH_OSD_FLAG_IGNORE_OVERLAY,并且把m->get_flags 带的标志也设置上,但是取消掉一些标志:
  3. 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()

  1. 首先加agent_lock 锁, 该锁保护OSDService里所有和agent相关的字段
  2. 如果agent_stop_flag 为true,就解锁agent_lock, 并退出,否则继续以下操作。
  3. 首先扫描agent_queue队列,如果agent_queue为空,就在条件变量agent_cond上等待
  4. 从队列agent_queue中取出level 最高的 PG的集合top(始终从level 最高的取),如果top集合为空,就把该集合从agent_queue中删除,并使agent_valid_iterator 集合内的pg 指针设为false,使其无效。
  5. 检查如果正在进行的agent操作数agent_ops 大于 配置的最大允许的agent 操作的数量g_conf->osd_agent_max_ops ,或者 非agent_active, 就等待
  6. 如果agent_valid_iterator 有效,就从agent_queue_pos 获取该PG set 中的一个pg,否则从该set的头开始, 获取相应的PG的指针
  7. 获取max,也就是agent 操作的最大数目,以及agent_flush_quota 的值。
  8. 调用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 操作的函数。其主要的流程如下:

  1. 首先调用pg的lock() 加锁, agent_state 数据结构受它保护
  2. 检查如果agent_state 为空,或者agent_state->is_idle(),就返回true,解锁退出。
  3. 调用函数加载hit_set, agent_load_hit_sets
  4. 调用函数 pgbackend->objects_list_partial来扫描本 PG的对象,从agent_state->position 开始位置扫描,结构保存在 ls 向量中,最小扫描1 一个对象,最大扫描osd_pool_default_cache_max_evict_check_size 个对象
  5. 对扫描到的ls 中的对象,做检查:
    1. 跳过 hitset 对象
    2. 跳过 degraded 对象
    3. 跳过 missing 的对象
    4. 跳过 object context 不存在的对象
    5. 跳过对象的obs 不存储
    6. 跳过正在scrub的对象
    7. 跳过已经被blocked 的对象
    8. 跳过有正在读写请求的对象
    9. 如果base_pool 不支持omap,而该对象有omap,就跳过
    10. 如果agent_state->evict_mode != TierAgentState::EVICT_MODE_IDLE,调用 agent_maybe_evict,
    11. 否则如果当前的flush_mode 不是IDLE状态,就调用agent_maybe_flush 函数
    12. 如果started 大于agent_max, 也就是已经发起的agent 操作数目大于最大允许的agent_max数目,就停止,退出。 并设置next的指针,也就是下次开始扫描的对象的开始位置。 如果ls 没有扫描结束,就设置为当前指针p
  6. 更新agent_state->hist_age, 如果agent_state->hist_age 大于g_conf->osd_agent_hist_halflife, 就重置为0,agent_state->temp_hist.decay()
  7. 比较扫描的指针,如果pg的对象扫描了一圈,total_started 为 0, 也就是没有agent 操作,就需要delay, 设置need_delay 为true
  8. 调用函数 hit_set_in_memory_trim 删除一个hitset
  9. 如果需要delay,就掉函数agent_delay, 把该pg从agent_queue 中删除。
  10. 调用 agent_choose_mode 重新计算 agent的mode

agent_maybe_evict

函数agent_maybe_evict 负责一个对象的evict 的操作

  1. 首先检查对象的状态
    1. 如果不是after_flush, 就需要clean 状态
    2. 如果该对象还有watcher,说明还有客户端使用,跳过
    3. 对象是否blocked
    4. 跳过cache_pined的对象
    5. 验证 clone 对象 是否一致
  2. 如果evict_mode 为 EVICT_MODE_FULL , 就直接发请求删除。
  3. 如果evict_mode 为 EVICT_MODE_SOME ,就需要:
    1. 首先确保 该对象 clean的时间大于 pool.info.cache_min_evict_age
    2. 其次计算effort 大于 agent_state->evict_effort

agent_maybe_flush

start_flush

  1. 首先调用obc->ssc->snapset.get_filtered 把已经删除的snap 过滤掉
  2. 检查更老的snap 是否 cleana)首先查找比较当前的snap 更old的 clone 为nextb)如果该clone 处于miss状态,就直接返回 -ENOENT
  3. 获取该next的 object_context 存储,并且为dirty,直接返回EBUSY, 如果不存在,就默认为clean状态 例如: cloes: 1 4 5 8 当前 snap 为 5, 就必须确保1 4 处于clean 状态
  4. 如果blocking设置为true,就设置blocked4)在正在flush_ops里查找到,也就是正在flushing5)正式开始flush Flush 必须按照顺序进行

hit_set_persist

void ReplicatedPG::hit_set_persist()hit_set_count hit_set_period 多长时间保存一次 hit set