SeaweedFS 是一个高效的分布式文件系统,设计目标是快速存储和检索大量的文件。其架构简单、可扩展,并能处理数十亿个文件。SeaweedFS 通过将文件切分成小块,并分布式存储于多个卷服务器(Volume Server),从而实现了横向扩展。下面我将详细介绍 SeaweedFS 的工作原理及其关键组件。

工作原理及组件

1. 核心概念和组件

SeaweedFS 主要由以下核心组件构成:

  • Master Server(主控服务器): Master Server 负责管理文件系统的元数据,如文件的分布、卷的状态等。它并不参与实际文件的存储操作,而是作为中央协调者,指导客户端如何与实际存储服务器交互。
    Master Server 支持水平扩展,通过 Leader 选举实现高可用性。
  • Volume Server(卷服务器): Volume Server 是实际存储文件数据的服务器。每个 Volume Server 存储多个卷(Volume),每个卷存储多个文件块。Volume Server 负责读取和写入文件,并直接与客户端交互。Volume Server 可以在集群中水平扩展,新增的 Volume Server 会注册到 Master Server。
  • Filer(文件服务器): Filer 提供文件级别的存储管理,支持文件目录结构,并允许用户对文件进行常规的文件系统操作(如文件的读、写、删除、重命名等)。Filer 可以与外部存储系统集成,如 S3、Azure Blob 或 Google Cloud Storage。
  • Client(客户端): 客户端与 Master Server 交互,通过元数据信息获知文件存储的位置,然后直接与 Volume Server 进行数据交换,上传或下载文件。客户端可以是 SeaweedFS 自带的工具,也可以通过 HTTP/REST API 或者 SDK 与 SeaweedFS 交互。
  • Topology(拓扑结构): SeaweedFS 通过分层次的拓扑结构来组织文件存储,拓扑的基本组成单元是 “rack”(机架)和 “data center”(数据中心)。Master Server 可以根据不同的机架或数据中心分配存储,从而实现跨区域的文件分布和冗余。

2. 工作流程

SeaweedFS 的核心工作流程分为文件写入和文件读取两个过程。

文件写入流程

  1. 客户端请求 Master Server:当客户端需要写入文件时,首先会向 Master Server 请求一个新的文件 ID(fileId)。Master Server 根据当前卷的使用情况,返回一个未使用的 fileId 以及对应的卷服务器(Volume Server)的位置信息。
  2. 写入文件数据:客户端根据 Master Server 返回的卷服务器位置,直接将文件上传到指定的卷服务器中,文件数据将存储在卷内。SeaweedFS 默认会为每个文件生成一个卷内的唯一 ID。
  3. 更新元数据:上传完成后,客户端会通知 Master Server 该文件已经存储完毕。Master Server 更新元数据,以便后续可以查找文件的位置。
    • Volume 编号和文件 ID:每个文件被分配到特定的卷(Volume)中,并在该卷中拥有唯一的文件 ID。文件的完整标识符由卷 ID 和文件 ID 组成,例如 3,017b9d77,其中 3 是卷 ID,017b9d77 是文件 ID。
  4. 冗余与副本:为了提供容错性,SeaweedFS 支持配置文件的冗余副本策略。在文件写入过程中,客户端可以选择写入多个卷服务器,以确保文件在服务器

Docker部署服务

docker-compose.yml文件

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
version: '3.9'

services:
master:
image: chrislusf/seaweedfs:3.73 # use a remote image
ports:
- 9333:9333
- 19333:19333
- 9324:9324
command: "master -ip=master -ip.bind=0.0.0.0 -metricsPort=9324"
volume:
image: chrislusf/seaweedfs:3.73 # use a remote image
ports:
- 8080:8080
- 18080:18080
- 9325:9325
command: 'volume -mserver="master:9333" -ip.bind=0.0.0.0 -port=8080 -metricsPort=9325'
depends_on:
- master
filer:
image: chrislusf/seaweedfs:3.73 # use a remote image
ports:
- 8888:8888
- 18888:18888
- 9326:9326
command: 'filer -master="master:9333" -ip.bind=0.0.0.0 -metricsPort=9326'
tty: true
stdin_open: true
depends_on:
- master
- volume
s3:
image: chrislusf/seaweedfs:3.73 # use a remote image
ports:
- 8333:8333
- 9327:9327
command: 's3 -filer="filer:8888" -ip.bind=0.0.0.0 -metricsPort=9327'
depends_on:
- master
- volume
- filer
webdav:
image: chrislusf/seaweedfs:3.73 # use a remote image
ports:
- 7333:7333
command: 'webdav -filer="filer:8888"'
depends_on:
- master
- volume
- filer
prometheus:
image: prom/prometheus:v2.21.0
ports:
- 9090:9090
volumes:
- ./prometheus:/etc/prometheus
command: --web.enable-lifecycle --config.file=/etc/prometheus/prometheus.yml
depends_on:
- s3

Golang 示例

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
package main

import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
)

const (
masterUrl = "http://localhost:9333" // SeaweedFS master server URL
)

// AssignResponse SeaweedFS Master 返回的 /dir/assign 响应结构
type AssignResponse struct {
Fid string `json:"fid"`
Url string `json:"url"`
PublicUrl string `json:"publicUrl"`
Count int `json:"count"`
}

// UploadFile 上传文件到 SeaweedFS
func UploadFile(fileName string, data []byte) (string, error) {
// 获取分配的文件ID
resp, err := http.Get(masterUrl + "/dir/assign")
if err != nil {
return "", err
}
defer resp.Body.Close()

assignData, err := io.ReadAll(resp.Body)
//{"fid":"4,0dda65a460","url":"172.24.0.3:8080","publicUrl":"172.24.0.3:8080","count":1} <nil>
fmt.Println("resp:", string(assignData), err)
if err != nil {
return "", err
}

// 解析返回的 JSON 响应,获取 fid
var assignResp AssignResponse
err = json.Unmarshal(assignData, &assignResp)
if err != nil {
return "", err
}
fmt.Println(assignResp)

// 上传文件到 Volume 服务器
uploadUrl := fmt.Sprintf("http://%s/%s", "localhost:8080", assignResp.Fid)
req, err := http.NewRequest("POST", uploadUrl, bytes.NewReader(data))
if err != nil {
return "", err
}
req.Header.Set("Content-Type", "application/octet-stream")

client := &http.Client{}
resp, err = client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusCreated {
return "", fmt.Errorf("failed to upload file, status code: %d", resp.StatusCode)
}
return assignResp.Fid, nil
}

func main() {
fileName := "example.txt"
fileContent := []byte("Hello SeaweedFS2!")

fid, err := UploadFile(fileName, fileContent)
if err != nil {
log.Fatalf("failed to upload file: %v", err)
}
// FID: 4,0dda65a460
fmt.Printf("File uploaded with FID: %s\n", fid)
}

验证文件是否上传成功

1
curl http://localhost:8080/4,0dda65a460

参考文章

github

docker