Ceph Large omap objects现象及原理分析

Large omap objects现象

以下是真实的问题场景,以此文进行记录并分享。

Q1:集群出现了Large omap objects告警,这是什么问题?有什么影响?

Q2:Large omap objects告警的触发条件是什么?

Q3:这个告警怎么处理?或者怎么优化解决?

随着Ceph对象存储的产品不断成熟,用户数量的不断增加,对集群的性能考验也愈发严峻。特别是某些大型用户在特定场景下需要对单个bucket进行上传大量的对象,同时如果用户直接或者间接(应用程序调用list object接口)对单个bucket进行多次list object操作,会有一些IO响应慢,日志中可能会出现slow request,则可能会发现ceph出现了large omap objects告警。本文基于ceph luminous版本展开讨论,该版本引入了large omap objects告警功能。

集群出现如下large omap objects告警,下面对此展开分析:

出现上述告警后,运维人员可以通过ceph health detail查看具体的告警信息进行定位,一般large omap objects出现在存储池default.rgw.buckets.index。每个bucket在存储池default.rgw.buckets.index中都对应一个rados索引对象。存储池default.rgw.buckets.index中对象的格式为.dir.<marker>。

Bucket属性

先简单介绍bucket属性,并举例列出bucket的rados索引对象和bucket内对象名称的关系。

图1

上面给出了bucket的id,marker,owner,quota等属性。这里我们关注marker,通过marker信息,可以到对应的pool default.rgw.buckets.index找到该bucket,如下:

向bucket test1 分别上传obj1,obj2,obj3三个对象:

我们可以通过rados listomapkeys命令查看bucket的索引对象下的对象。

也就是说存储池default.rgw.buckets.index中bucket 对应的索引对象记录了bucket中对象等信息。这里联想单个bucket下大量对象的问题,大家应该会想到large omap objects的生成跟这个索引下的对象存在一定关系。

告警产生

接下来通过large omap objects中的告警信息追溯该告警产生的原因,并找出对应的指标值。

1. Large omap objects的告警来源如下:

void PGMap::get_health_checks(...) const

{

utime_t now = ceph_clock_now();

const auto max = cct->_conf->get_val<uint64_t>("mon_health_max_detail");

const auto& pools = osdmap.get_pools();

...

if (!detail.empty()) {

ostringstream ss;

ss << pg_sum.stats.sum.num_large_omap_objects << " large omap objects";//ceph -s中告警信息显示

auto& d = checks->add("LARGE_OMAP_OBJECTS", HEALTH_WARN, ss.str());

stringstream tip;

tip << "Search the cluster log for 'Large omap object found' for more "

<< "details.";

detail.push_back(tip.str());

d.detail.swap(detail);

}

...

}

从上述代码中可以发现large omap objects的数量统计来源于变量:pg_sum.stats.sum.num_large_omap_objects,那么通过对变量num_large_omap_objects的跟踪,得知large omap objects数量的来源是从ceph的deep scrub中检测出来的。

2. Ceph deep scrub检测large omap objects

void PG::scrub_finish()

