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_Stub 是 protoc 自动生成的。
它的 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。
重置选举定时器。
检查 PrevLogIndex 和 PrevLogTerm。
删除冲突日志。
追加新日志。
更新 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()