文章
努力加载图片中...
RustFS学习、SpringBoot 集成与MinIO迁移
  • 4576 字

  • 6 分钟

  • 14 次

  • 2025-12-29
标签:

1 前言

近期 MinIO 宣布开源版本进入 “维护模式”。从 5 月 22 日版起,阉割了 Web 的大部分功能,以及至今的种种迹象来看,MinIO 似乎要朝着闭源商业版发展,对我这种穷小子来说,还是很无奈的。

我用的是 4 月 22 日版的 MinIO,包括这个博客也是,但是这个版本现在只有 Linux 版和 Docker 版了,Window 版的我在网上找遍了都找不到。

因为我的博客是要长期维护和更新的,所以我打算从 MinIO 迁移到最近很火的 RustFS 上,并记录我的迁移过程。

我将从 RustFS 学习、SpringBoot 的集成和从 MinIO 迁移的过程三个方向来记录,这样思路比较清晰一点。

2 RustFS 学习

2.1 RustFS 介绍

先贴一下我从官网找到的资料

总结一下

  1. RustFS 是使用目前全球最流行的、内存安全的 Rust 语言开发的 高性能 , 分布式对象存储软件。RustFS 100% 兼容 S3 协议,使用 Apache2 许可证发行的开源软件。RustFS 即将从商业应用正式向开源,向全球进行发布,帮助全世界降低存储成本、提升数据安全。
  2. 官网说过 RustFS 是 MinIO 的平替产品,从官网的对比上看,性能和 MinIO 相同、使用难度低、但是资源占用更少、内存更加稳定、存储速度很快。最主要的是许可证友好、社区活跃度高,生态趋势十分向好。

除此之外,官网还说明了 Nginx 如何进行反向代理的配置: RustFS Nginx反向代理配置 | RustFS 文档中心

提供了 Java 的 SDK: Java SDK | RustFS 文档中心

在 SDK 中,对集成和使用都做了详细的说明,并且附加了代码示例,其中还有如何进行预签名上传和下载,分片上传等等,相当周到。

2.2 使用 Docker 安装 RustFS

我电脑是 Windows 11 系统,服务器是 Linux 的,所以我平时都是使用 Docker 来安装这些东西,本次我也主要使用 Docker 来安装 RustFS。

Docker 安装就很简单了,官网给出了指南: Docker 安装 RustFS | RustFS 文档中心

我使用 Docker Compose 来安装,Docker Compose 的配置可以参考 RustFS 官方仓库的文件: rustfs/docker-compose-simple.yml at main · rustfs/rustfs · GitHub

首先新建一个 rustfs_demo_docker 文件夹,在文件夹中新建一个 docker-compose.yml 文件,然后填入以下配置

yml
version: "3.9" services: rustfs: image: rustfs/rustfs:latest container_name: rustfs-server security_opt: - "no-new-privileges:true" ports: - "9000:9000" # S3 API 对外端口 - "9001:9001" # 控制台对外端口 environment: # 数据卷(多个路径用逗号分隔) - RUSTFS_VOLUMES=/data/rustfs0 # API 和控制台监听地址 - RUSTFS_ADDRESS=0.0.0.0:9000 - RUSTFS_CONSOLE_ADDRESS=0.0.0.0:9001 - RUSTFS_CONSOLE_ENABLE=true - RUSTFS_EXTERNAL_ADDRESS=:9000 # CORS 设置,控制台与 S3 API 都放开来源 - RUSTFS_CORS_ALLOWED_ORIGINS=* - RUSTFS_CONSOLE_CORS_ALLOWED_ORIGINS=* # 访问密钥 - RUSTFS_ACCESS_KEY=rustfsadmin - RUSTFS_SECRET_KEY=rustfsadmin volumes: - ./rustfs_data_0:/data/rustfs0 - ./logs:/app/logs networks: - rustfs-network restart: unless-stopped healthcheck: test: [ "CMD", "sh", "-c", "curl -f http://localhost:9000/health && curl -f http://localhost:9001/rustfs/console/health" ] interval: 30s timeout: 10s retries: 3 start_period: 40s networks: rustfs-network: driver: bridge

我都是参照官网和仓库改的

打开命令行,输入 docker-compose up Pasted image 20251215140445

打开浏览器,输入 http://localhost:9001 访问 Web 端 Pasted image 20251215140644

喔吼,居然还是中文的,太贴心了。

输入 docker-compose.yml 中,环境变量里定义的 RUSTFS_ACCESS_KEYRUSTFS_SECRET_KEY 也就是账号密码。 Pasted image 20251215140842

