加载中...
不想等待可以点我关掉

引言

之前网站用的一直是储存在Onedrive上,再通过OneManager挂载,提供文件直链后经过自己写的一个中间件处理,转换格式。在过去两年提供了快且相对稳定的文件托管服务。

但是,存在不少问题:

  1. gographics/imagick存在内存泄漏——定时重启,后改用Imgproxy过渡
  2. 未使用CDN——使用的是家里公网IP直接提供服务,而现在好几家都提供了免费国内CDN,有点跟不上潮流了
  3. 大小文件储存过于分散——原先架构不适合缓存大文件,需要使用大文件的情况要经过另外一个Cloudflare Worker再请求OneManager。同时还有部分文件备份用Openlist对外暴露Webdav端点,该部分数据缺乏冗余
  4. Serverless额度消耗——并且由于分散在Cloudflare和Vercel,一次请求消耗两倍额度。尤其是Vercel
  5. 本站前端降级组件——不太优雅,一直计划去除
  6. …

最近的话首先就是Nginx漏洞了,因为当时用的耗子面板本身就停止了更新软件源,换到1panel时考虑到本身架构就问题不少,索性将请求全部回源至Cloudflare Worker,速度降了不少。而后来微软再背刺一手,不行了,必须得迁移了

本文一共两部分:

  1. 搭建Garage储存集群:官方文档已经相当详尽了,仅作记录。同时参考了快速搭建&迁移S3 —— 手把手教你从MinIO迁移到Garage一文,重复部分省略
  2. FileGate处理中间件配置——为了兼容旧储存,同时规范链接结构,我参考BiliBili的图片链接格式化参数重新写了一套中间件,并添加了许多新功能,我不想再重构了😭

需求

常规的单点(主从)储存架构是单节点承载所有储存压力,其他节点顶多同步相同副本,起到冗余和备份作用(主从复制),无法真正实现“共享储存”。而我手上大多数服务器资源都是硬盘大小不一,性能差异大,不太稳定的炸弹。哪怕只是为了容灾,也得写好复杂的备份脚本,意义不大。

理想情况下是将这些存储统一为一个分布式对象存储池,由集群统一管理对象数据,节点间做好数据同步和冗余。同时我希望使用兼容S3标准的储存后端以便其他程序快速接入。有很多这类型的程序,比如MinIO、RustFS等等

但……他们都太吃性能了,或者说我对访问性能的要求不高,没必要耗费过多的性能在这些服务上。而且一个停更,一个爆漏洞,着实是不太敢用

而本文的主角——Garage,是由Deuxfleurs开发的“专为中小规模的自托管场景而设计的 S3 兼容的分布式对象存储服务”,具备轻量,方便部署,容灾能力强等优点。符合我对这个服务的所有要求

集群结构

服务器系统容量
家里云Linux350GB
OracleLinux120GB
学校宿舍小主机Windows500GB
阿里云Linux10GB
神秘设备Linux400GB

Garage作者曾尝试过交叉编译到Windows,但失败了,因此没有提供Windows的版本
本文通过WSL1部署其Linux版本

搭建 Garage

准备工作

编写配置文件garage.toml

对于二进制运行情况,应放置在/etc/garage.toml

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
# ========================
# 基础存储路径配置
# ========================
metadata_dir = "/var/lib/garage/meta" # Garage 元数据存放目录
data_dir = "/var/lib/garage/data" # 对象数据(真正文件内容)存放目录,大文件、静态资源都在这里
db_engine = "lmdb" # 可选:sqlite / lmdb
metadata_auto_snapshot_interval = "6h" # 如果设置此值,Garage 将定期自动对元数据数据库文件进行快照

# ========================
# 数据可靠性与性能
# ========================
replication_factor = 3 # 副本数量,即一个对象会被存几份,比如:3 表示:允许同时坏 2 个节点而不丢数据
compression_level = 9 # 数据压缩级别(0~9),比如:2:轻量压缩,CPU 开销低,性价比高

# ========================
# 集群 RPC 通信(3901)
# ========================
rpc_bind_addr = "[::]:3901"
rpc_public_addr = "100.xx.xx.xx:3901" # 对外公布的 RPC 地址,单机可用 localhost,多机必须填真实 IP:3901
rpc_secret = "用openssl rand -hex 32生成" # RPC 通信密钥,用于集群节点之间认证,各节点保持一致

# ========================
# S3 相关
# ========================
[s3_api]
s3_region = "garage" # S3 区域名(随便写,但要统一)
api_bind_addr = "[::]:3900"
root_domain = ".s3.api.hzchu.top" # S3 API 使用的域名后缀