{

...

if (deep_scrub) {

if ((scrubber.shallow_errors == 0) && (scrubber.deep_errors == 0))

info.history.last_clean_scrub_stamp = now;

info.stats.stats.sum.num_shallow_scrub_errors = scrubber.shallow_errors;

info.stats.stats.sum.num_deep_scrub_errors = scrubber.deep_errors;

info.stats.stats.sum.num_large_omap_objects = scrubber.omap_stats.large_omap_objects;

info.stats.stats.sum.num_omap_bytes = scrubber.omap_stats.omap_bytes;

info.stats.stats.sum.num_omap_keys = scrubber.omap_stats.omap_keys;

dout(25) << func << " shard " << pg_whoami << " num_omap_bytes = "

<< info.stats.stats.sum.num_omap_bytes << " num_omap_keys = "

<< info.stats.stats.sum.num_omap_keys << dendl;

publish_stats_to_osd();

} else {

...

}

上述的关键变量,并给他们做出解释。

num_large_omap_objects:指存储池中omap key或omap bytes超过阈值的shard数。

num_omap_bytes:对象的omap大小,这里的对象可以是bucket所有shard。

num_omap_keys:对象的omap key数量,这里的对象可以是bucket所有shard。

跟踪变量num_large_omap_objects这里的large_omap_objects是从下面的函数获取。

3.获取变量large_omap_objects

void PGBackend::be_omap_checks(...) const

{

...

ScrubMap::object& obj = it->second;

omap_stats.omap_bytes += obj.object_omap_bytes;

omap_stats.omap_keys += obj.object_omap_keys;

if (obj.large_omap_object_found) {

pg_t pg;

auto osdmap = get_osdmap();

osdmap->map_to_pg(k.pool, k.oid.name, k.get_key(), k.nspace, &pg);

pg_t mpg = osdmap->raw_pg_to_pg(pg);

omap_stats.large_omap_objects++;

warnstream << "Large omap object found. Object: " << k

<< " PG: " << pg << " (" << mpg << ")"

<< " Key count: " << obj.large_omap_object_key_count

<< " Size (bytes): " << obj.large_omap_object_value_size

<< '\n';

break;

}

...

}

从上述判断if (obj.large_omap_object_found)得知,需要根据变量large_omap_object_found来确定large omap对象的判断依据。

4.Ceph deep scrub获取omap相关属性,并将单个shard的omap_keys和omap_bytes跟阈值进行比较,判断当前shard是否为large omap objects。

int ReplicatedBackend::be_deep_scrub(...)

{

...

int max = g_conf->osd_deep_scrub_keys;

while (iter->status() == 0 && iter->valid()) {

pos.omap_bytes += iter->value().length();

++pos.omap_keys;//获取omap_keys数量

--max;

fixme: we can do this more efficiently.

bufferlist bl;

::encode(iter->key(), bl);

::encode(iter->value(), bl);

pos.omap_hash << bl;

iter->next();

...

}

if (pos.omap_keys > cct->_conf->

osd_deep_scrub_large_omap_object_key_threshold ||

pos.omap_bytes > cct->_conf->

osd_deep_scrub_large_omap_object_value_sum_threshold) {

dout(25) << func << " " << poid

<< " large omap object detected. Object has " << pos.omap_keys

<< " keys and size " << pos.omap_bytes << " bytes" << dendl;

o.large_omap_object_found = true;//比较每个分片的阈值

o.large_omap_object_key_count = pos.omap_keys;

o.large_omap_object_value_size = pos.omap_bytes;

map.has_large_omap_object_errors = true;

}

...

}

当变量large_omap_object_found为true时需要满足以下两个条件之一即可:

1.pos.omap_keys>osd_deep_scrub_large_omap_object_key_threshold

2.pos.omap_bytes>osd_deep_scrub_large_omap_object_value_sum_threshold

默认阈值:

osd_deep_scrub_large_omap_object_key_threshold为200000(20万)

osd_deep_scrub_large_omap_object_value_sum_threshold为1G

通过上述变量的跟踪,可以得知large omap objects是通过ceph deep scrub进行发现并上报的,遍历每个索引对象bucket中的分片(分片配置下文会讲),当分片的omap_keys的数量或omap_bytes的大小超过上述阈值时,会影响到large omap objects,并产生告警。

分片配置

当用户将大量对象(数百万个)都存在一个bucket中时,存储桶的索引操作会让性能受到严重影响。RGW中存在配置可以对索引进行分片,减少性能瓶颈。在对象存储中存在配置分片(shard)存在两个方法:

1. rgw_override_bucket_index_max_shards(默认值为0)

2. bucket_index_max_shards(这个在multisite中应用较多,这里不讨论)

在图1中看到的bucket info中rgw_override_bucket_index_max_shards值为0 ,表示桶的索引分片处于关闭状态。下面我们可以看下这个配置的生效阶段。

1. 获取bucket分片值

int RGWRados::init_complete()

{

...

bucket_index_max_shards = (cct->_conf->rgw_override_bucket_index_max_shards ? cct->_conf->rgw_override_bucket_index_max_shards :

get_zone().bucket_index_max_shards);

if (bucket_index_max_shards > get_max_bucket_shards()) {

bucket_index_max_shards = get_max_bucket_shards();//最大值不能超过65521

ldout(cct, 1) << func << " bucket index max shards is too large, reset to value: "

<< get_max_bucket_shards() << dendl;

}

ldout(cct, 20) << func << " bucket index max shards: " << bucket_index_max_shards << dendl;

...

}

上述bucket分片值在创建桶的时候进行了初始化,如下:

int RGWRados::create_bucket(...)

{

...

if (pmaster_num_shards) {

info.num_shards = *pmaster_num_shards;

} else {

info.num_shards = bucket_index_max_shards;

}

...

int r = init_bucket_index(info, info.num_shards);

...

}

bucket_index_max_shards最终写入到bucket info的num_shards中。

对象的shard分配

下面我们通过上传一个对象来跟踪该对象最终是怎么分配到具体的shard中的。为了测试方便我们将rgw_override_bucket_index_max_shards设置为5.

RGW中上传对象的函数入口:

int RGWRados::get_bucket_index_object(const string& bucket_oid_base, const string& obj_key,//上传的对象名称

uint32_t num_shards, RGWBucketInfo::BIShardsHashType hash_type, string *bucket_obj, int *shard_id)

{

int r = 0;

switch (hash_type) {

case RGWBucketInfo::MOD:

if (!num_shards) {

By default with no sharding, we use the bucket oid as itself

(*bucket_obj) = bucket_oid_base;

if (shard_id) {

*shard_id = -1;

}

} else {

uint32_t sid = rgw_bucket_shard_index(obj_key, num_shards);//用hash算法获取shard_id

...

}

}

break;

default:

r = -ENOTSUP;

}

return r;

}