成功了,非常顺利。我记得一开始安装 MinIO 还遇到了很多问题,特别是那恼人的 Invalid Login

简单创建一个存储桶(bucket),上传几个文件试试。 Pasted image 20251215141201

桶名不能用下划线哦 Pasted image 20251215141225

Pasted image 20251215141307

Pasted image 20251215141323

Pasted image 20251215141341

在存储桶列表中,点击设置,将同的访问权限改为公有 Pasted image 20251215141500

然后在浏览器打开新的标签页,按照格式:http://localhost:9000/[bucket_name]/[file_name] 访问一下上传的图片 Pasted image 20251215141709

成功了,很顺利。接下来就是 RustFS 的使用了。

2.3 SpringBoot 集成

demo 仓库地址:rustfs_demo: RustFS Learning Demo

先快速搭一个 SpringBoot 项目。

首先创建一个 Java 项目,选好项目地址,Build system 选 Maven,我用 JDK 17 比较多,所以 JDK 我就选 17 了。

Pasted image 20251215142146

Name 标黄是因为我以及创建好了一个,项目名重复了。

打开 pom.xml 文件,在 <properties> 标签下添加下面几个配置:

xml
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>4.0.0</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> </dependencies>

正好 SpringBoot 4 出了,顺便弄来耍耍。 这里的依赖不需要写版本号,因为 spring-boot-starter-parent 已经预定义好了,如果不清楚哪些不需要写版本号,可以去 Maven 仓库看看:Maven Repository: org.springframework.boot » spring-boot-starter-parent » 4.0.0

将示例的 Main 类,更名为 RustFSDemoApplication,然后将内容修改

java
package com.example; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class RustFSDemoApplication { public static void main(String[] args) { SpringApplication.run(RustFSDemoApplication.class, args); } }

这时候就可以跑了,运行 RustFSDemoApplicationPasted image 20251215142923

当然,能直接跑还是归功于 SpringBoot 约定大于配置和自动配置的特点,为了更加灵活的适配我们的项目,我们还需要编写配置文件。

resource 目录下,创建 application.yml 文件,添加一些无关紧要的简单配置

yml
server: port: 9220 # 端口命名随意 spring: application: name: rustfs-demo-application # 应用名也随意

大功告成了。

现在,参照官网的 SDK 来简单集成 RustFS。 Java SDK | RustFS 文档中心

首先在 pom.xml 文件中,在 <dependencies> 中添加依赖

xml
<dependency> <groupId>software.amazon.awssdk</groupId> <artifactId>s3</artifactId> <version>2.40.8</version> </dependency>

为了方便配置的管理,我们将项目需要的配置,写在 application.yml 中,然后通过 ConfigurationProperties + EnableConfigurationProperties 这两个注解读取配置,并创建 S3 客户端,将其注册为 Bean。

首先在 application.yml 中,添加如下配置

yml
rustfs: endpoint: http://localhost:9000 access-key: rustfsadmin secret-key: rustfsadmin

在和 RustFSDemoApplication 同级目录下,创建 model.properties.RustFSProperties 类(model 和 properties 是包名,点击新建 Java 类的时候直接复制上面这个,IDEA 会自动创建包)。然后写入以下内容

java
package com.example.model.properties; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; @Data @ConfigurationProperties(prefix = "rustfs") public class RustFSProperties { private String endpoint; private String accessKey; private String secretKey; }

这个类的主要作用的读取 application.yml 中的配置

然后同样在和 RustFSDemoApplication 同级目录下,创建 config.RustFSConfig ,写入以下内容

