Using Garage for Object Storage Service

on 2025-06-16

背景

我在家中搭建了一台基于黑群晖的 NAS 系统(非官方版本的群晖 DSM)用来存储一些文件和照片

为了防止数据的丢失, 我需要定期备份其中的数据

对于数据备份通常遵循3-2-1原则

3-2-1原则:

3份数据副本: 存储三份数据,1份原件+2份拷贝

2种存储介质: HDD, NAS, 云端, U盘,或者光盘

1个异地备份: 将其中一份备份放在异地

按照这个备份方案可以最大程度降低数据丢失的风险

而群晖自带了一个备份工具, Hyper Backup

Hyper Backup

支持将数据备份至云服务或者外接硬盘等

Hyper Backup Setting

对于云服务而言, 最通用的是 Amazon 的 S3 对象存储协议, 有非常多的云服务厂商都提供兼容 S3 协议的对象存储服务

同时也可以自己部署开源项目作为替代

Minio 部署

S3 兼容的开源项目中最常见与名气最大的是 Minio

Minio

这里是一个我之前使用的 compose.yaml 文件

services:
  minio:
    image: minio/minio:latest
    container_name: minio
    network_mode: bridge
    restart: unless-stopped
    volumes:
      - ./volumes/minio_data:/data
    environment:
      - TZ=Asia/Shanghai
      - MINIO_ROOT_USER=minio_user
      - MINIO_ROOT_PASSWORD=minio_password
      - MINIO_DOMAIN=minio.example.com
      - MINIO_BROWSER_REDIRECT_URL=http://minio-con.example.com
    command: server /data --console-address ":9001"
    healthcheck:
      test: ["CMD", "mc", "ready", "local"]
      start_period: 30s
      interval: 10s
      timeout: 10s
      retries: 5
    labels:
      - traefik.enable=true
      - traefik.http.routers.minio.rule=Host(`minio.example.com`)||HostRegexp(`^.+\.minio\.example\.com$`)
      - traefik.http.routers.minio.service=minio
      - traefik.http.routers.minio.entrypoints=websecure
      - traefik.http.routers.minio.tls.certresolver=dnspod
      - traefik.http.routers.minio.tls.domains[0].main=minio.example.com
      - traefik.http.routers.minio.tls.domains[0].sans=*.minio.example.com
      - traefik.http.services.minio.loadbalancer.server.port=9000
      - traefik.http.routers.minio_console.rule=Host(`minio-con.example.com`)
      - traefik.http.routers.minio_console.service=minio_console
      - traefik.http.routers.minio_console.entrypoints=websecure
      - traefik.http.routers.minio_console.tls.certresolver=dnspod
      - traefik.http.services.minio_console.loadbalancer.server.port=9001

将密码和域名修改后, 使用 docker compose up -d 启动

之后可以在 Console 页面里增加你的 bucket

环境变量中的用户名密码可以直接作为 bucket 的 access_key 与 secret_key 使用

RPI Zero 2W 部署 Minio

我之前一直在云服务器上部署 Minio 作为我的群晖备份服务器, 但由于各种原因, 现在想要使用之前闲置的 Rpi Zero 2W

Raspberry Pi Zero 2W

树莓派 Zero 2W 提供给了一个4核的 CPU, 但每个核心也只有 1GHz 的频率, 同时内存也只有捉襟见肘的 512 MB, 从参数来看基本只能作为一个科技玩具

但在对象存储这个高 IO 场景下, 只要它能正确处理请求并写入本地就可以满足要求

于是我在它上面部署了 Minio, 但我接着发现 Minio 似乎并不适合它

  1. Minio 内存使用太大

    我的 Zero 2W 使用的是 ArchLinux Arm 系统, 启动后各种系统进程就需要占用 120+ MiB 的内存

    运行 Minio 后, Minio 的常驻内存大约有 200+ MiB, dockerd 也会使用 40 MiB 左右的内存

  2. CPU 周期性升高

    Minio 运行时会有周期性的 CPU 升高的情况, 可能与其内部的周期性任务有关

我的场景本质上是一个接受网络输入并写入本地的过程, 我不希望这个场景消耗太多的 CPU 和 内存资源

于是我又重新在 Github 上寻找是否有更符合我需求的服务

Garage

Garage 是另一个 S3 兼容的开源项目, 由 Rust 语言开发, 目前刚推出 2.0 版本

Garage

按照官方的快速开始文档的内容, 可以整理为一个 compose.yaml 文件

configs:
  garage_toml:
    content: |
        metadata_dir = "/var/lib/garage/meta"
        data_dir = "/var/lib/garage/data"
        db_engine = "sqlite"

        replication_factor = 1

        rpc_bind_addr = "[::]:3901"
        # rpc_public_addr = "127.0.0.1:3901"
        rpc_secret = "$(openssl rand -hex 32)"

        [s3_api]
        s3_region = "us-east-1"
        api_bind_addr = "[::]:3900"
        root_domain = ".zero.example.com"

        # [s3_web]
        # bind_addr = "[::]:3902"
        # root_domain = ".web.example.com"
        # index = "index.html"

        # [k2v_api]
        # api_bind_addr = "[::]:3904"

        # [admin]
        # api_bind_addr = "[::]:3903"
        # admin_token = "$(openssl rand -base64 32)"
        # metrics_token = "$(openssl rand -base64 32)"

