基于Raft分布式Kv存储:RPC调用

RPC 的调用过程可以分成三部分:服务注册、客户端发起调用、服务端处理并返回响应

下面以 Raft 节点 0 调用节点 1 的 AppendEntries 为例。

一、整体调用链

复制代码
Raft::sendAppendEntries()
        ↓
RaftRpcUtil::AppendEntries()
        ↓
protobuf 生成的 raftRpc_Stub::AppendEntries()
        ↓
MprpcChannel::CallMethod()
        ↓
序列化请求并通过 TCP 发送
        ↓
节点1的 RpcProvider 收到数据
        ↓
根据服务名和方法名找到 Raft 服务
        ↓
protobuf 生成的 raftRpc::CallMethod()
        ↓
Raft::AppendEntries() RPC 入口
        ↓
Raft::AppendEntries1() 真正处理 Raft 逻辑
        ↓
done->Run()
        ↓
序列化响应并通过 TCP 返回
        ↓
节点0反序列化响应
        ↓
sendAppendEntries() 处理响应结果

二、调用之前:服务端注册 RPC 服务

每个 KVServer 启动时都会创建一个 RpcProvider

复制代码
RpcProvider provider;

provider.NotifyService(this);
provider.NotifyService(this->m_raftNode.get());

provider.Run(m_me, port);

这里注册了两个服务:

复制代码
this                  → KvServer 服务
m_raftNode.get()      → Raft 服务

对应的方法是:

复制代码
KvServer:
    Get
    PutAppend

Raft:
    RequestVote
    AppendEntries
    InstallSnapshot

NotifyService() 做了什么

复制代码
服务名称
方法数量
每个方法的名称
每个方法的 MethodDescriptor

然后保存为类似下面的映射:

复制代码
m_serviceMap["raftRpc"]
    ├── service对象:Raft*
    └── 方法表
         ├── "AppendEntries"
         ├── "InstallSnapshot"
         └── "RequestVote"

注册完成后,provider.Run() 启动 TCP 服务,等待其他节点调用。

三、第一步:Raft 发起调用

Leader 向某个 Follower 同步日志时,会调用:

复制代码
bool ok = m_peers[server]->AppendEntries(
    args.get(),
    reply.get()
);

其中:

复制代码
server  → 目标节点编号
args    → AppendEntries 请求
reply   → 用于接收响应

m_peers 中每个元素都是一个 RaftRpcUtil,表示当前节点到某个其他 Raft 节点的 RPC 调用对象。

四、第二步:进入 RaftRpcUtil

复制代码
bool RaftRpcUtil::AppendEntries(
    raftRpcProctoc::AppendEntriesArgs* args,
    raftRpcProctoc::AppendEntriesReply* response) {

    MprpcController controller;

    stub_->AppendEntries(
        &controller,
        args,
        response,
        nullptr
    );

    return !controller.Failed();
}

这里创建了:

复制代码
controller → 记录 RPC 通信是否失败
args       → 请求对象
response   → 响应对象
stub_      → 远程 Raft 服务的客户端代理

stub_ 在构造函数中创建:

复制代码
stub_ = new raftRpcProctoc::raftRpc_Stub(
    new MprpcChannel(ip, port, true)
);

可以理解为:

复制代码
Stub          决定"调用哪个 RPC 方法"
MprpcChannel  决定"怎么通过网络发送"

五、第三步:Stub 把调用转交给 Channel

raftRpc_Stubprotoc 自动生成的。

它的 AppendEntries() 大致如下:

复制代码
void raftRpc_Stub::AppendEntries(
    RpcController* controller,
    const AppendEntriesArgs* request,
    AppendEntriesReply* response,
    Closure* done) {

    channel_->CallMethod(
        descriptor()->method(0),
        controller,
        request,
        response,
        done
    );
}

其中:

复制代码
descriptor()->method(0)

表示 raftRpc 服务中的第 0 个方法,也就是:

复制代码
rpc AppendEntries(...)

Stub 本身不进行 socket 编程,只把方法描述和请求交给 MprpcChannel

六、第四步:MprpcChannel 封装网络请求

所有 RPC 最后都会进入:

1. 取得服务名和方法名

复制代码
const auto* serviceDescriptor = method->service();

std::string serviceName = serviceDescriptor->name();
std::string methodName = method->name();

对于这次调用:

复制代码
serviceName = "raftRpc"
methodName  = "AppendEntries"

2. 序列化请求参数

复制代码
request->SerializeToString(&argsStr);

原来的 C++ 对象:

复制代码
AppendEntriesArgs

会转换成 protobuf 二进制数据。

3. 构造 RPC 请求头

复制代码
message RpcHeader {
    bytes service_name = 1;
    bytes method_name = 2;
    uint32 args_size = 3;
}

本次请求头大致是:

复制代码
service_name = "raftRpc"
method_name  = "AppendEntries"
args_size    = 请求参数的字节数

4. 构造完整网络数据

最终发送的数据格式是:

复制代码
┌──────────────────────┐
│ RpcHeader 的长度     │  Varint32
├──────────────────────┤
│ RpcHeader            │
│ service_name         │
│ method_name          │
│ args_size            │
├──────────────────────┤
│ 请求参数 args        │
└──────────────────────┘

对应代码逻辑:

复制代码
WriteVarint32(rpcHeaderStr.size());
WriteString(rpcHeaderStr);
sendRpcStr += argsStr;

5. 通过 TCP 发送

复制代码
send(m_clientFd, sendRpcStr.c_str(), sendRpcStr.size(), 0);

发送之后,当前调用会阻塞等待响应:

复制代码
recv(m_clientFd, recvBuf, 1024, 0);

