ceph的 cache tier实现分析
Posted OshynSong
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ceph的 cache tier实现分析相关的知识,希望对你有一定的参考价值。
1 基本介绍
1.1 设计思想
数据的存储可划分为active和inactive两大类,active数据是小部分,会频繁访问,使用更高性能的底层存储介质进行存储;inactive的数据是全集,使用廉价的存储介质存储。这种分冷热的思想与CPU的多级缓存、操作系统的cache、各类软件系统的软件缓存是相通的,但ceph通过软件层面的配合,充分利用不同的硬件介质和软件存储模式,为上层用户提供透明统一的访问接口。
目的:在兼顾存储成本前提下,通过配合使用不同的存储介质,并使用不同的软件存储模式,达到更好的数据存取性能。
说明:cache tier是ceph在其抽象的ObjectStore层面实现的冷热分层,上层的RGW/CEPHFS/RBD均可受益,其中RGW本身还可据此实现生命周期功能。
1.2 实现要点
- CRUSH Map:通过指定不同的root,并创建多个相应的crush_rule,用来从软件上定义如何使用底层存储介质
- pool的crush_rule:创建不同存储池,为各自设置对应的crush_rule
- pool的存储策略:通过crush_rule的类型来指定软件层面的存储模式,包括多副本(副本数可配)、EC(K、M可配)
- tier关联:动态配置两个存储池的关联关系和迁移策略,包括none、writeback、forward、readonly、readforward、proxy、readproxy
- Objecter:屏蔽底层实现,为客户端的读写请求提供统一接口
1.3 总体架构
ceph官网提供的tier存储技术的总体架构如下图:
对于设置了cache tier的存储池,客户的读写请求是无感的,通过单独的objecter模块负责实现cache tier和storage tier的联动,并根据管理员配置的各项cache tier的策略,自动实现数据的更新和同步。
1.4 特殊性
- 适用于热点访问场景
添加tier之后,能够提升总体的性能与具体的数据存取场景高度依赖,因为需要将热存储池的数据迁移到冷存储池,因此对访问小部分热点数据的场景比较适合,绝大多数请求只会访问一小部分数据。 - 通用场景性能较差且benchmark困难
通用的非热点访问的场景下,都不是cache友好的,都会有性能损失。另外通用的benckmark方法无法发挥tier的优势,不会只访问一小部分的数据,因此性能数据也表现不佳。 - 对象遍历操作不稳定
由于tier层的小部分热数据的存在,如果用户直接调用librados库的对象遍历API,可能会得到不一致的结果,但是直接使用RGW/RBD/CephFS没有影响。 - 复杂性提升
使用tier后,会为Rados层带来额外的复杂性,可能会增加出现bug的风险。
2 功能实现分析
cache tier是在ceph抽象的ObjectStore层面,处于底层的统一的KV存储层,其具体实现依附于ceph的存储池,cache tier作为存储池的属性,依据用户配置,存储池在完成具体的读写操作时提供相应的cache tier功能。ceph将存储池进行分片(PG),在具体实现功能时又是基于PG为最基本的单位来实现。
2.1 参数设计
cache tier的参数是存储池的属性,底层数据结构定义为pg_pool_t:
struct pg_pool_t ... 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 consi CACHEMODE_READFORWARD = 4, /// forward reads, write to cache flush later CACHEMODE_READPROXY = 5, /// proxy reads, write to cache flush later CACHEMODE_PROXY = 6, /// proxy if not in cache cache_mode_t; ... set uint64_t tiers; /// pools that are tiers of us int64_t tier_of; /// pool for which we are a tier(-1为没有tier) // Note that write wins for read+write ops int64_t read_tier; /// pool/tier for objecter to direct reads(-1为没有tier) int64_t write_tier; /// pool/tier for objecter to direct write(-1为没有tier) 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 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 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 deca ///by this params than pre hit_set uint32_t hit_set_search_last_n; /// accumulate atmost N hit_sets for temperature bool is_tier() const return tier_of >= 0; bool has_tiers() const return !tiers.empty(); void clear_tier() tier_of = -1; clear_read_tier(); clear_write_tier(); clear_tier_tunables(); bool has_read_tier() const return read_tier >= 0; void clear_read_tier() read_tier = -1; bool has_write_tier() const return write_tier >= 0; void clear_write_tier() write_tier = -1; void clear_tier_tunables() if (cache_mode != CACHEMODE_NONE) flags |= FLAG_INCOMPLETE_CLONES; cache_mode = CACHEMODE_NONE; target_max_bytes = 0; target_max_objects = 0; cache_target_dirty_ratio_micro = 0; cache_target_dirty_high_ratio_micro = 0; cache_target_full_ratio_micro = 0; hit_set_params = HitSet::Params(); hit_set_period = 0; hit_set_count = 0; hit_set_grade_decay_rate = 0; hit_set_search_last_n = 0; grade_table.resize(0); ... ;
这些参数详细记录了cache tier的具体实现细节,总结如下:
- tier的存储池:自身的tier pool ID(
tier_of
字段),以及以自身作为tier的其他pool的ID集合(tiers
字段) - cache mode:定义了六种cache模式,默认为
CACHEMODE_NONE
,按照用户配置执行相应的操作,为cache tier实现的核心部分 - cache tunable参数:在执行flush和evict操作时,用户可为cache这两种行为设置的多种触发参数,以便和具体的应用场景匹配
2.2 实现主体
基于为cache tier定义的各项参数,在具体实现的时候,依附于PG为主体实现具体的功能。pg_pool_t
作为底层存储的定义,属于PGPool
的info
字段,用来记录一个存储池的详细属性。PG
类定义了在存储池上完成最基本数据读写的实体,包含了一个它所属的存储池的pool
字段。
struct PGPool CephContext* cct; epoch_t cached_epoch; int64_t id; string name; uint64_t auid; pg_pool_t info; ... ; class PG : public DoutPrefixProvider ... protected: PGPool pool; ... ;
PG
类定义了针对全部数据请求的所有功能,主要包括shard分片、recovery和backfill功能、状态收集和统计、blocked请求的等待处理、scrub处理等功能,对于具体的数据读写请求的处理,定义了必须进一步实现的全部抽象接口,其中所有请求的入口接口如下:
virtual void do_request(OpRequestRef& op, ThreadPool::TPHandle &handle) = 0;
目前的PG针对数据读写请求的全部实现都由PrimaryLogPG
类负责,这里主要关注cache tier相关的具体实现。
2.3 Objecter
Objecter本身实现上是一个Dispatcher,对外可作为OSD的一个client,负责向tier的storage层的存储池读写数据;对内作为本存储池的OSD的一个成员,负责在
各个cache mode的具体实现中,生成具体的Operation OP并提交。
2.3.1 注册与启动
OSD的main函数创建了7个messenger用来处理各类网络rpc消息,其中包括为Objecter创建对独立messenger。OSD类为一个Dispatcher,负责实现网络处理的各个接口,其包含一个OSDService对象,负责完成除网络交互之外的具体实现。Objecter为OSDService的一个成员,由OSDService在构造时动态创建Objecter对象,并在构造函数调用Objecter的init函数初始化Objecter的内部数据结构。
在对象创建完成之后,main函数启动这7个messenger,此时还不能无法处理网络请求,随后调用OSD的init函数,其负责将其OSDService成员的Objecter成员注册到相应messenger的dispatcher队列:
objecter_messenger->add_dispatcher_head(service.objecter);
同时,也会将OSD自身添加到其他messenger的Dispatcher队列,并完成一系列的如初始化MON和MGR的client对象、启动OSD的多个工作线程池、启动tick等初始化操作,其中包括连接MON进行鉴权,在鉴权通过之后调用OSDService成员的final_init函数,负责启动Objecter:
void OSDService::final_init() objecter->start(osdmap.get());
2.3.2 Tick与OSDSession
Objecter的start函数用来启动objecter服务,其工作内容为:启动tick并拷贝一份osdmap。tick的间隔时间由配置文件制定:objecter_tick_interval
,默认为5秒。Objecter会通过拷贝的osdmap信息,主动向其他目标OSD发起连接,并以内嵌类OSDSession表示,本身维护了一个osd ID到OSDSession的映射结构map<int,OSDSession*>
。OSDSession的成员包括目标OSD的ID、底层网络连接等基础信息之外,主要包括到目标OSD的各类OP操作的队列:
struct OSDSession ... map ops; map linger_ops; map command_ops; ... int osd; ConnectionRef con; ... ;
每一次tick的任务就是遍历Objecter维护的OSDSession映射结构,循环检查它的OP队列,包括普通的OP队列、linger OP队列、command OP队列,依据如下标准执行是否发送依次MPing消息:
- 普通OP:配置的
objecter_timeout
,默认10秒,依据OP的开始时间是否超过这个超时时间来决定是否发送 - linger OP(执行比较缓慢的操作):只要存在于队列中就会发送
- command OP:只要存在于队列中就会发送
2.3.3 ObjectOperation
Objecter负责为cache tier的具体实现中需要与其他存储池的OSD进行数据交互时提供支持,其本质就是执行一系列ObjectStore抽象的操作,并为cache tier封装一些特定的操作,这部分统一由ObjectOperation类负责完成,实现非常直接:
struct ObjectOperation vector ops; int flags; int priority; vector out_bl; vector out_handler; vector out_rval; ;
一共就上述6个成员,最重要的就是OSDOp构成的数组,对于一个具体的操作,可能对应多个OSDOp操作,或者可以将多个操作的OP合并后发送,另外包含一个标志位和优先级;同时定义了三个同样与OSDOp对应的输出信息的数组,包括输出内容、输出handler、返回值。
在此基础上,使用一个add_op
函数添加一个新的OSDOp成员到内部的ops
数组中,并返回引用,再定义了大量的针对Object的具体操作,用来支持cache tier的具体实现,主要分为如下几类:
- ObjectStore抽象的对象操作:如
stat
、read
、write
、append
、getxattr
、omap_get_keys
、omap_get_values
等 - PG和Scrub操作:包括
pg_ls
、scrub_ls
,用来从其他OSD获取PG、Scrub信息 - 针对cache tier特殊操作:包括
is_dirty
、undirty
、hit_set_ls/get
、copy_get
、copy_from
、cache_flush/try_flush
、cache_evict
、cache_pin
、cache_unpin
针对cache tier的操作为实现cache tier的不同模式提供了便利,其中flush和evict直接影响cache tier的行为,其具体处理方式如下:
- flush操作:若给定的cache tier中的对象是dirty的,就将其写入到backing tier;如果对象是clean,则不做任何操作。当该操作与update并发执行时,
cache_flush
将会阻塞update操作,cache_try_flush
则会立即返回一个EAGAIN错误而不会阻塞。 - evict操作:如果给定的cache tier中的对象是clean则将其从cache tier中删除,否则返回EBUSY。
另外提供的cache_pin
、cache_unpin
还可以将某个对象在cache tier中进行锁定和解锁。
3.3.4 具体实现
Objecter要主动向其他OSD发起各类OP操作,封装了具体的方法,包括write
、read
、pg_read
、getxattr
等,每个方法使用的具体OP,通过上述ObjectOperation类封装的工厂方法来构造,创建好之后调用统一的op_submit
函数进行提交,最终从维护的OSDSession映射结构中获取底层网络连接,调用其send_message
方法将OP发送出去。对于read
方法,其具体实现如下:
Op *prepare_read_op( const object_t& oid, const object_locator_t& oloc, ObjectOperation& op, snapid_t snapid, bufferlist *pbl, int flags, Context *onack, version_t *objver = NULL, int *data_offset = NULL, uint64_t features = 0, ZTracer::Trace *parent_trace = nullptr) Op *o = new Op(oid, oloc, op.ops, flags | global_op_flags | CEPH_OSD_FLAG_READ, onack, objver, data_offset, parent_trace); o->priority = op.priority; o->snapid = snapid; o->outbl = pbl; if (!o->outbl && op.size() == 1 && op.out_bl[0]->length()) o->outbl = op.out_bl[0]; o->out_bl.swap(op.out_bl); o->out_handler.swap(op.out_handler); o->out_rval.swap(op.out_rval); return o; ceph_tid_t read( const object_t& oid, const object_locator_t& oloc, ObjectOperation& op, snapid_t snapid, bufferlist *pbl, int flags, Context *onack, version_t *objver = NULL, int *data_offset = NULL, uint64_t features = 0) Op *o = prepare_read_op(oid, oloc, op, snapid, pbl, flags, onack, objver, data_offset); if (features) o->features = features; ceph_tid_t tid; op_submit(o, &tid); return tid; void Objecter::op_submit(Op *op, ceph_tid_t *ptid, int *ctx_budget) shunique_lock rl(rwlock, ceph::acquire_shared); ... _op_submit(op, rl, ptid); void Objecter::_op_submit(Op *op, shunique_lock& sul, ceph_tid_t *ptid) ... MOSDOp * m = _prepare_osd_op(op); ... _send_op(op, m); void Objecter::_send_op(Op *op, MOSDOp *m) ... op->session->con->send_message(m); ...
Objecter作为可与其他OSD进行数据读写交互的模块,除了发送OP之外,还需接收响应,因此实现了Dispatcher的全部接口,重新定义ms_dispatch
函数来处理其他OSD返回给自己的信息。通过接收的消息类型进行分发,只支持如下几种消息:
CEPH_MSG_OSD_OPREPLY
:普通的OSD读写等OP操作的响应,可进行fast dispatch,调用handle_osd_op_reply
处理CEPH_MSG_OSD_BACKOFF
:执行BACKOFF的消息,调用handle_osd_backoff
处理CEPH_MSG_WATCH_NOTIFY
:可进行fast dispatch,调用handle_watch_notify
处理MSG_COMMAND_REPLY
:仅在发送消息的源也是OSD时才会处理,调用handle_command_reply
处理,否则不处理MSG_GETPOOLSTATSREPLY
:获取存储池统计信息的返回,调用handle_get_pool_stats_reply
处理CEPH_MSG_POOLOP_REPLY
:执行存储池操作的返回,调用handle_pool_op_reply
处理CEPH_MSG_STATFS_REPLY
:获取文件系统信息的返回,调用handle_fs_stats_reply
处理CEPH_MSG_OSD_MAP
:交互OSDMAP信息的处理,调用handle_osd_map
处理
这里我们主要关注第一类消息,就是针对OSD普通的读写等OP操作的响应对处理,具体流程如下:
- 从维护的OSDSession映射结构中获取该消息对应的OSDSession
- 判断是否需要重试,如果需要就从OSDSession中删除这个OP,并重新调用
_op_submit
提交该请求 - 获取消息的结果,判断是否需要进行redirect,如果需要就从OSDSession删除该OP,并添加新的redirect目的地后调用
_op_submit
重新提交 - 获取消息的输出数据、返回值,并据此调用这个OP创建时注册的处理完成时的回调handler
2.4 IO请求
由于cache tier依附于PG实现,因此cache tier的IO请求路径就是对OSD的任意一次IO请求路径,在这个路径上会判断PG所在的存储池是否是其他存储池的cache tier或是否有其他存储池为自身的cache tier,如果均没有则执行正常的读写请求;否则就依据具体的策略并基于Objecter提供的方法来完成对cache tier的处理。
2.5 各cache mode处理
对于配置了cache tier存储池,按照3.4节点IO请求路径,具体在maybe_handle_cache_detail
函数中,依据不同的cache模式进行switch-case分别进行处理, 在maybe_handle_cache_detail
函数中具体分为如下六种模式依次处理,详见下述分析。
2.5.1 FORWARD
FORWARD模式表示所有到达cache tier存储池的请求都不会处理,直接将它的后端存储池的ID回复给请求方,并返回-ENOENT
的错误号,具体实现比较简单。
该模式的用途是在删除WRITEBACK
模式的cache tier时,需将其cache mode先设置为FORWARD,并主动调用cache tier的flush和evict操作,确保cache tier存储池的对象全部evict和flush到后端存储池,保证这个过程中不会有新的数据写入。
2.5.2 READONLY
READONLY模式是指对于所有的写请求,都直接调用do_cache_redirect
函数,与FORWARD模式同样处理;对于所有的读请求,会先判断是否存在于cache tier存储池中,如果存在就直接返回,否则会先调用Objecter从后端存储池读取一份数据,并创建一个ObjectContext对象保存,将读取数据返回给客户。
start_copy
会创建一个CopyOp对象,该对象保存了请求的参数、返回值、大小、数据缓冲区、omap和xattr缓冲区等,最终调用OSDService的Objecter成员的read方法向目标存储池的OSD发起读取请求。之后调用wait_for_blocked_object
将该OP加入到内部维护的一个称为waiting_for_blocked_object
的map结构中,key为tid,value为OP。start_copy
在发起请求之前会设置好成功时的回调函数,这个回调函数会调用kick_object_context_blocked
用来从维护的map结构中查询到之前的OP,调用requeue_ops
将这个OP加入到OSD的请求队列中重新执行,并从map结构中删除。注意:requeue_ops
会使用enqueue_front
插入到OSD的ShardedOpWQ的开头。
2.5.3 PROXY
PROXY模式下,针对读写请求都会执行proxy,也就是作为一个代理向后端存储池发起请求并返回给客户端,除非强制要求先进行promote操作。
对于写请求调用do_proxy_write
,则会直接调用会调用OSDService的Objecter成员的mutate方法,将写请求直接写入到后端的存储池中,并记录到内部维护的proxywrite_ops
、in_progress_proxy_ops
两个map结构,另外设置了成功时的回调函数,在写入完成之后从维护的map结构中删除,并返回给客户端CEPH_OSD_FLAG_ACK | CEPH_OSD_FLAG_ONDISK
的响应。对于读请求调用do_proxy_read
,与写请求处理类似,直接作为代理端发送请求到后端存储池并等待结果完成,同样也会分别记录到两个map结构并在完成时删除。
这种模式下,读写请求的对象的数据都不会在cache tier存储池中保存,自身扮演为一个代理(proxy)的角色,这是与FORWARD模式的区别。
2.5.4 WRITEBACK
WRITEBACK模式是最复杂也是最有实用价值的模式,其具体实现会按照请求类型、cache状态综合判断,并复用前三种模式下的一些处理细节进行综合处理。处于这种模式下的cache tier存储池,其处理流程如下:
- 判断cache tier存储池的状态是否已满,如果已满,则对于读请求直接调用
do_proxy_read
,对于写请求直接将OP加入到waiting_for_cache_not_full
队列,并在下一次有新的请求达到时重新放入OP队列处理。 - 在cache tier未满的情况下,先判断是否必须进行promote,如果需要就调用
promote_object
,先阻塞当前请求,从后端存储池读取一份数据到cache tier存储池,并在完成之后再将当前请求加入OP队列 - 在cache tier未满且不会强制promote时,这也是最常见的情况下:对于写入请求,会先阻塞,调用
promote_object
从后端读取一份数据并保存,完成之后将当前请求重新加入OP队列,这样下一次执行这个读请求时,会判断已经存在于cache tier中,就直接写入在cache tier存储池中;对于读请求,则会首先调用do_proxy_read
从后端存储池读取数据但不保存在cache tier存储池中,之后再判断本地读请求是否需要跳过promote,这是在创建该OP时设置的一个flag,通过op->need_skip_promote
来判断,在所有的OP请求中有两种场景会设置不需promote,否则都会执行promote从后端存储池读取一份数据保存在cache tier存储池
其中会设置skip promote标志的两种情况如下:
CEPH_OSD_OP_DELETE
请求会设置skip promote- read、sync_read、sparse_read、checksum、writefull请求若设置了
CEPH_OSD_OP_FLAG_FADVISE_NOCACHE
或CEPH_OSD_OP_FLAG_FADVISE_DONTNEED
标志位就会设置skip promote
2.5.5 READFORWARD
READFORWARD是FORWARD与WRITEBACK模式的综合。对于所有读请求执行与FORWARD一样的处理,调用do_cache_redirect
,直接返回后端存储池给用户,并返回-ENOENT
的错误号;对于写请求则与WRITEBACK模式相同处理,先调用promote_object
从后端读取一份数据保存并加入到OP队列重新执行,把数据写入到cache tier存储池中。
2.5.6 READPROXY
READPROXY是PROXY与WRITEBACK模式的综合。对于所有读请求执行与PROXY模式一样的处理,调用do_proxy_read
,仅从后端存储池读取数据并返回给用户,不存储读取到的对象;对于写请求则与WRITEBACK模式相同处理,先调用promote_object
从后端读取一份数据并保存并加入到OP队列重新执行,把数据写入到cache tier存储池中。
3 总结
cache tier是ceph在底层抽象的统一软件定义存储ObjectStore的基础上,实现的一种缓存存储层,具体依托于ceph的CRUSH MAP和CRUSH RULE,针对不同的存储池配置相应的规则,从而映射到具有存储性能差异的硬件上,从而提升总体性能并降低存储成本。cache tier的实现具体是在底层的存储池层面,因此对于所有基于ObjectStore抽象实现的上层应用都可受惠,包括RGW、CEPHFS、RBD。cache tier本质上可以看做CPU与内存间的高速缓存的一个外延,当数据从内存要写入磁盘时,通过使用性能较高的磁盘作为cache tier层,来弥补内存与普通磁盘间的性能gap,该思想虽然也有如文件系统的page cache、磁盘本身的缓存等实现,但ceph的cache tier相当于是从软件定义存储的角度来对这种思想进行补充。
3.1 实用性
根据上述分析,cache tier虽然提供了六种模式,但FORWARD本身是一种为WRITEBACK切换提供的过渡模式,并无实用价值。PROXY模式则完全充当一个代理来转发请求,同样并无太大的实用价值。另外四种模式,最重要的就是WRITEBACK模式,这是cache tier实现的最具实用性的模式,在这种模式下配置相应的cache tier参数控制其行为。另外三种READONLY、READPROXY、READFORARD三种是针对读请求的比例不同而提供的针对性模式。下面总结了他们的实用场景:
- WRITEBACK:写请求较多或读写比较均匀的场景,读写的数据都会在cache tier存储池中保存,通过配置flush、evict参数执行向后端存储池的更新和剔除
- READONLY:适合以读为主的场景,对于极少量读写请求直接让客户端去写入到后端存储池,而读请求则会将对象缓存起来,提升总体读请求的性能
- READPROXY与READFORWARD:适合以写为主的场景,对于少量的读请求,可以选择使用proxy方式进行处理,也可以选择让客户端直接写入到后端存储池
3.2 局限性
cache tier的实现本身依附于存储池的分片,也就是PG,而PG本身要实现包括副本同步、快照、scrub、recovery等一系列功能,cache tier的实现与他们都混合在一起,大大增加了整体代码实现的复杂性,增加了在极端场景出现bug的可能性,从而对系统稳定性会有一定的影响。因此对于cache tier功能本身需要进行严格的测试,同时对于开启了cache tier的存储池,也需要谨慎的配置各项参数,严密的进行监控。
cache tier提供的多种模式虽然针对不同场景而设置,但并不全面,还有进一步开发的空间。首先可借鉴CPU多级缓存的思想,ceph可支持超过两级的cache tier的配置,但是由于目前的代码实现将其混合在PG中,这种扩展难度很大改动也非常大。其次,对于写请求较少的场景,仅提供了READONLY模式,也可提供类似CPU缓存的write through的模式,同时也还有write allocate等处理方式,均可借鉴,但这些扩展都受目前的实现方式的制约。
以上是关于ceph的 cache tier实现分析的主要内容,如果未能解决你的问题,请参考以下文章