inline函数如下,rgw_bucket_shard_index将obj_key和设置的shard数,通过哈希和求模算法,得到具体的分片序号。根据哈希的伪随机可知,同名对象的shard分配是固定的,也就是如果对象删除后,再次上传,在相同的shard上仍然能找到同名的对象。

static inline uint32_t rgw_bucket_shard_index(const std::string& key,

int num_shards) {

uint32_t sid = ceph_str_hash_linux(key.c_str(), key.size());

uint32_t sid2 = sid ^ ((sid & 0xFF) << 24);

return rgw_shards_mod(sid2, num_shards);

}

这里做如下测试,向同一个bucket中上传两次同名文件,但是大小不一样,验证其shard分片:

1.查看创建桶的shard数,rgw_override_bucket_index_max_shards值为5.对应的bucket的分片数也为5,格式为.dir.<markder>.<shard>.

2. 向测试桶testbk中上传对象test_obj,对象存储在分片2中,即.dir.<marker>.2。

3. 删除原test_obj,并上传不同大小的test_obj,对象仍然存储在分片2中

从上述测试结果来看,验证了同名对象分配的shard是不变的。

Bucket list请求

这次问题发生的前提条件就是通过RGW日志发现client存在大量的bucket list请求,导致IO无法及时响应,存在slow request问题。我们下面探索一下list请求的底层处理。

RGW请求处理

RGW作为client,在发出list object请求时,调用关系如下:

1. RGWRados::open_bucket_index调用如下函数,根据bucket_info.num_shards获取bucket全部的分片,便于后面进行遍历查找。

2. issue_bucket_list_op操作分类两个主要步骤:

1) 定义bucket list操作类型,便于OSD进行对应的请求处理。

2) 进行异步操作。

static bool issue_bucket_list_op(...) {

librados::ObjectReadOperation op;

cls_rgw_bucket_list_op(op, start_obj, filter_prefix,

num_entries, list_versions, pdata);

return manager->aio_operate(io_ctx, oid, &op);

}

上述函数首先定义了bucket list的操作类型,然后异步执行这个操作。我们进一步查看这个OP的类型如下。

通过cls_rgw_bucket_list_op定义了这个请求的OP类型,以便于后续OSD接收到请求后对应的请求处理。

void cls_rgw_bucket_list_op(...)

{

...

op.exec(RGW_CLASS, RGW_BUCKET_LIST, in, new ClsBucketIndexOpCtx<rgw_cls_list_ret>(result, NULL));

}

#define RGW_CLASS "rgw"

#define RGW_BUCKET_LIST "bucket_list"

以下是RGW请求发送及OSD请求处理关系图

