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分为两个重要的操作
-
ibrados::ObjectReadOperation::omap_get_keys2 ->librados::ObjectReadOperation::omap_get_keys:定义请求OP类型为CEPH_OSD_OP_OMAPGETKEYS
-
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加速,同时修改以上两个参数值到一个合理值。