ClickHouse问题分析:删除系统表时卡住,长时间不恢复
问题现象
使用drop table system.query_log sync时,发现客户端一直卡住,长时间无法恢复。日志打印如下:
DatabaseCatalog: Waiting for table bd5888b9-a84c-41a8-bc7e-cefadf81cc43 to be finally dropped
进一步试验发现,如果我在卡住期间通过http连接后执行新的sql,卡住的命令就能正常执行了。但是,如果没有新的sql执行,会一直卡住。
问题分析
1、先了解下drop table xxx sync的流程。
# drop table system.query_log sync
InterpreterDropQuery::execute
executeToTable
executeToTableImpl
checkTableCanBeDropped
StorageMergeTree::shutdown
DatabaseAtomic::dropTable
detachTableUnlocked
tryRemoveSymlink
enqueueDroppedTableCleanup
if no delay
tables_marked_dropped.push_front // 放在list的开头
else
tables_marked_dropped.push_back // 默认8min后删除
tables_marked_dropped_ids.insert // 在set中存入需要删除的表
(*drop_task)->schedule() // dropTableDataTask 在DatabaseCatalog::loadDatabases中初始化
if no delay // 有sync或者no delay或者database_atomic_wait_for_drop_and_detach_synchronously为true
waitForTableToBeActuallyDroppedOrDetached // 等待table被实际删除
waitTableFinallyDropped //
wait_table_finally_dropped.wait // wait_table_finally_dropped 被通知,且 tables_marked_dropped_ids 中没有此表
# drop table的后台线程
DatabaseCatalog::dropTableDataTask
// 只有不再使用且drop时间早于当前时间的table,才能被删除
if table 可以被删
tables_marked_dropped.erase
dropTableFinally
StorageMergeTree::drop // 删数据
shutdown // 直接就return
dropAllData // 真正开始删除了
clearPartsFromFilesystem // 文件系统
disk->removeRecursive // 磁盘
remove // 删元数据
removeUUIDMappingFinally // 删uuid映射
tables_marked_dropped_ids.erase
wait_table_finally_dropped.notify_all() // 通知不再wait
if 还有表需要被删
dropTableDataTask // 继续删表,否则不再调度
从流程上可以知道:
(1)如果不加sync(即no delay),则默认是至少8min后才会真正删除数据。而在添加了sync后,则将需要删除的表放在list的开头,也不用等待8min。
(2)drop的主流程,也不会真正删除表,而是存入一个tables_marked_dropped中,由后台线程dropTableDataTask来操刀。如果设置了sync,drop的主流程,会等待后台线程的通知,然后才会返回客户端删除成功。
(3)真正删除时,需要保证该table没有流程在使用,通过查看该table的shared_ptr是否是unique来确定。
(4)每次只会删除一张表。如果还有待删除的表,则会继续,不需再等待。如果没有表需要删除,则不在触发此任务。
(5)每次执行完drop table后,其实都会触发query_log的记录,进而重建该表,因此,删除query_log是没有意义的。
2、再了解下query_log的流程
# query_log的机制
# query_log建立过程
Server::main
global_context->initializeSystemLogs
SystemLogs::SystemLogs // 初始化各个system log,包括query log
createSystemLog // 根据config中的配置生成
SystemLog<LogElement>::SystemLog
logs.emplace_back
log->startup() // 启动log的后台线程 savingThreadFunction
# query log的添加
SystemLog<LogElement>::add // 一般用法,context.getQueryLog; query_log->add
queue.push_back
# query log的监控线程
SystemLog<LogElement>::savingThreadFunction
flush_event.wait_for // 等待超时,一定时间刷新一次
flushImpl // queue中的log写到query_log表
prepareTable // 有表,则要比较列信息是否有变,变化了则需要重建,旧表重命名为query_log_N。没有表,则新建
write // 数据写入
这里比较特别的地方是:
(1)query_log在有log记录的时候,如果发现没有表的话,会走创建表的流程。如果已经有表了,则会比较列的结构是否一致,不一致的话,需要重建表,并将旧表重命名。
(2)如果在config.xml中修改表级的TTL,即使重启server,query_log也是不会变化的,因为已经存在该表,且表的列信息是一样的。如要让它生效,则需要删除query_log(会自动重建),或者采用ALTER TABLE的方式。这也是为什么第一次在config.xml中配置query_log时可以生效,而后续修改或者配置时又无法生效的原因。
3、问题分析
结合删除表时会卡住的时候报错以及正常流程时应该要有的日志(Removing metadata {} of dropped table),可知,是在dropTableDataTask中判断table是否没有地方在使用时出现了问题。
auto it = std::find_if(tables_marked_dropped.begin(), tables_marked_dropped.end(), [&](const auto & elem)
{
bool not_in_use = !elem.table || elem.table.unique();
bool old_enough = elem.drop_time <= current_time;
min_drop_time = std::min(min_drop_time, elem.drop_time);
tables_in_use_count += !not_in_use;
return not_in_use && old_enough;
});
即elem.table.unique()不满足。
然后,梳理整个流程和现象(在卡住期间通过http连接后执行新的sql,卡住的命令就能正常执行了)可以推理出,应该去看添加新log的流程。
template <typename LogElement>
void SystemLog<LogElement>::prepareTable()
{
String description = table_id.getNameForLogs();
table = DatabaseCatalog::instance().tryGetTable(table_id, context);
if (table)
{
...
}
if (!table)
{
/// Create the table.
...
table = DatabaseCatalog::instance().getTable(table_id, context);
}
}
在prepareTable中,得到了table的副本,而且一直没有释放,elem.table.unique()就一直无法满足。只有在新添加日志的时候,table变量被覆盖,原来的query_log的智能指针就被释放了,从而卡住的流程可以继续。
- 点赞
- 收藏
- 关注作者
评论(0)