因此这个项目的 RPC 调用方式是同步调用:

复制代码
发送请求
  ↓
等待远端处理
  ↓
收到响应后才返回

七、第五步:服务端 RpcProvider 接收请求

节点 1 的 RpcProvider 收到 TCP 数据后,进入消息处理函数。

它按照相反顺序解析:

复制代码
读取 RpcHeader 长度
    ↓
读取并反序列化 RpcHeader
    ↓
得到 service_name
    ↓
得到 method_name
    ↓
读取 args_size 字节的请求参数

最后得到:

复制代码
service_name = raftRpc
method_name  = AppendEntries
args_str     = 序列化的 AppendEntriesArgs

八、第六步:找到对应服务和方法

RpcProvider 查询之前注册的映射:

复制代码
auto serviceIterator = m_serviceMap.find(serviceName);
auto methodIterator =
    serviceIterator->second.m_methodMap.find(methodName);

找到:

复制代码
service → 节点1的 Raft 对象
method  → AppendEntries 的 MethodDescriptor

然后根据方法描述创建正确类型的参数对象:

复制代码
Message* request =
    service->GetRequestPrototype(method).New();

Message* response =
    service->GetResponsePrototype(method).New();

这里创建出来的实际类型是:

复制代码
request  → AppendEntriesArgs
response → AppendEntriesReply

再反序列化请求:

复制代码
request->ParseFromString(argsStr);

九、第七步:创建响应回调 done

服务端创建一个 protobuf 回调:

复制代码
Closure* done = google::protobuf::NewCallback(
    this,
    &RpcProvider::SendRpcResponse,
    conn,
    response
);

它相当于提前绑定了:

复制代码
RpcProvider::SendRpcResponse(conn, response);

业务方法处理完以后调用:

复制代码
done->Run();

就会把响应发送给客户端。

十、第八步:CallMethod 分发到 Raft

RpcProvider 执行:

复制代码
service->CallMethod(
    method,
    nullptr,
    request,
    response,
    done
);

这里的 service 实际指向节点 1 的 Raft 对象。

protobuf 生成的 raftRpc::CallMethod() 内部类似:

复制代码
switch (method->index()) {
    case 0:
        AppendEntries(controller, request, response, done);
        break;

    case 1:
        InstallSnapshot(controller, request, response, done);
        break;

    case 2:
        RequestVote(controller, request, response, done);
        break;
}

本次 method->index() == 0,因此调用 AppendEntries()

因为 Raft 重写了这个虚函数,最终进入:

复制代码
void Raft::AppendEntries(
    RpcController* controller,
    const AppendEntriesArgs* request,
    AppendEntriesReply* response,
    Closure* done) {

    AppendEntries1(request, response);
    done->Run();
}

十一、第九步:执行真正的 Raft 逻辑

四参数的 AppendEntries() 只是 RPC 入口,真正的业务逻辑在:

复制代码
AppendEntries1(request, response);

这里会处理:

检查 Leader 任期。

必要时退回 Follower。

重置选举定时器。

检查 PrevLogIndexPrevLogTerm

删除冲突日志。

追加新日志。

更新 commitIndex

填写 AppendEntriesReply

例如:

复制代码
response->set_term(m_currentTerm);
response->set_success(true);

RPC 框架不关心这些 Raft 规则,它只负责把请求送进来。

十二、第十步:发送响应

业务逻辑处理完成后:

复制代码
done->Run();

触发:

复制代码
RpcProvider::SendRpcResponse(conn, response);

它执行:

复制代码
response->SerializeToString(&responseStr);
conn->send(responseStr);

响应通过原来的 TCP 连接返回节点 0。

十三、第十一步:客户端接收响应

节点 0 的 MprpcChannel 正在阻塞等待:

复制代码
recv(m_clientFd, recvBuf, 1024, 0);

收到数据后:

复制代码
response->ParseFromArray(recvBuf, recvSize);

响应数据被直接写入调用者传进来的:

复制代码
AppendEntriesReply* reply

然后调用层层返回:

复制代码
MprpcChannel::CallMethod()
    ↓
raftRpc_Stub::AppendEntries()
    ↓
RaftRpcUtil::AppendEntries()
    ↓
Raft::sendAppendEntries()
相关推荐
笨蛋不要掉眼泪1 小时前
RabbitMQ消息队列:SpringAMQP
java·分布式·rabbitmq
发量惊人的中年网工2 小时前
SD-WAN如何赋能工业物联网?打造工厂网络远程连接和数据传输新方案
运维·网络·分布式·物联网
段一凡-华北理工大学2 小时前
AI推动工业智能化转型~系列文章05:特征工程:工业 AI 的第一生产力
数据库·人工智能·分布式·搜索引擎·特征工程·高炉智能化
斯普润布特3 小时前
Kafka KRaft 三节点 ARM64 Docker 部署
分布式·kafka
笨蛋不要掉眼泪4 小时前
RabbitMQ消息队列:交换机机制
java·分布式·rabbitmq
zcmodeltech4 小时前
智慧农业沙盘模型物联网控制系统设计:基于STM32与Modbus RTU的传感器-执行器闭环方案
服务器·分布式·stm32·嵌入式硬件·物联网·能源
想你依然心痛4 小时前
HarmonyOS 5.0智慧农业开发实战:构建分布式农业物联网与区块链农产品溯源系统
人工智能·分布式·物联网·区块链·智慧农业·harmonyos·开发实战
珠***格17 小时前
分布式光伏电站:四可装置如何实现 “可观、可测、可控、可调”
网络·人工智能·分布式·架构·能源
L16247619 小时前
消息队列(MQ)总览:RabbitMQ & Kafka 完整手册
分布式·kafka·rabbitmq