基于 BRPC+Protobuf+Etcd 的分布式文件存储 RPC 服务实现详解
一、系统整体架构:从 "通信" 到 "治理" 的全链路设计
这套文件存储系统是一套分布式 RPC 服务解决方案,核心目标是提供 "可靠的文件上传 / 下载能力",并通过服务治理确保分布式环境下的可用性。整体分为 5 层,各层职责清晰、解耦协作:
层级 | 核心组件 | 作用 |
---|---|---|
协议定义层 | Protobuf 文件(file.proto 等) | 定义 RPC 通信的 "契约":数据结构 + 服务接口 |
业务逻辑层 | FileServiceImpl | 实现文件读写的核心业务逻辑 |
服务封装层 | FileServer、FileServerBuilder | 封装 BRPC 服务器,简化服务启停与配置 |
服务治理层 | Registry、Discovery、ServiceManager | 基于 Etcd 实现服务注册 / 发现、负载均衡 |
客户端测试层 | GTest 测试用例 | 验证服务可用性,覆盖全量 RPC 接口 |
简单理解:客户端通过 "服务治理层" 找到可用的服务节点,通过 "协议定义层" 构造请求,调用 "业务逻辑层" 的文件处理接口,整个过程由 "服务封装层" 保障服务稳定运行。
二、协议定义层:用 Protobuf 制定 "通信规则"
Protobuf(Protocol Buffers)是 Google 的序列化协议,这里用来定义 "客户端和服务端该怎么说话"------ 包括传递的数据格式(消息)和可调用的方法(服务)。这是整个系统的 "语言基础",客户端和服务端必须完全遵守。
2.1 核心消息(Message)定义
消息是 RPC 通信的 "数据载体",每个消息对应一个具体的数据结构,以下是关键消息的解析:
消息名称 | 作用 | 关键字段说明 |
---|---|---|
GetSingleFileReq | 单文件下载请求 | request_id(请求唯一标识)、file_id(文件 ID) |
GetSingleFileRsp | 单文件下载响应 | success(是否成功)、errmsg(错误信息)、file_data(文件数据) |
GetMultiFileReq | 多文件下载请求 | file_id_list(文件 ID 列表,repeated 表示 "多个") |
GetMultiFileRsp | 多文件下载响应 | file_data(map 结构:file_id→文件数据,方便匹配) |
PutSingleFileReq | 单文件上传请求 | file_data(包含文件名、大小、二进制内容) |
PutSingleFileRsp | 单文件上传响应 | file_info(返回文件元信息:file_id、大小、名称) |
FileDownloadData | 文件下载数据载体 | file_id(文件 ID)、file_content(二进制文件内容) |
FileUploadData | 文件上传数据载体 | file_name(文件名)、file_size(大小)、file_content(二进制内容) |
MessageType | 消息类型枚举(扩展用) | STRING(文字)、IMAGE(图片)、FILE(文件)、SPEECH(语音) |
UserInfo/ChatSessionInfo | 聊天相关(扩展用) | 支持将文件服务集成到聊天系统,存储头像、附件等 |
关键特性解析:
-
optional
:字段可选(如 user_id,非必填); -
repeated
:字段可重复(如 file_id_list,对应 "列表"); -
map
:键值对结构(如多文件响应的 file_data,快速通过 file_id 查文件); -
oneof
:消息类型互斥(如 MessageContent 的 msg_content,只能是文字 / 图片 / 文件 / 语音中的一种)。
2.2 服务(Service)定义
服务是 RPC 的 "方法集合",定义了客户端可调用的远程函数。这里FileService
包含 4 个核心接口,覆盖文件的全量操作:
service FileService {
// 下载单个文件:传入file_id,返回文件数据
rpc GetSingleFile(GetSingleFileReq) returns (GetSingleFileRsp);
// 下载多个文件:传入file_id列表,返回file_id→文件数据的映射
rpc GetMultiFile(GetMultiFileReq) returns (GetMultiFileRsp);
// 上传单个文件:传入文件数据,返回文件元信息(含生成的file_id)
rpc PutSingleFile(PutSingleFileReq) returns (PutSingleFileRsp);
// 上传多个文件:传入多个文件数据,返回多个文件的元信息
rpc PutMultiFile(PutMultiFileReq) returns (PutMultiFileRsp);
}
三、服务端实现:从 "业务逻辑" 到 "服务启停"
服务端是文件存储的 "核心执行层",负责接收客户端请求、处理文件读写,并通过 BRPC 提供 RPC 能力。代码采用 C++ 实现,用 "面向对象 + 设计模式" 封装,可读性和可维护性强。
3.1 业务逻辑核心:FileServiceImpl
FileServiceImpl
是FileService
接口的具体实现,所有文件读写的业务逻辑都在这里,相当于 "干活的工人"。
3.1.1 构造函数:初始化存储路径
FileServiceImpl(const std::string &storage_path): _storage_path(storage_path) {
umask(0); // 清除文件权限掩码,确保生成的文件可读写
mkdir(storage_path.c_str(), 0775); // 创建存储目录(权限:所有者读写执行,组读写执行)
if (_storage_path.back() != '/') _storage_path.push_back('/'); // 确保路径以"/"结尾
}
作用:初始化文件存储目录,避免后续拼接文件名时出错。
3.1.2 核心方法:单文件下载(GetSingleFile)
逻辑流程:
-
从请求中获取
file_id
(文件 ID 即存储的文件名); -
拼接存储路径(
存储目录+file_id
),读取文件内容; -
构造响应:若读取成功,填充文件数据;若失败,返回错误信息。
关键代码:
void GetSingleFile(...) {
brpc::ClosureGuard rpc_guard(done); // 自动释放RPC回调资源,避免内存泄漏
response->set_request_id(request->request_id()); // 回传request_id,方便链路追踪
std::string fid = request->file_id();
std::string filename = _storage_path + fid; // 拼接文件路径
std::string body; // 存储读取的文件内容
// 调用utils.hpp的readFile函数读取文件
if (!readFile(filename, body)) {
response->set_success(false);
response->set_errmsg("读取文件数据失败!");
LOG_ERROR("{} 读取文件数据失败!", request->request_id());
return;
}
// 读取成功,构造响应
response->set_success(true);
response->mutable_file_data()->set_file_id(fid); // 设置文件ID
response->mutable_file_data()->set_file_content(body); // 设置文件二进制内容
}
3.1.3 核心方法:单文件上传(PutSingleFile)
逻辑流程:
-
生成唯一
file_id
(通过uuid()
函数,避免文件名冲突); -
拼接存储路径,将请求中的文件内容写入磁盘;
-
构造响应:返回文件元信息(file_id、大小、名称)。
关键差异:上传需要 "生成唯一 ID",下载需要 "根据 ID 找文件",这是两者的核心区别。多文件上传 / 下载逻辑类似,只是通过循环批量处理多个文件。
3.2 服务封装:FileServer 与 FileServerBuilder
FileServiceImpl
只负责 "干活",而FileServer
负责 "管理服务",FileServerBuilder
负责 "创建服务"------ 这是典型的构建者模式,把 "对象创建" 和 "对象使用" 解耦,方便配置不同参数(如端口、线程数、Etcd 地址)。
3.2.1 FileServer:服务 "管理者"
封装 BRPC 服务器实例和 Etcd 注册客户端,只暴露一个start()
方法启动服务:
class FileServer {
public:
using ptr = std::shared_ptr<FileServer>; // 智能指针,自动管理内存
// 构造函数:传入Etcd注册客户端和BRPC服务器
FileServer(const Registry::ptr ®_client, const std::shared_ptr<brpc::Server> &server)
: _reg_client(reg_client), _rpc_server(server) {}
// 启动服务:阻塞直到收到停止信号(如Ctrl+C)
void start() { _rpc_server->RunUntilAskedToQuit(); }
private:
Registry::ptr _reg_client; // Etcd注册客户端
std::shared_ptr<brpc::Server> _rpc_server; // BRPC服务器实例
};
3.2.2 FileServerBuilder:服务 "创建者"
分 3 步创建FileServer
实例,步骤清晰,支持灵活配置:
-
make_reg_object
:创建 Etcd 注册客户端,将服务注册到 Etcd; -
make_rpc_server
:创建 BRPC 服务器,添加FileServiceImpl
业务逻辑,配置端口、线程数; -
build
:校验配置,生成FileServer
实例。
关键代码(make_rpc_server):
void make_rpc_server(uint16_t port, int32_t timeout, uint8_t num_threads, const std::string &path = "./data/") {
_rpc_server = std::make_shared<brpc::Server>();
FileServiceImpl *file_service = new FileServiceImpl(path); // 创建业务逻辑实例
// 将业务逻辑注册到BRPC服务器(SERVER_OWNS_SERVICE:服务器负责释放实例内存)
if (_rpc_server->AddService(file_service, brpc::ServiceOwnership::SERVER_OWNS_SERVICE) == -1) {
LOG_ERROR("添加Rpc服务失败!");
abort(); // 严重错误,终止程序
}
// 配置BRPC服务器:超时时间、线程数
brpc::ServerOptions options;
options.idle_timeout_sec = timeout; // 空闲连接超时时间(秒)
options.num_threads = num_threads; // 处理请求的线程数
// 启动BRPC服务器,监听指定端口
if (_rpc_server->Start(port, &options) == -1) {
LOG_ERROR("服务启动失败!");
abort();
}
}
四、服务治理层:让分布式服务 "可管可控"
在分布式环境中,服务可能有多个节点(如多台服务器部署文件服务),客户端需要知道 "该连哪个节点"------ 这就是 "服务治理" 的作用。这套系统基于Etcd 实现服务注册发现,基于RR(轮询) 实现负载均衡。
4.1 Etcd 封装:Registry 与 Discovery
Etcd 是分布式键值存储,这里相当于服务的 "通讯录":服务端把自己的地址注册到 Etcd(Registry),客户端从 Etcd 获取服务地址(Discovery)。
4.1.1 Registry:服务 "注册者"
将服务节点地址注册到 Etcd,并通过 "租约(Lease)" 机制保持心跳 ------ 如果服务节点宕机,租约过期,Etcd 会自动删除该节点信息,避免客户端连接无效节点。
关键代码:
class Registry {
public:
using ptr = std::shared_ptr<Registry>;
// 构造函数:连接Etcd,创建3秒租约(心跳间隔3秒)
Registry(const std::string &host)
: _client(std::make_shared<etcd::Client>(host))
, _keep_alive(_client->leasekeepalive(3).get())
, _lease_id(_keep_alive->Lease()) {}
// 注册服务:key=服务名(如/service/file_service),val=服务地址(如127.0.0.1:8000)
bool registry(const std::string &key, const std::string &val) {
auto resp = _client->put(key, val, _lease_id).get(); // 绑定租约
if (!resp.is_ok()) {
LOG_ERROR("注册数据失败:{}", resp.error_message());
return false;
}
return true;
}
};
4.1.2 Discovery:服务 "发现者"
客户端用Discovery
从 Etcd 获取服务地址,分两步:
-
启动时 "拉取" 已有服务节点(避免错过已上线的节点);
-
实时 "监控" Etcd 变化(服务上线 / 下线时收到通知)。
关键逻辑:
-
通过
etcd::Watcher
监控 Etcd 的键值变化; -
服务上线(PUT 事件)时,调用
_put_cb
通知; -
服务下线(DELETE 事件)时,调用
_del_cb
通知。
4.2 信道管理:ServiceChannel 与 ServiceManager
客户端拿到服务地址后,需要管理 "连接"(即 BRPC 的 Channel),并实现负载均衡 ------ServiceChannel
管理单个服务的所有信道,ServiceManager
管理所有服务的信道。
4.2.1 ServiceChannel:单个服务的 "信道池"
为单个服务(如 file_service)维护多个节点的信道,用RR 轮询选择信道,实现负载均衡:
-
append
:新增服务节点的信道; -
remove
:删除下线节点的信道; -
choose
:轮询选择一个信道(线程安全,用 mutex 保护)。
关键代码(choose 方法):
ChannelPtr choose() {
std::unique_lock<std::mutex> lock(_mutex); // 线程安全
if (_channels.empty()) {
LOG_ERROR("当前没有能够提供 {} 服务的节点!", _service_name);
return ChannelPtr();
}
// RR轮询:取当前下标,然后自增(取模避免越界)
int32_t idx = _index++ % _channels.size();
return _channels[idx];
}
4.2.2 ServiceManager:全局服务的 "信道管家"
管理所有需要关注的服务(通过declared
声明),接收Discovery
的服务上下线通知,更新对应服务的信道:
-
declared
:声明 "我关心哪个服务"(如只关心 file_service); -
onServiceOnline
:服务上线时,添加信道到对应ServiceChannel
; -
onServiceOffline
:服务下线时,删除对应信道; -
choose
:获取指定服务的一个信道(供客户端调用)。
五、客户端测试:用 GTest 验证服务可用性
客户端测试是保障服务正确运行的关键,这里用GTest框架编写测试用例,覆盖所有 4 个 RPC 接口,验证 "上传→下载→数据一致性" 全流程。
5.1 测试用例设计:覆盖全场景
测试用例按 "功能 + 操作" 分类,顺序执行(单文件上传→单文件下载→多文件上传→多文件下载),用全局变量传递file_id
(下载需要依赖上传生成的 ID)。
5.1.1 单文件上传测试(put_test, single_file)
步骤:
-
读取本地文件(如./Makefile);
-
构造
PutSingleFileReq
,填充文件名称、大小、内容; -
发起 RPC 调用,断言响应成功(
success=true
); -
保存返回的
file_id
,供后续下载测试使用。
关键断言:
ASSERT_TRUE(zrt::readFile("./Makefile", body)); // 断言本地文件读取成功
ASSERT_FALSE(cntl->Failed()); // 断言RPC调用无网络错误
ASSERT_TRUE(rsp->success()); // 断言服务端处理成功
ASSERT_EQ(rsp->file_info().file_size(), body.size()); // 断言文件大小一致
single_file_id = rsp->file_info().file_id(); // 保存file_id
5.1.2 单文件下载测试(get_test, single_file)
步骤:
-
用上传保存的
single_file_id
构造GetSingleFileReq
; -
发起 RPC 调用,断言响应成功;
-
将下载的文件内容写入本地(如 make_file_download);
-
(隐含验证)对比本地原文件和下载文件,确认数据一致。
5.2 客户端初始化:连接服务的 "准备工作"
main 函数中完成测试前的初始化,核心是 "获取服务信道":
-
初始化日志(配置日志文件、级别);
-
创建
ServiceManager
,声明关注 file_service; -
创建
Discovery
,连接 Etcd,监听服务上下线; -
从
ServiceManager
获取 file_service 的信道; -
执行所有测试用例。
六、关键技术点解析:为什么这么设计?
这套系统用到了多个工业级技术和设计思想,理解这些技术点能更深入掌握分布式服务开发。
6.1 BRPC:高性能 RPC 框架
BRPC 是百度开源的 RPC 框架,优势在于高性能、易用性强:
-
Service
注册:通过AddService
将业务逻辑绑定到服务器; -
Controller
:管理 RPC 调用的上下文(如错误信息、超时); -
ClosureGuard
:自动释放Closure
资源,避免内存泄漏; -
多协议支持:默认使用 "baidu_std" 协议,也支持 HTTP、gRPC 等。
6.2 Protobuf:高效序列化
相比 JSON、XML,Protobuf 的优势是 "体积小、速度快",适合 RPC 通信:
-
oneof
:避免冗余字段(如消息类型只需要一种,不用传所有类型的字段); -
optional
:节省带宽(非必需字段可省略); -
二进制序列化:比文本格式(如 JSON)体积小 50% 以上,解析速度快 10 倍以上。
6.3 设计模式:解耦与复用
-
构建者模式(FileServerBuilder):复杂对象(FileServer)的创建分步骤,支持不同配置(如不同端口、不同存储路径);
-
智能指针(shared_ptr) :自动管理内存,避免内存泄漏(如
FileServer::ptr
、ChannelPtr
); -
观察者模式(Discovery+ServiceManager) :
Discovery
监控 Etcd 变化,ServiceManager
作为观察者接收通知,解耦 "监控" 和 "处理"。
6.4 线程安全:避免并发问题
服务端和客户端都是多线程运行,必须保证线程安全:
-
std::mutex
:保护共享资源(如ServiceChannel
的_channels
列表,避免同时读写); -
brpc::ClosureGuard
:BRPC 回调在多线程中执行,Guard 确保资源正确释放。
七、系统使用与扩展建议
7.1 实际使用流程
-
编译 Proto:用 protoc 编译 file.proto,生成 C++ 代码(.h 和.cc 文件);
-
编译服务端:链接 BRPC、Etcd、Protobuf 库,编译服务端程序;
-
启动服务端 :指定存储路径、Etcd 地址、端口(如
./file_server --storage_path ./data --etcd_host ``http://127.0.0.1:2379`` --port 8000
); -
编译客户端:链接 GTest、BRPC、Etcd 库,编译测试程序;
-
运行测试:启动客户端测试程序,验证服务可用性。
7.2 扩展建议
这套系统是基础版本,实际项目中可根据需求扩展:
-
文件分片上传:大文件(如 1GB 以上)分块上传,避免单次请求过大;
-
权限控制 :利用
user_id
字段实现权限校验(如只有上传者能下载); -
缓存优化:热门文件缓存到内存,减少磁盘 IO;
-
监控告警:添加 Prometheus 监控(服务 QPS、文件存储量),配置告警(服务下线、磁盘满);
-
数据备份:定期备份存储目录,避免数据丢失。
八、总结
这套分布式文件存储 RPC 服务,从 "协议定义" 到 "业务实现",再到 "服务治理",形成了完整的解决方案。核心优势在于:
-
易用性:通过构建者模式简化服务配置,客户端调用像本地函数一样简单;
-
可靠性:Etcd 服务治理确保客户端总能连接到可用节点,RR 负载均衡避免单点压力;
-
可扩展性:模块化设计,新增功能(如分片上传)只需扩展对应模块,不影响其他逻辑。
对于需要分布式文件存储的场景(如聊天系统的附件传输、云存储服务),这套系统可以作为基础框架,快速迭代开发。