StarRocks元数据无法合并

一、先说结论

如果您的StarRocks版本在3.1.4及以下,并且使用了metadata_journal_skip_bad_journal_ids来跳过某个异常的journal,结果之后就出现了FE的元数据无法进行Checkpoint的现象,那么选择升级版本到3.1.4以上,就可以解决。

二、背景

已经平稳运行了几个月的StarRocks集群突然奔溃,在恢复过程中,发现其元数据从5月16号之后一直没有进行过合并,导致BDB文件非常多,其恢复时间达到了将近2小时。

现象:

  1. 查看FE的meta/image下面的image文件的生成时间,发现为2024.5.16。
  2. 查询FE的meta/bdb目录下的.jdb文件数量的数量到达了8000多个,大小到了81G。
  3. 每次FE恢复元数据重放需要将近2小时小时。

三、排查原因

通过现象已经明确了其没有做Checkpoint操作,导致image文件一直没有更新,那么首先想到两个可能:

一是由于某些原因一直没有触发元数据的CheckPoint操作。

二是进行了CheckPoint的操作,但是由于某些原因,Checkpoint操作一直无法成功。

于是准备在日志中找相关的佐证。于是开始查询查看源码,查看进行Checkpoint操作的类Checkpoint.java(路径:fe/fe-core/src/main/java/com/starrocks/leader/Checkpoint.java),从下面的源码中可以看出,在replayAndGenerateGlobalStateMgrImage方法中,可以看到进行Checkpoint操作时其打印了相关日志,于是在fe的相关日志中查询是否输出了这些日志信息,根据其是否出现,可以判定是否做了Checkpoint操作,并且Checkpoint操作是否成功。

private boolean replayAndGenerateGlobalStateMgrImage(long logVersion) {
    assert belongToGlobalStateMgr;
    long replayedJournalId = -1;
    // generate new image file
    LOG.info("begin to generate new image: image.{}", logVersion);
    globalStateMgr = GlobalStateMgr.getCurrentState();
    globalStateMgr.setJournal(journal);
    try {
        globalStateMgr.loadImage(imageDir);
        globalStateMgr.replayJournal(logVersion);
        globalStateMgr.clearExpiredJobs();
        globalStateMgr.saveImage();
        replayedJournalId = globalStateMgr.getReplayedJournalId();
        if (MetricRepo.hasInit) {
            MetricRepo.COUNTER_IMAGE_WRITE.increase(1L);
        }
        GlobalStateMgr.getServingState().setImageJournalId(logVersion);
        LOG.info("checkpoint finished save image.{}", replayedJournalId);
        return true;
    } catch (Exception e) {
        LOG.error("Exception when generate new image file", e);
        return false;
    } finally {
        // destroy checkpoint globalStateMgr, reclaim memory
        globalStateMgr = null;
        GlobalStateMgr.destroyCheckpoint();
    }
}

因为个人主观觉得Checkpoint失败的可能性比较大,因此开始在fe的warn日志中执行下列语句检索相关的异常,

cat fe.warn.log | grep 'Exception when generate new image file'

可以看到在fe日志中出现了大量的Checkpoint操作失败的相关日志信息。

由以上日志再配合源码,可以发现其触发了Checkpoint操作,但是Checkpoint过程中出现了异常,导致CheckPoint过程失败,也就造成了整个image一直没有更新。继而继续查询日志,找失败的原因,在该日志信息的后面可以看到其报错信息,其信息如下:

基于上面的操作,已经得到了出现Checkpoint失败的原因,也就是得到的journal id和期望值不一致。

记录一个Linux的查询文件命令

由于StarRocks在恢复期间,整个日志文件非常的大,而且刷新很快,通过tail more等看起来非常耗时,因此百度之后发现如下两个命令配合,可以非常的方便定位异常点:

cat -n 文件 | grep ‘异常信息’ 这个命令,可以查询出整个文件中出现异常的行,并且显示行号。

sed -n ‘开始行号,读取行数p’ 文件 这个命令,可以从文件的某一行开始向下读取多少行。

四、解决过程与原理分析

基于上面找到的Checkpoint失败的原因,首先在高于3.1.4版本的各个版本的发布信息中查询,没有发现相关问题修复的bugfix记录。

然后在StarRocks中文论坛中进行相关的检索,虽然有相关的问题,但是都不太一致,其中最相近的一个帖子为:

