文章目录
-
- [一、当前 `filer.sync` 的整体结构](#一、当前
filer.sync的整体结构) - 二、一次文件创建的完整流程
- [三、B 更新完成后会不会返回值?](#三、B 更新完成后会不会返回值?)
- 四、它是同步更新还是异步更新?
- 五、失败时会发生什么?
- [六、A 会收到 B 的更新确认吗?](#六、A 会收到 B 的更新确认吗?)
- 七、双向同步时会不会循环?
- [一、当前 `filer.sync` 的整体结构](#一、当前
一、当前 filer.sync 的整体结构
假设启动:
bash
weed filer.sync \
-a=computer-a:8888 \
-b=computer-b:8888
逻辑可以理解为:
text
生产者
|
v
A Filer
|
| metadata subscription
v
filer.sync 进程
|
| 读取 A 的文件数据
| 写入 B 的数据和 metadata
v
B Filer
filer.sync 可以运行在 A、B 任意一台机器,或者第三台机器上。A 和 B 本身不直接建立"文件同步通知"关系,实际中转者是 filer.sync 进程。
如果没有设置 active-passive 模式,程序会同时启动两个方向:
text
A -> B
B -> A
二、一次文件创建的完整流程
1. 客户端写入 A
生产者向 A 写入文件时,通常会经历:
- 文件数据写入 A 的 volume server,或者通过 A filer 代理写入。
- A filer 创建或更新 metadata entry。
- A filer 将 metadata 变化写入 metadata log。
- A filer 通过
SubscribeMetadata将变化发送给订阅者。
这里的"通知"是 A filer 通知 filer.sync,不是 A filer 直接调用 B filer。
对应代码入口主要在:
weed/command/filer_sync.goweed/pb/filer_pb_tail.goweed/server/filer_grpc_server_sub_meta.go
2. filer.sync 从 A 接收事件
filer.sync 会通过:
go
pb.FollowMetadata(...)
订阅 A 的 metadata 变化。
收到事件后会执行:
go
processEventFnWithOffset
-> processor.AddSyncJob(resp)
其中 resp 会包含:
- 源目录
- 文件名
- 新旧 entry
- 文件 chunk 信息
- 事件时间戳
TsNs - signatures
如果是批量事件,客户端还会逐个处理 resp.Events 中的事件。
3. 事件进入 MetadataProcessor
MetadataProcessor 负责:
- 限制最大并发数;
- 检查同一路径是否有冲突任务;
- 控制目录 rename、delete 和文件更新之间的顺序;
- 启动真正的同步 goroutine;
- 维护已经完成的 watermark。
核心代码在:
weed/command/filer_sync_jobs.go
当并发达到上限,或者路径冲突时,接收协程会等待:
go
for len(t.activeJobs) >= t.concurrencyLimit || t.conflictsWith(resp) {
t.activeJobsCond.Wait()
}
所以当前实现不是无限制地把所有事件放进一个大队列。
4. filer.sync 根据事件类型更新 B
事件最终进入:
go
genProcessFunction(...)
对应代码:
weed/command/filer_sync.go
不同事件的处理方式不同:
新建文件
text
读取 A 的 entry
|
读取 A 的文件 chunks
|
把 chunks 上传到 B
|
调用 B 的 CreateEntry
FilerSink.CreateEntry 会:
- 查询 B 是否已有相同 entry;
- 从 A 读取源 chunk;
- 将 chunk 写入 B 的 volume server,或者通过 B filer 写入;
- 组装新的 B-side entry;
- 调用 B filer 的
CreateEntry。
代码位置:
weed/replication/sink/filersink/filer_sink.go
文件更新
更新时通常会:
- 查询 B 当前 entry;
- 比较 A 的旧 chunks 和新 chunks;
- 只复制新增或变化的 chunks;
- 删除或保留旧 chunks;
- 调用 B 的
UpdateEntry。
代码位置:
weed/replication/sink/filersink/filer_sink.go
删除文件
调用 B filer 的删除接口:
go
filer_pb.Remove(...)
代码位置:
weed/replication/sink/filersink/filer_sink.go
重命名文件或目录
优先调用 B 的:
go
AtomicRenameEntry
如果目标端旧路径不存在,则可能退化为创建新路径。
代码位置:
weed/replication/sink/filersink/filer_sink.go
三、B 更新完成后会不会返回值?
会返回,但这个返回值只返回给 filer.sync,不会返回给 A 上最初写文件的客户端。
例如:
go
filer_pb.CreateEntry(...)
或者:
go
client.UpdateEntry(...)
底层都是 gRPC 请求,B filer 会返回响应和错误。filer.sync 将其转换成 Go 的 error:
text
B filer RPC 成功
-> error == nil
-> 同步任务成功
B filer RPC 失败
-> error != nil
-> 重试
-> 最终失败则记录失败状态
因此关系是:
text
A 写入请求的返回值
只代表 A 写入是否成功
B 更新 RPC 的返回值
返回给 filer.sync
filer.sync 的同步结果
不会回传给 A 原始写入客户端
也就是说,A 的生产者通常无法从自己的写入请求响应中知道 B 是否已经同步完成。
四、它是同步更新还是异步更新?
是异步更新。
典型时间线如下:
text
T1 生产者向 A 写入文件
T2 A filer 写入 metadata
T3 A filer 发送 metadata 事件
T4 filer.sync 收到事件
T5 filer.sync 排队并启动任务
T6 filer.sync 从 A 读取 chunk
T7 filer.sync 将 chunk 写入 B
T8 filer.sync 调用 B CreateEntry/UpdateEntry
T9 B filer 返回成功
T10 filer.sync 将该任务标记为完成
所以:
text
T1 成功 != T9 已完成
A 写入成功后,B 可能还没有更新完成。
五、失败时会发生什么?
MetadataProcessor 中使用:
go
util.Retry("metadata processor", ...)
同步任务失败后会根据 SeaweedFS 的重试策略进行重试。
最终结果分为两种:
text
重试后成功
-> processed_total + 1
-> pending_tasks - 1
重试后仍失败
-> failed_total + 1
-> pending_tasks - 1
-> watermark 不跨过该失败事件
失败事件会阻止 watermark 越过它,目的是让重启后的 filer.sync 重新从该位置继续处理,而不是永久跳过这个事件。
六、A 会收到 B 的更新确认吗?
不会。
A filer 只负责:
text
保存自己的 metadata
向订阅者发送 metadata 事件
它并不知道 filer.sync 是否已经成功更新 B。
A 不会收到类似这样的确认:
text
"文件已经成功同步到 B"
除非业务侧自己:
- 查询 B;
- 读取 Prometheus 指标;
- 调用未来设计的同步状态 API;
- 或自行实现应用层确认机制。
七、双向同步时会不会循环?
双向模式下:
text
A -> B
B -> A
B 收到来自 A 的变更后,可能再次产生 metadata 事件。代码使用 Signatures 和 filer signature 判断事件是否来自目标同步链路:
go
if sig == targetFilerSignature {
return nil
}
这样可以跳过由对端同步过来的事件,避免:
text
A -> B -> A -> B -> ...