分布式文件存储服务设计与实现:基于 brpc+MinIO+Redis+etcd 的全栈方案
在分布式系统中,文件存储服务需要解决高可用、高性能、可扩展三大核心问题。本文将详细解析一套基于 brpc(RPC 框架)、MinIO(对象存储)、Redis(缓存 / 元数据存储)、etcd(服务注册发现)的分布式文件存储服务实现,包含服务端核心逻辑、依赖封装、RPC 接口设计及客户端测试全流程,助力开发者快速搭建企业级文件存储解决方案。
一、系统架构总览
本文件存储服务采用分层设计,整体架构如下:
┌─────────────────┐ ┌─────────────────────────────────────┐
│ 客户端层 │ │ 服务端层 │
│ (测试/业务客户端)│◄────►│ ┌─────────┐ ┌─────────────────┐ │
└─────────────────┘ │ │ RPC服务 │ │ 核心依赖层 │ │
│ │(brpc) │◄─►│ MinIO+Redis+LRU │ │
┌─────────────────┐ │ └─────────┘ └─────────────────┘ │
│ 服务注册发现层 │ │ ▲ │
│ (etcd) │◄────► │ │ │
└─────────────────┘ │ ┌─────────┐ ┌─────────────────┐ │
│ │服务构建器│ │ 元数据管理 │ │
│ │(Builder)│◄─►│MultipartManager │ │
│ └─────────┘ └─────────────────┘ │
└─────────────────────────────────────┘
核心功能特性
-
支持单文件上传 / 下载 、多文件批量上传 / 下载 、分块上传 / 合并三种存储模式
-
基于 MinIO 实现可靠的对象存储,兼容 S3 协议,支持分布式部署
-
Redis 持久化分块上传元数据,LRU 缓存热点文件信息,提升访问性能
-
etcd 实现服务注册与发现,支持服务动态扩容、故障自动切换
-
完善的错误处理、参数校验、资源释放机制,保证服务稳定性
二、服务端核心实现解析
2.1 服务初始化流程(main 函数)
服务启动遵循「参数解析→日志初始化→依赖初始化→服务构建→启动」的标准化流程,代码结构清晰,可维护性强:
int main(int argc, char *argv[]) {
// 1. 命令行参数解析(google::gflags)
google::ParseCommandLineFlags(&argc, &argv, true);
// 2. 日志初始化(支持调试/发布模式切换)
zrt::init_logger(FLAGS_run_mode, FLAGS_log_file, FLAGS_log_level);
LOG_INFO("日志初始化完成,运行模式:{}", FLAGS_run_mode ? "发布" : "调试");
try {
// 3. Redis客户端初始化(分块元数据存储)
sw::redis::ConnectionOptions redis_opts;
redis_opts.host = FLAGS_redis_host;
redis_opts.port = FLAGS_redis_port;
redis_opts.password = FLAGS_redis_password;
redis_opts.db = FLAGS_redis_db;
auto redis_client = std::make_shared<sw::redis::Redis>(redis_opts);
redis_client->ping(); // 连接验证,失败直接抛异常
LOG_INFO("Redis客户端初始化成功");
// 4. MinIO客户端初始化(文件存储核心)
zrt::MinIOClient::ptr minio_client = std::make_shared<zrt::MinIOClient>(
FLAGS_minio_endpoint, FLAGS_minio_access_key, FLAGS_minio_secret_key, FLAGS_minio_bucket
);
LOG_INFO("MinIO客户端初始化成功");
// 5. 构建服务(Builder模式封装复杂初始化)
zrt::FileServerBuilder fsb;
fsb.set_minio_params(FLAGS_minio_endpoint, FLAGS_minio_access_key, FLAGS_minio_secret_key, FLAGS_minio_bucket);
fsb.set_redis_client(redis_client);
fsb.make_rpc_server(FLAGS_listen_port, FLAGS_rpc_timeout, FLAGS_rpc_threads, FLAGS_storage_path);
fsb.make_reg_object(FLAGS_registry_host, FLAGS_base_service + FLAGS_instance_name, FLAGS_access_host);
// 6. 启动服务
auto server = fsb.build();
LOG_INFO("文件存储服务启动成功,监听端口:{}", FLAGS_listen_port);
server->start(); // 阻塞运行,直到收到退出信号
} catch (const std::exception& e) {
LOG_ERROR("服务启动失败:{}", e.what());
return -1;
}
return 0;
}
关键设计亮点
-
参数化配置:通过 gflags 定义所有可配置项(端口、依赖地址、缓存大小等),支持启动时动态调整
-
失败快速反馈:核心依赖(Redis/MinIO)初始化失败时直接抛异常,终止服务启动,避免无效运行
-
Builder 模式 :通过
FileServerBuilder封装服务构建细节,解耦初始化逻辑,提升代码可读性
2.2 核心依赖封装
2.2.1 MinIOClient:对象存储核心封装
MinIOClient 是对 MinIO C++ SDK 的二次封装,屏蔽底层细节,提供简洁的文件操作接口,适配服务端存储需求:
class MinIOClient {
public:
using ptr = std::shared_ptr<MinIOClient>;
// 构造函数:解析endpoint、验证Bucket存在性
MinIOClient(const std::string& endpoint, const std::string& access_key,
const std::string& secret_key, const std::string& bucket_name);
// 基础操作:上传/下载/删除
bool upload(const std::string& object_key, const std::string& data);
bool download(const std::string& object_key, std::string& out_data);
bool remove(const std::string& object_key);
// 分块上传相关:上传分块、合并分块
bool upload_chunk(const std::string& file_id, uint32_t chunk_index, const std::string& data);
bool merge_chunks(const std::string& file_id, uint32_t total_chunks, const std::string& target_object_key);
};
核心逻辑解析
-
endpoint 解析 :自动识别
http/https协议,提取主机名和端口,兼容 MinIO 默认端口(9000)和自定义端口 -
Bucket 校验:初始化时检查 Bucket 是否存在,避免后续操作失败
-
分块处理:
-
分块存储路径规范:
multipart/{file_id}/{chunk_index},便于管理和清理 -
合并分块时先下载所有分块→合并→上传完整文件→清理临时分块,保证数据一致性
- 错误处理:捕获 SDK 异常和网络异常,返回布尔值并打印详细日志,便于问题排查
2.2.2 Redis 与分块元数据管理
Redis 主要用于存储分块上传的元数据(文件大小、分块数、已上传分块索引等),配合 MultipartManager 实现分块状态管理:
class MultipartManager {
public:
using ptr = std::shared_ptr<MultipartManager>;
// 初始化分块上传:生成file_id,存储元数据到Redis(24小时过期)
std::string init_upload(const std::string& file_name, uint64_t file_size, uint32_t chunk_size = 5MB);
// 标记分块上传完成
bool mark_chunk_uploaded(const std::string& file_id, uint32_t chunk_index);
// 检查所有分块是否上传完成
bool is_all_chunks_uploaded(const std::string& file_id);
// 清理元数据(合并完成后调用)
bool clean_up(const std::string& file_id);
};
设计亮点
-
元数据序列化 :使用 JSON 序列化
MultipartMeta结构体,便于 Redis 存储和读取 -
LRU 缓存:内存缓存热点元数据,减少 Redis 访问次数,提升响应速度
-
过期清理:Redis 键设置 24 小时过期时间,避免未完成的分块上传占用存储空间
2.2.3 LRUCache:热点数据缓存
基于双向链表 + 哈希表实现线程安全的 LRU 缓存,用于缓存文件元信息和 file_id→MinIO路径 映射,提升访问性能:
template <typename Key, typename Value>
class LRUCache {
public:
void put(const Key& key, const Value& value); // 插入/更新缓存
std::optional<Value> get(const Key& key); // 获取缓存,未命中返回nullopt
void erase(const Key& key); // 删除缓存项
private:
std::list<std::pair<Key, Value>> _cache_list; // 双向链表:头部=最近使用,尾部=最久未使用
std::unordered_map<Key, typename std::list<std::pair<Key, Value>>::iterator> _cache_map; // 哈希表:快速查找
std::mutex _mutex; // 线程安全锁
};
核心特性
-
线程安全:所有操作加互斥锁,支持多线程 RPC 服务并发访问
-
淘汰策略:超出最大容量时,淘汰最久未使用的节点,避免内存溢出
-
可选回调:支持设置节点淘汰时的回调函数(如清理关联资源)
2.3 RPC 服务实现(FileServiceImpl)
FileServiceImpl 实现了定义的 RPC 接口,核心逻辑围绕「文件上传 / 下载」和「分块上传管理」展开,每个接口都包含参数校验→核心业务→结果响应的标准化流程:
2.3.1 单文件上传核心逻辑
void PutSingleFile(google::protobuf::RpcController *controller,
const ::zrt::PutSingleFileReq *request,
::zrt::PutSingleFileRsp *response,
::google::protobuf::Closure *done) {
brpc::ClosureGuard rpc_guard(done); // 自动释放RPC资源,避免内存泄漏
response->set_request_id(request->request_id());
// 1. 参数校验(文件名、文件大小)
if (request->file_data().file_name().empty() || request->file_data().file_size() == 0) {
response->set_success(false);
response->set_errmsg("文件名或文件大小非法");
return;
}
// 2. 生成唯一file_id(基于UUID)
std::string fid = uuid();
// 3. 定义MinIO存储路径(规范:files/single/{file_id}/{filename})
std::string object_key = "files/single/" + fid + "/" + request->file_data().file_name();
// 4. 上传到MinIO
bool upload_ok = _minio->upload(object_key, request->file_data().file_content());
if (!upload_ok) {
response->set_success(false);
response->set_errmsg("文件上传到MinIO失败");
return;
}
// 5. 缓存元数据(file_id→元信息、file_id→MinIO路径)
FileMessageInfo file_info;
file_info.set_file_id(fid);
file_info.set_file_name(request->file_data().file_name());
file_info.set_file_size(request->file_data().file_size());
_file_meta_cache->put(fid, file_info);
_file_id_to_object_key->put(fid, object_key);
// 6. 响应结果
response->set_success(true);
*response->mutable_file_info() = file_info;
}
2.3.2 分块上传完整流程
分块上传分为三个阶段,通过三个接口协同实现:
-
InitMultipartUpload:初始化上传,生成 file_id 和分块配置(分块大小、总块数)
-
UploadPart:上传单个分块,校验分块索引合法性,更新上传状态
-
CompleteMultipartUpload:校验所有分块是否上传完成,合并分块为完整文件
核心亮点:
-
参数校验:严格校验 file_id、分块索引、分块数据,避免非法请求
-
原子性保证:分块合并失败时,清理已上传的临时分块,避免垃圾数据
-
状态一致性:通过 Redis 持久化分块状态,服务重启后可恢复上传进度
2.4 服务构建器(FileServerBuilder)
采用 Builder 设计模式,封装服务构建的复杂流程,将「依赖设置→RPC 服务器构建→服务注册」解耦,简化服务初始化:
class FileServerBuilder {
public:
void set_minio_params(...) { /* 设置MinIO配置 */ }
void set_redis_client(...) { /* 设置Redis客户端,初始化分块管理器和缓存 */ }
void make_rpc_server(...) { /* 构建brpc服务器,注册FileServiceImpl */ }
void make_reg_object(...) { /* 注册服务到etcd */ }
FileServer::ptr build() { /* 构建最终的FileServer实例 */ }
};
设计优势
-
隐藏构建细节:调用者无需关注 RPC 服务器配置、服务注册流程,只需设置核心依赖
-
依赖校验:
build()前检查核心依赖是否初始化,避免空指针异常 -
扩展性强:新增依赖(如监控模块)时,只需在 Builder 中添加对应的
set_xxx方法,不影响原有逻辑
三、客户端测试实现
客户端基于 gtest 框架,实现全接口测试,覆盖单文件、多文件、分块上传的完整流程,确保服务可用性:
3.1 测试架构
┌─────────────────────────────────────┐
│ 测试用例设计(按功能分组) │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ 单文件测试 │ │ 多文件测试 │ │
│ │ - 上传 │ │ - 批量上传 │ │
│ │ - 下载 │ │ - 批量下载 │ │
│ └─────────────┘ └─────────────┘ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ 分块上传测试 │ │ 兼容性测试 │ │
│ │ - 初始化 │ │ - 异常参数 │ │
│ │ - 上传分块 │ │ - 服务降级 │ │
│ │ - 合并文件 │ │ - 并发访问 │ │
│ │ - 下载文件 │ └────────────┘ │
│ └─────────────┘ │
└─────────────────────────────────────┘
3.2 核心测试用例解析
分块上传测试(核心流程)
// 1. 初始化分块上传
TEST(multipart_test, init_multipart_upload) {
std::string test_file_content;
ASSERT_TRUE(zrt::readFile("./Makefile", test_file_content)) << "读取测试文件失败";
zrt::FileService_Stub stub(channel.get());
zrt::InitMultipartUploadReq req;
zrt::InitMultipartUploadRsp rsp;
brpc::Controller cntl;
req.set_request_id(zrt::uuid());
req.set_file_name("Makefile_multipart");
req.set_file_size(test_file_content.size());
stub.InitMultipartUpload(&cntl, &req, &rsp, nullptr);
// 校验结果
ASSERT_FALSE(cntl.Failed());
ASSERT_TRUE(rsp.success());
ASSERT_FALSE(rsp.file_id().empty());
// 保存参数供后续测试
multipart_file_id = rsp.file_id();
multipart_total_chunks = rsp.total_chunks();
multipart_chunk_size = rsp.chunk_size();
}
// 2. 上传所有分块
TEST(multipart_test, upload_part) {
if (multipart_file_id.empty()) GTEST_SKIP() << "分块初始化未成功";
zrt::FileService_Stub stub(channel.get());
for (uint32_t i = 0; i < multipart_total_chunks; ++i) {
zrt::UploadPartReq req;
zrt::UploadPartRsp rsp;
brpc::Controller cntl;
// 截取当前分块数据
uint32_t start = i * multipart_chunk_size;
uint32_t end = std::min((i+1)*multipart_chunk_size, (uint32_t)multipart_original_content.size());
std::string chunk_data = multipart_original_content.substr(start, end - start);
req.set_request_id(zrt::uuid());
req.set_file_id(multipart_file_id);
req.set_chunk_index(i);
req.set_chunk_data(chunk_data);
stub.UploadPart(&cntl, &req, &rsp, nullptr);
ASSERT_FALSE(cntl.Failed());
ASSERT_TRUE(rsp.success());
}
}
// 3. 合并分块并下载验证
TEST(multipart_test, complete_multipart_upload) {
// 合并分块逻辑...
}
TEST(get_test, multipart_file) {
// 下载合并后的文件,校验内容一致性...
}
测试设计亮点
-
依赖前置用例:通过全局变量传递测试参数(如 file_id),确保测试流程顺序执行
-
结果校验全面:不仅校验接口返回的 success 状态,还校验文件大小、内容一致性、元数据正确性
-
异常处理:跳过前置用例失败的测试,避免无效报错
-
可视化验证 :下载文件保存到本地(如
multipart_merged_download_Makefile),支持手动校验
3.3 测试执行流程
-
初始化日志和服务发现(通过 etcd 获取服务端地址)
-
按「单文件→多文件→分块上传」顺序执行测试用例
-
每个上传用例执行后,立即执行对应的下载用例,验证数据一致性
-
测试完成后,自动清理测试文件(可选)
四、核心亮点与最佳实践
4.1 代码质量保障
-
严格的参数校验:所有接口都对输入参数(文件名、文件大小、分块索引等)进行合法性校验,避免非法请求
-
资源自动释放 :使用
brpc::ClosureGuard、智能指针(shared_ptr)自动释放资源,避免内存泄漏 -
完善的错误日志:每个关键步骤都打印日志(INFO/ERROR),包含 request_id、file_id 等上下文,便于问题追踪
-
线程安全:LRU 缓存、Redis 操作均加锁,支持高并发访问
4.2 性能优化
-
缓存分层:内存 LRU 缓存热点元数据,Redis 持久化冷数据,平衡性能和可靠性
-
多线程 RPC:brpc 服务器支持配置 IO 线程数(建议≥CPU 核心数),提升并发处理能力
-
分块上传:大文件分块上传,避免单次请求数据量过大导致的超时或内存占用过高
4.3 可扩展性设计
-
依赖注入:通过 Builder 模式注入 MinIO、Redis 等依赖,便于替换为其他存储方案(如 S3、MySQL)
-
接口标准化:RPC 接口定义清晰,支持客户端多语言接入(C++、Java、Python 等)
-
服务注册发现:基于 etcd 实现服务动态扩容,客户端自动发现新服务节点,无需修改配置
五、扩展方向
-
权限控制:新增用户认证模块,支持基于文件的读写权限控制
-
文件加密:上传时对文件内容加密,下载时解密,保障数据安全
-
断点续传优化:支持暂停 / 恢复上传,客户端无需重新上传已完成的分块
-
监控告警:集成 Prometheus+Grafana,监控服务 QPS、响应时间、MinIO 存储使用率等指标
-
生命周期管理:新增文件过期清理机制,自动删除长期未访问的文件,释放存储空间
六、总结
本文实现的分布式文件存储服务,基于成熟的开源组件(brpc、MinIO、Redis、etcd),兼顾了可靠性、高性能、可扩展性,支持单文件、多文件、分块上传等多种场景,可直接用于企业级分布式系统。
核心设计思路是「分层解耦 + 依赖注入 + 标准化流程」:通过分层设计隔离不同职责,依赖注入提升灵活性,标准化流程(如服务初始化、接口实现)保证代码质量。开发者可基于本文代码,根据实际业务需求进行扩展,快速搭建符合自身场景的文件存储解决方案。
如果需要获取完整代码、编译脚本或部署文档,欢迎留言交流!