文章目录
概述
本文档详细分析 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;
}
关键点:
- 只有 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() 中启动
线程逻辑:
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 开始 - 读取新 journal:
cursor.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;
}
关键点:
- 从 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
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) {
// 处理异常...
}
}
关键点:
- 根据 opCode 分发:
switch (opCode)根据操作类型分发到对应的 replay 方法 - 反序列化:
journal.getData()反序列化出Backend对象 - 调用 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 时更新
}
}
}
关键点:
- 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()
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 = 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 -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
总结
- Leader 写入:
addBackend()→ 更新内存 →logAddBackend()→ 序列化 → 队列 → BDB JE - Follower Replay:Replayer 线程 → 读取 BDB JE →
loadJournal()→replayAddBackend()→ 更新内存 - 时序问题:如果 Follower 的 journal replay 还没追上,就看不到新注册的 BE
- 解决方案:等待 Follower 同步完成,或重启 Follower 使其从 Leader 重新同步
5659

被折叠的 条评论
为什么被折叠?



