码仔
发布于 2026-04-06 / 6 阅读
0
0

Spring Boot + RustFS 构建高性能 S3 兼容的对象存储服务

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

开发语言

C++

Go

Rust

开源许可证

LGPL-2.1 / LGPL-3.0

AGPL-3.0

Apache-2.0

元数据中心

有(集中式/分布式元数据)

无(去中心化)

无(去中心化)

块存储支持

✅ 支持 (RBD)

❌ 不支持

❌ 不支持

文件存储支持

✅ 支持 (CephFS)

❌ 不支持

❌ 不支持

架构设计

重型架构,设计复杂

轻量级架构,简洁高效

轻量级架构,极致性能

社区活跃度

中(处于上升期)

许可证友好度

中(LGPL 限制较多)

差(AGPL 对商用不友好)

优(Apache 协议极其友好)

性能表现

强依赖硬件配置与调优

高性能、低延迟,适合大对象

极高性能,利用 Rust 零成本抽象

文件协议

S3, RBD, CephFS 等多种

S3

S3

使用/运维难度

高(配置复杂,门槛高)

低(开箱即用,运维简单)

低(部署简单,配置精简)

扩容能力

EB 级

EB 级

EB 级

硬件资源需求

高(对 CPU/内存消耗大)

中(资源占用适中)

低(内存安全且占用极低)

内存稳定性

稳定

高并发下可能存在抖动/GC 压力

极稳定(无 GC,内存管理精细)

扩容难度

难度高(数据重平衡压力大)

难度低

难度低

数据重平衡

资源占用高,影响业务

资源占用低

资源占用低

商业支持

✅ 完善

✅ 完善

✅ 提供

成熟度/稳定性

极高(多年生产环境验证)

高(对象存储事实标准)

较低(新兴项目,稳定性待验证)

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();
    }
}

关键配置说明

配置项

说明

endpointOverride

覆盖默认的 AWS 端点,指向本地 RustFS

forcePathStyle(true)

必须开启,否则 SDK 会使用虚拟主机风格(bucket.endpoint)导致 DNS 解析失败

region

S3 协议要求,本地部署可设为任意值

文件存储服务封装

服务类设计

将 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);
    }

    // 通用方法...
}

设计要点:

  1. 前缀分类:不同业务使用不同前缀(resumes/knowledgebases/

  2. 统一接口:上传、下载、删除操作统一封装

  3. 依赖注入:通过构造器注入 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._-]", "_");
}

设计原则

原则

说明

示例

前缀分类

便于权限隔离和生命周期管理

resumes/knowledgebases/

日期分片

避免单目录文件过多,便于按时间归档清理

2026/01/02/

UUID 防冲突

确保文件名唯一性

a1b2c3d4_

保留原名

便于用户识别和搜索

_zhangsan_resume.pdf


评论