RPC 远程过程调用详解与应用

RPC(Remote Procedure Call),即远程过程调用。

RPC 的核心目的是实现进程间通信,在分布式环境中广泛应用。

RPC 框架面向开发者屏蔽了网络底层逻辑,使远程调用可以像本地调用一样方便。

分布式通信

软件系统越来越复杂,从单机走向了分布式(集群/分布式/异地 部署),就需要支持分布式环境下的通信技术和方案。

  1. TCP/UDP:最基础最底层的通信方式,所有应用层通信协议都基于 TCP/UDP。

  2. CORBA:底层结构基于面向对象模型。

    曾经是分布式环境解决互联的主流技术,开发和部署成本较高,现已基本被遗弃。

  3. Web Service:基于 HTTP + XML 的标准化 Web API,传统行业的信息系统仍在广泛使用。

  4. RESTful:基于 HTTP,可以使用 XM格式定义或JSON格式定义。

  5. RMI(Remote Method Invocation):Java 提供的远程方法调用,。

  6. MQ(Message Queue):消息队列(消息中间件),包括对JMS提供支持。

  7. RPC(Remote Procedure Call):远程过程调用,使远程调用像本地调用一样方便。

RPC 概述

RPC(Remote Procedure Call)远程过程调用,在分布式应用中,RPC 是一个计算机通信协议,是一种进程间通信的模式。该协议允许运行于一台计算机的程序调用另一个地址空间(通常为一个开放网络的一台计算机)的程序,而程序员就像调用本地程序一样,无需关注底层网络通信的实现。

RPC 是一种服务器 / 客户端(Client/Server)模式,经典实现是一个通过发送请求-接受回应进行信息交互的系统。

如果涉及的软件采用面向对象编程,那么远程过程调用亦可称作远程调用远程方法调用,例:Java RMI

RPC 与 HTTP

HTTP 调用也是 RPC 实现的一种方式,所有的网络通信底层都是基于 TCP/UDP 协议将数据转换为二进制报文传输。

RPC HTTP
性能 可以长连接,可直接通信
可以指定序列化协议
性能消耗低,传输效率高
需要建立三次握手
一般是JSON或XML格式
序列化更消耗性能,消息头臃肿
传输协议 支持自定义协议,
通常支持广泛使用的RPC协议
只能HTTP
负载均衡 RPC 支持 使用中间件,如 Nginx。
应用范围 适用于企业内部使用
不建议传输较大的文本,视频等
行业标准,对内对外都适用
适用于多种异构环境
浏览器,APP接口,第三方接口调用都支持

传输协议:指的是应用层的协议,RPC 和 HTTP都属于应用层协议,都是基于网络层 TCP 协议。

RPC 应用场景

RPC在分布式系统中的系统环境建设和应用程序设计中有着广泛的应用,应用包括如下方面:

  1. 分布式操作系统的进程间通讯
  2. 分布式应用程序设计
  3. 分布式程序的调试
  4. 构造分布式计算的软件环境
  5. 远程数据库服务

RPC 调用过程

RPC 核心组件主要由 5 个部分组成:客户端,客户端存根,网络传输模块,服务端存根,服务端。

RPC

RPC 调用通常会经历 4 个环节:

  1. 建立通信:两台服务之间通信,必备条件是建立网络连接。

  2. 服务寻址:A 服务调用 B 服务,需要知道B服务的主机地址(IP),端口,方法名称。

  3. 网络传输

    • 序列化:A 服务发起一个 RPC 调用,需要将调用方法和参数数据进行序列化传输。
    • 反序列化:B 服务收到请求后,需要将收到的数据进行反序列化。
  4. 服务调用:B 服务进行本地调用之后得到返回值,将返回值序列化返回给A服务,经过网络将二进制数据回传给A服务器。

一次 RPC 调用过程通常会有以下步骤:

  1. 客户端(Client)以本地调用的方式调用服务。
  2. 客户端存根(Client Stub)接收到调用后,负责将方法、入参等信息序列化(组装)成能够进行网络传输的消息体(对象数据转换为二进制)。
  3. 客户端存根(Client Stub)找到远程的服务地址,并且将消息通过网络发送给服务端。
  4. 服务端存根(Server Stub)收到消息后进行解码(反序列化操作)。
  5. 服务端存根(Server Stub)根据解码结果调用本地的服务。
  6. 服务端(Server)本地服务执行,并将结果返回给服务端存根(Server Stub)。
  7. 服务端存根(Server Stub)序列化结果。
  8. 服务端存根(Server Stub)将序列化的结果通过网络发送给消费者。
  9. 客户端存根(Client Stub)接收到消息,并进行解码(反序列化)。
  10. 服务消费方得到最终结果。

