【源码分析】StarRocks EditLog 写入与 Replay 完整流程分析

文章目录

    • 概述
    • 完整流程概览
    • [第一部分:Leader FE 写入 EditLog](#第一部分:Leader FE 写入 EditLog)
      • [1.1 用户操作触发](#1.1 用户操作触发)
      • [1.2 addBackend() 方法执行](#1.2 addBackend() 方法执行)
      • [1.3 EditLog.logAddBackend() 方法](#1.3 EditLog.logAddBackend() 方法)
      • [1.4 EditLog.logEdit() 方法](#1.4 EditLog.logEdit() 方法)
      • [1.5 EditLog.submitLog() 方法](#1.5 EditLog.submitLog() 方法)
      • [1.6 JournalWriter 线程处理](#1.6 JournalWriter 线程处理)
    • [第二部分:Follower FE Replay EditLog](#第二部分:Follower FE Replay EditLog)
      • [2.1 Journal Replay 线程启动](#2.1 Journal Replay 线程启动)
      • [2.2 replayJournalInner() 方法](#2.2 replayJournalInner() 方法)
      • [2.3 EditLog.loadJournal() 方法](#2.3 EditLog.loadJournal() 方法)
      • [2.4 SystemInfoService.replayAddBackend() 方法](#2.4 SystemInfoService.replayAddBackend() 方法)
    • [第三部分:为什么 Follower 可能看不到 BE?](#第三部分:为什么 Follower 可能看不到 BE?)
      • [3.1 时序问题](#3.1 时序问题)
      • [3.2 代码验证](#3.2 代码验证)
    • 第四部分:完整数据流图
    • 第五部分:关键代码位置总结
      • [Leader 写入流程](#Leader 写入流程)
      • [Follower Replay 流程](#Follower Replay 流程)
    • 第六部分:诊断和验证
      • [6.1 检查 Leader 的 EditLog 写入](#6.1 检查 Leader 的 EditLog 写入)
      • [6.2 检查 Follower 的 Replay 状态](#6.2 检查 Follower 的 Replay 状态)
      • [6.3 检查 Follower 的 BE 视图](#6.3 检查 Follower 的 BE 视图)
      • [6.4 检查 Replay 日志](#6.4 检查 Replay 日志)
    • 总结

概述

本文档详细分析 StarRocks FE 中 EditLog 的写入(Leader)和 Replay(Follower) 的完整流程,以 ADD BACKEND 操作为例。


完整流程概览

复制代码
用户操作 (ALTER SYSTEM ADD BACKEND)
    ↓
Leader FE: SystemInfoService.addBackend()
    ↓
Leader FE: 更新内存 (idToBackendRef) + 写 EditLog
    ↓(todo:这里的操作有时间周期吗)
EditLog.logAddBackend() → logEdit() → submitLog() → journalQueue
    ↓
JournalWriter 线程: 从队列取任务 → 序列化 → 写入 BDB JE
    ↓(todo:这里的操作有时间周期吗)
BDB JE: 持久化到磁盘 + 复制到 Follower
    ↓
Follower FE: Journal Replay 线程读取 BDB JE
    ↓
EditLog.loadJournal() → 根据 opCode 分发
    ↓
SystemInfoService.replayAddBackend()
    ↓
Follower FE: 更新内存 (idToBackendRef)

第一部分:Leader FE 写入 EditLog

1.1 用户操作触发

场景 :用户执行 ALTER SYSTEM ADD BACKEND "host:port"; 或 BE 启动时自动注册

入口SystemInfoService.addBackends()addBackend(host, heartbeatPort)

1.2 addBackend() 方法执行

位置SystemInfoService.java:203-224

java 复制代码
private void addBackend(String host, int heartbeatPort) {
    // 1. 创建新的 Backend 对象
    Backend newBackend = new Backend(
        GlobalStateMgr.getCurrentState().getNextId(), 
        host, 
        heartbeatPort
    );
    
    // 2. 更新 Leader 的内存状态(立即生效)
    Map<Long, Backend> copiedBackends = Maps.newHashMap(idToBackendRef);
    copiedBackends.put(newBackend.getId(), newBackend);
    idToBackendRef = ImmutableMap.copyOf(copiedBackends);  // ✅ Leader 立即能看到 BE
    
    // 3. 更新 reportVersion
    Map<Long, AtomicLong> copiedReportVersions = Maps.newHashMap(idToReportVersionRef);
    copiedReportVersions.put(newBackend.getId(), new AtomicLong(0L));
    idToReportVersionRef = ImmutableMap.copyOf(copiedReportVersions);
    
    // 4. 添加到集群
    setBackendOwner(newBackend);
    
    // 5. ⭐ 关键:记录到 EditLog(用于同步到 Follower)
    GlobalStateMgr.getCurrentState().getEditLog().logAddBackend(newBackend);
    LOG.info("finished to add {} ", newBackend);
}

关键点

  • 步骤 2 :Leader 的 idToBackendRef 立即更新(Leader 能立即看到 BE)
  • 步骤 5 :调用 logAddBackend() 记录到 EditLog(用于同步到 Follower)

1.3 EditLog.logAddBackend() 方法

位置EditLog.java:1179-1181

java 复制代码
public void logAddBackend(Backend be) {
    logEdit(OperationType.OP_ADD_BACKEND, be);  // OP_ADD_BACKEND = 50
}

作用 :将操作类型和 Backend 对象传递给 logEdit() 方法


1.4 EditLog.logEdit() 方法

位置EditLog.java:976-980

java 复制代码
protected void logEdit(short op, Writable writable) {
    long start = System.nanoTime();
    Future<Boolean> task = submitLog(op, writable, -1);  // 提交到队列
    waitInfinity(start, task);  // 等待写入完成
}

作用

  • 调用 submitLog() 提交日志任务
  • 调用 waitInfinity() 等待写入完成(阻塞直到成功)

1.5 EditLog.submitLog() 方法

位置EditLog.java:985-1023

注意:只有 Leader 才能写 EditLog

java 复制代码
private Future<Boolean> submitLog(short op, Writable writable, long maxWaitIntervalMs) {
    // ⚠️ 检查:只有 Leader 才能写 EditLog
    Preconditions.checkState(GlobalStateMgr.getCurrentState().isLeader(),
            "Current node is not leader, submit log is not allowed");
    
    DataOutputBuffer buffer = new DataOutputBuffer(OUTPUT_BUFFER_INIT_SIZE);
    
    // 1. 序列化操作
    try {
        JournalEntity entity = new JournalEntity();
        entity.setOpCode(op);              // OP_ADD_BACKEND = 50
        entity.setData(writable);         // Backend 对象
        entity.write(buffer);             // 序列化到 buffer
    } catch (IOException e) {
        LOG.info("failed to serialized: {}", e);
    }
    
    // 2. 创建 JournalTask
    JournalTask task = new JournalTask(buffer, maxWaitIntervalMs);
    
    // 3. 放入队列(阻塞直到有空间)
    int cnt = 0;
    while (true) {
        try {
            if (cnt != 0) {
                Thread.sleep(1000);
            }
            this.journalQueue.put(task);  // ⭐ 放入队列,等待 JournalWriter 处理
            break;
        } catch (InterruptedException e) {
            LOG.warn("failed to put queue, wait and retry {} times..: {}", cnt, e);
        }
        cnt++;
    }
    return task;
}

关键点

  1. 只有 Leader 才能写 EditLog(Follower 会抛出异常)
  2. 序列化 :将 OperationType.OP_ADD_BACKENDBackend 对象序列化到 buffer
  3. 放入队列journalQueue 是一个 BlockingQueue<JournalTask>,由 JournalWriter 线程消费

1.6 JournalWriter 线程处理

位置JournalWriter 是一个后台线程,从 journalQueue 取任务并写入 BDB JE

流程

  1. 从队列取任务JournalTask task = journalQueue.take()
  2. 写入 BDB JE:将序列化后的数据写入 BDB JE(Berkeley DB Java Edition)
  3. 持久化 :BDB JE 将数据持久化到磁盘(meta/bdb/ 目录)
  4. 复制到 Follower:BDB JE 的复制机制自动将数据复制到 Follower FE

BDB JE 存储位置

  • Leader: meta/bdb/ 目录(本地持久化)
  • Follower: 通过 BDB JE 复制机制自动同步

第二部分:Follower FE Replay EditLog

2.1 Journal Replay 线程启动

位置GlobalStateMgr.java:1795-1884(Replayer 线程)

启动时机 :FE 启动时,在 GlobalStateMgr.initialize() 中启动

线程逻辑

java 复制代码
Daemon replayer = new Daemon("replayer", 2000L) {
    @Override
    protected void runOneCycle() {
        boolean hasLog = false;
        try {
            if (cursor == null) {
                // 1. 从上次 replay 的位置开始
                LOG.info("start to replay from {}", replayedJournalId.get());
                cursor = journal.read(replayedJournalId.get() + 1, JournalCursor.CUROSR_END_KEY);
            } else {
                cursor.refresh();  // 刷新 cursor,读取新的 journal
            }
            
            // 2. Replay journal(带流控)
            hasLog = replayJournalInner(cursor, true);
            metaReplayState.setOk();
        } catch (Throwable e) {
            LOG.error("replayer thread catch an exception when replay journal {}.",
                    replayedJournalId.get() + 1, e);
            // 处理异常...
        }
    }
};

关键点

  • 周期性执行 :每 2 秒执行一次(2000L
  • 从上次位置继续replayedJournalId.get() + 1 表示从上次 replay 的下一个 journal 开始
  • 读取新 journalcursor.refresh() 刷新 cursor,读取 BDB JE 中的新 journal

2.2 replayJournalInner() 方法

位置GlobalStateMgr.java:1937-1999

java 复制代码
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;
        try {
            // 1. 从 cursor 读取下一个 journal entity
            entity = cursor.next();
            
            // EOF 或没有更多 journal
            if (entity == null) {
                break;
            }
            
            // 2. ⭐ 关键:调用 EditLog.loadJournal() 处理 journal
            EditLog.loadJournal(this, entity);
            
        } catch (Throwable e) {
            // 处理异常...
            throw e;
        }
        
        // 3. 更新 replayedJournalId
        replayedJournalId.incrementAndGet();
        LOG.debug("journal {} replayed.", replayedJournalId);
        
        // 4. 流控:避免一次 replay 太多 journal
        if (flowControl) {
            long cost = System.currentTimeMillis() - startTime;
            if (cost > REPLAYER_MAX_MS_PER_LOOP) {
                LOG.warn("replay journal cost too much time: {} replayedJournalId: {}", cost, replayedJournalId);
                break;  // 超时,下次继续
            }
            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;
}

关键点

  1. 从 cursor 读取cursor.next() 从 BDB JE 读取下一个 journal entity
  2. 调用 loadJournalEditLog.loadJournal(this, entity) 处理 journal
  3. 流控机制:避免一次 replay 太多 journal,防止阻塞其他操作
  4. 更新 replayedJournalId:记录已 replay 的 journal ID

2.3 EditLog.loadJournal() 方法

位置EditLog.java:114-1689

java 复制代码
public static void loadJournal(GlobalStateMgr globalStateMgr, JournalEntity journal)
        throws JournalInconsistentException {
    short opCode = journal.getOpCode();  // 获取操作类型
    if (opCode != OperationType.OP_SAVE_NEXTID && opCode != OperationType.OP_TIMESTAMP) {
        LOG.debug("replay journal op code: {}", opCode);
    }
    
    try {
        switch (opCode) {
            // ... 其他操作类型 ...
            
            case OperationType.OP_ADD_BACKEND: {  // opCode = 50
                // 1. 反序列化 Backend 对象
                Backend be = (Backend) journal.getData();
                
                // 2. ⭐ 关键:调用 SystemInfoService.replayAddBackend()
                GlobalStateMgr.getCurrentSystemInfo().replayAddBackend(be);
                break;
            }
            
            case OperationType.OP_DROP_BACKEND: {
                Backend be = (Backend) journal.getData();
                GlobalStateMgr.getCurrentSystemInfo().replayDropBackend(be);
                break;
            }
            
            // ... 其他操作类型 ...
        }
    } catch (Throwable e) {
        // 处理异常...
    }
}

关键点

  1. 根据 opCode 分发switch (opCode) 根据操作类型分发到对应的 replay 方法
  2. 反序列化journal.getData() 反序列化出 Backend 对象
  3. 调用 replay 方法replayAddBackend(be) 更新 Follower 的内存状态

2.4 SystemInfoService.replayAddBackend() 方法

位置SystemInfoService.java:909-934

java 复制代码
public void replayAddBackend(Backend newBackend) {
    // 1. 兼容性处理(旧版本)
    if (GlobalStateMgr.getCurrentStateJournalVersion() < FeMetaVersion.VERSION_30) {
        newBackend.setBackendState(BackendState.using);
    }
    
    // 2. ⭐ 关键:更新 Follower 的 idToBackendRef(Copy-on-Write)
    Map<Long, Backend> copiedBackends = Maps.newHashMap(idToBackendRef);
    copiedBackends.put(newBackend.getId(), newBackend);
    idToBackendRef = ImmutableMap.copyOf(copiedBackends);  // ✅ Follower 现在能看到 BE
    
    // 3. 更新 reportVersion
    Map<Long, AtomicLong> copiedReportVerions = Maps.newHashMap(idToReportVersionRef);
    copiedReportVerions.put(newBackend.getId(), new AtomicLong(0L));
    idToReportVersionRef = ImmutableMap.copyOf(copiedReportVerions);
    
    // 4. 添加到集群
    if (newBackend.getBackendState() == BackendState.using) {
        final Cluster cluster = GlobalStateMgr.getCurrentState().getCluster();
        if (null != cluster) {
            // replay log
            cluster.addBackend(newBackend.getId());
        } else {
            // 这种情况发生在加载 image 时,cluster 还没创建
            // BE 会在 loadCluster 时更新
        }
    }
}

关键点

  1. Copy-on-Write 更新 :使用 Copy-on-Write 模式更新 idToBackendRef
  2. Follower 现在能看到 BEidToBackendRef 更新后,Follower 的 getBackends() 方法能返回这个 BE
  3. 添加到集群 :将 BE 添加到 DEFAULT_CLUSTER

第三部分:为什么 Follower 可能看不到 BE?

3.1 时序问题

场景:初始化集群时,先启动 FE,然后启动 BE

复制代码
时间线:
T1: 启动 3 个 FE
    - FE1 成为 Leader
    - FE2, FE3 成为 Follower(可能还在启动/同步中)

T2: 启动 3 个 BE
    - BE1 连接到 Leader FE1,注册成功
    - Leader FE1: 
      * addBackend() → idToBackendRef 立即更新 ✅
      * logAddBackend() → 写入 EditLog → BDB JE
    - BE2, BE3 同样注册到 Leader

T3: Follower FE2, FE3 的状态
    - 如果 Follower 的 journal replay 还没追上
    - replayedJournalId < Leader 的最新 journal ID
    - Follower 还没 replay 到 BE 注册的 EditLog
    - 因此 idToBackendRef 还是空的(或只有部分 BE)
    - 导致 getClusterAvailableCapacityB() 返回 0
    - 触发 "Cluster has no available capacity" 错误 ❌

3.2 代码验证

Follower 的容量检查

java 复制代码
// SystemInfoService.getClusterAvailableCapacityB()
public long getClusterAvailableCapacityB() {
    List<Backend> clusterBackends = getBackends();  // 从 idToBackendRef 获取
    long capacity = 0L;
    for (Backend backend : clusterBackends) {
        if (backend.isDecommissioned()) {
            capacity -= backend.getDataUsedCapacityB();
        } else {
            capacity += backend.getAvailableCapacityB();
        }
    }
    return capacity;  // 如果 idToBackendRef 为空,返回 0
}

如果 Follower 的 idToBackendRef 还是空的

  • getBackends() 返回空列表
  • capacity = 0
  • checkClusterCapacity() 抛出异常:Cluster has no available capacity

第四部分:完整数据流图

复制代码
┌─────────────────────────────────────────────────────────────────┐
│ Leader FE                                                       │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│ 1. addBackend(host, port)                                      │
│    ├─> 创建 Backend 对象                                        │
│    ├─> 更新 idToBackendRef (立即生效) ✅                        │
│    └─> logAddBackend(be)                                       │
│         └─> logEdit(OP_ADD_BACKEND, be)                        │
│              └─> submitLog()                                    │
│                   ├─> 序列化 JournalEntity                      │
│                   └─> journalQueue.put(task)                    │
│                                                                 │
│ 2. JournalWriter 线程                                          │
│    ├─> 从 journalQueue 取任务                                  │
│    ├─> 写入 BDB JE                                             │
│    └─> 持久化到 meta/bdb/                                      │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘
                            │
                            │ BDB JE 复制机制
                            ▼
┌─────────────────────────────────────────────────────────────────┐
│ Follower FE                                                     │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│ 3. Replayer 线程(每 2 秒执行一次)                            │
│    ├─> cursor = journal.read(replayedJournalId + 1, END)       │
│    └─> replayJournalInner(cursor)                              │
│         └─> while (entity = cursor.next())                     │
│              └─> EditLog.loadJournal(this, entity)             │
│                   └─> switch (opCode)                           │
│                        case OP_ADD_BACKEND:                    │
│                          └─> replayAddBackend(be)              │
│                               └─> 更新 idToBackendRef ✅        │
│                                                                 │
│ 4. 容量检查                                                     │
│    ├─> getClusterAvailableCapacityB()                          │
│    │   └─> getBackends() → 从 idToBackendRef 获取              │
│    └─> 如果 idToBackendRef 为空 → capacity = 0 → 报错 ❌        │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

第五部分:关键代码位置总结

Leader 写入流程

  1. 用户操作入口SystemInfoService.addBackend() (203-224)
  2. 写 EditLogEditLog.logAddBackend() (1179-1181)
  3. 提交到队列EditLog.logEdit() (976-980) → submitLog() (985-1023)
  4. JournalWriter 写入 :后台线程从 journalQueue 取任务,写入 BDB JE

Follower Replay 流程

  1. Replay 线程GlobalStateMgr.Replayer (1795-1884)
  2. 读取 JournalreplayJournalInner() (1937-1999)
  3. 分发处理EditLog.loadJournal() (114-1689)
  4. 更新内存SystemInfoService.replayAddBackend() (909-934)

第六部分:诊断和验证

6.1 检查 Leader 的 EditLog 写入

bash 复制代码
# 在 Leader FE 上查看日志
tail -100 fe/log/fe.log | grep -i "add.*backend\|logAddBackend"

期望看到

复制代码
finished to add Backend [id=xxx, host=xxx, heartbeatPort=xxx]

6.2 检查 Follower 的 Replay 状态

sql 复制代码
-- 在 Leader 上查看
SHOW FRONTENDS;

-- 关注:
-- - Follower 的 ReplayedJournalId
-- - 是否接近 Leader 的 ReplayedJournalId

6.3 检查 Follower 的 BE 视图

sql 复制代码
-- 在 Follower 上查看(如果能连接)
SHOW BACKENDS;

-- 如果看不到 BE,说明还没 replay 到 BE 注册的 EditLog

6.4 检查 Replay 日志

bash 复制代码
# 在 Follower FE 上查看日志
tail -100 fe/log/fe.log | grep -i "replay\|replayed journal"

期望看到

复制代码
replayed journal from xxx - yyy

总结

  1. Leader 写入addBackend() → 更新内存 → logAddBackend() → 序列化 → 队列 → BDB JE
  2. Follower Replay :Replayer 线程 → 读取 BDB JE → loadJournal()replayAddBackend() → 更新内存
  3. 时序问题:如果 Follower 的 journal replay 还没追上,就看不到新注册的 BE
  4. 解决方案:等待 Follower 同步完成,或重启 Follower 使其从 Leader 重新同步
相关推荐
MACKEI2 小时前
业务域名验证文件添加操作手册
java·开发语言
车载测试工程师2 小时前
CAPL学习-AVB交互层-媒体函数1-回调&基本函数
网络·学习·tcp/ip·媒体·capl·canoe
apihz2 小时前
货币汇率换算免费API接口(每日更新汇率)
android·java·开发语言
Web极客码2 小时前
如何选择最适合的内容管理系统(CMS)?
java·数据库·算法
gf13211112 小时前
python_检测音频人声片段
开发语言·python·音视频
爱笑的眼睛112 小时前
Flask上下文API:从并发陷阱到架构原理解析
java·人工智能·python·ai
asdfg12589632 小时前
数组去重(JS)
java·前端·javascript
程序猿追2 小时前
体验LongCat-Image-Edit图像编辑模型:在昇腾NPU上的部署与推理全流程分享
python·大模型·华为云