MIT6.824 rpc 相关知识整理
RPC 基本概念
RPC 全称为 Remote Procedure Call,即 远程过程调用,他可以让用户在不知道底层网络协议的情况下,在本地的计算机就可以调用远端服务器的一些处理方法,然后服务器会把处理的结果返回到用户本地,就像这个方法写在用户本地空间中一样。
Client 端(由上到下):
- Application:应用层,表示和用户直接交互的部分,用户在该层确定自己想要调用的函数,并且输入参数
- stub:原意是烟蒂,很形象的表示这只是个函数空壳,表示用户想要调用的函数,这里可以是函数名
- RPC lib:RPC 库,包括一系列的编码解码工作,对于用户端来说,是编码用户的函数调用请求进入网络层,或者是解码服务端的结果成为上层结构可以理解的语言。
- Network Layer:网络层传输协议,这里不再赘述,它可以是 TCP 或者 UDP。
Server 端(由下到上):
- Network Layer:网络层传输协议,同上
- RPC lib:RPC 库,对服务端来说,用于解码用户的调用函数请求,或者是函数结果的封装。
- Dispatcher:用户的请求到达该层后,服务端需要通过 Dispatcher,根据请求的函数,找到服务器中对应的函数,并把任务交到这个函数上
- Handler:具体的函数逻辑
RPC 的操作流程为:
- 用户端写好函数调用,向下传递到网络层并发送到服务端
- 服务端上行用户请求,通过 Dispatcher 找到具体的 Handler,由 Handler 处理具体的逻辑
- 服务端处理完毕后,通过 RPC lib 编码发还到客户端
- 客户端经过解码后获得结果,用户获得 Application 层获得结果返回。
RPC 处理失败
失败原因
可能导致请求失败的原因有:
- 服务端没有收到这个请求
- 服务端执行了请求,但响应之前宕机了
- 服务端执行了请求并发送了响应,但在响应之前网络故障了
解决方案
方案1: At Least Once
客户端会不断重试请求,直到收到请求被执行的肯定确认。适用于只读和幂等操作。
At least once 会造成资源浪费,服务端出现不一致的情况,不是一个很好的方法。
方案2: At Most Once
客户端不会自动重试请求。
如果没有针对失败请求的显式重试机制,请求可能会丢失并且永远不会被执行。
如果重试请求,服务器负责检测重复请求并确保只有一个请求成功。
Go RPC 实现了 At-Most-Once
语义,如果没有得到响应,只会返回一个错误。客户端可以选择重试一个失败请求,但服务端要自己处理重复的请求。
labrpc 代码解析
labrpc模拟网络的通信,可以模仿网络的延迟、丢包等故障,方便测试lab2的稳定性。顺便学习一下大佬是如何使用golang进行编程的。这里实现的RPC框架虽然只是用于模拟,相对简单,但麻雀虽小五脏俱全。其中用到了go语言的反射机制,gRPC的go语言实现也是利用这个反射机制。
1. 从end.Call()开始
end.Call("Raft.AppendEntries", &args, &reply)
表示发起一个RPC调用,其中Raft
是服务端的结构体名称,AppendEntries
是需要调用的方法名。args
是调用方法时传给服务端的参数,reply
是服务端返回的响应。这两个参数都需要以指针的形式传递。
并发调用的场景下:同一ClientEnd
上可以同时进行多个Call()
调用。并发调用Call()
时,它们可能会被服务器以不同的顺序处理,因为网络可能会重新排序消息。
使用Go的channel
和select
语句,方法能够优雅地处理正常情况和异常情况(如网络层销毁)。此外,它使用了labgob
作为序列化工具,这是MIT
6.824课程中的一个自定义序列化库,用于在RPC调用中编码和解码数据。
1 |
|
该方法下向 e.ch
管道发送请求。
那这个管道是从哪里来的呢?
1 |
|
在项目测试代码的网络构建之初,就会构建这样一个管道,随后每个终端end
创建之后都会共用这个管道。
1 |
|
另外,当网络构建之初,开启一个goroutine
,监听来自ClientEnd
的消息。收到消息后,就会开启一个协程go rn.processReq(xreq)
,对请求进行处理。
2. 处理请求 processReq(req)
这个方法非常的冗长,因为为了模拟网络的不同的状态,进行了不同的处理。接下来我们只讨论网络正常的情况下,网络是如何处理请求的。
首先通过Network.servers
这个servername到具体server的映射,找到对应的服务器,也就是下面代码里的server
。
1 |
|
网络创建一个管道,然后将
server.dispatch(req)
的结果放入其中。在server.dispatch(req)
这个方法里,首先解析出服务名和方法名,随后通过service, ok := rs.services[serviceName]
找到对应的服务,再调用这个服务的service.dispatch(methodName, req)
,即将具体的请求发送给该服务器提供服务的相应方法。
发送给相应服务后,会执行以下两行代码
1 |
|
method
是一个reflect.Method
类型的变量,通过查找方法名称查找得到(method, ok := svc.methods[methname]
)。
method.Func
是reflect.Value
类型,它包含了对应方法的实际函数。这里,function
变量被赋予了这个函数的反射值。
function.Call
是一个通过反射调用函数的方法。[]reflect.Value{svc.rcvr, args.Elem(), replyv}
是调用函数所需参数的反射值组成的切片:
svc.rcvr
是服务对象的接收者,即服务实例本身。这是因为在Go中,方法是绑定在接收者上的,调用一个方法实际上是在特定的接收者实例上调用该方法。args.Elem()
是请求参数的反射值。args
是一个通过reflect.New
创建的指针类型的反射值,指向实际的参数值。Elem
方法返回指针指向的实际值的反射值。replyv
是用于存放方法调用结果的变量的反射值。它先通过方法签名确定返回值类型,之后allocate动态创建的,以便方法可以将返回值写入其中。
到这RPC调用就基本完成了,将reply
结果放入req.replyCh
中即可。
最后再到end.Call()
结束:rep := <-req.replyCh
。
当代RPC框架 Dubbo
Dubbo 是一个分布式服务框架,致力于提供高性能和透明化的 RPC 远程服务调用方案,以及 SOA 服务治理方案。简单的说,Dubbo 就是个服务框架,说白了就是个远程服务调用的分布式框架。
Dubbo特点
- 远程通讯: 提供对多种基于长连接的 NIO 框架抽象封装(非阻塞 I/O 的通信方式,Mina/Netty/Grizzly),包括多种线程模型,序列化(Hessian2/ProtoBuf),以及“请求-响应”模式的信息交换方式。
- 集群容错: 提供基于接口方法的透明远程过程调用(RPC),包括多协议支持(自定义 RPC 协议),以及软负载均衡(Random/RoundRobin),失败容错(Failover/Failback),地址路由,动态配置等集群支持。
- 自动发现: 基于注册中心目录服务,使服务消费方能动态的查找服务提供方,使地址透明,使服务提供方可以平滑增加或减少机器。
微服务的分布式特性,使得应用间的依赖、网络交互、数据传输变得更频繁,因此不同的应用需要定义、暴露或调用 RPC 服务,那么这些 RPC 服务如何定义、如何与应用开发框架结合、服务调用行为如何控制?这就是 Dubbo 服务开发框架的意义,Dubbo 在微服务应用开发框架之上抽象了一套 RPC 服务定义、暴露、调用与治理的编程范式。
Dubbo 与 gRPC
Dubbo 与 gRPC 最大的差异在于两者的定位上:
- gRPC 定位为一款 RPC 框架,Google 推出它的核心目标是定义云原生时代的 rpc 通信规范与标准实现;
- Dubbo 定位是一款微服务开发框架,它侧重解决微服务实践从服务定义、开发、通信到治理的问题,因此 Dubbo 同时提供了 RPC 通信、与应用开发框架的适配、服务治理等能力。