brpc 与 Etcd 二次封装

brpc二次封装

封装思想

二次封装原因分析

brpc的核心功能是RPC调用,但是在分布式系统中,单纯的RPC调用无法满足需求,需要考虑清楚三个问题

  • **提供服务的主体:**服务发现,例如通过注册中心(Etcd)动态获取服务列表(后续会将两者借结合)
  • **高效管理服务通信的方式:**负载均衡、连接复用等
  • **扩展和维护服务端实现:**动态增减服务节点,避免大量代码被修改

综上所述,brpc的二次封装主要目的是信道管理,不是对RPC方法的封装

核心思想

指定服务的信道管理类

  • 一个服务可能会有多个节点提供服务,每个节点都对应一个独立的Channel
  • 封装的目标就是将这些节点的信道管理起来,建立服务于信道之间的映射关系(一般情况下应该是多对一的映射关系,也就是一个服务名对应多个节点的Channel)
  • 负载均衡实现:通过轮询策略,在多个节点中选择一个Channel发起请求

全局信道管管理类

  • 对多个服务的信道进行统一管理
  • 提供一个全局的入口,便于通过服务名称动态获取对应的信道
  • 动态加载或者移除服务信道,支持线上服务的动态拓展和下线

封装实现逻辑分析

  • 服务与信道的管理
    • 将每个服务节点的信道管理起来,每个节点有独立的Channel
    • 通过服务ID找到对应的信道组
  • 发起RPC调用时的信道选择
    • 根据服务名称获取信道组;在信道组中根据负载均衡策略选择一个信道
    • 使用选择的信道发起RPC调用
  • 动态拓展
    • 当新的服务节点上线时,动态创建并添加信道
    • 当服务节点下线时,动态移除信道,保证资源回收

具体实现

cpp 复制代码
#pragma once
#include <brpc/channel.h>
#include <string>
#include <vector>
#include <unordered_map>
#include <mutex>
#include "logger.hpp"

namespace mag {

// 封装单个服务的信道管理类
class ServiceChannel {
public:
    using ptr = std::shared_ptr<ServiceChannel>;
    using ChannelPtr = std::shared_ptr<brpc::Channel>;

    // 构造函数,初始化服务名称和轮询计数器
    ServiceChannel(const std::string &name) : _service_name(name), _index(0) {}

    // 服务上线了一个节点时调用,新增信道
    void append(const std::string &host) {
        auto channel = std::make_shared<brpc::Channel>();
        brpc::ChannelOptions options;
        options.connect_timeout_ms = -1; // 设置连接超时为无限等待
        options.timeout_ms = -1;        // 设置请求超时为无限等待
        options.max_retry = 3;          // 设置最大重试次数
        options.protocol = "baidu_std"; // 使用百度标准协议

        // 初始化信道
        int ret = channel->Init(host.c_str(), &options);
        if (ret == -1) {
            LOG_ERROR("初始化{}-{}信道失败!", _service_name, host);
            return;
        }

        // 加锁保护,更新信道集合和主机映射
        std::unique_lock<std::mutex> lock(_mutex);
        _hosts.insert(std::make_pair(host, channel));
        _channels.push_back(channel);
    }

    // 服务下线了一个节点时调用,移除信道
    void remove(const std::string &host) {
        std::unique_lock<std::mutex> lock(_mutex);
        auto it = _hosts.find(host);
        if (it == _hosts.end()) {
            LOG_WARN("{}-{}节点删除信道时,没有找到信道信息!", _service_name, host);
            return;
        }

        // 从信道集合中移除对应的信道
        for (auto vit = _channels.begin(); vit != _channels.end(); ++vit) {
            if (*vit == it->second) {
                _channels.erase(vit);
                break;
            }
        }

        // 从主机映射中移除
        _hosts.erase(it);
    }

    // 使用RR轮转策略,获取一个Channel用于发起RPC调用
    ChannelPtr choose() {
        std::unique_lock<std::mutex> lock(_mutex);
        if (_channels.size() == 0) {
            LOG_ERROR("当前没有能够提供 {} 服务的节点!", _service_name);
            return ChannelPtr();
        }

        // 通过轮询选择一个信道
        int32_t idx = _index++ % _channels.size();
        return _channels[idx];
    }

private:
    std::mutex _mutex;                               // 互斥锁,保护多线程环境下的成员变量
    int32_t _index;                                  // 当前轮转下标计数器
    std::string _service_name;                       // 服务名称
    std::vector<ChannelPtr> _channels;               // 当前服务对应的信道集合
    std::unordered_map<std::string, ChannelPtr> _hosts; // 主机地址与信道映射关系
};

// 总体的服务信道管理类
class ServiceManager {
public:
    using ptr = std::shared_ptr<ServiceManager>;

