简介
SeaweedFS是一种简单的、高度可扩展的分布式文件系统。它可以:
- 存储数十亿的文件 storage billions of files
- 快速获取文件 serve the files fast
weed-fs起初是为了搞一个基于Fackbook的Haystack论文的实现,Haystack旨在优化Fackbook内部图片存储和获取。后来这个基础上,weed-fs作者又增加了若干feature,形成了目前的weed-fs。
SeaweedFS最初作为一个对象存储来有效地处理小文件。中央主服务器(master)只管理文件卷(volume),而不是管理中央主服务器中的所有文件元数据(参考hdfs NN实现),它允许这些卷服务器管理文件及其元数据。这减轻了中央主服务器的并发压力,并将文件元数据传播到卷服务器,允许更快的文件访问(只需一个磁盘读取操作)。每个文件的元数据只有40字节的磁盘存储开销。使用O(1)磁盘读取。
Github: github.com/chrislusf/s…
官方文档:github.com/chrislusf/s…
Components
Quick Start
Docker
1.启动容器
1.1启动master集群
$docker run -p 9333:9333 --name master0 chrislusf/seaweedfs master -port 9333 -peers {host2}:9444,{host3}:9555 &
$docker run -p 9444:9444 --name master1 chrislusf/seaweedfs master -port 9444 -peers {host1}:9333,{host3}:9555 &
$docker run -p 9555:9555 --name master2 chrislusf/seaweedfs master -port 9555 -peers {host1}:9333,{host2}:9444 &
#{host}用对应容器ip替换
此时可以访问 http://localhost:9333/
1.2启动volume集群
$docker run -p 8080:8080 -p 18080:18080 --name volume0 --link master0 chrislusf/seaweedfs volume -max=5 -mserver={master}:9333 -port=8080 &
$docker run -p 8081:8081 -p 18081:18081 --name volume1 --link master0 chrislusf/seaweedfs volume -max=5 -mserver={master}:9333 -port=8081 &
$docker run -p 8082:8082 -p 18082:18082 --name volume2 --link master0 chrislusf/seaweedfs volume -max=5 -mserver={master}:9333 -port=8082 &
#{master}用leader master ip替换
2.上传文件
2.1 请求分配文件号
GET http://localhost:9333/dir/assign
2.2 上传文件
POST http://localhost:8082/7,019238e0c0
3.获取文件
GET http://localhost:8082/7,019238e0c0
Master
原理
master server 集群之间的副本同步是基于raft协议。
master server 保存整个 volume server 的拓扑信息,结构: Topology -> Data Center-> Rack -> Data Node -> Volume。可以通过 Data Center-> Rack -> Data Node 查找一个 Data Node 上的所有 volume 信息。
另外,也需要根据volume去查找它所在的所有节点 (如Master API中的/dir/lookup) ,所以 leader master 还维护着另一个结构: Topology -> Collection -> VolumeLayout-> Data Node List 其中,Collection 对 VolumeLayout 的组织是按备份方式 (解释见下方) 分类区分,VolumeLayout 保存每个 volume id 到其具体位置 Data Node List 的映射,同时保存了 volume 的可读写性质。
leader master 跟各个 volume server 通过心跳保持连接, volume server通过心跳将本地的卷信息(增\删\过期等)上报给leader master。
然后leader master再将 volume 的位置信息通过 grpc 同步 (KeepConnected) 给其余 master。所以, 当查询一个 volume 的具体位置时,leader master 直接从本地的 Topology 中读取, 非 leader master 则从本地 Master Client 的 vidMap 中获取数据。
当要上传文件时,leader master 还负责分配一个全局唯一且递增的 id, 作为file id。
参数介绍
示例:$weed master -port 9333 -peers {host2}:9444,{host3}:9555
参数 类型 说明
-ip String Master 服务器ip地址(默认”localhost”)
-port Int http监听端口(默认9333)
-peers String 代表服务器集群,逗号分隔所有主节点ip:端口,示例127.0.0.1:9093,127.0.0.1:9094
-volumePreallocate 无 为volumes预先分配磁盘空间
-volumeSizeLimitMB Uint Master停止指向过量的volumes写的限定(默认30000)
-cpuprofile String Cpu profile输出文件
-defaultReplication String 如果没有指定默认备份类型。默认”000”
-garbageThreshold String 清空和回收空间的阈值(默认”0.3”)
-ip.bind String 需要绑定的ip地址(默认”0.0.0.0”)
-maxCpu Int 最大cpu数量。0表示所有可用的cpu
-mdir String 存储元数据的数据目录(默认”/tmp”)
-memprofile String 内存配置文件输出文件
-pulseSeconds Int 心跳检测的时间间隔单位为秒(默认5)
-secure.secret String 加密json web token方法
-whiteList String 逗号分隔具有写权限的Ip地址。如果是空的,没有限制。即白名单
defaultReplication
- 000 不备份, 只有一份数据
- 001 在相同的rackj里备份一份数据
- 010 在相同数据中心内不同的rack间备份一份数据
- 100 在不同的数据中心备份一份数据
- 200 在两个不同的数据中心各复制2次
- 110 在不同的rack备份一份数据, 在不同的数据中心备份一次
如果数据备份类型是 xyz形式
- x 在别的数据中心备份的份数
- y 不相同数据中心不同的racks备份的份数
- z 在别的服务器相同的rack的备份份数
核心组件
启动流程
虚线表示启动协程
流程说明
-
构造master server
-
-
实例化masterServer
-
基于gorilla.mux绑定func
-
StartRefreshWritableVolumes
- leader会遍历本地内存map中的所有volume, 将超过指定大小的 volume 从 volumeLayout 的可写列表中删除。
- 会向各个 volume 发出 Compact 指令, 采用复制算法+两阶段提交进行空洞压缩:遍历本地所有 Collection 中的 volumeLayout 中的 Volume Location List, 对于非只读 Volume, 调用对应 Volume Server 的
VacuumVolumeCheck
获取 GarbageRatio, 根据指定垃圾率查看是否需要压缩文件空洞, 对于需要压缩的 Volume 则将其从 leader master 的 VolumeLayout 的可写列表中删除, 并且向各个 Volume Location List 指定的 volume server 发起VacuumVolumeCompact
请求进行压缩, 如果都压缩成功了则向各个 Volume Server 发起VacuumVolumeCommit
请求以提交压缩, 并将该 Volume 加入 leader master VolumeLayout 可写列表中, 否则向 volume server 发起 VacuumVolumeCleanup 请求以清除压缩内容. 该期间这个 Volume 不可写。 - 启动定时任务,每15分钟压缩一次。
-
启动处理volume grow request协程,会通过rpc调用volume server的接口申请grow volume
-
-
启动raft server seaweedfs使用了自研的raft框架。raft是一个管理副本日志的共识算法,各个master间通过raft协议选举、heartbeat、同步log。
raft演示:thesecretlivesofdata.com/raft
github:github.com/chrislusf/r…
- 启动grpc
将master server和raft server注册到grpc并启动。项目使用的是google.golang.org/grpc@v1.40.0
-
如果raft在15s后还没选出leader,则手动选第一个peer成为leader
-
通过master client的KeepConnected从leader master 进行数据同步,master接收volume变动信息,并且将数据保存在master client的vidMap中
-
开始监听客户端http请求
Volume
原理
volume server 是文件实际存储的位置, 对文件的组织是按如下格式进行的:Store -> DiskLocations -> Volumes -> Needles
其中 DiskLocations 对应不同的目录, 每个目录中有很多 volumes, 每个 volume 实际是一个硬盘文件 .dat,其中分段保存一批上传的业务文件, 为了快速定位一个业务文件在 .dat 中的位置, 每个 volume 对应个 .idx 文件, 用于保存所有业务文件在 .dat 文件中的偏移量和文件大小。 为加快查询索引速度, Volume Server 启动时会将 .idx 读到 LevelDb 中,虽然一个业务文件在多个 volume server 作为主备, 但各 volume server 是对等的,平等接收外部请求, 当收到上传业务文件后, 会将文件传到其它机器。
目录
.idx
一个index对应一行
心跳
volume server 和 leader master 之间通过 grpc doHeartbeat 进行持续的heartbeat;
volume server 从 leader master 获取 volumeSizeLimit 作为单个volume的大小限制;
leader master 从 volume server 统计各 DiskLocation 中的 volume 总数、最大的文件key id、.dat 文件大小、fileCount、deleteCount、deletedSize 等指标;volume server 会周期性地向 leader master 上报这些数据,同时当有新 volume 增加、删除、过期回收时,也会向 leader master 报告该类数据,这样 leader master 就会获得哪些 Volume 分布在哪些 Volume Server, 并能实时感知volume server 的运行状态。
空间回收
由于 Needle 的删除是已追加的方式进行软删除的,所以 .dat 文件中可能会存在已删除的文件。在超过一定比例后就需要回收进行空间压缩。
压缩的过程实际是将 .idx 文件读取到 LevelDb 中,并且将已经删除和过期的文件过滤,并且将其输出到临时 .idx 的拷贝文件 .cpx 中去,并且按其中的记录读取 .dat 文件中对应的数据输出到 .cpd 文件中去。
执行完上述复制压缩过程之后,再进行 commit,将刚才生成的 .cpx 和 .cpd 文件 rename 成 .idx 和 .dat 文件。如果这个间隔中产生了有新的 Needle, 则将新产生的 Needle 追加到新生成的文件的尾部。
Needle格式
Needle业务文件在 .dat 文件中存储的格式分三部分: [Header][Data][EXT]
各部分的组织如下:
Header: [Cookie(4字节)][NeedleId(8字节)][DataSize(4字节)]
Data: [Data][CheckSum(4字节)][LastAppendNs(4字节)][Padding]
EXT: [Flags(1字节)]
[NameSize(1字节)][Name]
[MimeSize(1字节)][Mime]
[LastModified(5字节)]
[TTL:Count(1字节)][TTL:Unit(1字节)]
[PairsSize(2字节)][Pairs]
其中 Name 到 Pairs 字段是可选的, 由 Flags 字段的相应位标志有没有该字段
参数介绍
示例:$weed volume -max=5 -mserver=localhost:9333 -port=8080
参数 类型 说明
-ip string Ip地址或服务器名称
-dir string 存储数据文件的目录dir[,dir]…(默认”/tmp”)
-max string Volumes的最大值,count[,count]…(默认”7”)
-port Int http监听端口号(默认8080)\
-cpuprofile string Cpu profile输出文件
-dataCenter string 当前volume服务的数据中心名称
-rack string 当前volume服务器的rack 名称
-idleTimeout Int 连接空闲时间秒数(默认30)
-images.fix.orientation (true/false) 上传时调整jpg方向
-index string 选择内存~性能平衡模式[memory|leveldb|boltdb|btree]。(默认”memory”)
-ip.bind string 需要绑定的ip地址(默认”0.0.0.0”)
-maxCpu Int 最大cpu数量。0表示所有可用的cpu
-memprofile string 内存配置文件输出文件
-mserver string 用逗号分隔的master服务器列表(默认”localhost:9333”)
-port.public Int 端口对外开放
-publicUrl string 公开访问地址
-pulseSeconds Int 心跳之间的秒数,必须小于或等于master 服务器设置(默认5)
-read.redirect (true/false) 重新定向转移或非本地 volumes
-whiteList string 逗号分隔具有写权限的Ip地址。如果是空的,没有限制。
启动流程
Master Server API
完整api见:github.com/chrislusf/s…
/dir/assign
分配 volume,即分配一个数据节点和file id, 之后可以用这个id将数据上传到对应的数据节点节点上去。leader master 首先查找本地的 VolumeLayout 中有没有对应副本数的节点,如果有就直接响应对应节点,否则进行 volume 扩容,通过grpc 调用对应 volume server 的 AllocateVolume
方法,申请分配 volume,对应的 volume server 将该新增的 volume 信息通过SendHeartbeat
上报给 leader master,而 leader master 通过 KeepConnected
机制将该变化同步给其余 masterClient,从而完成 volume 的分配。
/dir/lookup
查找一个 volume 的 location,如果是 leader master,则从本地 topology 获取;否则从 master client 的 vidMap 获取,其中保存对应 volume 的 location 是通过 KeepConnected
机制从 leader master 获取的。
/vol/grow
新增 volume,逻辑与上面/dir/assign 中的扩容操作一致。
/vol/vacuum
使用复制算法 + 两阶段提交,申请压缩 volume server 的磁盘文件空洞,逻辑与 master server 启动时的压缩操作一致。
/col/delete
删除一个集合 Collection 下的所有 volume。从 leader master 的 topology 中获取一个Collection的所有 volume server,然后通过DeleteCollection
向各 volume server 发起rpc调用,完成删除磁盘上的所有数据之后,leader master 再从本地 topology 中删除该 volume 的信息。
由于只有在 leader master 上才有该信息,所以并不用通知其它 master 。但是 volume server 会周期性地调用 CollectHeartbeat
收集本地 volume 信息,再通过 SendHeartbeat
上报给 leader master,leader master 会根据收到的信息对比之前该节点中的老数据,然后计算得到新下线或者上线的 volume信息,即计算增量,之后再通过 KeepConnected
机制广播给其它 master。
Volume API
完整api见:github.com/chrislusf/s…
获取文件
GET/HEAD /vid/fid/filename /vid/fid.ext /vid,fid.ext
从 volume server 获取一个文件,其中 fid 格式为 [NeedleId(8字节)][Cookie(8字节)]。当 server 接收到请求,会从所有 DiskLocation 中查找看是否有该 volume,
如果没有,就向 master 发起一个 /dir/lookup 请求,获取其 location,然后返回 302 跳转给调用方;
如果数据就在该 volume server,就直接读出数据,对比 cookie 和 checkSum 是否正确,如果正确就将数据返回给调用方。
删除文件
DELETE /vid/fid/filename /vid/fid.ext /vid,fid.ext
当 volume server 收到请求,从本地读出文件,并且检查 cookie,如果匹配,则将文件从本地的 Needle Mapper 中删除,再往 .dat 文件尾部追加一个 data 为空的 Needle 文件,并且往 .idx 文件中增加一个 size 为无效的记录,以标志该 Needle 已经被删除了。即逻辑删除,非物理删除。
当本地的文件被删除后,再往其它 volume server 发出同样的删除请求,以删除所有其它的备份文件。
从这里也能看出,删除文件并不是从 .dat 文件中真正删除,所以频繁地删除操作会产生大量空洞,导致 volume server 磁盘的有效存储空间不断减少,这就需要 compact 操作压缩 .dat 文件了。
上传文件
PUT /vid/fid/filename /vid/fid.ext /vid,fid.ext
当 volume server 收到请求,会依此检查文件是否已存在,cookie、checkSum 和 data 是否相等,如果相等则直接返回,否则往 .dat 文件尾部追加一个 Needle 文件,并且往 .idx 文件中增加一条记录。
当 volume server 增加完本地的文件之后,会再向其它 volume server 发出同样的写入请求,以提交所有其它的备份文件。
结语
其实呢,作者研究seaweedfs的主要目的在于学习golang在实战项目中的应用,包括golang语言特性、成熟轮子的使用、go的面向对象思想、服务端架构设计、网络、文件、异常处理、raft、grpc等等。所以选择了这样一个业务功能简单的分布式文件系统项目,另一方面也是可以和用java开发的HDFS进行对比,这样可以收获更多更深的思考。当然本文中没有详细体现出两者的区别,可能会在深度使用过seaweedfs之后再做介绍。
彩蛋
一直拖到大年初一才写完,此刻也正在返乡的高铁上,也祝大家虎年大吉吧。