无法生成image文件,fe重启后元数据还原, 其中提到了通过dump操作进行恢复,但是查询StarRocks相关的文档之后,并没有发现其中有相关操作的描述。
在这里插入图片描述

然后遇事不决翻源码,查看最近和Checkpoint相关的代码不是是否出现了变更,跟踪相关部分的源码发现在GlobalStateMgr.java(fe/fe-core/src/main/java/com/starrocks/server/GlobalStateMgr.java)类中的replayJournalInner方法,最近进行了一次bugfix,查看这次bugfix的问题。

本次bugfix操作的说明如下:

If metadata_journal_skip_bad_journal_ids is configured, and replay of journal failed on EditLog.loadJournal, the cursor will ignore the next journal by mistake, because the bad log data has been read and cursor is already on the next journal. So the next data should only be ignored when failure happens in cursor.next().

可以看到这个异常和使用过 metadata_journal_skip_bad_journal_ids 这个配置有关,回想5月16那天是否对StarRocks进行过相关操作。

5月16号那天StarRocks异常崩溃,在重启恢复过程中,出现了某一个journal无法加载的错误(got interrupt exception or inconsistent exception when replay journal 447554621 wii exit.),导致一直无法恢复,当时通过查询StarRocks中文论坛,找到了一篇如下的帖子:3.1.5 存算分离集群FE 宕机后没法启动, 其中提到了配置 metadata_journal_skip_bad_journal_ids 跳过该journal,当时采用了这个操作,跳过了那个journal,结合目前来看,极大可能就是这个配置导致了FE无法进行Checkpoint.

在来分析本次bugfix到底干了啥,本次bugfix的源码变更地方如下:

可以发现,其就是在获取和或加载某个journal失败的时候,调用了cursor.skipNext(),查看这个具体代表的含义:在其实现类BDBJEJournal.java(fe/fe-core/src/main/java/com/starrocks/journal/bdbje/BDBJEJournal.java)中,其实现如下:

public void skipNext() {
    LOG.error("!!! DANGER: CURSOR SKIP {} !!!", nextKey);
    nextKey++;
}

再看具体报错should replay to 539780547 but actual replayed journal id is 539780546的地方源码,其在

GlobalStateMgr.java(fe/fe-core/src/main/java/com/starrocks/server/GlobalStateMgr.java)中的replayJournal方法中,其实现如下:

public void replayJournal(long toJournalId) throws JournalException {
    if (toJournalId <= replayedJournalId.get()) {
        LOG.info("skip replay journal because {} <= {}", toJournalId, 
                 replayedJournalId.get());
        return;
    }

    long startJournalId = replayedJournalId.get() + 1;
    long replayStartTime = System.currentTimeMillis();
    LOG.info("start to replay journal from {} to {}", startJournalId, toJournalId);

    JournalCursor cursor = null;
    try {
        cursor = journal.read(startJournalId, toJournalId);
        replayJournalInner(cursor, false);
    } catch (InterruptedException | JournalInconsistentException e) {
        LOG.warn("got interrupt exception or inconsistent exception when replay 
                 journal {}, will exit, ",replayedJournalId.get() + 1,e);
        // TODO exit gracefully
        Util.stdoutWithTime(e.getMessage());
        System.exit(-1);

    } finally {
        if (cursor != null) {
            cursor.close();
        }
    }

    // verify if all log is replayed
    if (toJournalId != replayedJournalId.get()) {
        throw new JournalException(String.format(
                "should replay to %d but actual replayed journal id is %d",
                toJournalId, replayedJournalId.get()));
    }

    streamLoadMgr.cancelUnDurableTaskAfterRestart();

    long replayInterval = System.currentTimeMillis() - replayStartTime;
    LOG.info("finish replay from {} to {} in {} msec", startJournalId, toJournalId, 
             replayInterval);
}

从报错新增可以看到,其判断条件是:toJournalId != replayedJournalId.get(), 那么解下来看这两个值是如何得到的。

在Checkpoint.java中可以看到toJournal来自于 journal.getFinalizedJournalId();通过其实现可以看到, 其取的是当前元数据中的最后的一个journalId。

public long getFinalizedJournalId() {
        List<Long> dbNames = bdbEnvironment.getDatabaseNamesWithPrefix(prefix);
        assert (dbNames != null);

        StringBuilder msg = new StringBuilder("database names: ");
        for (long name : dbNames) {
            msg.append(name).append(" ");
        }
        LOG.info(msg.toString());

        if (dbNames.size() < 2) {
            return 0;
        }

        return dbNames.get(dbNames.size() - 1) - 1;
    }

接下来再看整个日志的重放过程,replayedJournalId的初始值,就是image文件的后缀(在fe/fe-core/src/main/java/com/starrocks/persist/Storage.java的reload方法中可以看到),然后其在重放的过程中会不断+1,replayedJournalId更新的逻辑如下:

protected boolean replayJournalInner(JournalCursor cursor, boolean flowControl)
        throws JournalException, InterruptedException, JournalInconsistentException {
    long startReplayId = replayedJournalId.get();
    long startTime = System.currentTimeMillis();
    long lineCnt = 0;
    while (true) {
        JournalEntity entity = null;
        boolean readSucc = false;
        try {
            entity = cursor.next();

            // EOF or aggressive retry
            if (entity == null) {
                break;
            }

            readSucc = true;

            // apply
            EditLog.loadJournal(this, entity);
        } catch (Throwable e) {
            if (canSkipBadReplayedJournal(e)) {
                LOG.error("!!! DANGER: SKIP JOURNAL, id: {}, data: {} !!!",
                        replayedJournalId.incrementAndGet(), 
                          journalEntityToReadableString(entity), e);
                if (!readSucc) {
                    cursor.skipNext();
                }
                continue;
            }
            // handled in outer loop
            LOG.warn("catch exception when replaying journal, id: {}, data: {},",
             replayedJournalId.get() + 1, journalEntityToReadableString(entity), e);
            throw e;
        }

        replayedJournalId.incrementAndGet();
        LOG.debug("journal {} replayed.", replayedJournalId);

        if (feType != FrontendNodeType.LEADER) {
            journalObservable.notifyObservers(replayedJournalId.get());
        }
        if (MetricRepo.hasInit) {
            // Metric repo may not init after this replay thread start
            MetricRepo.COUNTER_EDIT_LOG_READ.increase(1L);
        }

        if (flowControl) {
            // cost too much time
            long cost = System.currentTimeMillis() - startTime;
            if (cost > REPLAYER_MAX_MS_PER_LOOP) {
                LOG.warn("replay journal cost too much time: {} replayedJournalId: {}",
                         cost, replayedJournalId);
                break;
            }
            // consume too much lines
            lineCnt += 1;
            if (lineCnt > REPLAYER_MAX_LOGS_PER_LOOP) {
                LOG.warn("replay too many journals: lineCnt {}, replayedJournalId: {}",
                         lineCnt, replayedJournalId);
                break;
            }
        }

    }
    if (replayedJournalId.get() - startReplayId > 0) {
        LOG.info("replayed journal from {} - {}", startReplayId, replayedJournalId);
        return true;
    }
    return false;
}

分析以上代码:其在while循环中不断读取journal,并且通过replayedJournalId.incrementAndGet()不断刷新其值。

可以发现在修改前后的不同点

  • 修改前:不管的读取还是加载节点异常,其值都加1,而且如果某个失败,并且在 metadata_journal_skip_bad_journal_ids 配置了可以跳过,那么循环会继续,但是游标都会+1.
  • 修改后:加了判断,只有在日志加载中失败,游标才会+1,如果是读取成功,但是加载失败并且又被跳过,则其游标不会+1.

由此,豁然开朗,因为某个journal在EditLog加载过程中失败了,并且这个journal又因为被配置在了metadata_journal_skip_bad_journal_ids中,因此这个错误被被跳过,但是按照修改之前的代码,游标还是+1,跳过了下一个journal,导致replayedJournal的值少自增了一次,导致其值比正常的小了1。

由此,我们就确定了这哥bugfix确实解决了这个问题,因此我们的解决方案就是升级版本。

五、结果

查看该bugfix合并进入3.1相关分支的时间,发现其为2023.11.9,目前出问题的版本为3.1.4,其发布时间为2023.11.2,确定这个bugfix没有被合并进入, 于是选择进行升级,将其升级到了3.2.13.

经过5个小时左右的升级恢复,成功生成了新的image文件,BDB文件也从原先的8000多个讲到了80多个。

六、参考

整个过程中,收集到的一些相关参考资料

  1. metadata_journal_skip_bad_journal_ids:https://forum.mirrorship.cn/t/topic/10178
  2. 无法生成image文件,fe重启后元数据还原: https://forum.mirrorship.cn/t/topic/7774
  3. bugfix:https://github.com/StarRocks/starrocks/pull/33839
  4. 元数据dump相关(来自Doris,StarRocks未查询到):https://cdnd.selectdb.com/zh-CN/docs/admin-manual/maint-monitor/metadata-operation?_highlight=bdb#%E6%9F%A5%E7%9C%8B-bdbje-%E4%B8%AD%E7%9A%84%E6%95%B0%E6%8D%AE
  5. 元数据管理源码分析:https://xie.infoq.cn/article/6f2f9f56916f0eb2fdb6b001a

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/944061.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

使用qrcode.vue生成当前网页的二维码(H5)

使用npm&#xff1a; npm install qrcode.vue 使用yarn&#xff1a; yarn add qrcode.vue package.json&#xff1a; 实现&#xff1a; <template><div class"code"><qrcode-vue :value"currentUrl" :size"size" render-as&…

【STM32】RTT-Studio中HAL库开发教程十:EC800M-4G模块使用

文章目录 一、简介二、模块测试三、OneNet物联网配置四、完整代码五、测试验证 一、简介 EC800M4G是一款4G模块&#xff0c;本次实验主要是进行互联网的测试&#xff0c;模块测试&#xff0c;以及如何配置ONENET设备的相关参数&#xff0c;以及使用STM32F4来测试模块的数据上报…

STM32完全学习——FATFS0.15移植SD卡

一、下载FATFS源码 大家都知道使用CubMAX可以很快的将&#xff0c;FATFS文件管理系统移植到单片机上&#xff0c;但是别的芯片没有这么好用的工具&#xff0c;就需要自己从官网下载源码进行移植。我们首先解决SD卡的驱动问题&#xff0c;然后再移植FATFS文件管理系统。 二、SD…

【知识】cuda检测GPU是否支持P2P通信及一些注意事项

转载请注明出处&#xff1a;小锋学长生活大爆炸[xfxuezhagn.cn] 如果本文帮助到了你&#xff0c;欢迎[点赞、收藏、关注]哦~ 代码流程 先检查所有GPU之间是否支持P2P通信&#xff1b;然后尝试启用GPU之间的P2P通信&#xff1b;再次检查所有GPU之间是否支持P2P通信。 test.cu&…

Mysql大数据量表分页查询性能优化

一、模拟场景 1、产品表t_product,数据量500万+ 2、未做任何优化前,cout查询时间大约4秒;LIMIT offset, count 时,offset 值较大时查询时间越久。 count查询 SELECT COUNT(*) AS total FROM t_product WHERE deleted = 0 AND tenant_id = 1 分页查询 SELECT * FROM t_…

go语言的成神之路-筑基篇-对文件的操作

目录 一、对文件的读写 Reader?接口 ?Writer接口 copy接口 bufio的使用 ioutil库? 二、cat命令 三、包 1. 包的声明 2. 导入包 3. 包的可见性 4. 包的初始化 5. 标准库包 6. 第三方包 ?7. 包的组织 8. 包的别名 9. 包的路径 10. 包的版本管理 四、go mo…

Qt 应用程序转换为服务

一、在 Windows 上将 Qt 应用程序转换为服务 方法1&#xff1a; 创建一个 Windows 服务应用程序&#xff1a; Windows 服务应用程序是一个没有用户界面的后台进程&#xff0c;通常由 Win32 Service 模板创建&#xff0c;或者直接编写 main() 函数以实现服务逻辑。 修改 Qt 应…

【HarmonyOS之旅】HarmonyOS概述(一)

目录 1 -> HarmonyOS简介 2 -> HarmonyOS发展历程 3 -> HarmonyOS技术特性 3.1 -> 硬件互助&#xff0c;资源共享 3.1.1 -> 分布式软总线 3.1.2 -> 分布式设备虚拟化 3.1.3 -> 分布式数据管理 3.1.4 -> 分布式任务调度 3.1.5 -> 分布式连接…

5、栈应用-表达式求值

本章内容使用上述栈结构函数&#xff0c;来完成表达式求值操作。 表达式例如&#xff1a;3*(7-2) 或者 (0-12)*((5-3)*32)/(22) 。 1、实现思路 a、建立OPTR&#xff08;运算符&#xff09;和OPND&#xff08;数字&#xff09;两个栈&#xff0c;后输入字符串以结束 b、自左向…

【软件】教务系统成绩提交工具使用步骤

【软件】教务系统成绩提交工具使用步骤 零、快速开始 安装 与大多数软件一样&#xff0c;安装步骤很简单&#xff0c;一直点击“下一步”即可快速完成安装&#xff0c;安装完成后&#xff0c;在桌面会有一个软件图标&#xff0c;双击即可打开软件主界面。 导入成绩到Excel中…

书签管理工具的使用技巧

分类与筛选技巧 多层级分类&#xff1a;创建多层级的文件夹结构&#xff0c;如先按大的主题分类&#xff0c;再在每个主题下细分小类。例如&#xff0c;先创建 “工作”“学习”“生活” 等大文件夹&#xff0c;在 “工作” 文件夹下再细分 “项目文档”“办公软件”“行业资讯…

如何安全获取股票实时数据API并在服务器运行?

以下是安全获取股票实时数据 API 并在服务器运行的方法&#xff1a; 选择合适的券商或交易平台 评估自身需求&#xff1a;明确自己的交易策略、交易品种、交易频率等需求&#xff0c;以及对 股票api 的功能、性能、稳定性等方面的要求。调研券商或平台&#xff1a;了解不同券商…

图像处理-Ch2-空间域的图像增强

Ch2 空间域的图像增强 文章目录 Ch2 空间域的图像增强Background灰度变换函数(Gray-level Transformation)对数变换(Logarithmic)幂律变换(Power-Law)分段线性变换函数(Piecewise-Linear)对比度拉伸(Contrast-Stretching)灰度级分层(Gray-level Slicing) 直方图处理(Histogram …

告别 Shuffle!深入探索 Spark 的 SPJ 技术

随着 Spark > 3.3&#xff08;在 3.4 中更加成熟&#xff09;中引入的存储分区连接&#xff08;Storage Partition Join&#xff0c;SPJ&#xff09;优化技术&#xff0c;您可以在不触发 Shuffle 的情况下对分区的数据源 V2 表执行连接操作&#xff08;当然&#xff0c;需要…

解决Springboot整合Shiro自定义SessionDAO+Redis管理会话,登录后不跳转首页

解决Springboot整合Shiro自定义SessionDAORedis管理会话&#xff0c;登录后不跳转首页 问题发现问题解决 问题发现 在Shiro框架中&#xff0c;SessionDAO的默认实现是MemorySessionDAO。它内部维护了一个ConcurrentMap来保存session数据&#xff0c;即将session数据缓存在内存…

【蓝桥杯——物联网设计与开发】基础模块8 - RTC

目录 一、RTC &#xff08;1&#xff09;资源介绍 &#x1f505;简介 &#x1f505;时钟与分频&#xff08;十分重要‼️&#xff09; &#xff08;2&#xff09;STM32CubeMX 软件配置 &#xff08;3&#xff09;代码编写 &#xff08;4&#xff09;实验现象 二、RTC接口…

API安全学习笔记

必要性 前后端分离已经成为web的一大趋势&#xff0c;通过TomcatNgnix(也可以中间有个Node.js)&#xff0c;有效地进行解耦。并且前后端分离会为以后的大型分布式架构、弹性计算架构、微服务架构、多端化服务&#xff08;多种客户端&#xff0c;例如&#xff1a;浏览器&#x…

2011-2020年各省城镇职工基本医疗保险年末参保人数数据

2011-2020年各省城镇职工基本医疗保险年末参保人数数据 1、时间&#xff1a;2011-2020年 2、来源&#xff1a;国家统计局 3、指标&#xff1a;省份、时间、城镇职工基本医疗保险年末参保人数 4、范围&#xff1a;31省 5、指标解释&#xff1a;参保人数指报告期末按国家有关…

【蓝桥杯——物联网设计与开发】拓展模块4 - 脉冲模块

目录 一、脉冲模块 &#xff08;1&#xff09;资源介绍 &#x1f505;原理图 &#x1f505;采集原理 &#xff08;2&#xff09;STM32CubeMX 软件配置 &#xff08;3&#xff09;代码编写 &#xff08;4&#xff09;实验现象 二、脉冲模块接口函数封装 三、踩坑日记 &a…

Kubernetes Gateway API-2-跨命名空间路由

1 跨命名空间路由 Gateway API 具有跨命名空间路由的核心支持。当多个用户或团队共享底层网络基础设施时,这很有用,但必须对控制和配置进行分段,以尽量减少访问和容错域。 Gateway 和 Route(HTTPRoute,TCPRoute,GRPCRoute) 可以部署到不同的命名空间中,路由可以跨命名空间…