一、从netty角度分析
首先我们都知道,spark1.6+之后整个rpc网络都是采用的netty实现。
1、netty基本原理
netty可以理解为客户端和服务端的通信。服务端的固定唯一的,客户端是随机多个的,客户端向服务端发送消息,服务端接受并处理消息。
netty的客户端持有一个send方法,调用send方法就可以屏蔽网络上的实现将消息发送给服务端。netty的服务端会事先注册好一个或多个入站处理器,当有消息进入时。会经过入站处理器,一般来讲入站处理器会将消息交给不同的处理器来实现不同的处理逻辑。
2、spark对于netty的封装实现
2.1 RpcEnv
实现类NettyRpcEnv。
RpcEnv是sparkRpc网络的初始入口,在SparkContext初始化时会创建spark执行环境SparkEnv。而在初始化SparkEnv时就会创建RpcEnv。
在RpcEnv的实现类NettyRpcEnv中有着几个必要重要的成员变变量。
dispatcher:Dispatcher 负责将rpc消息路由到对此消息进行处理的rpcEndpoint上
transportContext:TransportContext 传输上下文
clientFactory:TransportClientFactory 用于发送消息和响应请求的传输客户端工厂
fileDownloadFactory:TransportClientFactory 用于文件下载的传输客户端工厂
server:TransportServer 传输服务端
outboxes:ConcurrentHashMap[RpcAddress, Outbox] 待异步发送消息的临时存储
2.2 TransportContext
TransportContext作为整个Rpc网络的上下文,提供了两个主要的方法:
createServer:创建传输服务端
createClientFactory:创建传输客户端工厂
首先我们知道,TransportContext作为网络上下文存在于每一个成员节点上。而从网络成员的角度上来看,一个节点可以做为唯一一个服务端,但是可以同时作为多个客户端。所以此处,创建服务端的方法是唯一,但是创建客户端就需要一个客户端工厂来代为创建。
2.2 TransportServer
TransportServer作为Rpc网络的服务端,负责接受并处理发送过来的消息。而发送过来的消息会经过一个入站处理器ChannelInboundHandlerAdapter,spark对其的实现类为TransportChannelHandler。
2.3 TransportChannelHandler
TransportChannelHandler作为入栈处理器,其核心的方法为channelRead方法,即实际处理消息的方法。对于两个消息类型RequestMessage、ResponseMessage分别采用两个不同的处理器的handler方法来处理消息TransportRequestHandler、TransportResponseHandler。
在进入到对于的处理的handler方法中会发现对于不同的消息会有对应的执行逻辑。
2.4 TransportClientFactory
TransportClientFactory作为Rpc网络的客户端的工厂类,其最重要的作用就是创建Rpc客户端。
createClient:创建传输客户端。
2.5 TransportClient
TransportClient作为Rpc网络的客户端,其最主要的用就是通过channel.writeAndFlush的形式向客户端发送消息。TransportClient提供可以下几个方法来发送不同的消息:
fetchChunk:从远端协商好的流中请求单个块
stream:使用流id,从元端获取流数据
uploadStream:使用流id,从元端获取发送数据
sendRpc:向服务端发送RPC的请求,通过At least Once Delivery原则保证请求不会丢失
sendRpcSync:向服务端发送异步的RPC请求,并根据指定的超时时间等待响应
send:向服务端发送rpc请求,但是并不期望能获取响应
2.6 Dispatcher
在服务端,对于任何接受进来的消息,都会先交给Dispatcher。Dispatcher包含一个内部类EndpointData,EndpointData中有一个成员Inbox,Inbox负责存储接受进来的消息,之后有由线程池异步获取消息提交处理
Dispatcher中存在以下几个重要的成员变量:
endpoints:端点实例名称与端点数据EndpointData之间映射关系
endpointRefs:端点实例RpcEndpoint与端点实例引用RpcEndpointRef的映射关系
receivers:存储端点数据EndpointData的阻塞队列
threadpool:对于消息进行异步调度执行的线程池。
Dispatcher最核心的方法为:
postMessage:即将消息放入对应RpcEndpoint对应的EndpointData的Inbox中。
2.7 Inbox
存在于服务端的Dispatcher的内部类EndpointData中。对于每一个客户端的对应这一个Inbox。
Dispatcher会将收到的消息,放入对应端点的EndpointData的Inbox中,并且将对应的EndpointData放入内部的Receiver Queue中,另外Dispatcher创建时会启动一个单独线程进行轮询Receiver Queue。进行消息消费。
2.8 OutBox
存在于服务端的消息发信箱,对于待发送的消息会放入OutBox中,同时紧接着将消息通过TransportClient发出。消息的放入和发出是在同一个线程中进行的。
二、从系统组件的角度分析
首先,我们都知道spark早期的rpc采用的akka,1.6版本采用netty之后,依然沿用akka的actor网络模型。其最主要的两个角色为RpcEndpoint,RpcEndpointRef。
1、actor网络模型
。。。
2、spark的网络模型实现
2.1、RpcEndpoint
RpcEndpoint定义为Rpc端点,是一个特质。其定义了receive,receiveAndReply一系列方法,如果一个组件想要作为一个rpc服务端,必须是此特质的实现类,并实现其定义方法。
其最主要的方法为:
receive:接受消息并处理,但是不需要回复
receiveAndReply:接受消息并处理,但是需要回复
self:返回当前端点的RpcEndpointRef
2.2、RpcEndpointRef
RpcEndpointRef定义为Rpc端点的引用,是一个抽象类。其唯一的是是实现类为NettyRpcEndpointRef,其构造方法中包含一个RpcAddress的变成,标识其指向的RpcEndpoint。一个成员如果想要向一个Rpc端点发送发送消息就必须持有该Rpc端点的引用。
其最主要的方法为:
send:发送异步单项消息,不会等待返回
ask:异步方式进行消息ask
askSync:同步方式进行消息ask
2.3、总结
因此对于各个spark成员来讲,系统屏蔽了netty层面的复杂构造。一个组件如果想加入Rpc网络,只需要实现RpcEndpoint特质或者持有Rpc端点的引用NettyRpcEndpointRef即可。
对于NettyRpcEndpointRef的的持有者,只需要调用其send放即可将消息提交给对应的RpcEndpoint实现类的receive或receiveAndReply。而不需要关注netty层面的设计。
三、消息传递流程
(1)首先从RpcEndpointRef的send开始触发一次消息传递。
(2)RpcEndpointRef会将消息交给RpcEnv,通过其send方法给其指向的RpcEndpoint实现类发送消息。
(3)对于发往本地的消息,会直接放入本地的Dispatcher的endpoints的Inbox中(不经过网络,直接本机处理)。对与发送远端的消息,会通过TransportClient的send方法发送。
(4)TransportServer接受消息后经过其入栈处理器,送往某一个处理器。
(5)在具体的处理器中,如果是消息请求,会调用RpcHandler的receive方法,最后将其放入服务端的Dispatcher的endpoints的Inbox中。
(6)对于Inbox中的消息,会有MessageLoop线程不断从其中获取消息,根据消息类型调用RpcEndpoint实现类的receive或receiveAndReply方法。
(7)RpcEndpoint实现类的receive或receiveAndReply方法处理消息,流程结束。