Spark RPC之Send流程

简言

RPC框架设计到序列化、协议、动态管理、服务注册、加密、网络编程等一系列技术,要弄懂一个RPC框架是很难的,这篇文章只是针对Spark 2.4.4 版本的RPC调用流程做一个简单分析。
Spark是一个分布式计算引擎,节点间的shuffle、各个组件之间的通信等必不可少,比如Work向Master发送心跳包:啊,我还活着!。
Spark底层使用Netty作为节点间通信的桥梁,将Netty封装在network-common包中(消息格式、粘包拆包、编码解码、链路管理、加密等),为负责网络调用的上层代码(RPC请求、Shuffle数据传输以及资源文件传输等)提供可靠的服务,上层调用封装在rpc包里。底层网络和上层调用拆分开,实现高类聚,低耦合。
这讲我们先来谈谈上层调用发送请求的层次,接下来的文章在聊接受信息流程和底层网络调用中的消息格式,粘包拆包。不会太抽象只是单纯的send和recive,也不会太底层,毕竟Netty没学好。
Spark 的RPC结合了Reactor模型和Actor模型,在学习Spark的RPC之前我们需简单的了解一下这两个模型,在介绍发送请求之前,先了解Actor模型,下一讲聊recive流程时我们再了解Reactor模型。

Actor模型

Actor模型有效解决了多线程并发条件下锁等一系列线程模型问题,以异步而非阻塞的方式完成消息的传递处理(消息发送到Maillbox,发送端无需阻塞等待消息处理,Actor以先进先出的原则从Maiibox里获取消息并处理)。这些Actor共同构成ActorSystem,每个Actor有关于它的引用ActorRef,用来向所属的Actor发送消息。Actor模型图如下(图片来源于网络)
在这里插入图片描述
在Spark RPC 中

  • RpcEnv 对应ActorSystem, RpcEnv完成Endpoint的注册和移除。
  • Endpoint对应Actor,Endpoint可以是client,Worker,MapOutTracker,heartbeat等,每个Endpoint有一个生命周期constructor -> onStart -> recive* -> onStop.
  • EndpointRef 对应ActorRef,每个EndpointRef包含一个address(host,port)对应Endpoint所在的RpcEnv地址,包含一个name对应Endpoint名称,有了地址和名称就可以通过ask/send方法向远端发送rpc请求并被对应的Endpoint处理,ask方法可同步阻塞等待也可异步调用。
  • Inbox、Outbox 对应Mailbox,每个Endpoint对应一个Inbox(server端),一个Outbox(client端),Inbox和Outbox内部都维护一个messages消息列表,messages只是一个普通链表,无并发功能,所有在操作时要加锁synchronized保证同步。
    Endpoint 图(图片来源于网络)
    在这里插入图片描述

Spark RPC 发送端组件

简单了解Actor模型后,我们来看看Spark如何利用这个模型构建RPC的。

先来了解一下投递规则

  • At-most-once:消息会被投递0次或一次,发送的消息可能会丢失。
  • At-least-once:消息可能会多次投递并保证至少会成功一次,发送的消息可能会重复但不会丢失。
  • Exactly-once:消息只会向接受者准确的发送一次,发送的消息不会丢失也不会重复。

可以看到At-most-once成本最低性能最高,因为它在发送完消息后不会记录任何状态,At-least-once会记录’收到’状态,也就是发送者收到接收端发送的 确认收到消息,而Exactly-once保证一致性要求就高了。

client端用到的主要组件(图片来源于自己)
在这里插入图片描述

  1. EndpointRef:除了有地址和名称外,有向外投递消息的方法,在Spark 2.4.4版本中或更早版本,最大重连次数和每次重连等待时间已经没有使用了,意味者消息投递原则统一为了At-most-once。
  2. Dispatcher:消息调度分发器主要是recive流程会在下篇文章介绍,这里只需要知道它可以保存注册成功的Endpoint,和分发相应的Message到Endpoint对应的Inbox中即可。在NettyRpcEnv的ask方法中,如果发送端和接受是同一个address则会调用Dispatcher内部的postLocalMessage方法去处理。
  3. Outbox:NettyRpcEnv中维护一个outboxes线程安全的映射,一个RpcAddress远端地址对应一个Outbox,Outbox内有传输客户端TransportClient并且维护了一个向远端NettyRpcEnv上所有Endpoint发送的messages消息列表(缓存OutboxMessagge消息),Outbox有send方法和drainOutbox方法来发送OutboxMessage,调用流程后面再介绍。
  4. TransportContext传输上下文,用来创建TransportClientFactory(创建传输客户端TransportClient)和传输服务端TransportServer,内含RpcHandler(对调用TransportClient的sendRPC方法发送的消息进行处理的程序)和初始化Netty提供的Channel的Pipeline方法。
  5. Channel:Netty对Nio SocketChannel的封装,其pipeline里完成编码解码等,具体参考网络,后面的文章会介绍Spark的网络底层消息格式,编码解码。底层网络定义的Message根据请求和回复可以划分为RequestMessage和ResponseMessage,RequestMessage又分为StreamRequest、ChunkFetchRequest、RpcRequest和OnewayMesage,其中OnewayMessage不要求答复,这里先以RpcRequest为例熟悉RPC发送逻辑和接受逻辑。