[s3_web]
bind_addr = "[::]:3902"
root_domain = ".oss.hzchu.top" # Web 访问域名后缀,<bucket>.web.domain.com
index = "index.html" # 默认首页文件,访问 bucket 根目录时返回这个文件,常用于 Hugo / 静态站点

# ========================
# 管理接口(3903)
# ========================
[admin] # Required
api_bind_addr = "[::]:3903"
admin_token = "用openssl rand -base64 32生成" # 管理接口访问 Token,用于 garage 命令或 HTTP 管理 API,可以
metrics_token = "用openssl rand -base64 32生成" # 指标接口 Token,用于 Prometheus / metrics 拉取

配置用意如下:

  1. replication_factor: 副本因子/数据副本总数/集群冗余度,在经过连丢数据后实在是不敢相信自己的运气了,一个副本不太妥,那就再来一个,一共3个
  2. compression_level: 数据压缩等级,我这里一方面大部分数据是备份存档,另一方面对读取性能要求也不高,因此设为最高的压缩等级。当然对已经压缩过的文件,进一步压缩通常收益有限,可以根据 CPU 与存储空间需求选择较低的压缩等级。
  3. rpc_public_addr: 该节点对其他节点宣称的自身IP地址,作为分布式储存节点,节点之间两两相互连接,可以看作是网状拓扑,这个IP地址需要能够被其他节点直接访问。显然,搞来这么多公网地址还是太奢侈了,我这里使用tailscale来进行虚拟组网,填写自身分配到的 IP

replication_factor = 3 表示对象最多保存 3 份副本。结合不同 Zone 的布局,可以提高节点故障时的数据可靠性;但它并不意味着任意情况下同时失效 2 个节点都能保证服务完全正常。

如需要配置多个储存位置,可以参考多硬盘支持

部署

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
services:
garage:
image: dxflrs/garage:v2.3.0
container_name: garage
network_mode: "host"
volumes:
- ./garage_data/garage.toml:/etc/garage.toml
- ./garage_data/meta:/var/lib/garage/meta # 存储元数据的目录 建议ssd
- ./garage_data/data:/var/lib/garage/data # 存储对象数据块的目录 建议大容量硬盘
restart: unless-stopped
environment:
- TZ=Asia/Shanghai

webui:
image: khairul169/garage-webui:latest
container_name: garage-webui
restart: unless-stopped
volumes:
- ./garage_data/garage.toml:/etc/garage.toml:ro
ports:
- 3909:3909 # 第三方webui
environment:
API_BASE_URL: "http://garage:3903"
S3_ENDPOINT_URL: "http://garage:3900"
# 面板密码,使用 htpasswd -nbBC 10 "YOUR_USERNAME" "YOUR_PASSWORD" 生成
# 注意,由于docker-compose会将所有 $ 符号展开为内部变量,所以$要改写成$$
AUTH_USER_PASS: "YOUR_USERNAME:$$2y$$10$$UX/dcbFO9QGVpv9gLn1doeD5OJtJ.g6l8YkFwdpBXN2rA9m3O2sQC"
  1. network_mode默认是bridge,在使用虚拟组网工具时可能存在问题
  2. 我只在一台节点上部署了webui作为“主节点”,其他节点可以去掉这部分

用以下别名来在宿主机中直接使用容器内garage实例

1
alias garage="docker exec -ti garage /garage"

可以通过包管理器获取,也可以直接下载二进制文件

随后可以通过systemd持久化运行

考虑到小主机内存空间比较小,WSL2会在后台启动一个有真正的Linux内核的虚拟机,比较占用性能,同时,WSL2 在访问宿主机硬盘时是通过基于网络的9P协议来访问的。在访问小文件时效率不行。并且在我这里,元数据跟储存是不在同一个盘上,也不好直接放在内置的虚拟硬盘镜像内,故采用WSL1来运行

WSL1不是真正的Linux,简单来说,它是一个兼容层,将程序运行时调用的Linux系统调用转译为适用于Windows NT内核上的。

没装WSL先装下

powershell
1
dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart

重启后,设置默认版本为WSL1

powershell
1
wsl --set-default-version 1

随后在微软商店搜索喜欢的发行版下载,我这里选择Debian

其他的像更换软件源的操作就不重复了

安装大体上与在 Linux 上使用二进制发行一致,部分区别如下:

  1. WSL1不支持使用systemd管理服务,因此需要在Windows下开机自动唤醒WSL并在里面启动Garage。
  2. WSL内Windows文件路径会被挂载到/mnt/{盘符}下,根据需要配置合适路径

Windows下常用NSSM来注册应用为服务,但我尝试了很多次,使用System账户登陆时无法使用,指定以普通用户登陆又一直提醒“账户密码错误”,索性改用Windows的任务计划程序。