RPC 框架是把 2、3、4、7、8、9 这些步骤封装起来。

RPC 实现原理

  • 动态代理:解决代理逻辑代码和业务隔离,添加增强操作。
  • 反射:实现自动查找到远程方法。
  • 序列化:为网络传输做好准备。
  • 网络编程:建立网络通道进行内容传输。
  1. 动态代理

    RPC 自动给接口生成一个代理类,在项目中引入了接口后,运行时实际绑定的是这个代理类。

    例如,Dubbo 就使用 JDK 自带的动态代理 org.apache.dubbo.rpc.proxy.jdk.JdkProxyFactoryorg.apache.dubbo.rpc.proxy.javassist.JavassistProxyFactory对字节码增加实现代理。

  2. 反射

    传入远程服务名和方法名,通过反射自动定位到需要被调用的方法,再传入入参,从而进行RPC调用。

    可参考 Dubbo的 org.apache.dubbo.rpc.proxy.InvokerInvocationHandler 类中的实现

  3. 序列化

    序列化目的是将对象数据转换化可网络传输的二进制。每次RPC调用都会伴随频繁的序列化和反序列化操作。需要考虑序列化后的内容大小,序列化和反序列化的耗时和性能。

    常见的序列化框架有:Hessian、Protobuf、Thrift。

  4. 网络编程

    RPC 远程调用就一定会涉及到网络编程,需要可靠的通信来保障每一次调用的顺序进行。

    比较流行的网络通信框架有 Netty、Mina,都出自同一个作者(Trustin Lee

RPC 异步实现

成熟的 RPC 框架除了同步调用方式外,通常还会提供异步调用、事件通知(异步监听)、callback 调用功能。

异步调用

RPC 异步调用是指客户端发起请求后,不必等待获取结果(实际立即返回的是 null),同时可以获取一个 Future 对象,然后从 Future 中去获取结果。这样客户端不需要开启多线程就可以并行调用多个远程服务的接口。

如果一个业务需要调多个接口,多个接口之间没有顺序依赖,可以使用异步调用提高效率。

以 Dubbo 为例,使用 RpcContext 实现异步调用

在 consumer.xml 中配置:

1
2
3
<dubbo:reference id="asyncService" interface="org.apache.dubbo.samples.governance.api.AsyncService">
<dubbo:method name="sayHello" async="true" />
</dubbo:reference>

调用代码:

1
2
3
4
5
6
7
8
9
10
11
12
// 此调用会立即返回null
asyncService.sayHello("world");
// 拿到调用的Future引用,当结果返回后,会被通知和设置到此Future
CompletableFuture<String> helloFuture = RpcContext.getContext().getCompletableFuture();
// 为Future添加回调
helloFuture.whenComplete((retValue, exception) -> {
if (exception == null) {
System.out.println(retValue);
} else {
exception.printStackTrace();
}
});

或者也可以这样调用:

1
2
3
4
5
6
CompletableFuture<String> future = RpcContext.getContext().asyncCall(
() -> {
asyncService.sayHello("oneway call request1");
}
);
future.get();

事件通知

成熟的 RPC 框架通常会提供事件通知功能,在调用之前,调用之后,出现异常时的事件通知。

在异步调用时,是通过 Future 的 get 获取结果,而 get 是阻塞的。还可以使用事件通知的方式获取结果,即在调用完之后完全不管,可以干其它事情,通过一个监听去侦听,当 RPC 框架有结果返回时会通知监听器,然后进行逻辑处理。

以 Dubbo 为例,提供了事件通知功能在调用之前、调用之后、出现异常时,会触发 oninvokeonreturnonthrow 三个事件,可以配置当事件发生时,通知哪个类的哪个方法。

服务消费者 Notify 接口:

1
2
3
4
public interface Notify {
public void onreturn(Person msg, Integer id);
public void onthrow(Throwable ex, Integer id);
}

服务消费者 Notify 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class NotifyImpl implements Notify {
public Map<Integer, Person> ret = new HashMap<Integer, Person>();
public Map<Integer, Throwable> errors = new HashMap<Integer, Throwable>();

public void onreturn(Person msg, Integer id) {
System.out.println("onreturn:" + msg);
ret.put(id, msg);
}

public void onthrow(Throwable ex, Integer id) {
errors.put(id, ex);
}
}

服务消费者 Notify 配置

1
2
3
4
<bean id ="demoNotify" class = "org.apache.dubbo.callback.implicit.NotifyImpl" />
<dubbo:reference id="demoService" interface="org.apache.dubbo.callback.implicit.IDemoService" version="1.0.0" group="cn" >
<dubbo:method name="get" async="true" onreturn = "demoNotify.onreturn" onthrow="demoNotify.onthrow" />
</dubbo:reference>

callbackasync 功能正交分解,async=true 表示结果是否马上返回,onreturn 表示是否需要回调。

两者叠加存在以下几种组合情况:

  • 异步回调模式:async=true onreturn="xxx"
  • 同步回调模式:async=false onreturn="xxx"
  • 异步无回调 :async=true
  • 同步无回调 :async=false

callback调用

成熟的 RPC 框架通常支持服务端回调客户端功能。RPC 通信基本都是长连接的,也就支持全双工通信,客户端可以调服务端,服务端同样可以调客户端。

RPC 客户端调只管发送调用请求,不用管结果响应,服务端的结果响应通过回调客户端来处理。

以 Dubbo 为例,Dubbo 的 参数回调将基于长连接生成反向代理,这样就可以从服务器端调用客户端逻辑。可以参考 dubbo 项目中的示例代码

回调服务接口:定义了一个业务方法,增加一个回调监听器

1
2
3
public interface PayService {
void toPay(String pay, CallbackListener listener);
}

回调监听器接口:

1
2
3
public interface CallbackListener {
void notify(String msg);
}

服务提供者接口实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@Service
public class PayServiceImpl implements PayService {
private final Map<String, CallbackListener> listeners = new ConcurrentHashMap<String, CallbackListener>();

public PayServiceImpl() {
Thread t = new Thread(new Runnable() {
public void run() {
while (true) {
try {
for (Map.Entry<String, CallbackListener> entry : listeners.entrySet()) {
try {
entry.getValue().notify(getResult(entry.getKey()));
} catch (Throwable t) {
listeners.remove(entry.getKey());
}
}
Thread.sleep(5000); // 定时触发变更通知
} catch (Throwable t) { // 防御容错
t.printStackTrace();
}
}
}
});
t.setDaemon(true);
t.start();
}

@Override
public void toPay(String order, CallbackListener listener) {
listeners.put(order, listener);
listener.notify(getResult(order)); // 发送通知
}

private String getResult(String order) {
return "Result: " + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
}
}

服务提供者配置:

1
2
3
4
5
6
7
8
<bean id="payService" class="com.callback.impl.PayServiceImpl" />
<dubbo:service interface="com.callback.PayService" ref="payService" connections="1" callbacks="1000">
<dubbo:method name="toPay">
<dubbo:argument index="1" callback="true" />
<!--也可以通过指定类型的方式-->
<!--<dubbo:argument type="com.demo.CallbackListener" callback="true" />-->
</dubbo:method>
</dubbo:service>

服务消费者配置:

1
<dubbo:reference id="payService" interface="com.callback.PayService" />

服务消费者调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Service
public class OrderServiceImpl implements OrderService {
private Logger logger = LoggerFactory.getLogger(OrderServiceImpl.class);

@Autowired
private PayService payService;

@Override
public void toPay(String order) {
CallbackListener listener = new CallbackListener() {
@Override
public void notify(String result) {
logger.info("pay result:" + result);
}
};
// 可以把 callback 对象注册到 Spring 容器
// 每次使用同一个 callback 对象,服务端也得到同一个 callback 对象(推荐)
payService.toPay("order", listener);
}
}

如果服务端完成一个业务逻辑需要多多次通知,可以采用 callback 方法。这一点在异步监听中是没有办法实现的,callback 调用更新是一次性买卖,在高并发场景下建议使用监听方式,因为 callback 方式客户端会对 callback 实例的个数有限制。

RPC 常用框架

一个好的 RPC 框架要具备可靠性和易用性,可靠性方面要保证 I/O,序列化等准确处理,还要考虑网络的不确定性,心跳,网络闪断等因素;j易用性方面要考虑超时与重试机制的控制,同步和异步调用的使用。

一些优秀的开源 RPC 框架:Dubbo(阿里),BRPC(百度),gRPC(Google),Thrift(Facebook),Motan(微博)。

相关资料

  1. 把RPC框架整明白
  2. RPC概念与应用
  3. RPC原理及应用
  4. 一文搞懂RPC核心原理
  5. 详解varint编码原理
  6. varint和zigzag编码
  7. RPC与Http的区别
  8. 动手实现一个分布式RPC框架
  9. 你应该知道的RPC原理
  10. 搞懂RPC核心原理
  11. 分布式架构基础:Java RMI详解
作者

光星

发布于

2022-01-29

更新于

2022-07-12

许可协议

评论