ClickHouse问题分析:删除系统表时卡住,长时间不恢复

进击的阿张 发表于 2021/12/28 17:31:50 2021/12/28
【摘要】 使用drop table system.query_log sync时,发现客户端一直卡住,长时间无法恢复。

问题现象

使用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的智能指针就被释放了,从而卡住的流程可以继续。

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区),文章链接,文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件至:cloudbbs@huaweicloud.com进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容。
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。