【问题描述】
L版本 12.2.13
空置一年的集群,检查发现大量OSD的meta容量占用特别高;
重启这些OSD,会长时间卡在BlueFS::_replay()函数里;
这些OSD最终有的可以启动,有的会因为Log的校验报错而停止启动;
replay时间以小时计,盘越大的时间越长。
独立DB分区的OSD从未出现过Log容量无限增长的问题,而未单独配置block.db的OSD则很大概率出此问题
最后无奈只能删除OSD重建,但是集群空置一段时间后,这个现象又回来了。
从BlueStore.cc中的kv提交线程开始分析:
void BlueStore::_kv_sync_thread()
{
dout(10) << __func__ << " start" << dendl;
std::unique_lock<std::mutex> l(kv_lock);
assert(!kv_sync_started);
bool bluefs_do_check_balance = false;
kv_sync_started = true;
kv_cond.notify_all();
while (true) {
assert(kv_committing.empty());
if (kv_queue.empty() &&
((deferred_done_queue.empty() && deferred_stable_queue.empty()) ||
!deferred_aggressive) &&
(bluefs_do_check_balance == false)) {
闲置的时候,上面所有queue都空,bluefs_do_check_balance初始为false,进入到
if流程,在等待默认1秒没有新的kv写时,bluefs_do_check_balance被置为true
if (kv_stop)
break;
dout(20) << __func__ << " sleep" << dendl;
std::cv_status status = kv_cond.wait_for(l,
std::chrono::milliseconds(int64_t(cct->_conf->bluestore_bluefs_balance_interval * 1000)));
dout(20) << __func__ << " wake" << dendl;
if (status == std::cv_status::timeout) {
bluefs_do_check_balance = true;
}
} else {
下一次循环时,由于bluefs_do_check_balance被置为true,进入到else流程,省略掉很多无关代码,以...代替
} else {
deque<TransContext*> kv_submitting;
deque<DeferredBatch*> deferred_done, deferred_stable;
uint64_t aios = 0, costs = 0;
... ...
// we will use one final transaction to force a sync
KeyValueDB::Transaction synct = db->get_transaction();
上面这行代码挺关键,用意是获取db记录的最后一个transaction,后面用它来触发一次sync操作,确保本次循环所commit的事务落盘。
大致的流程如下:
... ...
for (auto txc : kv_committing) {
if (txc->state == TransContext::STATE_KV_QUEUED) {
txc->log_state_latency(logger, l_bluestore_state_kv_queued_lat);
int r = cct->_conf->bluestore_debug_omit_kv_commit ? 0 : db->submit_transaction(txc->t);
assert(r == 0);
txc->state = TransContext::STATE_KV_SUBMITTED;
... ...
} else {
assert(txc->state == TransContext::STATE_KV_SUBMITTED);
txc->log_state_latency(logger, l_bluestore_state_kv_queued_lat);
}
... ...
}
... ...
PExtentVector bluefs_gift_extents;
if (bluefs &&
after_flush - bluefs_last_balance >
cct->_conf->bluestore_bluefs_balance_interval) {
bluefs_last_balance = after_flush;
int r = _balance_bluefs_freespace(&bluefs_gift_extents);
assert(r >= 0);
if (r > 0) {
for (auto& p : bluefs_gift_extents) {
bluefs_extents.insert(p.offset, p.length);
}
bufferlist bl;
::encode(bluefs_extents, bl);
dout(10) << __func__ << " bluefs_extents now 0x" << std::hex
<< bluefs_extents << std::dec << dendl;
synct->set(PREFIX_SUPER, "bluefs_extents", bl);
}
}
... ...,
上面的流程大致为:对kv_committing队列里的事务,调用db->submit_transaction提交,而后进行bluefs的空闲空间判断,空闲空间过大时执行回收空间并更新bluefs_extents记录;更新后的bluefs_extents记录序列化后,附加到此前获得的synct事务中,期望伴随着synct事务一起提交并sync,接下来
// submit synct synchronously (block and wait for it to commit)
int r = cct->_conf->bluestore_debug_omit_kv_commit ? 0 : db->submit_transaction_sync(synct);
assert(r == 0);
这里调用了db->submit_transaction_sync ,注意前面提交业务Io时,用的db->submit_transaction提交,两个函数有什么不同,等会儿分析。
回顾上面的流程,在集群空闲没有业务IO时,周期性的过程:
- KeyValueDB::Transaction synct = db->get_transaction(); // 一定被执行
- db->submit_transaction(txc->t); // 一定不执行,因为没有新的事务进来
- _balance_bluefs_freespace(&bluefs_gift_extents); // 周期性执行
- synct->set(PREFIX_SUPER, "bluefs_extents", bl); // 低频率执行,因为后面synct的提交可能逐渐撑大bluefs空间,而Log compaction又释放掉空间导致bluefs free比例过大,引发第三步的空间回收过程。
- db->submit_transaction_sync(synct); // 一定会执行
从上面的过程可以初步划定范围,导致BlueFS容量无限增长的可能触发点在这两个位置:
- _balance_bluefs_freespace(&bluefs_gift_extents);
- db->submit_transaction_sync(synct);
当出现问题时,OSD重启时会在BlueFS::_replay(bool noop)函数中长时间循环,可以推断,出问题的OSD,有巨量的BlueFS Log在做回放
先对_balance_bluefs_freespace做分析,函数中有可能操作硬盘的调用为:
bluefs->reclaim_blocks(bluefs_shared_bdev, reclaim, &extents);
这里的bluefs_shared_bdev
,在有单独DB设备时,它是BDEV_DB,否则是BDEV_SLOW
int BlueFS::reclaim_blocks(unsigned id, uint64_t want,
PExtentVector *extents)
{
std::unique_lock<std::mutex> l(lock);
dout(1) << __func__ << " bdev " << id
<< " want 0x" << std::hex << want << std::dec << dendl;
assert(id < alloc.size());
assert(alloc[id]);
int64_t got = alloc[id]->allocate(want, alloc_size[id], 0, extents);
ceph_assert(got != 0);
if (got < 0) {
derr << __func__ << " failed to allocate space to return to bluestore"
<< dendl;
alloc[id]->dump();
return got;
}
for (auto& p : *extents) {
block_all[id].erase(p.offset, p.length);
block_total[id] -= p.length;
log_t.op_alloc_rm(id, p.offset, p.length);
}
flush_bdev();
int r = _flush_and_sync_log(l);
assert(r == 0);
if (logger)
logger->inc(l_bluefs_reclaim_bytes, got);
dout(1) << __func__ << " bdev " << id << " want 0x" << std::hex << want
<< " got " << *extents << dendl;
return 0;
}
上面的函数,传入的extents大概率是空的。所以log_t并未被更新
紧接着调用了 _flush_and_sync_log函数,看看里面做了什么
int BlueFS::_flush_and_sync_log(std::unique_lock<std::mutex>& l,
uint64_t want_seq,
uint64_t jump_to)
{
while (log_flushing) {
dout(10) << __func__ << " want_seq " << want_seq
<< " log is currently flushing, waiting" << dendl;
assert(!jump_to);
log_cond.wait(l);
}
if (want_seq && want_seq <= log_seq_stable) {
dout(10) << __func__ << " want_seq " << want_seq << " <= log_seq_stable "
<< log_seq_stable << ", done" << dendl;
assert(!jump_to);
return 0;
}
if (log_t.empty() && dirty_files.empty()) {
dout(10) << __func__ << " want_seq " << want_seq
<< " " << log_t << " not dirty, dirty_files empty, no-op" << dendl;
assert(!jump_to);
return 0;
}
... ...
dirty_files
是空的,到这里log_t.empty()
应该成立,所以函数返回了,什么也没做。
至此
3. _balance_bluefs_freespace(&bluefs_gift_extents);导致Log无限增长的原因排除了。
只剩下最后一个嫌疑点:
5. db->submit_transaction_sync(synct);
问题出在BlueFS::sync_metadata()
函数里面
经过前面的分析,log_t大概率就是empty的,所以,上面的if条件导致下面的if没有执行机会,也就没能触发log compaction
社区有修复的PR,包含在在12.2.14版本
https://github.com/ceph/ceph/pull/34876
不免会有新的疑问:总有非empty的log来触发这个compaction吧?
追踪调用链,在rocksdb的DBImpl实现里面,只有两种情况会触发BlueRocksDirectory::Fsync()
- 首次收到woptions.sync == true的写请求的时候
- 切换到新的memtable时
假如这两种情况触发时,写下去的事务都是空的呢?
就很可能出现BlueRocksDirectory::Fsync()
被调用,但log_t里面啥也没有的情况,
下面这个PR虽没有被社区接受,原因是end of life,但我相信它确实堵住了源头
https://github.com/ceph/ceph/pull/36108
综上,问题已经比较明晰
- 在集群无业务负载时,周期性
db->submit_transaction_sync
调用,提交空的事务 - 在首次提交以及切换memtable时,触发
BlueRocksDirectory::Fsync()
,但由于log_t为空,未能执行log compact - 第1、2经年累月的反复执行,导致WAL File的log不断增长,又得不到compaction处理,日志空间不断增大。
- 当日志增大到极限大小,约500GB时,会出现日志损坏的情况(具体原因还不明)
- 此时,OSD重启时,就会经历漫长的replay过程,而且最终因日志损坏而失败,OSD无法启动。
解决方案
我认为,仅仅是我认为,将如下两个PR一起使用,能够解决问题:
- 让compact log总是有机会执行: https://github.com/ceph/ceph/pull/34876
- 尽可能的避免提交空事务: https://github.com/ceph/ceph/pull/36108