java
package com.example.config; import ch.qos.logback.core.net.server.Client; import com.example.model.properties.RustFSProperties; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.s3.S3Client; import java.net.URI; @Configuration @EnableConfigurationProperties(RustFSProperties.class) public class RustFSConfig { @Autowired private RustFSProperties rustFSProperties; @Bean public S3Client s3Client() { return S3Client.builder() .endpointOverride(URI.create(rustFSProperties.getEndpoint())) // 中国北京区域,官网说这个可以写死,因为 RustFS 不会校验 .region(Region.CN_NORTH_1) .credentialsProvider( StaticCredentialsProvider.create( AwsBasicCredentials.create( // 写 accessKey 和 secretKey rustFSProperties.getAccessKey(), rustFSProperties.getSecretKey() ) ) ) // 这个一定要开启 .forcePathStyle(true) .build(); } }

这个类作用是启用 RustFSProperties 配置,并且根据配置将 S3Client 注册为 Bean。

至此,RustFS 就集成好了,这里可以跑一下程序看看有没有报错。接下来就是要使用这个 S3Client 编写一个简单工具类,实现单个文件的上传和下载。

这里就不多废话了,直接贴代码

  1. utils.RustFSUtil
java
package com.example.utils; import com.example.model.properties.RustFSProperties; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.web.multipart.MultipartFile; import software.amazon.awssdk.core.ResponseBytes; import software.amazon.awssdk.core.sync.RequestBody; import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.s3.model.*; @Component @Slf4j public class RustFSUtil { @Autowired private S3Client s3Client; @Autowired private RustFSProperties rustFSProperties; /** * 创建 Bucket * * @param bucketName Bucket 名称 */ public void createBucket(String bucketName) { try { s3Client.createBucket( CreateBucketRequest.builder() .bucket(bucketName) .build() ); log.info("Bucket: {} 创建成功", bucketName); } catch (BucketAlreadyExistsException | BucketAlreadyOwnedByYouException e) { log.error("Bucket: {} 已经存在", bucketName, e); throw new RuntimeException("Bucket 创建失败,原因:Bucket: " + bucketName + " 已经存在"); } } /** * 上传单个文件 * * @param file 要上传的文件 * @param bucketName Bucket 名称 * @return 文件的 URL */ public String uploadSingleFile(MultipartFile file, String bucketName) { try { s3Client.putObject( PutObjectRequest.builder() .bucket(bucketName) .key(file.getOriginalFilename()) .build(), RequestBody.fromInputStream(file.getInputStream(), file.getSize()) ); log.info("文件: {} 上传成功", file.getOriginalFilename()); return rustFSProperties.getEndpoint() + "/" + bucketName + "/" + file.getOriginalFilename(); } catch (Exception e) { log.error("上传文件失败,原因: {}", e.getMessage(), e); throw new RuntimeException(e); } } /** * 下载单个文件 * * @param bucketName Bucket 名称 * @param fileName 文件名称 * @return 文件的二进制数据 */ public byte[] downloadSingleFile(String bucketName, String fileName) { try { ResponseBytes<GetObjectResponse> objectAsBytes = s3Client.getObjectAsBytes( GetObjectRequest.builder() .bucket(bucketName) .key(fileName) .build() ); byte[] bytes = objectAsBytes.asByteArray(); log.info("文件: {} 下载成功", fileName); return bytes; } catch (Exception e) { log.error("上传文件失败,原因: {}", e.getMessage(), e); throw new RuntimeException(e); } } }
  1. service.FileService
java
package com.example.service; import jakarta.servlet.http.HttpServletResponse; import org.springframework.web.multipart.MultipartFile; public interface FileService { String createBucket(String bucketName); String uploadSingleFile(MultipartFile file, String bucketName); void downloadSingleFile(String bucketName, String fileName, HttpServletResponse response); }
  1. service.impl.FileServiceImpl
java
package com.example.service.impl; import com.example.service.FileService; import com.example.utils.RustFSUtil; import jakarta.servlet.ServletOutputStream; import jakarta.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; import java.io.IOException; @Slf4j @Service public class FileServiceImpl implements FileService { @Autowired private RustFSUtil rustFSUtil; @Override public String createBucket(String bucketName) { try { rustFSUtil.createBucket(bucketName); return "Bucket: " + bucketName + " 创建成功"; } catch (Exception e) { return e.getMessage(); } } @Override public String uploadSingleFile(MultipartFile file, String bucketName) { return rustFSUtil.uploadSingleFile(file, bucketName); } @Override public void downloadSingleFile(String bucketName, String fileName, HttpServletResponse response) { byte[] bytes = rustFSUtil.downloadSingleFile(bucketName, fileName); response.setContentType("application/octet-stream"); response.setHeader("Content-Disposition", "attachment; filename=" + fileName); response.setContentLength(bytes.length); try (ServletOutputStream outputStream = response.getOutputStream()) { outputStream.write(bytes); outputStream.flush(); } catch (IOException e) { log.error("下载文件失败,原因: {}", e.getMessage(), e); response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); throw new RuntimeException(e); } } }
  1. controller.FileController
java
package com.example.controller; import com.example.service.FileService; import jakarta.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; @RestController @RequestMapping("/file") public class FileController { @Autowired private FileService fileService; /** * 创建 Bucket * @param bucketName Bucket 名称 * @return 创建结果 */ @PostMapping("/createBucket") public String createBucket(String bucketName) { return fileService.createBucket(bucketName); } /** * 上传单个文件 * * @param file 要上传的文件 * @param bucketName Bucket 名称 * @return 文件的 URL */ @PostMapping("/uploadSingleFile") public String uploadSingleFile(MultipartFile file, String bucketName) { return fileService.uploadSingleFile(file, bucketName); } /** * 下载单个文件 * * @param bucketName Bucket 名称 * @param fileName 文件名称 * @param response HttpServletResponse 对象 */ @GetMapping("/downloadSingleFile") public void downloadSingleFile(String bucketName, String fileName, HttpServletResponse response) { fileService.downloadSingleFile(bucketName, fileName, response); } }

使用 ApiFox 进行接口测试:

  1. 创建桶 Pasted image 20251215155726

Pasted image 20251215155803

Pasted image 20251215155816

  1. 上传文件 Pasted image 20251215155952

Pasted image 20251215160045

  1. 下载文件 Pasted image 20251215160130

都没问题,相当顺利。

2.4 预签名上传、预签名下载

上面的文件上传和下载功能,在生产环境不是很优雅。

因为本质上,上传是先把文件上传到后端,后端再把文件上传到 RustFS。下载则是先把文件从 RustFS 下载到后端,然后响应给前端。

文件在中间中转了两次,后端服务则相当于一个中转站,但实际上这样有点多此一举了。如果是大文件上传的话,还会长时间占用后端线程。

预签名上传和预签名下载,本质上是后端通过 RustFS,给前端用户一个临时的上传/下载凭证,让前端用户通过这个凭证,直接将文件上传到 RustFS ,或者直接从 RustFS 将文件下载下来。

如果像我的博客一样,前后端分开两个服务器部署,担心这么做会直接暴露 RustFS 的话,就可以在前端服务器,使用 Nginx 做反向代理,让 Nignx 帮你转发。

首先,需要注册预签名所需的 Bean,在 config.RustFSConfig 中,添加如下代码

java
@Bean public S3Presigner s3Presigner() { return S3Presigner.builder() .endpointOverride(URI.create(rustFSProperties.getEndpoint())) .region(Region.US_EAST_1) .credentialsProvider( StaticCredentialsProvider.create( AwsBasicCredentials.create( // 写 accessKey 和 secretKey rustFSProperties.getAccessKey(), rustFSProperties.getSecretKey() ) ) ) .serviceConfiguration( S3Configuration.builder() // 启用 pathStyleAccessEnabled .pathStyleAccessEnabled(true) .build() ) .build(); }

注意 serviceConfiguration 这个配置,pathStyleAccessEnabled 一定要启用,官网没说这个。如果不启用,预签名的 url 就会使用虚拟主机风格,就是 http://[bucket_name].localhost:9000 这种,用这个上传和下载文件会失败!我花了三个小时才排查出来的(泪)。如果启用了路径风格,也就是 path Style,就不会了。

然后创建一个专门用于处理预签名的工具类 utils.RustFSPresignedUtil ,然后添加如下代码

java
package com.example.utils; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import software.amazon.awssdk.services.s3.model.GetObjectRequest; import software.amazon.awssdk.services.s3.model.PutObjectRequest; import software.amazon.awssdk.services.s3.presigner.S3Presigner; import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest; import software.amazon.awssdk.services.s3.presigner.model.PresignedGetObjectRequest; import software.amazon.awssdk.services.s3.presigner.model.PresignedPutObjectRequest; import software.amazon.awssdk.services.s3.presigner.model.PutObjectPresignRequest; import java.time.Duration; @Component @Slf4j public class RustFSPresignedUtil { @Autowired private S3Presigner s3Presigner; /** * 预签名上传文件 * * @param fileName 要上传的文件 * @param bucketName Bucket 名称 * @return 预签名的 URL */ public String uploadSingleFileByPresignedUrl(String fileName, String bucketName) { try { PresignedPutObjectRequest presignedPutObjectRequest = s3Presigner.presignPutObject( PutObjectPresignRequest.builder() .putObjectRequest( PutObjectRequest.builder() .bucket(bucketName) .key(fileName) // 移除强制Content-Type,让客户端自由设置 .build() ) // 十分钟有效时间 .signatureDuration(Duration.ofMinutes(10)) .build() ); return presignedPutObjectRequest.url().toString(); } catch (Exception e) { log.error("上传预签名获取失败,原因: {}", e.getMessage(), e); throw new RuntimeException("上传预签名获取失败,原因: " + e.getMessage()); } } /** * 预签名下载文件 * * @param bucketName Bucket 名称 * @param fileName 文件名称 * @return 预签名的 URL */ public String downloadSingleFileByPresignedUrl(String bucketName, String fileName) { try { PresignedGetObjectRequest presignedGetObjectRequest = s3Presigner.presignGetObject( GetObjectPresignRequest.builder() .getObjectRequest( GetObjectRequest.builder() .bucket(bucketName) .key(fileName) .build() ) // 十分钟有效时间 .signatureDuration(Duration.ofMinutes(10)) .build() ); return presignedGetObjectRequest.url().toString(); } catch (Exception e) { log.error("下载预签名获取失败,原因: {}", e.getMessage(), e); throw new RuntimeException("下载预签名获取失败,原因: " + e.getMessage()); } } }

添加接口方法:

  1. FileService
java
package com.example.service; import jakarta.servlet.http.HttpServletResponse; import org.springframework.web.multipart.MultipartFile; public interface FileService { String createBucket(String bucketName); String uploadSingleFile(MultipartFile file, String bucketName); void downloadSingleFile(String bucketName, String fileName, HttpServletResponse response); String uploadSingleFileByPresignedUrl(String fileName, String bucketName); String downloadSingleFileByPresignedUrl(String bucketName, String fileName); }
  1. FileServiceImpl
java
@Autowired private RustFSPresignedUtil rustFSPresignedUtil; @Override public String uploadSingleFileByPresignedUrl(String fileName, String bucketName) { return rustFSPresignedUtil.uploadSingleFileByPresignedUrl(fileName, bucketName); } @Override public String downloadSingleFileByPresignedUrl(String bucketName, String fileName) { return rustFSPresignedUtil.downloadSingleFileByPresignedUrl(bucketName, fileName); }
  1. FileController
java
@Autowired private FileService fileService; /** * 获取上传文件的预签名 URL * * @param fileName 要上传的文件 * @param bucketName Bucket 名称 * @return 预签名 URL */@GetMapping("/uploadSingleFileByPresignedUrl") public String uploadSingleFileByPresignedUrl(String fileName, String bucketName) { return fileService.uploadSingleFileByPresignedUrl(fileName, bucketName); } /** * 获取下载文件的预签名 URL * * @param bucketName Bucket 名称 * @param fileName 文件名称 * @return 预签名 URL */@GetMapping("/downloadSingleFileByPresignedUrl") public String downloadSingleFileByPresignedUrl(String bucketName, String fileName) { return fileService.downloadSingleFileByPresignedUrl(bucketName, fileName); }

可以启动项目测试一下,先是获取上传文件的预签名 url: Pasted image 20251215200707

使用 ApiFox 来上传: Pasted image 20251215200824

注意使用 put 方法,并且文件要使用 binary 来传

上传成功 Pasted image 20251215201003

获取下载文件的预签名 url Pasted image 20251215201116

用 get 方法直接请求该 url ,也可以直接用浏览器访问,访问成功。 Pasted image 20251215201206

3 从 MinIO 迁移至 RustFS

可以参考 RustFS 官方微信公众号的教程: # 如何从MinIO迁移至RustFS? 手把手教程

喔吼,没想到 MinIO Client(mc)这么万能,居然还能跨服务迁移哈哈。那这样数据迁移就很简单了,因为我之前发布过使用 mc 进行数据迁移的教程,这边就再复习一次吧。非常简单。 MinIO-Windows系统下Docker高版本与本机低版本的安装、数据迁移记录

在官网下载 mc.exe ,也就是 MinIO Client 工具。 MinIO下载和安装 | 用于创建高性能对象存储的代码和下载内容

随便放到一个目录下,然后在该目录打开 CMD,按照以下格式输入命令:

shell
mc alias set <别名1> < MinIO ip:端口> <Access Key> <Secret Key> mc alias set <别名2> < RustFS ip:端口> <Access Key> <Secret Key> mc cp --recursive <(源)别名1>/<名>/ <(目标)别名2>/<名>/

注意桶名后面要加斜杠 /,不然可能会出现迁移后路径不对的问题。

Snipaste_2025-12-28_13-23-26.png

我这里是将我服务器的数据,迁移到了本地。

这样就迁移完成了,我这里就不展示了。因为我迁移完就把本地的这个 RustFS 容器删了,只留了数据卷。

4 总结

  1. 了解了 RustFS 是什么。
  2. 学会了 RustFS 的安装与部署。
  3. 学会了如何在 SpringBoot 中操作 RustFS。
  4. 学会了在 SpringBoot 中通过预签名实现 RustFS 的文件上传和下载。
  5. 复习了对象存储服务的数据迁移操作。
作者: Xigrut发布时间: 2025-12-29 00:34:15上次编辑时间: 2026-06-18 19:22:16 许可协议: CC BY-NC-SA 4.0
留言区