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 天前
什么是Ceph?它的技术特点是什么?部署挑战及解决方案如何?
ceph·计算机·存储
活跃的煤矿打工人2 天前
【星海随笔】删除ceph
linux·服务器·ceph
怡雪~3 天前
k8s使用ceph
ceph·容器·kubernetes
怡雪~5 天前
Kubernetes使用Ceph存储
ceph·容器·kubernetes
学Linux的语莫15 天前
负载均衡,高可用,监控服务搭建总结
linux·服务器·分布式·ceph·lvs
运维小文15 天前
cephFS的使用以及K8S对接cephFS
ceph·云原生·容器·kubernetes·对象存储·cephfs
学Linux的语莫18 天前
ceph集群搭建,ceph块存储,文件存储,对象存储
linux·服务器·分布式·ceph
Rverdoser18 天前
K8S对接ceph的RBD块存储
ceph·容器·kubernetes
学Linux的语莫22 天前
Ceph对象存储
linux·运维·服务器·ceph
q_9722 天前
ceph基本概念
ceph