基于Raft分布式Kv存储:RPC

在这个项目里,RPC 可以理解为整个分布式 KV 系统的"远程通信层"。

它负责让不同进程、不同端口上的对象能够互相调用方法。RPC 本身不实现 Raft 算法,也不存储 KV 数据;它负责把请求送过去、调用正确的方法,再把结果送回来。

一、项目中有两类 RPC

复制代码
客户端 Clerk
    │  Get / Put / Append
    ▼
KVServer
    │  RequestVote / AppendEntries / InstallSnapshot
    ▼
其他 Raft 节点

对应两份 .proto

通信双方 RPC 服务 方法
Clerk → KVServer kvServerRpc GetPutAppend
Raft → Raft raftRpc RequestVoteAppendEntriesInstallSnapshot

1. Clerk 与 KVServer 之间的 RPC

接口定义在 kvServerRPC.proto (line 43)(C:/Users/LENOVO/Desktop/KVstorageBaseRaft-cpp-main/src/raftRpcPro/kvServerRPC.proto:43):

复制代码
service kvServerRpc {
    rpc PutAppend(PutAppendArgs) returns(PutAppendReply);
    rpc Get(GetArgs) returns(GetReply);
}

它负责把用户的 KV 操作发送给远程服务器。

Get RPC

客户端请求:

复制代码
message GetArgs {
    bytes Key = 1;
    bytes ClientId = 2;
    int32 RequestId = 3;
}

作用分别是:

Key:要查询的键。

ClientId:客户端唯一标识。

RequestId:该客户端的请求序号,用于重试和去重。

复制代码
bool ok = m_servers[server]->Get(&args, &reply);

这个调用看起来像普通 C++ 函数,但 m_servers[server] 代表远程服务器,实际会经过网络发送。

PutAppend RPC

客户端可以发起两种修改:

复制代码
Put(key, value);
Append(key, value);

最终都被转换成:

复制代码
PutAppend(key, value, "Put");
PutAppend(key, value, "Append");

请求中包含:

复制代码
message PutAppendArgs {
    bytes Key = 1;
    bytes Value = 2;
    bytes Op = 3;
    bytes ClientId = 4;
    int32 RequestId = 5;
}

ClientId + RequestId 很重要。因为 RPC 可能超时,客户端可能重发同一个请求。如果没有请求编号,一个 Append 请求重试两次,就可能真的追加两次。

RPC 负责传递编号,KVServer 负责判断是否重复执行。

2. RPC 帮助客户端寻找 Leader

Clerk 一开始不一定知道哪个服务器是 Leader。

clerk.cpp (line 21)(C:/Users/LENOVO/Desktop/KVstorageBaseRaft-cpp-main/src/raftClerk/clerk.cpp:21) 中,它会循环请求服务器:

复制代码
bool ok = m_servers[server]->Get(&args, &reply);

if (!ok || reply.err() == ErrWrongLeader) {
    server = (server + 1) % m_servers.size();
    continue;
}

这里有两种失败:

!ok:RPC 通信失败,例如连接失败、服务器宕机。

ErrWrongLeader:网络通信成功,但请求发给了 Follower。

这两种失败不能混为一谈。

ok == true 只表示:

复制代码
请求成功发出,并成功收到了可解析的响应

不表示业务操作一定成功。

Clerk 会继续尝试其他服务器,找到 Leader 后缓存其编号到 m_recentLeaderId,下次优先联系它。

3. KV 请求最终会进入 Raft

KVServer 收到 PutAppend RPC 后,不会直接修改本地数据库。

复制代码
Op op;
op.Operation = args->op();
op.Key = args->key();
op.Value = args->value();
op.ClientId = args->clientid();
op.RequestId = args->requestid();

m_raftNode->Start(op, &raftIndex, &term, &isLeader);

执行过程是:

复制代码
Clerk 发起 Put RPC
        ↓
Leader KVServer 收到请求
        ↓
把请求转换成 Op
        ↓
Raft::Start() 写入 Leader 日志
        ↓
通过 AppendEntries RPC 复制到其他节点
        ↓
多数节点复制成功
        ↓
日志提交并发送到 applyCh
        ↓
KVServer 执行 Put
        ↓
通过 RPC 向 Clerk 返回成功

所以客户端 RPC 和 Raft RPC 是串联起来的。

读取也经过共识顺序,有助于提供线性一致性读取。

二、Raft 节点之间的 RPC

复制代码
service raftRpc {
    rpc AppendEntries(AppendEntriesArgs)
        returns(AppendEntriesReply);

    rpc InstallSnapshot(InstallSnapshotRequest)
        returns(InstallSnapshotResponse);

    rpc RequestVote(RequestVoteArgs)
        returns(RequestVoteReply);
}

这三个 RPC 基本覆盖了 Raft 节点间的全部通信。

1. RequestVote:选举 Leader

当 Follower 长时间没有收到 Leader 心跳,就会:

复制代码
Follower
  ↓ 选举超时
Candidate
  ↓ currentTerm++
向所有其他节点发送 RequestVote RPC

请求携带:

复制代码
Term
CandidateId
LastLogIndex
LastLogTerm

接收节点检查:

Candidate 的任期是否足够新。

当前任期是否已经投票。

Candidate 的日志是否至少和自己一样新。

响应包含:

复制代码
Term
VoteGranted