    // 构造函数
    ServiceManager() {}

    // 获取指定服务的节点信道
    ServiceChannel::ChannelPtr choose(const std::string &service_name) {
        std::unique_lock<std::mutex> lock(_mutex);
        auto sit = _services.find(service_name);
        if (sit == _services.end()) {
            LOG_ERROR("当前没有能够提供 {} 服务的节点!", service_name);
            return ServiceChannel::ChannelPtr();
        }
        return sit->second->choose();
    }

    // 声明需要关注哪些服务的上下线
    void declared(const std::string &service_name) {
        std::unique_lock<std::mutex> lock(_mutex);
        _follow_services.insert(service_name);
    }

    // 服务上线时调用的回调接口,将服务节点管理起来
    void onServiceOnline(const std::string &service_instance, const std::string &host) {
        std::string service_name = getServiceName(service_instance);
        ServiceChannel::ptr service;

        {
            std::unique_lock<std::mutex> lock(_mutex);
            auto fit = _follow_services.find(service_name);
            if (fit == _follow_services.end()) {
                LOG_DEBUG("{}-{} 服务上线了,但是当前并不关心!", service_name, host);
                return;
            }

            // 获取或创建服务的信道管理对象
            auto sit = _services.find(service_name);
            if (sit == _services.end()) {
                service = std::make_shared<ServiceChannel>(service_name);
                _services.insert(std::make_pair(service_name, service));
            } else {
                service = sit->second;
            }
        }

        if (!service) {
            LOG_ERROR("新增 {} 服务管理节点失败!", service_name);
            return;
        }

        // 添加新的服务节点信道
        service->append(host);
        LOG_DEBUG("{}-{} 服务上线新节点,进行添加管理!", service_name, host);
    }

    // 服务下线时调用的回调接口,从服务信道管理中删除指定节点信道
    void onServiceOffline(const std::string &service_instance, const std::string &host) {
        std::string service_name = getServiceName(service_instance);
        ServiceChannel::ptr service;

        {
            std::unique_lock<std::mutex> lock(_mutex);
            auto fit = _follow_services.find(service_name);
            if (fit == _follow_services.end()) {
                LOG_DEBUG("{}-{} 服务下线了,但是当前并不关心!", service_name, host);
                return;
            }

            // 查找服务信道管理对象
            auto sit = _services.find(service_name);
            if (sit == _services.end()) {
                LOG_WARN("删除{}服务节点时,没有找到管理对象", service_name);
                return;
            }
            service = sit->second;
        }

        // 从信道管理中移除服务节点
        service->remove(host);
        LOG_DEBUG("{}-{} 服务下线节点,进行删除管理!", service_name, host);
    }

private:
    // 从服务实例中提取服务名称
    std::string getServiceName(const std::string &service_instance) {
        auto pos = service_instance.find_last_of('/');
        if (pos == std::string::npos) return service_instance;
        return service_instance.substr(0, pos);
    }

private:
    std::mutex _mutex;                                     // 互斥锁,保护多线程环境下的成员变量
    std::unordered_set<std::string> _follow_services;      // 需要关注的服务名称集合
    std::unordered_map<std::string, ServiceChannel::ptr> _services; // 服务名称到信道管理对象的映射关系
};

} 

Etcd 和 brpc两者结合

核心思想理解

背景理解成在一个城市中找咖啡店购买咖啡

Channel

  • 功能:负责通信,将顾客(请求)送达目标分店(服务节点)
  • Channel就像是一条连接顾客和咖啡店的 道路
  • 每条道路对应一个服务节点(例如不同的咖啡店分店);如果有多个分店(节点),会有多条道路,每条道路都是独立的

服务信道管理类

  • 这是一个管理所有道路的"导航系统"
  • 管理所有通往咖啡店的道路(信道),记录哪些道路有效,哪些需要维护
  • 每次有顾客需要服务时,根据具体规则(例如最近距离或排队时间最短)选择最优的一条道路

顾客想要一杯咖啡,但不知道具体该走哪条路,这时服务信道管理类会告诉你:走 "道路1" 或 "道路2"