RpcRequest发送逻辑

RPC 发送流程如下(图片来源于自己)
在这里插入图片描述

  1. 前面说到Actor模型里,向Actor发送信息,需要一个引用,对应上图右下角的EndpointRef,这个引用对应RpcEnv中一个注册好的Endpoint,调用NettyRpcEndpointRef的send或ask方法来发送请求,顾名思义,前者不要求回复,后者要求回复。消息先经过封装变为RequestMessage(注意这里的RequestMessage与前面介绍的RequestMessage并不是同一个,只是名字相同),然后调用NettyRpcEnv的ask方法。

  2. NettyRpcEnv的ask方法中会首先判断消息的接受地址是否是当前节点地址
    2.1、 接受地址与当前节点地址一致
    a)、创建一个Promise对象(Java中的Future, Scala中的Promise感兴趣的可以去了解下),设定Future完成后的回调(即onSuccess()和onFailure()方法)。
    b) 、调用Dispatcher.postLocalMessage方法,创建回调上下文LocalNettyRpcCallContext。
    c、调用Dispatcher.postMessage,将RequestMessage消息重构成RpcMessage(继承自InboxMessage,InboxMessage相当于一个标记,没有提供任何变量和方法,其子类会根据消息是否要求回复,添加变量)放置到对应endpoint的inbox内,Dispatcher内部的处理流程将在下一篇文章介绍。
    2.2、接受地址与当前节点地址不一致 调用上述封装好的RequestMessage中的serialize方法进行序列化以最小化其数据大小并将它与onSuccess()、onFailure()回调方法一同封装为RpcOutboxMessage,然后调用Dispatcher.postToOutbox投递出去。

  3. postToOutbox方法会判断EndpointRef是否包含了与接收端的TransportClient连接
    a)、 如果有则直接调用OutboxMessage中的sendWith方法(实际为TransportClient中的sendRpc方法,在sendRpc方法里,会定义一个独一无二的requestId绑定到TransportResponseHandler上,因为RpcRequest要求回复(RpcFailure/RpcResponse),当服务端回传RPC响应时,客户端会根据回传消息中的requestId,找到注册的回调函数,然后调用回调函数来执行服务端响应后的逻辑。最后调用channel.writeAndFlush刷写到socket底层),经过Server端处理发送到Endpoint对应的Inbox里。
    b)、 如果没有则通过Outbox.send方法将消息加入到与接受端地址对应的Outbox的messages消息列表里,并调用drainOutbox方法处理。

  4. messages消息列表只是普通链表,所以要加锁保证同步,drainOutbox方法里如果出现异常情况(Outbox已经停止或正在连接远端服务,有其他线程正在发送messages里的消息)则返回,Outbox对应的TransportClient为空则会先调用launchConnectionTask方法创建一个TransportClient,并重新调用drainOutbox方法,未出现异常情况则会循环从messages里取出message并调用OutboxMessage中的sendWith方法发送消息(这里与3a 中逻辑相同不在赘述)。

  5. NettyRpcEnv.ask方法最后有一个超时监测,超时则会抛出TimeoutException 异常。

至此Spark RPC 的 send流程以介绍完,省略了相关源码,代码一多容易引起不适,写完才发现字太多密密麻麻也会引起不适~,参考源码食用更加。
特别注意:如有不对,请指出。

参考:
书籍 《Spark内核设计的艺术》—耿嘉安著
Netty架构 https://baijiahao.baidu.com/s?id=1652411987807894303&wfr=spider&for=pc
Netty原理解析 https://mp.weixin.qq.com/s/YS_RLOd0iVwDcwH6JMyB8g
Netty 整体架构 https://blog.csdn.net/u013857458/article/details/82527722

Spark的网络通信 https://www.jianshu.com/p/7da49e332e70
Spark 1.6源码分析 https://zx150842.gitbooks.io/spark-1-6-1-source-code/
Spark rpc 框架概述 https://www.cnblogs.com/listenfwind/p/10224847.html

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值