Windows下服务由服务控制管理器管理,通过服务控制接口与Windows服务程序交互,对于没有实现这类接口的普通应用程序,NSSM可作为中介来响应并控制程序行为

先将启动参数保存到一个sh脚本里,随后可以通过wsl.exe -d Debian -u root -- /mnt/c/garage/start-garage.sh来以root身份执行。在任务计划程序里添加一个启动时执行的任务

然后不出意外就又出意外了,保存时也是一直提示密码错误,不得不感叹Windows的账户机制之精妙
我这里使用管理员权限用命令创建任务

1
2
3
4
5
6
7
8
# 操作:执行 WSL 脚本
$action = New-ScheduledTaskAction -Execute "wsl.exe" -Argument "-d Debian -u root -- /mnt/c/garage/start-garage.sh"
# 触发器:计算机启动时运行
$trigger = New-ScheduledTaskTrigger -AtStartup
# 定义安全设置:使用 S4U 模式实现无需登录即可运行,并开启最高权限
$principal = New-ScheduledTaskPrincipal -UserId "thun888" -LogonType S4U -RunLevel Highest
# 注册任务
Register-ScheduledTask -TaskName "GarageService" -Action $action -Trigger $trigger -Principal $principal
/mnt/c/garage/start-garage.sh
1
2
3
4
5
6
7
8
9
#!/bin/bash

export LANG=C.UTF-8
export LC_ALL=C.UTF-8
export NO_COLOR=1
export RUST_LOG_STYLE=never
export TERM=dumb

exec /mnt/c/garage/garage server >> /mnt/c/garage/garage.log 2>&1

配置反向代理

参考:快速搭建&迁移S3 —— 手把手教你从MinIO迁移到Garage

可能用的上的:

允许所有来源访问
1
2
3
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Methods *;
add_header Access-Control-Allow-Headers *;

管理

添加新节点

如果部署了webui,可以使用你设定的面板账密登录http://{服务器ip}:3909,切换到Cluster页面,并点击Connect连接其他节点

面板提供了获取其他节点ID的命令,如果是Docker部署的可以直接运行,二进制方式部署的可以运行garage node id

Tip

webui中的node id不严谨。garage node id输出为node_id_hex@ip:port,整体为该节点的连接参数,node_id是节点初始化时生成的Ed25519公钥(meta/node_key.pub),编码为十六进制后输出,可以通过cat meta/node_key.pub | od -A n -t x1 | tr -d ' \n'查看

这里,应填写这个整体,或者直接使用命令行garage [-c <config file path>] node connect <node_id_hex@ip:port>

在任意节点添加新节点后,新节点信息会被广播到其他节点,不需要在其他节点重复操作

配置储存池

选中某节点,为其分配角色

角色包括节点的Zone(区域)、Capacity(容量)和 Tags(标签)

标签仅用于标识,主要是区域,相同区域内的节点会视为同一个储存池,数据不会在这里面的节点上复制多份。我这里5个节点都在不同区域。

在完成配置后应用

配置完成后会输出集群状态,可见该集群有400GiB+的可用空间

1
2
3
4
5
6
7
8
9
aaa@aaa:/# garage stats
...
==== CLUSTER STATISTICS ====
Storage nodes:
...

Estimated available storage space cluster-wide (might be lower in practice):
data: 437.1 GiB
metadata: 16.1 GiB

创建存储桶和Key

名字随意,后续更名可以添加一个Alias(别称)再删掉原来的

等价于garage key create <name>,

默认不含创建存储桶权限,需使用garage key allow <name> --create-bucket赋予权限(使用garage key deny <name> --create-bucket 撤销权限)

等价于garage bucket create <name>

在创建的存储桶里为刚才创建的key赋予权限

owner权限可以管理桶,比如删除桶操作,小心使用

等价于garage bucket allow <bucket-name> --key <key-name> --read --write --owner

外围应用

作为一个 S3 储存服务,它可以接入到大多数软件中。可以分为两部分:

  1. 原生支持 S3 标准的软件
  2. 不支持 S3 标准的软件,其需要使用别的服务进行中转,转成 WebDAV 或别的协议

图片储存及管理

图床

过去我一直使用兰空图床(Lsky Pro)上传图片和管理图片,但它的开源版本已经好久没有更新了。安装起来也麻烦,挑PHP版本,又要挑数据库版本。放到今天性能也算不上多优秀。

本来我是想着直接改一下GoodBoyboy666/perfect-pic用的,实现一些更契合我文件格式转换的链接格式,可仔细一想,我需要的只是一个上传工具,没必要非得用在线形式,跨平台兼容性,干脆就用PicGo了

太久没用了,装下来发现样式变化好大

直接在插件商店里安装 S3,或者在插件目录手动执行 npm install picgo-plugin-s3

随后按照图中所示配置

Chronoframe

