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

2025博客之星年度评选已开启 10w+人浏览 1.7k人参与

概述

本文档详细分析 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

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

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

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

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() 中启动

线程逻辑

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

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

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

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 的容量检查

// 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 写入

# 在 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 状态

-- 在 Leader 上查看
SHOW FRONTENDS;

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

6.3 检查 Follower 的 BE 视图

-- 在 Follower 上查看(如果能连接)
SHOW BACKENDS;

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

6.4 检查 Replay 日志

# 在 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 重新同步
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

roman_日积跬步-终至千里

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值