RPC概念
RPC,即远程过程调用(Remote Procedure Call),是一种通过网络从远程计算机程序上请求服务的技术,而无需了解底层网络技术的协议。在RPC中,客户机和服务器位于不同的机器上,客户端通过网络调用在服务器端运行的过程,并将结果发送回客户机。这种技术允许程序像调用本地过程一样调用远程过程,使得跨平台、跨机器的服务调用成为可能。
- 两个进程间的相互调用
- 集群中不同节点服务的通信
Flink RPC实现技术
Akka+Netty(Pekko+Netty)
Flink 1.18版本之前是Akka和Netty,Flink内部节点之间的通信是用Akka,比如JobManager和TaskManager之间的通信。而operator之间的数据传输是利用Netty。Flink1.18版本内部RPC通信封装用的是Apache Pekko。Apache Pekko是Akka 2.6.x的一个分支。为什么会改因为Akka将来Apache许可证更改为Business Source License (BSL) v1.1,该协议不是开源的。
- Netty:相比更加基础一点,可以为不同的应用层通信协议(RPC,FTP,HTTP等)提供支持
- Akka:它是基于协程的,性能不容置疑;基于scala的偏函数,易用性也没有话说,但是它毕竟只是RPC通信。
- Pekko:Akka 2.6.x的一个分支
Akka、Pekko基本概念
Akka、Pekko 用于构建高并发、分布式、可容错、事件驱动的开发库。
- 提供基于异步非阻塞、高性能的事件驱动编程模型
- 轻量级的事件处理(每GB堆内存几百万Actor)
- 使用Akka可以在单机上构建高并发程序,也可以在网络中构建分布式程序。
注意:Akka是基于Actor模型的并发框架,每个Actor的实例在运行时只占用非常少的资源,大约只有300字节。这意味着在1G的内存中可以容纳接近300万个Actor,这使得Akka在处理大量并发请求时能够保持高效的内存使用。 - ActorSystem 是管理 Actor 生命周期的组件,Actor 是负责进行通信的组件
- 每个 Actor 都有一个 MailBox,别的 Actor 发送给它的消息都首先储存在 MailBox 中,通过这种方式可以实现异步通信。
- 每个 Actor 是单线程的处理方式,不断的从 MailBox 拉取消息执行处理,所以对于 Actor 的消息处理,不适合调用会阻塞的处理方法。
- Actor 可以改变他自身的状态,可以接收消息,也可以发送消息,还可以生成新的 Actor
- 每一个 ActorSystem 和 Actor都在启动的时候会给定一个 name,如果要从 ActorSystem 中,获取一个 Actor,则通过以下的方式来进行 Actor 的
获取:pekko.tcp://flink@localhost:6123/user/rpc/resourcemanager_* 来进行定位 - 如果一个 Actor 要和另外一个 Actor 进行通信,则必须先获取对方 Actor 的 ActorRef 对象,然后通过该对象发送消息即可。
- 通过 tell 发送异步消息,不接收响应,通过 ask 发送异步消息,得到 Future 返回,通过异步回到返回处理结果。
- 如果构建actor进行通信,Pekko版本中必须继承AbstractActor 实现createReceive()方法
Pekko Demo
PekkoData
- 定义了通信的类型信息也就是PekkoData
- 内部声明一个 字符串类型的info
package com.source.pekko;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PekkoData {
private String info;
}
PekkoRpcReceiverActor
- PekkoRpcReceiverActor接收Actor类继承了AbstractActor
- 也就是说该类可以进行接收发送消息
- 接收消息会进入到createReceive
- 根据消息类型匹配进入到handleMessage
- 获取发送者、自身的ActorRef
- 打印信息并向发送者回复消息
package com.source.pekko;
import org.apache.pekko.actor.AbstractActor;
import org.apache.pekko.actor.ActorRef;
import org.apache.pekko.japi.pf.ReceiveBuilder;
/**
* 继承AbstractActor定义自己的actor
* Actor可以发送和接收消息
*/
public class PekkoRpcReceiverActor extends AbstractActor {
/**2
* 实现接收消息
* @return
*/
@Override
public Receive createReceive() {
return ReceiveBuilder.create()
/**接收到PekkoData消息交给handleMessage处理
* flink PekkoRpcActor 155行也是这样处理的
*/
.match(PekkoData.class, this::handleMessage)
.build();
}
/**
* 处理具体消息
* @param message
*/
private void handleMessage(final PekkoData message) {
/** 获取发送者,发送者对应的就是actorRef */
ActorRef sender = getSender();
ActorRef self = getSelf();
/** 打印 */
System.out.println("PekkoRpcReceiverActor类收到:" +sender + ":发送者=>" + message.getInfo());
/** 回复消息 向发送者sender 回复word 的消息 回复者是当前actorRef*/
/** 4、Actor 可以改变他自身的状态,可以接收消息,也可以发送消息,还可以生成新的 Actor */
sender.tell(new PekkoData("word"),self);
}
}
PekkoRpcSenderActor
- PekkoRpcSenderActor 发送Actor类继承了AbstractActor
- 也就是说该类可以进行接收发送消息
- 接收消息会进入到createReceive
- 根据消息类型匹配进入到handleMessage
- 获取发送者的ActorRef
- 打印信息
package com.source.pekko;
import org.apache.pekko.actor.AbstractActor;
import org.apache.pekko.actor.ActorRef;
import org.apache.pekko.japi.pf.ReceiveBuilder;
/**
* 继承AbstractActor定义自己的actor
* Actor可以发送和接收消息
*/
public class PekkoRpcSenderActor extends AbstractActor {
/**
* 实现接收消息
* @return
*/
@Override
public Receive createReceive() {
return ReceiveBuilder.create()
/**接收到PekkoData消息交给handleMessage处理
* flink PekkoRpcActor 155行也是这样处理的
*/
.match(PekkoData.class, this::handleMessage)
.build();
}
private void handleMessage(final PekkoData message) {
/** 获取发送者,发送者对应的就是actorRef */
ActorRef sender = getSender();
/** 打印 */
System.out.println("PekkoRpcSenderActor类收到:" +sender + ":发送者=>" + message.getInfo());
}
}
Demo
- 创建ActorSystem,名字为flink
- 获取PekkoRpcReceiverActor的ActorRef这样就可以进行发送消息了、接收消息了
- 获取PekkoRpcSenderActor的ActorRef这样就可以进行发送消息了、接收消息了
- 通过PekkoRpcSenderActor的actorRef 向PekkoRpcReceiverActor发送消息
- PekkoRpcReceiverActor类中的createReceive接收到消息后会匹配类型转入handleMessage
- 打印信息,然后通过自身actorRef 向PekkoRpcSenderActor回复消息
- PekkoRpcSenderActor的createReceive方法接收到后转入handleMessage
- 打印回复信息。
- 结束程序。
package com.source.pekko;
import org.apache.pekko.actor.ActorRef;
import org.apache.pekko.actor.ActorSystem;
import org.apache.pekko.actor.Props;
public class Demo {
public static void main(String[] args) {
/**创建actorSystem*/
ActorSystem actorSystem = ActorSystem.create("flink");
/**构建PekkoRpcActor的ActorRef*/
ActorRef pekkoRpcRef = actorSystem.actorOf(Props.create(PekkoRpcReceiverActor.class), "PekkoRpcReceiverActor");
/**构建PekkoRpcSenderActor的ActorRef*/
ActorRef pekkoRpcSenderRef = actorSystem.actorOf(Props.create(PekkoRpcSenderActor.class), "PekkoRpcSenderActor");
/** pekkoRpcSenderActor作为发送者 向PekkoRpcActor发送 hello*/
pekkoRpcRef.tell(new PekkoData("hello"),pekkoRpcSenderRef);
}
}
Flink RPC通信
前置知识点
- ResourceManager:主要负责Flink集群中的计算资源,其中计算资源主要来自TaskManager注册。
- TaskManager(TaskExecutor):TaskManager负责向整个集群提供Slot计算资源。TaskManager会调用registerTaskExecutor()方法向ResourceManager注册
整体架构
Flink RPC框架设计相对比较复杂,底层基于Pekko构建的通信系统,Java 动态代理构建RpcGateway接口的代理类
Flink RPC UML图
如上图 Flink RPC UML图
- RpcGateway接口Flink RPC底层通信用到的动态代理,动态代理中使用的目标类实现的接口最终都是RpcGateway(也就是说动态代理创建的接口最上层都是RpcGateway)
- RpcEndpoint消息通信组件,底层都有的通信实体都要继承RpcEndpoint
- FenceRpcEndpoint类是内部会有一个fenceToken发送消息的时候两个 token一样的时候才能发成功,FencePekkoInvocationHandler、FenceRpcGateway也一样
- RpcEndpoint 内部使用到了RpcService、RpcService
- RpcService就是用来服务Flink RPC通信的服务类,内部会创建RpcEndpoint的自身代理,获取远程代理。RpcService实现类是PekkoRpcService
- RpcService 在具体通信类构建对象的时候super父类构造器也就是RpcEndpoint类的时候会初始化RpcServer代表自身代理。
- PekkoInvocationHandler、FencePekkoInvocationHandler实现了java InvocationHandler接口,也就是说他们里面肯定有实现的invoke方法
- Dispatcher及其子类、ResourceManager及其子类、JobMaster最终都继承了RpcEndpoint,也就是说他们都具备了通信的特质
RpcGateway
Rpc网关,用于远程调用的代理接接口,RPC通信的接口都继承RpcGateway,java动态代理类最终创建。
Proxy类:这个类提供了创建动态代理类和实例的静态方法。
public static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)
interfaces=>实现了RpcGateWay的接口
如:ResourceManagerGateway、JobMasterGateway、TaskExecutorGateway
RpcEndpoint
- RpcEndpoint抽象类中定义了RPC组件的基本实现,所有需要实现RPC服务的组件都会继承RpcEndpoint,
RpcEndpoint内部包含了endpointId 用来标识当前RPC节点的唯一标识,RpcEndpoint借助RpcService启动RpcServer。 - FencedRpcEndpoint继承RpcEndpoint,内部增加了fencingToken字段,实现了FencedRpcEndpoint的节点都会有一个fencingToken,当远程RPC调用时,会比较访问者和被访问者的fencingToken是否一致,一致了才会进行后续操作。
- FencedRpcEndpoint实现类有ResourceManager、JobMaster、TaskExecutor,RpcEndpoint的实现类有TaskExecutor
RpcService
创建时间ClusterEntrypoint 开始启动集群初始化的时候
private RpcService commonRpcService;
ClusterEntrypoint .runCluster ->
initializeServices ->
commonRpcService =
RpcUtils.createRemoteRpcService(
rpcSystem,
configuration,
configuration.get(JobManagerOptions.ADDRESS),
getRPCPortRange(configuration),
configuration.get(JobManagerOptions.BIND_HOST),
configuration.getOptional(JobManagerOptions.RPC_BIND_PORT));
内部提供了RpcServer的创建和启动方法,启动RpcServer(startServer)过程中,通过RpcEndpoint地址创建Akka actor实例,并基于Actor实例构建RpcServer接口的动态代理类
connect方法:连接到所提供地址下的远程rpc服务器。返回一个rpc网关(代理对象),该网关可以用于与rpc服务器通信
RpcServer
创建时间RpcEndpoint 构建的时候创建
RpcServer接口通过PekkoInvocationHandler动态代理类实现,所有远程获本地的执行请求,最终都会转换到PekkoInvocationHandler代理类中执行,也就是InvocationHandler的invoke方法
public ResourceManagersuper() ->
protected FencedRpcEndpoint super() ->
RpcEndpoint -> this.rpcServer = rpcService.startServer(this);
核心点:所有RpcEndpoint启动的时候调用start()方法,最终都会流转到RpcEndpoint的onStart()方法
原因如下:
ClusterEntrypoint.dispatcherResourceManagerComponentFactory.create() ->
DefaultDispatcherResourceManagerComponentFactory.create ->
resourceManagerService.start() ->
ResourceManagerServiceImpl.start() ->
StandaloneLeaderElection.startLeaderElection->
ResourceManagerServiceImpl.grantLeadership->
startNewLeaderResourceManager()->
startResourceManagerIfIsLeader->resourceManager.start();
RpcEndpoint.start ->
public void start() {
rpcEndpoint.tell(ControlMessages.START, ActorRef.noSender());
}
===========================================
PekkoRpcActor.createReceive() ->
handleControlMessage() ->
StoppedState.start() ->
RpcEndpoint.internalCallOnStart()->
onStart()
PekkoRpcActor
继承了AbstractActor,实现了createReceive(),也就是说Flink RPC 所有通信都会被createReceive
之后根据消息类型流转到对应的handleMessage(),消息类型有RemoteHandshakeMessage握手消息、ControlMessages 控制类消息比如start,其他消息(RpcInvocation)
总结
类名 | 详细介绍 |
---|---|
RpcGateway | 用于远程调用的代理接。RpcGateway 提供了获取其所代理的 RpcEndpoint 的地址的方法。在实现一个提供 RPC 调用的组件 时,通常需要先定一个接口,该接口继承 RpcGateway 并约定好提供的远程调用的方法。看源码继承实现RpcGateway需要定义有远程调用的方法 ResourceManagerGateway。 |
RpcEndpoint | 对 RPC 框架中提供具体服务的实体的抽象,所有提供远程调用方法的组件都需要继承该抽象类。另外,对于同一个 RpcEndpoint 的所有 RPC 调用都会在同一个线程(RpcEndpoint 的“主线程”)中执行,因此无需担心并发执行的线程安全问题。看源码,如果要进行通信就要实现RpcEndpoint,相当于消息通信实体。 |
RpcService | 是 RpcEndpoint 的运行时环境, RpcService 提供了启动 RpcEndpoint , 连接到远端 RpcEndpoint 并返回远端 RpcEndpoint 的代 理对象等方法。此外, RpcService 还提供了某些异步任务或者周期性调度任务的方法。内部包装了 ActorSystem 看源码,内部对ActorSystem封装,内部通过动态代理构建代理实现,以及远程连接RpcEndpoint进行消息发送。 |
RpcServer | 相当于 RpcEndpoint 自身的的代理对象(self gateway)。 RpcServer 是 RpcService 在启动了 RpcEndpoint 之后返回的对象,每 一个 RpcEndpoint 对象内部都有一个 RpcServer 的成员变量,通过 getSelfGateway 方法就可以获得自身的代理,然后调用该Endpoint 提供的服务。看源码RpcEndpoint实现启动的时候会调用。 |
PekkoRpcActor | 继承AbstractActor 接收到的消息进行处理,handleMessage根据消息类型就行处理。 |
源码调试
未完待续