早就想搞相册了,但一直没有合适的存储后端就没搞,直接这样配置就行。

Artalk 评论

Artalk使用upgit作为外置的上传器

安装upgit

1
2
3
4
5
6
7
# 下载本体
wget https://github.com/pluveto/upgit/releases/download/v0.2.25/upgit_linux_amd64
mv upgit_linux_amd64 /usr/bin/upgit
chmod +x /usr/bin/upgit

# 下载配置
curl https://github.com/pluveto/upgit/raw/refs/heads/main/config.sample.zh-CN.toml -o /usr/bin/config.toml

配置:

/usr/bin/config.toml
1
2
3
4
5
6
7
8
9
10
default_uploader = "s3"
rename = "images/{year}/{month}/{unix_ts}{ext}"

[uploaders.s3]
region = "garage"
bucket_name = "artalk"
access_key = "xxx"
secret_key = "xxxxxx"
endpoint = "https://s3.api.hzchu.top"
url_format = "https://gate.assets.hzchu.top/fs/garage-artalk/public/{path}@!comment"

url_format指的是输出链接格式,我这里是接入了其他服务。按照刚刚的配置,应填写为https://artalk.oss.hzchu.top/{path}

测试上传

1
upgit --application-path /upgit_data --config-file /usr/bin/config.toml 1.png

application-path是相对于当前运行目录的,用于存放历史和日志文件

Artalk里:upgit -c /usr/bin/config.toml --application-path /data

文件储存

文件管理

可以使用S3 browser

中转成WebDav协议

WebDav主要用来备份,以往使用OpenList的时候是直接在根目录新建一个文件夹,以待备份的程序命名,再在程序里填写用户信息和备份路径

但是很麻烦的一点是,我要为每个程序都去新建文件夹,有的时候程序还不支持配置备份路径。再者说,这么多程序共用一个用户,其实还是不太安全的。

所以我希望的一点是能够创建多个用户,用户的用户名就以这个程序来命名,每个用户有自己的储存目录,且彼此不可见。这样子即便不支持调整路径的应用,也可以被正确储存到对应的分类中

官方用的是Bagage,但好久没更新了,OpenList又太重了,干脆替换成drakkan/sftpgo

使用Docker部署

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
services:
sftpgo:
image: drakkan/sftpgo:v2.7.5
container_name: sftpgo
restart: unless-stopped

ports:
- "6180:8080" # WebAdmin
- "6181:6181" # WebDAV

volumes:
- ./data:/srv/sftpgo
- ./config:/var/lib/sftpgo

environment:
SFTPGO_WEBDAVD__BINDINGS__0__PORT: "6181"

# 禁用 FTP
SFTPGO_FTPD__BINDINGS__0__PORT: "-1"

# 禁用 SFTP
SFTPGO_SFTPD__BINDINGS__0__PORT: "-1"
# 指向本机,通过它访问本机garage实例
extra_hosts:
- "host.docker.internal:host-gateway"

创建虚拟文件夹

最主要是Key Prefix,这里配置为%username%/,为每个用户储存的文件都添上它所属用户名的前缀,也就是实现了上文提到的分类和隔离

接着是创建用户组,并配置虚拟文件夹

前者为挂载路径,后者为对应的文件夹,这里直接挂载到根目录

随后再创建用户,添加到刚才新建的组里

创建用户时也可以配置用户级的虚拟文件夹,但不支持%username%占位符

至于File system,在挂载根目录以后,就不会再往默认的本地磁盘储存写入文件了,不用管

维护

更多参考官方文档

元数据存储空间

在线页面看到的只有data_dir的可用空间,但是在实际部署时元数据储存位置通常放在速度更快的SSD上,如果容量较小的话还是要注意下的。以下输出表明 47.5GiB 空间里还有 13.2 GiB 空间可用,剩余 27.8% 的空间

1
2
3
4
5
6
7
aaa@aaa:~# garage stats
...
==== CLUSTER STATISTICS ====
Storage nodes:
ID Hostname Zone Capacity Part. DataAvail
2676a67bbb3d346e 0654d8408568 home 326.0 GiB 209 432.0 GiB/457.4 GiB (94.4%) 13.2 GiB/47.5 GiB (27.8%)
...

修改集群冗余度

如果在集群初始化后想修改集群冗余度,需要清除所有节点的拓扑结构

1
2
3
4
5
# 关闭garage,其他方式部署的自行关闭
systemctl stop garage
# 进入你配置的元数据目录
cd /var/lib/garage/meta # 替换为你的真实路径
mv cluster_layout cluster_layout_bak # 备份

清除拓扑结构不会丢失文件,但最好做好备份

随后修改config.toml里的replication_factor为想要的数值

启动各个节点,重新为节点分配角色、容量和区域