在实际开发中,当多个服务需要跨进程通信时,直接用 HTTP 或自定义协议会面临很多问题:比如服务地址硬编码导致扩容困难、服务下线后客户端还在调用僵尸节点、缺乏统一的日志和监控等。为了解决这些痛点,我基于百度的 BRPC 框架和 Etcd 服务注册中心,搭了一套轻量级 RPC 服务,支持服务自动注册、实时发现和负载均衡,今天就把整个实现过程拆解开讲清楚,代码可直接复用。
一、项目背景:为什么要造这个轮子?
先说说为什么选 BRPC 和 Etcd:
-
BRPC:相比 gRPC,BRPC 更贴合国内场景,支持百度_std、HTTP 等多种协议,性能稳定,而且自带连接池、重试等机制,不用重复造轮子;
-
Etcd:作为分布式键值存储,天生适合做服务注册中心 —— 支持租约(避免僵尸节点)、Watcher(实时感知服务变化),轻量且容易部署;
-
场景适配:这套方案针对中小型项目设计,没有过度封装,核心逻辑清晰,后续加熔断、监控都很方便。
整个服务的核心目标很简单:让客户端能 “无感” 调用服务端,不用关心服务端在哪、有多少个节点,服务下线后自动剔除。
二、整体架构:从注册到调用的闭环
先画个简单的架构图(文字描述更直观),整个流程分四步:
-
服务端启动:初始化 BRPC 服务 → 实现 Echo 业务逻辑 → 把自己的地址注册到 Etcd(带 3 秒租约);
-
客户端启动:连接 Etcd → 拉取已注册的服务节点 → 初始化信道管理;
-
实时发现:Etcd 通过 Watcher 监控服务节点变化,新节点上线时客户端自动加信道,节点下线时自动删信道;
-
客户端调用:通过轮询(RR)策略从信道池选一个节点,发起 RPC 调用,拿到响应后打印结果。
核心模块分为 4 个:日志模块(统一日志输出)、Etcd 注册发现模块(连接 Etcd)、BRPC 服务模块(业务逻辑载体)、信道管理模块(负载均衡 + 资源复用)。
三、核心模块实现:一步步拆轮子
1. 先搞定基础:日志模块(logger.hpp)
日志是排查问题的关键,我用了轻量级的 spdlog 库,支持调试 / 发布两种模式,还能自动打印文件名和行号。
1.1 日志模块代码
#pragma once
#include <spdlog/spdlog.h>
#include <spdlog/sinks/stdout_color_sinks.h>
#include <spdlog/sinks/basic_file_sink.h>
#include <iostream>
// 全局日志器,所有模块共用
std::shared_ptr<spdlog::logger> g_default_logger;
/**
* @brief 初始化日志器
* @param mode 运行模式:false-调试(输出控制台),true-发布(输出文件)
* @param file 发布模式下的日志文件路径
* @param level 日志等级(0-trace,1-debug,2-info,3-warn,4-error,5-critical)
*/
void init_logger(bool mode, const std::string &file, int32_t level) {
// 调试模式:控制台输出,最低等级(trace),方便开发定位
if (!mode) {
g_default_logger = spdlog::stdout_color_mt("rpc-debug");
g_default_logger->set_level(spdlog::level::trace);
g_default_logger->flush_on(spdlog::level::trace);
}
// 发布模式:文件输出,按参数控制等级,避免控制台刷屏
else {
if (file.empty()) {
std::cerr << "发布模式必须指定日志文件!" << std::endl;
exit(1);
}
g_default_logger = spdlog::basic_logger_mt("rpc-release", file);
// 等级范围校验,避免非法值
auto log_level = (level < 0 || level > 5) ? spdlog::level::info : (spdlog::level::level_enum)level;
g_default_logger->set_level(log_level);
g_default_logger->flush_on(log_level);
}
// 日志格式:[日志器名][时间][线程ID][等级] 内容(文件名:行号)
g_default_logger->set_pattern("[%n][%H:%M:%S][%t][%-8l] %v (%s:%#)");
}
// 日志宏定义,简化调用
#define LOG_TRACE(fmt, ...) g_default_logger->trace(fmt, ##__VA_ARGS__)
#define LOG_DEBUG(fmt, ...) g_default_logger->debug(fmt, ##__VA_ARGS__)
#define LOG_INFO(fmt, ...) g_default_logger->info(fmt, ##__VA_ARGS__)
#define LOG_WARN(fmt, ...) g_default_logger->warn(fmt, ##__VA_ARGS__)
#define LOG_ERROR(fmt, ...) g_default_logger->error(fmt, ##__VA_ARGS__)
#define LOG_FATAL(fmt, ...) g_default_logger->critical(fmt, ##__VA_ARGS__)
1.2 关键逻辑解释
-
全局日志器:
g_default_logger让所有模块不用重复创建日志器,避免资源浪费; -
模式区分:调试时输出控制台,带颜色区分等级(spdlog 的 stdout_color_sink),发布时输出文件,避免线上服务器控制台堆积日志;
-
格式设计:包含线程 ID 和文件名行号,比如
[rpc-debug][15:30:00][1234][debug] 初始化信道成功 (channel.hpp:45),定位问题时一眼就能找到代码位置。
2. 核心中的核心:Etcd 注册发现模块(etcd.hpp)
这是解决 “服务在哪” 的关键模块,分两个类:Registry(服务端用,注册服务)和Discovery(客户端用,发现服务)。
2.1 先明确依赖
需要安装 Etcd 的 C++ 客户端库(etcd-cpp-apiv3),Ubuntu 下可以用源码编译:
git clone https://github.com/etcd-cpp-apiv3/etcd-cpp-apiv3.git
cd etcd-cpp-apiv3 && mkdir build && cd build
cmake .. -DCMAKE_INSTALL_PREFIX=/usr/local
make -j4 && sudo make install
2.2 Etcd 模块代码
#pragma once
#include <etcd/Client.hpp>
#include <etcd/KeepAlive.hpp>
#include <etcd/Response.hpp>
#include <etcd/Watcher.hpp>
#include <etcd/Value.hpp>
#include <functional>
#include "logger.hpp" // 依赖日志模块
/**
* @brief 服务注册类(服务端用)
* 功能:将服务端地址注册到Etcd,带租约自动续期,服务下线后自动删除注册信息
*/
class Registry {
public:
using ptr = std::shared_ptr<Registry>; // 智能指针,避免内存泄漏
/**
* @brief 构造函数:初始化Etcd客户端和租约
* @param etcd_host Etcd服务地址(如http://127.0.0.1:2379)
* @param lease_ttl 租约时间(秒),默认3秒(太短频繁续期,太长僵尸节点存活久)
*/
Registry(const std::string &etcd_host, int lease_ttl = 3) {
// 初始化Etcd客户端
_client = std::make_shared<etcd::Client>(etcd_host);
// 创建租约:租约过期前会自动续期,服务端下线后续期失败,Etcd删除节点
_keep_alive = _client->leasekeepalive(lease_ttl).get();
if (!_keep_alive) {
LOG_FATAL("创建Etcd租约失败!Etcd地址:{}", etcd_host);
exit(1);
}
_lease_id = _keep_alive->Lease();
LOG_INFO("Etcd租约初始化成功,租约ID:{},TTL:{}秒", _lease_id, lease_ttl);
}
// 析构函数:取消租约,避免服务端正常退出后租约残留
~Registry() {
if (_keep_alive) {
_keep_alive->Cancel();
LOG_INFO("Etcd租约已取消,租约ID:{}", _lease_id);
}
}
/**
* @brief 注册服务到Etcd
* @param service_key 服务唯一标识(如/service/echo/instance1)
* @param service_value 服务访问地址(如127.0.0.1:7070)
* @return true-注册成功,false-失败
*/
bool registry(const std::string &service_key, const std::string &service_value) {
if (service_key.empty() || service_value.empty()) {
LOG_ERROR("服务Key或Value不能为空!Key:{},Value:{}", service_key, service_value);
return false;
}
// 带租约写入Etcd:Key=服务标识,Value=访问地址
auto resp = _client->put(service_key, service_value, _lease_id).get();
if (resp.is_ok()) {
LOG_INFO("服务注册成功!Etcd Key:{},Value:{}", service_key, service_value);
return true;
} else {

最低0.47元/天 解锁文章

被折叠的 条评论
为什么被折叠?