图2 RGW,OSD发送消息处理

Op类型定义

通过宏定义知道这个OP类型是bucket_list操作,结合图2,后续OSD收到该请求后会调用对应的函数进行处理。

紧接着librados::ObjectOperation::exec à ObjectOperation::call

查看ObjectOperation::call,增加了OSD的操作类型是CEPH_OSD_OP_CALL。

void call(const char *cname, const char *method, bufferlist &indata,

bufferlist *outdata, Context *ctx, int *prval) {

add_call(CEPH_OSD_OP_CALL, cname, method, indata, outdata, ctx, prval);

}

请求异步执行

发送对应的请求给OSD处理。_send_op中定义的消息类型是MOSDOp

class MOSDOp : public MOSDFastDispatchOp

通过类的定义可知,继承了fastdispatch。消息类型是CEPH_MSG_OSD_OP。

MOSDOp()

: MOSDFastDispatchOp(CEPH_MSG_OSD_OP, HEAD_VERSION, COMPAT_VERSION),

partial_decode_needed(true),

final_decode_needed(true) { }

OSD消息处理

通过发送的fastdispatch消息类型定义,OSD会从ms_fast_dispatch入口进行处理,调用关系如下,OSD将此消息进行入队操作,如图2。

OSD消息队列处理

PrimaryLogPG::do_osd_ops函数中涉及到omap key操作的有三种情况:

1.case CEPH_OSD_OP_CALL

这里会处理RGW过来的bucket list请求操作。

ClassHandler::ClassMethod *method = cls->get_method(mname.c_str());

if (!method) {

dout(10) << "call method " << cname << "." << mname << " does not exist" << dendl;

result = -EOPNOTSUPP;

break;

}

int flags = method->get_flags();

if (flags & CLS_METHOD_WR)

ctx->user_modify = true;

bufferlist outdata;

dout(10) << "call method " << cname << "." << mname << dendl;

int prev_rd = ctx->num_read;

int prev_wr = ctx->num_write;

result = method->exec((cls_method_context_t)&ctx, indata, outdata);

上述method是调用RGW的bucket list,也就是OP类型定义中确定的rgw_bucket_list函数。然后调用read_bucket_header à cls_cxx_map_read_header

再次调用do_osd_ops 的case CEPH_OSD_OP_OMAPGETHEADER

int cls_cxx_map_read_header(cls_method_context_t hctx, bufferlist *outbl)

{

PrimaryLogPG::OpContext **pctx = (PrimaryLogPG::OpContext **)hctx;

vector<OSDOp> ops(1);

OSDOp& op = ops[0];

int ret;

op.op.op = CEPH_OSD_OP_OMAPGETHEADER;

ret = (*pctx)->pg->do_osd_ops(*pctx, ops);

if (ret < 0)

return ret;

outbl->claim(op.outdata);

return 0;

}

2.case CEPH_OSD_OP_OMAPGETHEADER

1. omap get header的处理情况

case CEPH_OSD_OP_OMAPGETHEADER:

tracepoint(osd, do_osd_op_pre_omapgetheader, soid.oid.name.c_str(), soid.snap.val);

if (!oi.is_omap()) {

return empty header

break;

}

++ctx->num_read;

{

osd->store->omap_get_header(ch, ghobject_t(soid), &osd_op.outdata);

ctx->delta_stats.num_rd_kb += SHIFT_ROUND_UP(osd_op.outdata.length(), 10);

ctx->delta_stats.num_rd++;

}

break;

2. 如果ceph底层采用filestore,那么调用关系如下:

FileStore::omap_get_header

---> DBObjectMap::get_header

int DBObjectMap::get_header(const ghobject_t &oid,

bufferlist *bl)

{

MapHeaderLock hl(this, oid);

Header header = lookup_map_header(hl, oid);

if (!header) {

return 0;

}

return _get_header(header, bl);

}

从上述函数可知,最后是从数据库中获取对应数据,并存入内存变量bl中

3.case CEPH_OSD_OP_OMAPGETKEYS