候选人获得超过半数的票后成为 Leader。

没有 RequestVote RPC,各节点就无法跨进程完成投票和 Leader 选举。

2. AppendEntries:心跳和日志复制

这是项目里使用最频繁的 Raft RPC,作用有两个。

作为心跳

Leader 周期性发送没有日志的 AppendEntries

复制代码
Entries = 空

它告诉 Follower:

复制代码
Leader 还活着,不要发起新选举。

Follower 收到合法心跳后会重置选举定时器。

作为日志复制

客户端提交命令后,Leader 把日志通过 AppendEntries 发给 Follower:

复制代码
Term
LeaderId
PrevLogIndex
PrevLogTerm
Entries
LeaderCommit

Follower 检查上一条日志是否匹配:

复制代码
本地 PrevLogIndex 位置的 term
        ==
请求中的 PrevLogTerm

匹配则追加日志;不匹配则返回失败。

Leader 根据返回结果更新:

复制代码
m_nextIndex[server]
m_matchIndex[server]

如果复制失败,Leader 会调整 nextIndex,再次发送更早的日志。

当一条日志被多数节点确认后,Leader 推进 commitIndex,这条命令才真正提交。

3. InstallSnapshot:同步快照

如果某个 Follower 落后太多,Leader 需要的旧日志可能已经被压缩成快照。

这时不能再用普通 AppendEntries 补日志,而要发送:

复制代码
InstallSnapshot RPC

请求携带:

复制代码
LeaderId
Term
LastSnapShotIncludeIndex
LastSnapShotIncludeTerm
Data

其中 Data 是 KV 数据、客户端去重信息等序列化后的快照。

Follower 收到后会:

安装快照数据。

删除快照覆盖的旧日志。

更新快照索引和任期。

更新 commitIndexlastApplied

把快照交给上层 KVServer 恢复状态。

它解决的是"落后节点快速追赶"的问题。

三、底层 RPC 框架具体做了什么

业务代码只需要写:

复制代码
stub->AppendEntries(&controller, args, response, nullptr);

底层实际完成了很多工作。

1. 定义统一消息格式

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

一次请求大致是:

复制代码
Header长度
+ RpcHeader
+ protobuf序列化后的请求参数

例如:

复制代码
service_name = "raftRpc"
method_name  = "AppendEntries"
args_size    = 120
args         = AppendEntriesArgs的二进制数据

2. 客户端序列化并发送

从方法描述符获取服务名和方法名。

序列化请求参数。

构造 RPC 请求头。

连接目标 IP 和端口。

通过 TCP 发送数据。

阻塞等待响应。

将响应反序列化到 reply

通过 RpcController记录通信错误。

3. 服务端注册和查找服务

KVServer 启动时同时注册两个服务:

复制代码
provider.NotifyService(this);
provider.NotifyService(this->m_raftNode.get());

也就是:

复制代码
KvServer对象 → Get、PutAppend
Raft对象     → RequestVote、AppendEntries、InstallSnapshot

RpcProvider 保存服务名、方法名、服务对象之间的映射。

4. 服务端反序列化和方法分发

复制代码
解析 RpcHeader
  ↓
得到 service_name
  ↓
得到 method_name
  ↓
找到注册的 Service 对象
  ↓
创建对应的 request 和 response
  ↓
反序列化 request
  ↓
service->CallMethod(...)

protobuf 生成的 CallMethod 再把请求分发到正确的重写方法。

5. 把响应发回来

业务函数填写 response 后执行:

复制代码
done->Run();

它会调用:

复制代码
RpcProvider::SendRpcResponse(...)

将响应序列化并通过 TCP 发送给调用者。

四、RPC 不负责什么

RPC 只是通信机制,它不负责:

决定谁是 Leader。

判断是否应该投票。

检查日志是否匹配。

决定日志何时提交。

修改 KV 数据。

处理重复客户端请求。

制作或应用快照。

这些分别由 Raft、KVServer 和 Clerk 的业务逻辑负责。

最准确的划分是:

复制代码
RPC:把某个对象的方法调用跨网络送到另一个进程
Raft:保证多个节点对命令顺序达成一致
KVServer:执行命令并维护键值数据
Clerk:发起请求、寻找 Leader、失败重试
相关推荐
笨蛋不要掉眼泪8 小时前
RabbitMQ消息队列:发送者的可靠性
java·分布式·rabbitmq
cyadyx9 小时前
RabbitMQ 使用说明文档
分布式·rabbitmq
布鲁飞丝10 小时前
kafka 副本集设置和理解
分布式·kafka
songgz10 小时前
ZenithVM:从虚拟机到分布式操作系统内核
jvm·分布式·vm
番茄炒鸡蛋加糖10 小时前
分布式 ID
分布式
Francek Chen12 小时前
【大数据处理与分析】实验5:MapReduce初级编程实践
大数据·hadoop·分布式·mapreduce
独行侠影a1 天前
APScheduler+Redis 分布式定时任务:解决多实例任务重复执行
数据库·redis·分布式
香菜TTT1 天前
Kafka_深度解析_从架构原理到生产实践
分布式·架构·kafka·linq
zcmodeltech1 天前
智能矿井沙盘模型多系统协同控制系统设计:基于STM32与Modbus RTU的感知-传输-控制一体化方案
服务器·数据库·分布式·stm32·单片机·嵌入式硬件