本文基于 seaweedfs 3.46[1]
SeaweedFS 的架构包括 Master Server、Volume Server 和 Filer Server 。
启动 Master Server 启动一个 Master Server 可以使用以下命令:
启动入口以及所有的参数定义在 weed/command/master.go ,默认情况 http 监听端口使用 9333 ,grpc 监听端口则在 http 端口的基础上加 10000 (所有组件的默认规则)即 19333 :
Master Server 支持多节点(奇数)部署。使用 Raft 一致性算法来选举 Leader 节点,这样可以保证在 Leader 节点宕机的情况下,其他节点可以重新选举出新的 Leader 节点,从而保证系统的高可用性。
如下,启动一个由三个 Master Server 节点所组成的集群:
当 Master Server 启动时,它会尝试加入集群并参与 Leader 选举。一旦选举完成,Leader 节点将负责管理整个集群以及 Volume Server 。
首先会创建一个 Master Server 包装的 weed_server.RaftServer 对象:
在 weed_server.NewRaftServer() 方法中会创建好 Raft 节点所需的各种参数和对象,然后调用 github.com/seaweedfs/raft[2] 库创建 RaftServer 对象并启动 Raft 节点:
最后,会打印出当前的 Leader 节点,如果对 Raft 选举算法的处理细节感兴趣,可以继续深入 s.raftServer.Start() 的实现。
Raft 节点启动成功后,Master Server 会注册一些集群相关的接口,方便查看集群状态:
请求如下:
启动 Volume Server 启动一个 Volume Server 可以使用以下命令:
启动入口以及所有的参数定义在 weed/command/volume.go ,默认情况 http 监听端口使用 8080 ,grpc 监听端口使用 18080 。
其中,-mserver 为 Master Server 连接地址,当需要连接的 Master Server 为集群时,可以将多个 Master Server 的连接地址用逗号分隔; -dir 则用来指定 Volume Server 存储数据文件的目录。
和 Master Server 不同,Volume Server 支持横向扩展,其节点数量规模可以随着数据量和性能需求的变化而随时动态调整。
一旦 Volume Server 启动后,就会与 Master Server 保持通信,汇报自身的状态,并根据 Master Server 的指示执行创建、删除、修复等操作。
核心逻辑在 weed/server/volume_grpc_client_to_master.go 的 VolumeServer.doHeartbeat 方法。
首先会创建一个 Master Server 的 gRPC 连接客户端,并使用该客户端调用 SendHeartbeat 方法:
SendHeartbeat 方法是一个双向流式 RPC ,允许在一次调用中发送多个请求和响应,其 ProtoBuf 定义如下:
接着创建一个 goroutine 用来处理从 Master Server 发送过来的 Heartbeat 请求:
最后使用一个 for select 来监听来自 Volume Server 存储层的四个通道:NewVolumesChan、NewEcShardsChan、DeletedVolumesChan 和 DeletedEcShardsChan。每当有新的卷或 EC 分片被创建或删除时,会生成一个 Heartbeat 消息,并使用 stream.Send() 方法将其发送到 Master Server ,同时也会定期发送心跳消息给 Master Server :
复制 for {
select {
case volumeMessage := < - vs. store. NewVolumesChan:
glog. V( 0 ) . Infof( "volume server %s:%d adds volume %d" , vs. store. Ip, vs. store. Port, volumeMessage. Id)
if err = stream. Send( deltaBeat) ; err != nil {
glog. V( 0 ) . Infof( "Volume Server Failed to update to master %s: %v" , masterAddress, err)
return "" , err
}
case ecShardMessage := < - vs. store. NewEcShardsChan:
glog. V( 0 ) . Infof( "volume server %s:%d adds ec shard %d:%d" , vs. store. Ip, vs. store. Port, ecShardMessage. Id,
erasure_coding. ShardBits( ecShardMessage. EcIndexBits) . ShardIds( ) )
if err = stream. Send( deltaBeat) ; err != nil {
glog. V( 0 ) . Infof( "Volume Server Failed to update to master %s: %v" , masterAddress, err)
return "" , err
}
case volumeMessage := < - vs. store. DeletedVolumesChan:
glog. V( 0 ) . Infof( "volume server %s:%d deletes volume %d" , vs. store. Ip, vs. store. Port, volumeMessage. Id)
if err = stream. Send( deltaBeat) ; err != nil {
glog. V( 0 ) . Infof( "Volume Server Failed to update to master %s: %v" , masterAddress, err)
return "" , err
}
case ecShardMessage := < - vs. store. DeletedEcShardsChan:
glog. V( 0 ) . Infof( "volume server %s:%d deletes ec shard %d:%d" , vs. store. Ip, vs. store. Port, ecShardMessage. Id,
erasure_coding. ShardBits( ecShardMessage. EcIndexBits) . ShardIds( ) )
if err = stream. Send( deltaBeat) ; err != nil {
glog. V( 0 ) . Infof( "Volume Server Failed to update to master %s: %v" , masterAddress, err)
return "" , err
}
case < - volumeTickChan. C:
glog. V( 4 ) . Infof( "volume server %s:%d heartbeat" , vs. store. Ip, vs. store. Port)
vs. store. MaybeAdjustVolumeMax( )
if err = stream. Send( vs. store. CollectHeartbeat( ) ) ; err != nil {
glog. V( 0 ) . Infof( "Volume Server Failed to talk with master %s: %v" , masterAddress, err)
return "" , err
}
case < - ecShardTickChan. C:
glog. V( 4 ) . Infof( "volume server %s:%d ec heartbeat" , vs. store. Ip, vs. store. Port)
if err = stream. Send( vs. store. CollectErasureCodingHeartbeat( ) ) ; err != nil {
glog. V( 0 ) . Infof( "Volume Server Failed to talk with master %s: %v" , masterAddress, err)
return "" , err
}
case err = < - doneChan:
return
case < - vs. stopChan:
glog. V( 1 ) . Infof( "volume server %s:%d stops and deletes all volumes" , vs. store. Ip, vs. store. Port)
if err = stream. Send( emptyBeat) ; err != nil {
glog. V( 0 ) . Infof( "Volume Server Failed to update to master %s: %v" , masterAddress, err)
return "" , err
}
return
}
}
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. 32. 33. 34. 35. 36. 37. 38. 39. 40. 41. 42. 43. 44. 45. 46. 47. 48. 49. 50. 51. 52. 53. 54. 55. 56. 57. 58. 59. 60. 61. 62. 63. 64. 65. 66. 67. 68. 69.
启动 Filer Server 启动一个 Filer Server 可以使用以下命令:
启动入口以及所有的参数定义在 weed/command/filer.go ,默认情况 http 监听端口使用 8888 ,grpc 监听端口使用 18888 。
在这里,-master 为 Master Server 连接地址,同样地,当需要连接的 Master Server 为集群时,可以将多个 Master Server 的连接地址用逗号分隔; -s3 则代表要启动 S3 网关功能,默认监听 8333 端口。
Filer Server 可以理解为一个文件管理器,通过向下对接 Volume Server 与 Master Server,对外提供丰富的功能与特性,除了自身提供的 API 接口,还支持扩展其它比如 POSIX ,WebDAV,S3 等的文件操作接口。
Filer Server 通过外部数据库存储文件的元数据信息。默认情况下,使用的是 leveldb ,支持替换为其它流行的数据库,例如 Sqlite、MySql、Etcd 等,具体可以参考 wiki/Filer-Stores[3] 。
作为一个 API Server ,Filer Server 在架构上就是一个服务端+数据库模型,其节点的数量和规模可以根据不同的工作负载和使用情况进行优化和调整。
上传文件 首先分析 Filer Server 自身提供的 API 接口,上传文件可以直接调用 :
文件上传的接口定义在 weed/server/filer_server_handlers_write.go 的 PostHandler 方法:
跟踪到 fs.autoChunk 方法:
继续来到 fs.doPostAutoChunk 方法:
这些都比较好读,继续跟踪到核心逻辑处 fs.uploadReaderToChunks ,方法内首先会进行一些正确性校验和必要变量的初始化,然后开启一个循环,不断读取数据并将其转换为一个或多个 Chunk :
复制 func ( fs * FilerServer) uploadReaderToChunks( w http. ResponseWriter, r * http. Request, reader io. Reader, chunkSize int32, fileName, contentType string, contentLength int64, so * operation. StorageOption) ( fileChunks [ ] * filer_pb. FileChunk, md5Hash hash . Hash , chunkOffset int64, uploadErr error, smallContent [ ] byte) {
for {
bytesBufferLimitCond. L. Lock ( )
for atomic. LoadInt64( & bytesBufferCounter) >= 4 {
glog. V( 4 ) . Infof( "waiting for byte buffer %d" , atomic. LoadInt64( & bytesBufferCounter) )
bytesBufferLimitCond. Wait( )
}
atomic. AddInt64( & bytesBufferCounter, 1 )
bytesBufferLimitCond. L. Unlock ( )
bytesBuffer := bufPool. Get( ) . ( * bytes. Buffer)
glog. V( 4 ) . Infof( "received byte buffer %d" , atomic. LoadInt64( & bytesBufferCounter) )
limitedReader := io. LimitReader( partReader, int64( chunkSize) )
bytesBuffer. Reset( )
dataSize, err := bytesBuffer. ReadFrom( limitedReader)
wg. Add ( 1 )
go func( offset int64) {
defer func( ) {
bufPool. Put( bytesBuffer)
atomic. AddInt64( & bytesBufferCounter, - 1 )
bytesBufferLimitCond. Signal( )
wg. Done( )
}( )
chunks, toChunkErr := fs. dataToChunk( fileName, contentType, bytesBuffer. Bytes( ) , offset , so)
if toChunkErr != nil {
uploadErrLock. Lock ( )
if uploadErr = = nil {
uploadErr = toChunkErr
}
uploadErrLock. Unlock ( )
}
if chunks != nil {
fileChunksLock. Lock ( )
fileChunksSize := len ( fileChunks) + len ( chunks)
for _, chunk := range chunks {
fileChunks = append( fileChunks, chunk)
glog. V( 4 ) . Infof( "uploaded %s chunk %d to %s [%d,%d)" , fileName, fileChunksSize, chunk. FileId, offset , offset + int64( chunk. Size) )
}
fileChunksLock. Unlock ( )
}
}( chunkOffset)
chunkOffset = chunkOffset + dataSize
if dataSize < int64( chunkSize) {
break
}
}
wg. Wait( )
if uploadErr != nil {
fs. filer. DeleteChunks( fileChunks)
return nil, md5Hash, 0 , uploadErr, nil
}
slices. SortFunc( fileChunks, func( a, b * filer_pb. FileChunk) bool {
return a. Offset < b. Offset
})
return fileChunks, md5Hash, chunkOffset, nil, smallContent
}
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. 32. 33. 34. 35. 36. 37. 38. 39. 40. 41. 42. 43. 44. 45. 46. 47. 48. 49. 50. 51. 52. 53. 54. 55. 56. 57. 58. 59. 60. 61. 62. 63. 64. 65. 66. 67. 68. 69. 70. 71. 72. 73. 74. 75. 76. 77. 78. 79. 80. 81. 82. 83. 84. 85. 86. 87.
文件的分块操作都是在 Filer Server 完成的。而其中上传数据块的 fs.dataToChunk 方法会与 Master Server 进行交互。
该方法首先会调用 fs.assignNewFileInfo 向 Master Server 请求分配一个新的文件 ID(fid)以及上传 URL :
然后使用分配的 fid 调用上传 URL 上传数据块:
这个由 Master Server 所分配的上传 URL ,实际就是 Volume Server 的上传地址,例 http://127.0.0.1:8080/14,1f343c431d ,其中 14,1f343c431d 就是文件 ID ,其实这个文件 ID 更准确地说应该是代表一个数据块的文件 ID。
SeaweedFS 会根据 maxMB 参数,来把文件拆分成多个块存储,默认大小是 4MB 。即一个 100MB 大小的文件,上传到 SeaweedFS 后会被分成 25 个块存储,也就是申请分配了 25 个文件 ID 。
到这里,总算捋清流程了。
那还有一个 S3 接口的文件上传呢?
不用担心,SeaweedFS S3 只是做了一个 API 的代理转发,依旧转发到 Filer Server 自身提供的 API 接口,逻辑依旧和上面一致,代码位置在 weed/s3api/s3api_object_handlers.go :
下载文件 和上传文件一样,SeaweedFS S3 为文件下载做了一个代理转发,转发到 Filer Server 自身提供的 API 接口:
所以,当下载一个文件时:
直接来看 weed/server/filer_server_handlers_read.go 的 GetOrHeadHandler 接口:
根据代码,我们可以直接通过 metadata=true 查询参数查看文件的元数据信息:
其中最重要的就是 chunks 信息,里面定义了该文件的所有数据块信息,只要把所有数据块拼凑一起,就可以还原出整个文件。文件大小的原因,这里刚好只有一个块,其文件 ID 为 14,1f343c431d 。
继续解读文件下载的核心方法 filer.StreamContentWithThrottler ,首先获取所有文件 ID 所对应的 URL 列表:
然后,通过获取到的 URL 列表下载文件的所有 chunk :
可以总结出,下载文件本质也是和 Master Server 交互,通过文件 ID 获取到对应 Volume Server 的数据块下载地址列表,按照列表顺序请求下载数据块,最后重新整合成了一个完整的文件返回给客户端。
最后,附上文件下载的流程:
参考资料 [1]seaweedfs 3.46: https://github.com/seaweedfs/seaweedfs/tree/3.46
[2]github.com/seaweedfs/raft: https://github.com/seaweedfs/raft/tree/v1.1.0
[3]wiki/Filer-Stores: https://github.com/seaweedfs/seaweedfs/wiki/Filer-Stores