StarRocks元数据管理和HA高可用

207 阅读4分钟

StarRocks元数据管理主要由 FE 节点负责,元数据内容包含Catalog、数据库、表、物化视图、节点、心跳、用户权限等信息,元数据的核心特点如下:

  • 内存存储:元数据主要存储在 FE 节点的内存中,通过 GlobalStateMgr 类管理,以保证低延迟的查询响应。
  • 高可用性:通过多 FE 节点的 Leader-Follower 架构实现元数据的同步和一致性。
  • 持久化机制:使用 EditLog + Checkpoint 的方式将元数据持久化到磁盘,防止数据丢失。

1.BDBJE 和 数据的存储格式

     BDBJE(Berkeley DB Java Edition)‌是一个高性能、嵌入式的数据库引擎,主要用于存储和管理元数据,FE-Follower 可以通过提供的端口 (edit_log_port) 访问 BEBJE 数据库,在所有的 Follower 角色中,从 BDBJE 数据库中获取的数据是一致的。在 BDBJE 中存储的是元数据的操作日志,FE-Leader 负责写入,Fe-Follower 负责从BDBJE同步数据,并在本地进行数据回放。

BDBJE的数据格式:

图片

JournalId是自增ID,一条日志条目 + 1

其中 JournalEntity = OperationType + Writable data

2.FE-Leader数据的写入

   其中涉及到的主要组件有:GlobalStateMgr,Editlog,JournalWriter,BDBJEJournal,CheckpointWorker。几个组件的交互如下:

图片

GlobalStateMgr

全局状态管理器,负责协调和管理整个StarRocks集群的元数据和状态信息,包括节点管理(NodeMgr)、心跳检测(HeartbeatMgr)、权限管理(AuthorizationMgr)、RoutineLoad任务(RoutineLoadMgr)、StreamLoad任务(StreamLoadMgr)、物化视图管理(MaterializedViewMgr)等等模块

Editlog

负责记录集群的元数据变更各类操作(如表创建、数据插入等)

图片

JournalWriter

Journal日志异步批量写入组件,通过BlockingQueue接收来自 FE-Leader 的JournalTask,并将日志使用BDBJEJournal写入到BDBJE存储引擎中

BDBJEJournal

负责将FE-Leader 生成的元数据操作日志(如表结构变更、数据插入等)写入到BDBJE存储引擎中

CheckpointWorker

定期做checkpoint持久化成image文件写到本地磁盘中

GlobalStateMgr是FE管理元数据的一个单例,里面有各类管理器,这些管理器 new 出对应的对象然后赋值,相当于数据是存在内存,这些数据都需要通过 EditLog 封装成 JournalEntity -> JournalTask 加入到 journalWriter里的阻塞队列,journalWriter就会从阻塞队列取出对应任务调用BDBJEJournal进行写入。

比如HeartbeatMgr管理器,就会获取 EditLog 进行写入:

图片

journalWriter就会从阻塞队列取出对应任务调用BDBJEJournal进行写入

图片

3.FE-Follower数据的回放

      Follower 节点启动或加入集群时,先加载最新的快照(image )以初始化内存元数据,同时 Follower 节点的 BDBJE 客户端与 Leader 的 BDBJE 服务端 (fe.conf里的配置端口edit_log_port)建立 http 连接,拉取 journal 日志并在本地进行回放。

图片

对应数据的回放

图片

4.checkpoint生成数据快照

      checkpoint定期持久化为磁盘上的快照(image 文件),这个快照捕获了某一时刻的完整元数据状态。如果不做checkpoint会有啥问题呢?如果 FE 宕机重启时,需要将历史数据进行回滚,现在存储的是所有的操作日志,而随着历史日志的不断积累,回滚起来会很慢。做checkpoint就相当于把内存数据GlobalStateMgr通过转换成JSON数据存储到磁盘,然后在这个checkpoint的基础上再加上增量editlog,就会快很多。

图片

