RustFS 简介
RustFS 是一个基于 Rust 语言开发的高性能分布式对象存储软件,定位与 MinIO 高度相似,功能基本对齐 MinIO 开源版(包括分片上传、桶策略、版本控制、事件通知、生命周期管理等),完全兼容 AWS S3 协议,部署简单(Docker 一键启动),并提供现代化的可视化管理控制台。
根据官方同等硬件压测,RustFS 在小对象(4KB)场景下吞吐量约为 MinIO 的2.3 倍,大对象场景也高达1.8~2.2 倍。
与 MinIO 不同的是,RustFS 采用宽松的 Apache 2.0 许可证,对商业闭源产品更加友好。
由于 RustFS 完全兼容 S3 协议,我们可以直接使用 AWS S3 SDK 进行开发,无需额外适配。
RustFS vs Minio
Ceph 和 MinIO 在行业内毫无疑问是出于领先地位的!
RustFS 的性能和内存安全是其亮点,并且其使用的 Apache-2.0 协议相比 MinIO 的 AGPL 在商业化落地上的巨大优势(这也是很多企业选择新方案的核心考量点)。不过,RustFS 作为新兴项目,在稳定性上存在短板。
Spring Boot 集成实战
添加依赖
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3</artifactId>
<version>2.25.27</version>
</dependency>注意:使用 AWS SDK v2 而非 v1,v2 是完全重写的异步优先版本,性能更好,API 设计更现代。
配置属性
app:
storage:
endpoint: http://localhost:9000 # RustFS 服务地址
access-key: ${RUSTFS_ACCESS_KEY} # 访问密钥(建议使用环境变量)
secret-key: ${RUSTFS_SECRET_KEY} # 私密密钥
bucket: interview-guide # 存储桶名称
region: us-east-1 # 区域(S3协议需要,可任意设置)安全提示:生产环境中,access-key 和 secret-key 应通过环境变量或密钥管理服务(如 Vault、AWS Secrets Manager)注入,严禁硬编码。
配置属性类
使用 @ConfigurationProperties 实现类型安全的配置绑定:
@Data
@Component
@ConfigurationProperties(prefix = "app.storage")
public class StorageConfigProperties {
private String endpoint;
private String accessKey;
private String secretKey;
private String bucket;
private String region = "us-east-1";
}S3 客户端配置
@Configuration
@RequiredArgsConstructor
public class S3Config {
private final StorageConfigProperties storageConfig;
@Bean
public S3Client s3Client() {
AwsBasicCredentials credentials = AwsBasicCredentials.create(
storageConfig.getAccessKey(),
storageConfig.getSecretKey()
);
return S3Client.builder()
.endpointOverride(URI.create(storageConfig.getEndpoint()))
.region(Region.of(storageConfig.getRegion()))
.credentialsProvider(StaticCredentialsProvider.create(credentials))
.forcePathStyle(true) // 关键配置:使用路径风格访问
.build();
}
}关键配置说明:
文件存储服务封装
服务类设计
将 S3 操作封装为统一的 FileStorageService,对上层业务屏蔽存储细节:
@Slf4j
@Service
@RequiredArgsConstructor
public class FileStorageService {
private final S3Client s3Client;
private final StorageConfigProperties storageConfig;
// 文件操作
public String uploadResume(MultipartFile file) {
return uploadFile(file, "resumes");
}
public byte[] downloadResume(String fileKey) {
return downloadFile(fileKey);
}
public void deleteResume(String fileKey) {
deleteFile(fileKey);
}
// 知识库文件操作
public String uploadKnowledgeBase(MultipartFile file) {
return uploadFile(file, "knowledgebases");
}
public void deleteKnowledgeBase(String fileKey) {
deleteFile(fileKey);
}
// 通用方法...
}设计要点:
前缀分类:不同业务使用不同前缀(
resumes/、knowledgebases/)统一接口:上传、下载、删除操作统一封装
依赖注入:通过构造器注入 S3Client 和配置类
文件上传
/**
* 通用文件上传方法
*/
private String uploadFile(MultipartFile file, String prefix) {
String originalFilename = file.getOriginalFilename();
String fileKey = generateFileKey(originalFilename, prefix);
try {
PutObjectRequest putRequest = PutObjectRequest.builder()
.bucket(storageConfig.getBucket())
.key(fileKey)
.contentType(file.getContentType())
.contentLength(file.getSize())
.build();
s3Client.putObject(putRequest,
RequestBody.fromInputStream(file.getInputStream(), file.getSize()));
log.info("文件上传成功: {} -> {}", originalFilename, fileKey);
return fileKey;
} catch (IOException e) {
log.error("读取上传文件失败: {}", e.getMessage(), e);
throw new BusinessException(ErrorCode.STORAGE_UPLOAD_FAILED, "文件读取失败");
} catch (S3Exception e) {
log.error("上传文件到RustFS失败: {}", e.getMessage(), e);
throw new BusinessException(ErrorCode.STORAGE_UPLOAD_FAILED, "文件存储失败: " + e.getMessage());
}
}文件下载
/**
* 下载文件(通用方法)
*/
public byte[] downloadFile(String fileKey) {
// 先检查文件是否存在
if (!fileExists(fileKey)) {
throw new BusinessException(ErrorCode.STORAGE_DOWNLOAD_FAILED, "文件不存在: " + fileKey);
}
try {
GetObjectRequest getRequest = GetObjectRequest.builder()
.bucket(storageConfig.getBucket())
.key(fileKey)
.build();
return s3Client.getObjectAsBytes(getRequest).asByteArray();
} catch (S3Exception e) {
log.error("下载文件失败: {} - {}", fileKey, e.getMessage(), e);
throw new BusinessException(ErrorCode.STORAGE_DOWNLOAD_FAILED, "文件下载失败: " + e.getMessage());
}
}
/**
* 检查文件是否存在
*/
public boolean fileExists(String fileKey) {
try {
HeadObjectRequest headRequest = HeadObjectRequest.builder()
.bucket(storageConfig.getBucket())
.key(fileKey)
.build();
s3Client.headObject(headRequest);
return true;
} catch (NoSuchKeyException e) {
return false;
} catch (S3Exception e) {
log.warn("检查文件存在性失败: {} - {}", fileKey, e.getMessage());
return false;
}
}文件删除
/**
* 通用文件删除方法
*/
private void deleteFile(String fileKey) {
// 空键直接跳过
if (fileKey == null || fileKey.isEmpty()) {
log.debug("文件键为空,跳过删除");
return;
}
// 检查文件是否存在,避免不必要的删除操作
if (!fileExists(fileKey)) {
log.warn("文件不存在,跳过删除: {}", fileKey);
return;
}
try {
DeleteObjectRequest deleteRequest = DeleteObjectRequest.builder()
.bucket(storageConfig.getBucket())
.key(fileKey)
.build();
s3Client.deleteObject(deleteRequest);
log.info("文件删除成功: {}", fileKey);
} catch (S3Exception e) {
log.error("删除文件失败: {} - {}", fileKey, e.getMessage(), e);
throw new BusinessException(ErrorCode.STORAGE_DELETE_FAILED, "文件删除失败: " + e.getMessage());
}
}存储桶管理
应用启动时自动创建存储桶:
/**
* 确保存储桶存在
*/
public void ensureBucketExists() {
try {
HeadBucketRequest headRequest = HeadBucketRequest.builder()
.bucket(storageConfig.getBucket())
.build();
s3Client.headBucket(headRequest);
log.info("存储桶已存在: {}", storageConfig.getBucket());
} catch (NoSuchBucketException e) {
log.info("存储桶不存在,正在创建: {}", storageConfig.getBucket());
CreateBucketRequest createRequest = CreateBucketRequest.builder()
.bucket(storageConfig.getBucket())
.build();
s3Client.createBucket(createRequest);
log.info("存储桶创建成功: {}", storageConfig.getBucket());
} catch (S3Exception e) {
log.error("检查存储桶失败: {}", e.getMessage(), e);
}
}可以在应用启动时调用:
@Component
@RequiredArgsConstructor
public class StorageInitializer implements ApplicationRunner {
private final FileStorageService storageService;
@Override
public void run(ApplicationArguments args) {
storageService.ensureBucketExists();
}
}文件 Key 设计规范
良好的文件 Key 设计对于文件管理至关重要:
/**
* 生成文件存储键
* 格式: {prefix}/{yyyy/MM/dd}/{uuid}_{sanitized_filename}
* 示例: resumes/2026/01/02/a1b2c3d4_zhangsan_resume.pdf
*/
private String generateFileKey(String originalFilename, String prefix) {
LocalDateTime now = LocalDateTime.now();
String datePath = now.format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
String uuid = UUID.randomUUID().toString().substring(0, 8);
String safeName = sanitizeFilename(originalFilename);
return String.format("%s/%s/%s_%s", prefix, datePath, uuid, safeName);
}
/**
* 清理文件名中的特殊字符
*/
private String sanitizeFilename(String filename) {
if (filename == null) return "unknown";
return filename.replaceAll("[^a-zA-Z0-9._-]", "_");
}设计原则: