分布式文件存储 RPC 服务实现

基于 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

FileServiceImplFileService接口的具体实现,所有文件读写的业务逻辑都在这里,相当于 "干活的工人"。

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)

逻辑流程:

  1. 从请求中获取file_id(文件 ID 即存储的文件名);

  2. 拼接存储路径(存储目录+file_id),读取文件内容;

  3. 构造响应:若读取成功,填充文件数据;若失败,返回错误信息。

关键代码:

复制代码
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)

逻辑流程:

  1. 生成唯一file_id(通过uuid()函数,避免文件名冲突);

  2. 拼接存储路径,将请求中的文件内容写入磁盘;

  3. 构造响应:返回文件元信息(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 &reg_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实例,步骤清晰,支持灵活配置:

  1. make_reg_object:创建 Etcd 注册客户端,将服务注册到 Etcd;

  2. make_rpc_server:创建 BRPC 服务器,添加FileServiceImpl业务逻辑,配置端口、线程数;

  3. 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 获取服务地址,分两步:

  1. 启动时 "拉取" 已有服务节点(避免错过已上线的节点);

  2. 实时 "监控" 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)

步骤:

  1. 读取本地文件(如./Makefile);

  2. 构造PutSingleFileReq,填充文件名称、大小、内容;

  3. 发起 RPC 调用,断言响应成功(success=true);

  4. 保存返回的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)

步骤:

  1. 用上传保存的single_file_id构造GetSingleFileReq

  2. 发起 RPC 调用,断言响应成功;

  3. 将下载的文件内容写入本地(如 make_file_download);

  4. (隐含验证)对比本地原文件和下载文件,确认数据一致。

5.2 客户端初始化:连接服务的 "准备工作"

main 函数中完成测试前的初始化,核心是 "获取服务信道":

  1. 初始化日志(配置日志文件、级别);

  2. 创建ServiceManager,声明关注 file_service;

  3. 创建Discovery,连接 Etcd,监听服务上下线;

  4. ServiceManager获取 file_service 的信道;

  5. 执行所有测试用例。

六、关键技术点解析:为什么这么设计?

这套系统用到了多个工业级技术和设计思想,理解这些技术点能更深入掌握分布式服务开发。

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::ptrChannelPtr);

  • 观察者模式(Discovery+ServiceManager)Discovery监控 Etcd 变化,ServiceManager作为观察者接收通知,解耦 "监控" 和 "处理"。

6.4 线程安全:避免并发问题

服务端和客户端都是多线程运行,必须保证线程安全:

  • std::mutex:保护共享资源(如ServiceChannel_channels列表,避免同时读写);

  • brpc::ClosureGuard:BRPC 回调在多线程中执行,Guard 确保资源正确释放。

七、系统使用与扩展建议

7.1 实际使用流程

  1. 编译 Proto:用 protoc 编译 file.proto,生成 C++ 代码(.h 和.cc 文件);

  2. 编译服务端:链接 BRPC、Etcd、Protobuf 库,编译服务端程序;

  3. 启动服务端 :指定存储路径、Etcd 地址、端口(如./file_server --storage_path ./data --etcd_host ``http://127.0.0.1:2379`` --port 8000);

  4. 编译客户端:链接 GTest、BRPC、Etcd 库,编译测试程序;

  5. 运行测试:启动客户端测试程序,验证服务可用性。

7.2 扩展建议

这套系统是基础版本,实际项目中可根据需求扩展:

  1. 文件分片上传:大文件(如 1GB 以上)分块上传,避免单次请求过大;

  2. 权限控制 :利用user_id字段实现权限校验(如只有上传者能下载);

  3. 缓存优化:热门文件缓存到内存,减少磁盘 IO;

  4. 监控告警:添加 Prometheus 监控(服务 QPS、文件存储量),配置告警(服务下线、磁盘满);

  5. 数据备份:定期备份存储目录,避免数据丢失。

八、总结

这套分布式文件存储 RPC 服务,从 "协议定义" 到 "业务实现",再到 "服务治理",形成了完整的解决方案。核心优势在于:

  1. 易用性:通过构建者模式简化服务配置,客户端调用像本地函数一样简单;

  2. 可靠性:Etcd 服务治理确保客户端总能连接到可用节点,RR 负载均衡避免单点压力;

  3. 可扩展性:模块化设计,新增功能(如分片上传)只需扩展对应模块,不影响其他逻辑。

对于需要分布式文件存储的场景(如聊天系统的附件传输、云存储服务),这套系统可以作为基础框架,快速迭代开发。

相关推荐
文艺倾年3 小时前
【八股消消乐】手撕分布式协议和算法(基础篇)
分布式·算法
jc06203 小时前
4.3-中间件之Kafka
分布式·中间件·kafka
abcd_zjq3 小时前
VS2026+QT6.9+opencv图像增强(多帧平均降噪)(CLAHE对比度增强)(边缘增强)(图像超分辨率)
c++·图像处理·qt·opencv·visual studio
235163 小时前
【并发编程】详解volatile
java·开发语言·jvm·分布式·后端·并发编程·原理
Algebraaaaa4 小时前
Qt中的字符串宏 | 编译期检查和运行期检查 | Qt信号与槽connect写法
开发语言·c++·qt
虫师c4 小时前
分布式缓存实战:Redis集群与性能优化
redis·分布式·缓存·redis集群·高可用架构·生产环境·数据分片
全马必破三4 小时前
Node.js HTTP开发
网络协议·http·node.js
WnHj7 小时前
kafka的数据消费通过flinksql 入数到Doris的报错(Connection timed out)
分布式·kafka
Predestination王瀞潞9 小时前
IO操作(Num22)
开发语言·c++