生成镜像文件就是把各类对象转换成JSon数据写到image文件中。

public void saveImage(ImageWriter imageWriter, File curFile) throws IOException {
    if (!curFile.exists()) {
        if (!curFile.createNewFile()) {
            LOG.warn("Failed to create file, filepath={}", curFile.getAbsolutePath());
        }
    }

    // save image does not need any lock. because only checkpoint thread will call this method.
    LOG.info("start save image to {}. is ckpt: {}", curFile.getAbsolutePath(), GlobalStateMgr.isCheckpointThread());

    long saveImageStartTime = System.currentTimeMillis();
    try (OutputStream outputStream = Files.newOutputStream(curFile.toPath())) {
        imageWriter.setOutputStream(outputStream);
        try {
            saveHeader(imageWriter.getDataOutputStream());
            nodeMgr.save(imageWriter);
            localMetastore.save(imageWriter);
            alterJobMgr.save(imageWriter);
            recycleBin.save(imageWriter);
            variableMgr.save(imageWriter);
            resourceMgr.saveResourcesV2(imageWriter);
            exportMgr.saveExportJobV2(imageWriter);
            backupHandler.saveBackupHandlerV2(imageWriter);
            globalTransactionMgr.saveTransactionStateV2(imageWriter);
            colocateTableIndex.saveColocateTableIndexV2(imageWriter);
            routineLoadMgr.saveRoutineLoadJobsV2(imageWriter);
            loadMgr.saveLoadJobsV2JsonFormat(imageWriter);
            smallFileMgr.saveSmallFilesV2(imageWriter);
            pluginMgr.save(imageWriter);
            deleteMgr.save(imageWriter);
            analyzeMgr.save(imageWriter);
            resourceGroupMgr.save(imageWriter);
            authenticationMgr.saveV2(imageWriter);
            authorizationMgr.saveV2(imageWriter);
            taskManager.saveTasksV2(imageWriter);
            catalogMgr.save(imageWriter);
            insertOverwriteJobMgr.save(imageWriter);
            compactionMgr.save(imageWriter);
            streamLoadMgr.save(imageWriter);
            materializedViewMgr.save(imageWriter);
            globalFunctionMgr.save(imageWriter);
            storageVolumeMgr.save(imageWriter);
            dictionaryMgr.save(imageWriter);
            replicationMgr.save(imageWriter);
            keyMgr.save(imageWriter);
            pipeManager.getRepo().save(imageWriter);
            warehouseMgr.save(imageWriter);
        } catch (SRMetaBlockException e) {
            LOG.error("Save meta block failed ", e);
            throw new IOException("Save meta block failed ", e);
        }

        imageWriter.saveChecksum();

        long saveImageEndTime = System.currentTimeMillis();
        LOG.info("Finished save meta block {} in {} ms.",
                curFile.getAbsolutePath(), (saveImageEndTime - saveImageStartTime));
    }
}

同时会把镜像文件发给其他FE,同时删除掉旧的image镜像文件和journal日志。

    public Pair<Boolean, String> runCheckpointControllerWithIds(long imageJournalId, long maxJournalId) {
        // Step 1: create image
        .......

        // Step2: push image
        int needToPushCnt = nodesToPushImage.size();
        long newImageVersion = createImageRet.first ? maxJournalId : imageJournalId;
        if (needToPushCnt > 0) {
            pushImage(newImageVersion);
        }

        // Step3: Delete old journals
        // conditions: 1. new image created and no others node to push, this means there is only one FE in the cluster,
        //                delete the old journals immediately.
        //             2. needToPushCnt > 0 means there are other nodes in the cluster,
        //                we must make sure all the other nodes have got the new image and then delete old journals.
        if ((createImageRet.first && needToPushCnt == 0)
                || (needToPushCnt > 0 && nodesToPushImage.isEmpty())) {
            deleteOldJournals(newImageVersion);
        }

        return createImageRet;
    }
```更多大数据干货,欢迎关注我的微信公众号—BigData共享