Server(咖啡店)

  • 咖啡店作为服务端,负责提供咖啡服务
  • 主要工作
    • 注册所有的服务(菜单上列出的咖啡种类)
    • 启动服务端(开店营业)
    • 监听来自顾客的请求(顾客通过不同的道路来到店里)

当一个服务节点(分店)注册成功后,顾客就可以通过对应的道路来访问这个节点

服务注册中心(类似于咖啡店地图)

  • 服务注册中心(如 etcd)就像是一个共享的咖啡店地图,它记录了所有的分店地址
  • 存储服务节点的信息(例如每个分店的位置)
  • 动态更新服务节点状态(某个分店是否营业、新开分店等)

当某个分店暂停营业时,注册中心会通知所有导航系统(服务信道管理类),将对应的道路标记为不可用

Controller(请求的调度和跟踪)

  • Controller 就像一个订单管理系统,用于追踪顾客的请求是否成功
  • 记录每个请求的状态(比如:订单是否成功、是否被取消)
  • 跟踪请求的详细信息(比如:是否在途中发生了错误,或者咖啡店是否超时未响应)

顾客点了一杯咖啡,但发现等待超时了,Controller 会记录错误信息并反馈给顾客

负载均衡策略(防止一家咖啡店爆单)

  • 当一个城市有多个咖啡店分店,导航系统需要选择一个合适的分店为顾客服务
  • 实现负载均衡,例如通过轮询(Round Robin,RR)选择最少排队的分店

顾客想买咖啡,导航系统会选择离顾客最近的分店,或者选择当前空闲状态的分店

回调机制(异步处理通知)

  • 顾客下单后,不需要一直等待,可以去做其他事情。当订单完成时,咖啡店会通知顾客"咖啡做好了"
  • 在异步调用中,处理请求完成后的逻辑;例如顾客下单后,回调函数可以执行通知、记录订单状态等任务

顾客下单后离开座位,咖啡做好时,店员通过铃声(回调函数)通知顾客

ClosureGuard(保证回调机制一定会执行机制)

  • ClosureGuard 就像是一个"催促器",确保咖啡做好后一定会通知顾客
  • 确保异步请求完成时回调函数一定会被执行;即使在处理过程中发生错误,回调函数也能被调用,避免资源泄漏

店员忘记通知顾客,但系统(ClosureGuard)会强制通知,保证订单状态最终得到更新

etcd与RPC结合逻辑

Etcd:咖啡店地图和动态信息管理员

Etcd 在这个系统中负责 服务的注册和发现 ,就像是一个 咖啡店地图和动态信息管理员

  • 记录了所有分店的位置(也就是记录了服务节点的地址),Etcd通过路径管理对应的服务
  • 服务上线的时候新增一条道路,服务下线的时候则通知系统移除这条道路;Discovery类则相当于管理员,其负责Etcd通信,监听咖啡店上下线动态

ServiceManager:导航系统

主要功能就是路由选择和动态调整

  • 当顾客(RPC 调用)要去购买咖啡时,它会根据地图(Etcd 提供的服务信息),选择一个最近或最优的咖啡店分店
  • 它会根据负载均衡策略(比如轮询,Round Robin),动态地在分店中选择一个路径

服务节点注册逻辑

  • 它会把自己的位置(IP 和端口)上传到地图(Etcd)
  • 地图管理员(Discovery)会通知导航系统(ServiceManager)新增这个分店的信息
  • 导航系统为这家分店创建一条通路(信道)

客户端发起RPC调用

  • 顾客(客户端)使用导航系统(ServiceManager)查找最近的咖啡店
  • 导航系统根据地图信息,找到一个分店地址,并返回对应的通路(信道)

负载均衡

  • 导航系统会根据负载均衡策略(如轮询),在多个分店中选择一个最优的
  • 如果分店关门(信道不可用),导航系统会选择其他分店

RPC调用-开始点单

顾客通过信道(Channel)发送点单请求(RPC 调用),并等待咖啡做好(服务响应)

收到响应

  • 如果点单成功,顾客会收到咖啡(RPC 响应消息)
  • 如果失败,顾客会稍等片刻并重试,尝试其他分店

具体实现

服务发现

核心逻辑

  • Etcd动态服务发现
    • Etcd提供服务节点的注册和发现能力,监听服务节点的上线和下线事件
    • 通过Discovery类将服务节点的变化回调到ServiceManager,然后动态更新信道管理
  • 动态信道管理
    • ServiceManager管理所有的服务信道,同时通过负载均衡动态选择信道
    • 结合 Etcd 服务发现,实现动态扩展和容灾
  • 基于信道的RPC调用
    • 通过动态选择的信道,发起RPC调用