这里调用DBObjectMap::get_iterator获得数据库的迭代器,用来处理listomapkeys请求,从下面代码得知最终是根据oid从数据库中获取数据。最后将获得的key序列化到内存变量bl中。如果是对default.rgw.buckets.index中索引对象进行listomapkeys,那么这里encode到bl中的就是bucket内的对象名。

ObjectMap::ObjectMapIterator DBObjectMap::get_iterator(

const ghobject_t &oid)

{

MapHeaderLock hl(this, oid);

Header header = lookup_map_header(hl, oid);

if (!header)

return ObjectMapIterator(new EmptyIteratorImpl());

DBObjectMapIterator iter = _get_iterator(header);

iter->hlock.swap(hl);

return iter;

}

Object收到OSD消息的处理

C_ObjectOperation_decodekeys::finish如下:

void finish(int r) override {

if (r >= 0) {

bufferlist::iterator p = bl.begin();

try {

if (pattrs)

::decode(*pattrs, p);

if (ptruncated) {

std::set<std::string> ignore;

if (!pattrs) {

::decode(ignore, p);

pattrs = &ignore;

}

if (!p.end()) {

::decode(*ptruncated, p);

} else {

// the OSD did not provide this. since old OSDs do not

// enfoce omap result limits either, we can infer it from

// the size of the result

*ptruncated = (pattrs->size() == max_entries);

}

}

}

catch (buffer::error& e) {

if (prval)

*prval = -EIO;

}

}

}

将传入的bl进行反序列化得到该bucket下的对象名称。

rados listomapkeys

用户采用RGW进行list objects操作时,其实是对bucket instance的每个分片进行遍历。底层提供了rados listomapkeys命令可以对单个分片进行list操作,下面简单介绍其调用关系。

我们从命令说起,如下:

rados -p default.rgw.buckets.index listomapkeys .dir.<marker>.<shard_id>

命令执行的入口

rados_tool_common

->librados::IoCtx::omap_get_keys

在librados::IoCtx::omap_get_keys分为两个重要的操作

  1. ibrados::ObjectReadOperation::omap_get_keys2 ->librados::ObjectReadOperation::omap_get_keys:定义请求OP类型为CEPH_OSD_OP_OMAPGETKEYS

  2. librados::IoCtx::operate:发送操作的消息至OSD。后续的消息发送路径与RGW的请求一致。这里给出调用关系:librados::IoCtxImpl::operate_read---> ... Objecter::_send_op

OSD根据请求类型CEPH_OSD_OP_OMAPGETKEYS,在PrimaryLogPG::do_osd_ops对该场景进行处理,获取bucket instance分片下的omap keys即对象名称。

总结

1.large omap objects的告警信息来源于ceph deep scrub。

2.假设存在如下值,根据以上的分析逻辑,当pool default.rgw.buckets.index中单个bucket分片超过20万时会出现告警,那么在不产生large omap objects告警的情况下,单个bucket最多存放64*20万=1280万个对象。

osd_deep_scrub_large_omap_object_key_threshold=200000(20万,默认值)

rgw_override_bucket_index_max_shards=64

3.如果想提高bucket list性能,业界常用的做法是用SSD为索引pool加速,同时修改以上两个参数值到一个合理值。

相关推荐
斯普信专业组2 天前
k8s云原生rook-ceph pvc快照与恢复(下)
ceph·云原生·kubernetes
斯普信专业组2 天前
k8s云原生rook-ceph pvc快照与恢复(上)
ceph·云原生·kubernetes
斯普信专业组3 天前
Ceph、K8s、CSI、PVC、PV 深入详解
ceph·容器·kubernetes
mixboot15 天前
Ceph OSD.419 故障分析
ceph·osd
赵成ccc15 天前
离线部署三节点 Ceph 分布式存储
分布式·ceph
赵成ccc15 天前
三节点Ceph分布式存储搭建指南
分布式·ceph
免檒15 天前
windows11下基于docker单机部署ceph集群
ceph·后端·docker·容器
mixboot15 天前
Ceph集群OSD崩溃恢复
ceph·osd
羌俊恩19 天前
分布式存储之Ceph使用指南--部署篇(未完待续)
分布式·ceph·pg·osd·rados
大咖分享课25 天前
深度剖析:Ceph分布式存储系统架构
分布式·ceph·架构·分布式存储