services:
  garage:
    image: dxflrs/garage:v2.0.0
    container_name: garage
    restart: unless-stopped
    network_mode: bridge
    ports:
      - 80:3900
    volumes:
      - ./volumes/meta:/var/lib/garage/meta
      - ./volumes/data:/var/lib/garage/data
    configs:
    - source: garage_toml
      target: /etc/garage.toml

$(openssl rand -hex 32) 代表创建一个随机密钥, 运行后替换到文件中

  • rpc_bind_addr RPC 协议用于后续管理节点操作
  • s3_api S3 兼容协议端口
  • s3_web 可以通过浏览器直接访问存储的文件,实现匿名读取功能
  • k2v_api K2V 协议是 Garage 支持的另一种协议, 据说能有比 S3 协议更高的吞吐效率
  • admin 远程管理和监控设置

其中 s3_web, k2v_api, admin 都用不上, 因此直接注释掉

部署 Garage

使用 docker compose up -d 启动服务

之后按照以下顺序初始化节点

# 1. 查看当前节点状态, 命令会返回当前节点的 ID, 例如 3f4d09ad5e851f0d
docker exec -it garage /garage status

# 2. 为当前节点创建一个集群布局, -z <zone_name> -c <capacity>, 最后输入节点名称
docker exec -it garage /garage layout assign -z dc1 -c 16G <node_id>

# 3. 应用布局
docker exec -it garage /garage layout apply --version 1

# 4. 创建 bucket, 以 buckup 为例
docker exec -it garage /garage bucket create backup

# 5. 创建访问密钥, 创建完成后记得保存 access_key 和 secret_key
docker exec -it garage /garage key create backup-process-key

# 6. 将访问密钥分配给bucket
docker exec -it garage /garage bucket allow --read --write --owner backup --key backup-process-key

之后在 Hyper Backup 中设置自定义 S3 服务器

Custom S3 Server

其中请求类型有以下两种

  • 虚拟托管类型, 将 bucket 作为一个子域名进行访问, 如: <bucket>.example.com/<key>
  • 路径类型, 直接将 bucket 作为 URL 的一部分, 如: example.com/<bucket>/<key>

你需要根据实际的 DNS 解析方式(是否支持通配符子域名)来选择虚拟托管或路径类型

部署效果

Garage 在备份过程中,内存几乎没有超过 30MiB, 同时在没有请求的时候, CPU 也没有出现周期性的增加

Btop

整体使用资源量相当低

切换 Docker

部署完 Garage 之后, 相比实际 Garage 本身, Docker 守护进程消耗的内存反而更高, 性价比显得不太合理

于是我便准备替换掉 Docker, 替换品自然是 Podman

虽然 Docker 和 Podman 都是容器化工具, 但它们在架构上存在显著差异

Docker Architectures

Docker 采用的是 C-S 架构, Docker 在运行时会启动一个 dockerd 守护进程, 这个守护进程运行在后台并具有 root 权限, 你的所有操作实际上都是通过 CLI 与其交互完成

Podman Architectures

Podman 则不同, Podman 不存在 Daemon 进程, 所有的操作都由你的 CLI 直接完成, 从安全的角度来说, Podman 可以脱离 root 运行, 一个用户即使没有 root 权限, 也可以使用 Podman, 也就是所谓的 rootless

当然由于两者实现上的不同, 在切换时会有一点兼容性的问题

# 1. 在卸载前关闭你的所有 Docker 容器, 然后执行卸载工作
yay -R docker docker-compose docker-buildx

# 2. 删除残留文件
sudo rm -rf /var/lib/docker
sudo rm -rf /var/lib/containerd

# 3. 安装 Podman
yay -S podman podman-compose

# 4. 设置 subuid 和 subgid, rootless 模式下, 容器内部的 uid 和 gid
#    会被映射到宿主机用户空间的一段子 uid 和 gid 区间, 同时建议将这个范围设置大一点
#    通常一个镜像中可能不止一个 uid 或 gid
sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 <username>

# 5. 启动自动重启任务, 为了实现和 Docker 重启策略相同的功能
#    Podman 提供了一个 podman-restart.service 服务, 
#    会在系统启动时, 启动所有 restart-poliyc=always 的容器
#    但默认情况下只会对 root 用户的容器执行该操作
#    为了能够自动重启非 root 用户的容器, 需要额外执行以下操作
# 5.1 为该用户启用 linger 模式, 允许用户服务在用户未登录时运行
sudo loginctl enable-linger <username>

# 5.2 启用用户级的 systemd 服务文件
systemctl --user enable podman-restart.service

然后我们需要修改一下 compose.yaml 文件

services:
  garage:
    image: dxflrs/garage:v2.0.0
    container_name: garage
    restart: always
    network_mode: bridge
    ports:
      - 3900:3900
    volumes:
      - ./garage.toml:/etc/garage.toml
      - ./volumes/meta:/var/lib/garage/meta
      - ./volumes/data:/var/lib/garage/data
  1. 由于 podman-compose 还不支持 configs 特性, 因此我们需要将配置文件单独放置
  2. 我们的重启任务,只会对 restart-policy=always 的容器进行重启, 因此需要修改 restart: always
  3. 由于 rootless 模式下无法绑定 1-1024 范围内的特权端口,因此需将端口更改为非特权端口(如 3900)

之后就可以 使用 podman compose up -d 启动并按照之前的方式进行初始化, 当然初始化命令中的 docker 需要被替换为 podman