具体实现代码-服务发现

cpp 复制代码
//
///服务端逻辑
/

#include "../common/etcd.hpp"     // Etcd 服务发现和注册相关头文件
#include <gflags/gflags.h>        // Google Flags,用于解析命令行参数
#include <thread>                 // C++ 标准库的线程管理头文件
#include <brpc/server.h>          // brpc 服务端相关头文件
#include <butil/logging.h>        // brpc 默认日志设置
#include "main.pb.h"              // Protobuf 生成的 RPC 服务头文件

// 定义命令行参数
DEFINE_bool(run_mode, false, "程序的运行模式,false-调试; true-发布;");
DEFINE_string(log_file, "", "发布模式下,用于指定日志的输出文件");
DEFINE_int32(log_level, 0, "发布模式下,用于指定日志输出等级");

DEFINE_string(etcd_host, "http://127.0.0.1:2379", "服务注册中心地址");
DEFINE_string(base_service, "/service", "服务监控根目录");
DEFINE_string(instance_name, "/echo/instance", "当前实例名称");
DEFINE_string(access_host, "127.0.0.1:7070", "当前实例的外部访问地址");
DEFINE_int32(listen_port, 7070, "Rpc服务器监听端口");

// 实现 Echo 服务
class EchoServiceImpl : public example::EchoService {
public:
    EchoServiceImpl() {}
    ~EchoServiceImpl() {}

    // 重写 Echo 方法,用于处理客户端请求
    void Echo(google::protobuf::RpcController* controller,
              const ::example::EchoRequest* request,
              ::example::EchoResponse* response,
              ::google::protobuf::Closure* done) override {
        // 用于确保在处理完成后正确调用回调函数
        brpc::ClosureGuard rpc_guard(done);

        // 打印接收到的请求消息
        std::cout << "收到消息: " << request->message() << std::endl;

        // 构造响应消息
        std::string str = request->message() + "--这是响应!!";
        response->set_message(str);
    }
};

int main(int argc, char *argv[]) {
    // 1. 解析命令行参数并初始化日志
    google::ParseCommandLineFlags(&argc, &argv, true);
    init_logger(FLAGS_run_mode, FLAGS_log_file, FLAGS_log_level);

    // 2. 关闭 brpc 的默认日志输出
    logging::LoggingSettings settings;
    settings.logging_dest = logging::LoggingDestination::LOG_TO_NONE; // 禁用 brpc 默认日志
    logging::InitLogging(settings);

    // 3. 创建 RPC 服务端对象
    brpc::Server server;

    // 4. 添加服务到服务端对象
    EchoServiceImpl echo_service;
    int ret = server.AddService(&echo_service, brpc::ServiceOwnership::SERVER_DOESNT_OWN_SERVICE);
    if (ret == -1) {
        std::cerr << "添加 Rpc 服务失败!" << std::endl;
        return -1;
    }

    // 5. 启动服务器
    brpc::ServerOptions options;
    options.idle_timeout_sec = -1; // 设置连接空闲超时时间,-1 表示不超时
    options.num_threads = 1;       // 设置 IO 线程数量
    ret = server.Start(FLAGS_listen_port, &options);
    if (ret == -1) {
        std::cerr << "启动服务器失败!" << std::endl;
        return -1;
    }

    // 6. 注册服务到 Etcd
    Registry::ptr rclient = std::make_shared<Registry>(FLAGS_etcd_host);
    rclient->registry(FLAGS_base_service + FLAGS_instance_name, FLAGS_access_host); // 服务注册

    // 7. 启动服务器并等待终止信号
    server.RunUntilAskedToQuit();

    return 0;
}
cpp 复制代码
///
///客户端逻辑
//

#include "../common/etcd.hpp"     // Etcd服务发现相关头文件
#include "../common/channel.hpp"  // 服务信道管理相关头文件
#include <gflags/gflags.h>        // Google Flags,用于解析命令行参数
#include <thread>                 // C++标准库的线程管理头文件
#include "main.pb.h"              // Protobuf生成的RPC服务头文件

// 定义命令行参数
DEFINE_bool(run_mode, false, "程序的运行模式,false-调试; true-发布;");
DEFINE_string(log_file, "", "发布模式下,用于指定日志的输出文件");
DEFINE_int32(log_level, 0, "发布模式下,用于指定日志输出等级");

