【源码分析】StarRocks EditLog 写入与 Replay 完整流程分析
文章目录
- 概述
- 完整流程概览
- 第一部分:Leader FE 写入 EditLog
- 1.1 用户操作触发
- 1.2 addBackend() 方法执行
- 1.3 EditLog.logAddBackend() 方法
- 1.4 EditLog.logEdit() 方法
- 1.5 EditLog.submitLog() 方法
- 1.6 JournalWriter 线程处理
- 第二部分:Follower FE Replay EditLog
- 2.1 Journal Replay 线程启动
- 2.2 replayJournalInner() 方法
- 2.3 EditLog.loadJournal() 方法
- 2.4 SystemInfoService.replayAddBackend() 方法
- 第三部分:为什么 Follower 可能看不到 BE?
- 3.1 时序问题
- 3.2 代码验证
- 第四部分:完整数据流图
- 第五部分:关键代码位置总结
- Leader 写入流程
- Follower Replay 流程
- 第六部分:诊断和验证
- 6.1 检查 Leader 的 EditLog 写入
- 6.2 检查 Follower 的 Replay 状态
- 6.3 检查 Follower 的 BE 视图
- 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
privatevoidaddBackend(Stringhost,intheartbeatPort){// 1. 创建新的 Backend 对象BackendnewBackend=newBackend(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. 更新 reportVersionMap<Long,AtomicLong>copiedReportVersions=Maps.newHashMap(idToReportVersionRef);copiedReportVersions.put(newBackend.getId(),newAtomicLong(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
publicvoidlogAddBackend(Backendbe){logEdit(OperationType.OP_ADD_BACKEND,be);// OP_ADD_BACKEND = 50}作用:将操作类型和 Backend 对象传递给logEdit()方法
1.4 EditLog.logEdit() 方法
位置:EditLog.java:976-980
protectedvoidlogEdit(shortop,Writablewritable){longstart=System.nanoTime();Future<Boolean>task=submitLog(op,writable,-1);// 提交到队列waitInfinity(start,task);// 等待写入完成}作用:
- 调用
submitLog()提交日志任务 - 调用
waitInfinity()等待写入完成(阻塞直到成功)
1.5 EditLog.submitLog() 方法
位置:EditLog.java:985-1023
注意:只有 Leader 才能写 EditLog
privateFuture<Boolean>submitLog(shortop,Writablewritable,longmaxWaitIntervalMs){// ⚠️ 检查:只有 Leader 才能写 EditLogPreconditions.checkState(GlobalStateMgr.getCurrentState().isLeader(),"Current node is not leader, submit log is not allowed");DataOutputBufferbuffer=newDataOutputBuffer(OUTPUT_BUFFER_INIT_SIZE);// 1. 序列化操作try{JournalEntityentity=newJournalEntity();entity.setOpCode(op);// OP_ADD_BACKEND = 50entity.setData(writable);// Backend 对象entity.write(buffer);// 序列化到 buffer}catch(IOExceptione){LOG.info("failed to serialized: {}",e);}// 2. 创建 JournalTaskJournalTasktask=newJournalTask(buffer,maxWaitIntervalMs);// 3. 放入队列(阻塞直到有空间)intcnt=0;while(true){try{if(cnt!=0){Thread.sleep(1000);}this.journalQueue.put(task);// ⭐ 放入队列,等待 JournalWriter 处理break;}catch(InterruptedExceptione){LOG.warn("failed to put queue, wait and retry {} times..: {}",cnt,e);}cnt++;}returntask;}关键点:
- 只有 Leader 才能写 EditLog(Follower 会抛出异常)
- 序列化:将
OperationType.OP_ADD_BACKEND和Backend对象序列化到 buffer - 放入队列:
journalQueue是一个BlockingQueue<JournalTask>,由JournalWriter 线程消费
1.6 JournalWriter 线程处理
位置:JournalWriter是一个后台线程,从journalQueue取任务并写入 BDB JE
流程:
- 从队列取任务:
JournalTask task = journalQueue.take() - 写入 BDB JE:将序列化后的数据写入 BDB JE(Berkeley DB Java Edition)
- 持久化:BDB JE 将数据持久化到磁盘(
meta/bdb/目录) - 复制到 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()中启动
线程逻辑:
Daemonreplayer=newDaemon("replayer",2000L){@OverrideprotectedvoidrunOneCycle(){booleanhasLog=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(Throwablee){LOG.error("replayer thread catch an exception when replay journal {}.",replayedJournalId.get()+1,e);// 处理异常...}}};关键点:
- 周期性执行:每 2 秒执行一次(
2000L) - 从上次位置继续:
replayedJournalId.get() + 1表示从上次 replay 的下一个 journal 开始 - 读取新 journal:
cursor.refresh()刷新 cursor,读取 BDB JE 中的新 journal
2.2 replayJournalInner() 方法
位置:GlobalStateMgr.java:1937-1999
protectedbooleanreplayJournalInner(JournalCursorcursor,booleanflowControl)throwsJournalException,InterruptedException,JournalInconsistentException{longstartReplayId=replayedJournalId.get();longstartTime=System.currentTimeMillis();longlineCnt=0;while(true){JournalEntityentity=null;try{// 1. 从 cursor 读取下一个 journal entityentity=cursor.next();// EOF 或没有更多 journalif(entity==null){break;}// 2. ⭐ 关键:调用 EditLog.loadJournal() 处理 journalEditLog.loadJournal(this,entity);}catch(Throwablee){// 处理异常...throwe;}// 3. 更新 replayedJournalIdreplayedJournalId.incrementAndGet();LOG.debug("journal {} replayed.",replayedJournalId);// 4. 流控:避免一次 replay 太多 journalif(flowControl){longcost=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);returntrue;}returnfalse;}关键点:
- 从 cursor 读取:
cursor.next()从 BDB JE 读取下一个 journal entity - 调用 loadJournal:
EditLog.loadJournal(this, entity)处理 journal - 流控机制:避免一次 replay 太多 journal,防止阻塞其他操作
- 更新 replayedJournalId:记录已 replay 的 journal ID
2.3 EditLog.loadJournal() 方法
位置:EditLog.java:114-1689
publicstaticvoidloadJournal(GlobalStateMgrglobalStateMgr,JournalEntityjournal)throwsJournalInconsistentException{shortopCode=journal.getOpCode();// 获取操作类型if(opCode!=OperationType.OP_SAVE_NEXTID&&opCode!=OperationType.OP_TIMESTAMP){LOG.debug("replay journal op code: {}",opCode);}try{switch(opCode){// ... 其他操作类型 ...caseOperationType.OP_ADD_BACKEND:{// opCode = 50// 1. 反序列化 Backend 对象Backendbe=(Backend)journal.getData();// 2. ⭐ 关键:调用 SystemInfoService.replayAddBackend()GlobalStateMgr.getCurrentSystemInfo().replayAddBackend(be);break;}caseOperationType.OP_DROP_BACKEND:{Backendbe=(Backend)journal.getData();GlobalStateMgr.getCurrentSystemInfo().replayDropBackend(be);break;}// ... 其他操作类型 ...}}catch(Throwablee){// 处理异常...}}关键点:
- 根据 opCode 分发:
switch (opCode)根据操作类型分发到对应的 replay 方法 - 反序列化:
journal.getData()反序列化出Backend对象 - 调用 replay 方法:
replayAddBackend(be)更新 Follower 的内存状态
2.4 SystemInfoService.replayAddBackend() 方法
位置:SystemInfoService.java:909-934
publicvoidreplayAddBackend(BackendnewBackend){// 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. 更新 reportVersionMap<Long,AtomicLong>copiedReportVerions=Maps.newHashMap(idToReportVersionRef);copiedReportVerions.put(newBackend.getId(),newAtomicLong(0L));idToReportVersionRef=ImmutableMap.copyOf(copiedReportVerions);// 4. 添加到集群if(newBackend.getBackendState()==BackendState.using){finalClustercluster=GlobalStateMgr.getCurrentState().getCluster();if(null!=cluster){// replay logcluster.addBackend(newBackend.getId());}else{// 这种情况发生在加载 image 时,cluster 还没创建// BE 会在 loadCluster 时更新}}}关键点:
- Copy-on-Write 更新:使用 Copy-on-Write 模式更新
idToBackendRef - Follower 现在能看到 BE:
idToBackendRef更新后,Follower 的getBackends()方法能返回这个 BE - 添加到集群:将 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()publiclonggetClusterAvailableCapacityB(){List<Backend>clusterBackends=getBackends();// 从 idToBackendRef 获取longcapacity=0L;for(Backendbackend:clusterBackends){if(backend.isDecommissioned()){capacity-=backend.getDataUsedCapacityB();}else{capacity+=backend.getAvailableCapacityB();}}returncapacity;// 如果 idToBackendRef 为空,返回 0}如果 Follower 的idToBackendRef还是空的:
getBackends()返回空列表capacity = 0checkClusterCapacity()抛出异常: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 写入流程
- 用户操作入口:
SystemInfoService.addBackend()(203-224) - 写 EditLog:
EditLog.logAddBackend()(1179-1181) - 提交到队列:
EditLog.logEdit()(976-980) →submitLog()(985-1023) - JournalWriter 写入:后台线程从
journalQueue取任务,写入 BDB JE
Follower Replay 流程
- Replay 线程:
GlobalStateMgr.Replayer(1795-1884) - 读取 Journal:
replayJournalInner()(1937-1999) - 分发处理:
EditLog.loadJournal()(114-1689) - 更新内存:
SystemInfoService.replayAddBackend()(909-934)
第六部分:诊断和验证
6.1 检查 Leader 的 EditLog 写入
# 在 Leader FE 上查看日志tail-100fe/log/fe.log|grep-i"add.*backend\|logAddBackend"期望看到:
finished to add Backend [id=xxx, host=xxx, heartbeatPort=xxx]6.2 检查 Follower 的 Replay 状态
-- 在 Leader 上查看SHOWFRONTENDS;-- 关注:-- - Follower 的 ReplayedJournalId-- - 是否接近 Leader 的 ReplayedJournalId6.3 检查 Follower 的 BE 视图
-- 在 Follower 上查看(如果能连接)SHOWBACKENDS;-- 如果看不到 BE,说明还没 replay 到 BE 注册的 EditLog6.4 检查 Replay 日志
# 在 Follower FE 上查看日志tail-100fe/log/fe.log|grep-i"replay\|replayed journal"期望看到:
replayed journal from xxx - yyy总结
- Leader 写入:
addBackend()→ 更新内存 →logAddBackend()→ 序列化 → 队列 → BDB JE - Follower Replay:Replayer 线程 → 读取 BDB JE →
loadJournal()→replayAddBackend()→ 更新内存 - 时序问题:如果 Follower 的 journal replay 还没追上,就看不到新注册的 BE
- 解决方案:等待 Follower 同步完成,或重启 Follower 使其从 Leader 重新同步