RPC
1 RPC调用流程
1.1 clerk客户端调用远程服务
Clerk::PutAppend()
raftServerRpcUtil::PutAppend()
raftServerRpcUtil是client与kvserver通信的入口, 包含kvserver功能的一对一映射:Get/PutAppend,通过stub对象------raftKVRpcProctoc::kvServerRpc_Stub *stu进行对应的rpc远程调用。
针对raft节点之间的通信,设计了RaftRpcUtil类型,包含raft节点功能:AppendEntries/ InstallSnapshot/ RequestVote
kvServerRpc_Stub::PutAppend()
kvServerRpc_Stub 是 kvServerRPC.proto文件编译后生成的存根,用于实现rpc远程调用。
stub使用MprpcChannel作为参数来初始化,因此接下来会调用MprpcChannel::CallMethod():
stub = new raftKVRpcProctoc::kvServerRpc_Stub(new MprpcChannel(ip, port, false));
MprpcChannel::CallMethod()
MprpcChannel:实现rpc通信的类 ,建立和服务端的连接,发送rpc请求,和接收rpc回复
所有通过stub调用的rpc方法,都会走到MprpcChannel::CallMethod() : 生成rpc请求数据并序列化,使用socket发送请求并接收回复
1.2 kvserver 提供rpc服务
KvServer::KvServer()
构造函数中发布rpc服务类型 : 服务的类型有两种,分别是Raft和KvServer:
- class Raft : public raftRpcProctoc::raftRpc
- class KvServer : raftKVRpcProctoc::kvServerRpc
Raft类型的服务用于raft节点之间相互调用,KvServer类型的服务被client调用
RpcProvider::Run() 启动rpc服务 ,等待调用。项目中是阻塞等待,同步阻塞模型。
RpcProvider::OnMessage()
已建立连接用户的读写事件回调:调用Callmethod来调用本地的业务
RpcProvider:网络对象类,发布节点能提供的rpc服务,并启动rpc服务(接收连接,接收请求并执行)
service->CallMethod()
有两种服务类型(Raft和KvServer),根据请求的service的类型,动态调用对应的CallMethod();
client调用 KvServerRpc::CallMethod(),raft节点调用raftRpc::CallMethod()。
KvServerRpc::CallMethod()
在函数内部,最终调用KvServer本地的方法。
2 rpc基础
2.1 概念
- RPC(Remote Procedure Call Protocol) 远程过程调用协议。
- RPC是一种通过网络从远程计算机上请求服务,不需要了解底层网络技术的协议。
- 远程调用就像调用本地方法一样便捷。
2.2 常用RPC技术或框架
- 应用级的服务框架:阿里的 Dubbo/Dubbox、Google gRPC、Spring Boot/Spring Cloud。
- 远程通信协议:RMI、Socket、SOAP(HTTP XML)、REST(HTTP JSON)。
- 通信框架:MINA 和 Netty
2.3 rpc作用
- 微服务化:跨平台的服务之间远程调用;
- 分布式系统架构:分布式服务跨机器进行远程调用;
- 支持跨语言调用。
2.4 架构
服务注册 ,就是将提供某个服务的模块信息(通常是这个服务的ip和端口 )注册到1个公共的组件上去(注册中心,比如: zookeeper\consul)。
服务发现,就是新注册的这个服务模块能够及时的被其他调用者发现。不管是服务新增和服务删减都能实现自动发现。
一个完整的服务发现中心还需要支持**负载均衡** 、容灾处理等功能。
负载均衡:简单数就是将请求分散给各个机器上,维持各个机器的请求的均衡态势。确保不会大量请求都涌入到某几个机器上导致机器过载。
容灾处理:就是及时剔除掉故障的机器。例如某次调用19.4.11.11上的OrderServer出现故障,那么服务中心会将这个地址剔除掉,防止下次再访问到这个有故障的地址。
2.5 rpc调用流程
2.6 重要组成
客户端:调用方
客户端存根:client stub,保存服务端地址信息 ,将客户端的请求方法、参数序列化 ,通过网络发送给服务端。
服务端存根:server stub,接收请求,反序列化,调用本地服务
服务端:服务提供者
网络传输:tcp或http
2.7 基础功能
2.7.1 服务寻址
rpc所有函数都有一个自己的ID,在所有进程中都唯一。客户端远程调用,必须附上这个ID.
客户端查一下映射表,找出对应ID,服务端根据对应的ID提供服务。
Call ID映射表一般是哈希表。
2.7.2 序列化和反序列化
概念
- 序列化:将消息对象转换为二进制流。
- 反序列化:将二进制流转换为消息对象。
必要性
- 本地函数调用:只需要将数据压入栈中,然后让函数去栈中读取数据。
- 远程调用:无法通过栈传递参数。需要客户端将请求序列化,传送给服务端。
序列化的优势
- 二进制字节流便于网络传输
- 可跨平台、跨语言。
2.7.3 网络传输
基于TCP
通过socket连接发送序列化的调用接口名称、方法名称和参数等。
基于HTTP
客户端发送GET/PUT/POST/DELETE请求给服务端
服务端根据不同的请求参数和请求url进行方法调用,返回结果
对比
基于TCP更灵活,可减少网络开销,性能高。但实现复杂;
HTTP已经实现序列化,但http传输效率比tcp低。
2.8 服务治理
2.8.1 负载均衡
本项目实现的rpc没有做负载均衡,因为当前是Read Log模式,读写请求都是直接发送给leader。后面如果优化为从follower读,可能用得到负载均衡。
- 轮询算法(Round Robin) :轮询算法是最简单的负载均衡算法之一,它按照请求的顺序依次将每个请求分配到不同的服务器上。当有新的请求到来时,负载均衡器会依次将请求发送到不同的服务器,直到所有的服务器都被轮询过一遍,然后再从头开始。
- 最小连接数算法(Least Connections) :最小连接数算法会将新的请求分配到当前连接数最少的服务器上,以确保各服务器的负载尽可能均衡。这种算法考虑了服务器的负载情况,优先将请求发送到负载较低的服务器上。
- 最少响应时间算法(Least Response Time) :最少响应时间算法会将请求发送到响应时间最短的服务器上,以保证响应时间的最小化。这种算法通常需要负载均衡器记录每个服务器的响应时间,并动态调整请求的分配策略。
- 哈希算法(Hashing) :哈希算法根据请求的某些属性(如客户端IP地址、URL等)计算哈希值,并将请求发送到对应哈希值的服务器上。这种算法能够确保相同请求始终被发送到同一台服务器上,适用于需要保持会话一致性的场景。
- 加权轮询算法(Weighted Round Robin) :加权轮询算法在轮询算法的基础上 引入了权重的概念,不同的服务器具有不同的权重值。根据权重值的不同,负载均衡器会调整请求的分配比例,以实现负载均衡。
- 拓展:hash环也是一种重要的负载均衡算法,也可以提及。
3 本项目rpc
本项目实现的rpc比较简单,采用的是同步阻塞模式。
rpc请求格式:
- head_size(4个字节,存储head_str的长度)
- head_str(RpcHeader类型的序列化)
- args(PutAppendArgs/GetArgs 请求对象的序列化)
RpcHeader:
cpp
message RpcHeader
{
bytes service_name = 1;
bytes method_name = 2;
uint32 args_size = 3;
}
可以优化的点:
- 异步rpc
- rpc设为长连接还是短连接?长连接考虑设计一个定时器
- 负载均衡
- 服务发现,服务注册
序列化
1 Protobuf
kvServerRPC.proto文件
cpp
syntax = "proto3";
package raftKVRpcProctoc; //所在的命名空间
option cc_generic_services = true; //开启stub服务
message GetArgs{
bytes Key = 1 ;
bytes ClientId = 2 ;
int32 RequestId = 3;
}
message GetReply {
bytes Err = 1;
bytes Value = 2;
}
message PutAppendArgs {
bytes Key = 1;
bytes Value = 2 ;
bytes Op = 3;
bytes ClientId = 4;
int32 RequestId = 5;
}
message PutAppendReply {
bytes Err = 1;
}
service kvServerRpc
{
rpc PutAppend(PutAppendArgs) returns(PutAppendReply);
rpc Get (GetArgs) returns (GetReply);
}
.proto文件定义message类型和service类型,编译后,生成.pb.h文件和.pb.cc文件。
.proto文件中定义的message类型可以序列化和反序列化(SerializeToString/ParseFromString).
编译后会生成对应Stub(存根)类型 raftKVRpcProctoc::kvServerRpc_Stub,使用Stub对象可以调用.proto文件中定义的service。
2 boost
快照:kvserver、kvdb和raft节点使用boost库进行序列化