DEFINE_string(etcd_host, "http://127.0.0.1:2379", "服务注册中心地址");
DEFINE_string(base_service, "/service", "服务监控根目录");
DEFINE_string(call_service, "/service/echo", "需要调用的服务路径");

int main(int argc, char *argv[]) {
    // 解析命令行参数并初始化日志
    google::ParseCommandLineFlags(&argc, &argv, true);
    init_logger(FLAGS_run_mode, FLAGS_log_file, FLAGS_log_level);

    // 1. 创建服务信道管理对象(ServiceManager)
    //    - 用于动态管理服务信道(服务节点的通信通道)。
    auto sm = std::make_shared<ServiceManager>();
    sm->declared(FLAGS_call_service); // 声明需要管理的服务(/service/echo)

    // 绑定服务发现的回调函数
    auto put_cb = std::bind(&ServiceManager::onServiceOnline, sm.get(), std::placeholders::_1, std::placeholders::_2);
    auto del_cb = std::bind(&ServiceManager::onServiceOffline, sm.get(), std::placeholders::_1, std::placeholders::_2);

    // 2. 创建服务发现客户端(Discovery)
    //    - 连接到Etcd,监听服务节点的上下线事件。
    Discovery::ptr dclient = std::make_shared<Discovery>(FLAGS_etcd_host, FLAGS_base_service, put_cb, del_cb);

    while (true) {
        // 3. 通过服务信道管理对象选择一个信道(Channel)
        //    - 信道用于与服务节点通信。
        auto channel = sm->choose(FLAGS_call_service);
        if (!channel) {
            std::this_thread::sleep_for(std::chrono::seconds(1)); // 若没有可用信道,等待1秒后重试
            return -1;
        }

        // 4. 使用信道创建服务存根(Stub),发起RPC调用
        example::EchoService_Stub stub(channel.get());
        example::EchoRequest req;  // 创建请求对象
        req.set_message("你好~etcd&brpc~!"); // 设置请求内容

        brpc::Controller *cntl = new brpc::Controller();     // 创建RPC调用的控制器
        example::EchoResponse *rsp = new example::EchoResponse(); // 创建响应对象

        // 发起同步RPC调用
        stub.Echo(cntl, &req, rsp, nullptr);

        // 5. 处理RPC调用结果
        if (cntl->Failed()) {
            // 如果调用失败,打印错误信息并释放资源
            std::cout << "Rpc调用失败:" << cntl->ErrorText() << std::endl;
            delete cntl;
            delete rsp;
            std::this_thread::sleep_for(std::chrono::seconds(1)); // 等待1秒后重试
            continue;
        }

        // 如果调用成功,输出服务端返回的响应消息
        std::cout << "收到响应: " << rsp->message() << std::endl;

        // 释放资源并等待1秒后进行下一次调用
        delete cntl;
        delete rsp;
        std::this_thread::sleep_for(std::chrono::seconds(1));
    }
    return 0;
}

总结

客户端逻辑

实现了 RPC 客户端,用于通过 Etcd 动态发现服务并调用远程服务

  • **动态发现服务:**监听 Etcd,获取服务端的地址信息
  • 通过负载均衡选择一个可用的服务节点(信道 Channel
  • 使用动态选择的信道发起 RPC 请求,获取服务端返回的响应

服务端逻辑

  • 提供服务: 启动一个 EchoService 服务,用于处理客户端的请求
  • **服务注册:**将自身服务地址注册到 Etcd,以供客户端发现
相关推荐
BUG制造机.10 分钟前
修炼之道 ---其四
linux·c++
阿杰同学33 分钟前
如何实现 MySQL 的读写分离?
数据库·mysql
weisian1511 小时前
Redis篇--应用篇3--数据统计(排行榜,计数器)
数据库·redis·缓存
羊村懒哥1 小时前
mysql-二进制安装方式
数据库·mysql
言之。1 小时前
Redis单线程快的原因
数据库·redis·缓存
geovindu1 小时前
python: Oracle Stored Procedure query table
数据库·python·mysql·postgresql·oracle·sqlserver·mssql
神经网络的应用1 小时前
C++程序设计例题——第三章程序控制结构
c++·学习·算法
zfenggo2 小时前
c/c++ 无法跳转定义
c语言·开发语言·c++
图灵猿2 小时前
【Lua之·Lua与C/C++交互·Lua CAPI访问栈操作】
c语言·c++·lua
山人在山上2 小时前
arcgis server ip修改后服务异常解决方案
数据库·tcp/ip·arcgis