RPC 基本原理
RPC 是什么
RPC
是远程过程调用(Remote Procedure Call
)的缩写形式。
RPC
的概念与技术早在 1981
年由 Nelson
提出。1984
年,Birrell
和 Nelson
把其用于支持异构型分布式系统间的通讯。Birrell
的 RPC
模型引入存根进程(stub
) 作为远程的本地代理,调用 RPC
运行时库来传输网络中的调用。Stub
和 RPC runtime
屏蔽了网络调用所涉及的许多细节,特别是,参数的编码/译码及网络通讯是由 stub
和 RPC runtime
完成的,因此这一模式被各类 RPC
所采用。
什么叫 RPC 呢?
简单来说,就是“像调用本地方法一样调用远程方法”。如果我们的方法在本地,那么我们可以直接使用如下方式调用 UserService
:
1 | UserService service = new UserService(); |
那有时候我们的方法实现类不在本地,对于分布式场景来说这种情况是很常见的,例如我现在使用的电影服务,需要调用用户服务的实现类。我们有以下几种办法:
- 将用户服务的实现完全搬一份过来;
- 使用
okHttp
、httpclient
、restTemplate
等工具自己远程去调用接口; - 使用 RPC 。
第一种方法显然不可取,而通过 okhttp
等工具需要我们自己来编写请求逻辑,且需要服务端提供相应的 API
接口才可。
使用 RPC
则只需要服务端提供实现类即可,不必要求有接口的开放。
RPC
的使用伪代码大致如下:(Rpcfx
代指我们的 RPC
框架)1
2UserService service = Rpcfx.create(UserService.class, url);
User user = service.findById(1);
那么,RPC
是怎么实现将本地调用的方法转为远程调用的呢?
RPC原理
RPC
的简化版原理如下图:(核心是代理机制)
具体的调用流程为:
- 本地代理存根:
Stub
; - 本地序列化反序列化;
- 网络通信;
- 远程序列化反序列化;
- 远程服务存根:
Skeleton
; - 调用实际业务服务;
- 原路返回服务结果;
- 返回给本地调用方。
简单的说,就是当本地发起调用后,RPC
使用动态代理将调用拦截下来,将要调用哪个类、哪个方法、以及方法参数进行序列化。然后通过 HTTP
或 TCP
协议发送到服务方,服务方通过服务查找 ,取出具体要执行方法的实例。或者通过反射的方式来构建实例。最后将调用结果返回给客户端。
当然, RPC
框架需要特别注意异常处理、重试等机制。因为无论调用结果成功或失败,都需要将信息给到客户端来进行处理
RPC 是如何设计的
共享
客户端与服务端需要共享什么东西?
由于 RPC
是基于接口的远程调用,客户端和服务端需要共享接口和实体的定义。注意,这里强调了是定义,而不是直接说的共享接口和实体。
对于 java
平台而言我们可以直接共享接口和定义。但是对于跨语言间的调用,系统之间就只能使用双方都能理解的接口和实体的描述文件来进行统一。比如 WebService
就通过 WSDL
来进行描述,各语言都能从中进行解析取到共享的内容。
序列化
选择序列化方式也是 RPC
非常重要的一个决策点,我们通常会要求序列化框架用于以下特点:
- 序列化后的数据最好是易于人类阅读的;
- 实现的复杂度是否足够低;
- 序列化和反序列化的速度越快越好;
- 序列化后的信息密度越大越好,也就是说,同样的一个结构化数据,序列化之后占用的存储空间越小越好;
但事实上,没有任何一种序列化能同时满足这些要求,基于二进制的序列化实现结果小、传输快但是不易懂。基于文本的 json
和 xml
序列化很方便易懂,但是占用的内存势必更大。
不同的 RPC
框架在设计时都会偏向一些特定领域来进行实现,trade off
理论确实是万用的。
通信协议
基于 TCP
协议的话效率高,性能相对来说会更好些,但 TCP
也有自己的缺点,当遇到一些额外需求时,我们无法自己定义对象头,针对性的对某些调用进行处理,而 HTTP
则支持这样的操作,扩展性会更强一些。
服务注册与发现
在发送请求前,客户端需要知道服务提供方的地址,而这个地址通常使用注册中心的方式来进行管理。
注册中心需要负责服务的上线,通知客户端变动情况,下线处理等操作
常见的 RPC 框架有哪些
- 非常经典的
WebService
,基于HTTP
协议 和XML
序列化,跨平台的RPC
,非常完善,缺点是较为复杂,性能一般。(XML
描述比较冗余,解析比较消耗资源,HTTP
的效率较TCP
也相对更低); dubbo
,在RPC
基础上增加了服务治理,功能非常强大,目前还衍生出了dubbo-go
;Feign
,SpringCloud
体系基于RestTemplate
实现的RPC
,只支持java
;Hessian
,Hessian
自定义了一套二进制序列化化协议,在此基础上实现了RPC
,属于轻量级,跨平台类型;gRPC
,golang
开发的新一代RPC
;Thrift
。
一个简单的 RPC 实现
这里通过一个基于 HTTP
+ JSON
序列化 + Java
动态代理 + Spring Bean
查找(指的是,服务启动时将实现类注册为 Bean
,接收到请求时通过 getBean
获取实现类)
此实现没有路由、重试、负载均衡、集群相关的实现,仅用于加深理解,框架的整体调用流程图如下:
定义消费者的请求体
1 | @Getter |
定义服务提供者返回的响应体1
2
3
4
5
6
7@Getter
@Setter
public class RpcfxResponse {
private Object result; //请求结果
private boolean status;//状态码
private Exception exception;//异常信息
}
定义客户端创建代理类的入口 Rpcfx.create
1 | public final class Rpcfx { |
定义服务端查找实现类及调用的入口:
1 | public class RpcfxInvoker { |
服务端的实现类
1 | public class UserServiceImpl implements UserService { |
服务端设置: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
38
39
40
41
42
43
44
45
46
47
48@SpringBootApplication
@RestController
public class RpcfxServerApplication {
@Autowired
RpcfxInvoker invoker;
@Bean
public RpcfxInvoker createInvoker(@Autowired RpcfxResolver resolver){
return new RpcfxInvoker(resolver);
}
@Bean
public RpcfxResolver createResolver(){
return new DemoResolver();
}
public static void main(String[] args) throws Exception {
SpringApplication.run(RpcfxServerApplication.class, args);
}
@PostMapping("/")
public RpcfxResponse invoke(@RequestBody RpcfxRequest request) {
return invoker.invoke(request);
}
@Bean(name = "io.rpcfx.demo.api.UserService")
public UserService createUserService(){
return new UserServiceImpl();
}
class DemoResolver implements RpcfxResolver, ApplicationContextAware {
private ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
@Override
public Object resolve(String serviceClass) {
return this.applicationContext.getBean(serviceClass);
}
}
}
客户端调用方式:1
2
3
4
5
6
7
8
9
10@SpringBootApplication
public class RpcfxClientApplication {
public static void main(String[] args) {
//通过create创建代理
UserService userService = Rpcfx.create(UserService.class, "http://localhost:8080/");
User user = userService.findById(1);
System.out.println("find user id=1 from server: " + user.getName());
}
}
详细代码链接: https://gitee.com/zx-hoppo/rpcfx
RPC与服务化
在具体的分布式业务场景里,除了能够调用远程方法,我们还需要考虑以下内容
- 1、多个相同服务如何管理?
- 2、服务的注册发现机制?
- 3、如何负载均衡,路由等集群功能?
- 4、熔断,限流等治理能力。
- 5、重试等策略
- 6、高可用、监控、性能等等。
而这些点都与分布式服务化息息相关。虽然 RPC
的定义非常简单,仅代指隐藏调用过程而已,但现阶段的 RPC
框架早已不局限于此了。
在实际应用中,简单的系统间调用一般都不会使用 RPC
,因为无论是系统间接口和实体定义信息的共享还是维护注册信息等等,都会涉及到代码修改,相对来说更加的重量级一些。 有时候直接通过 httpclient
或者 restTemplate
这样的工具类发送请求会简单很多。
tips:
- 1.
RPC
和直接使用接口调用的应用场景不同,RPC
更适用于大型的分布式治理场景,而http
接口调用对于简单的系统交互来说,更加方便快捷; - 2.
RPC
和HTTP
不能进行比较,两者完全是不同的东西,我们只能说RPC
可以使用HTTP
来作为调用双方的通信协议; - 3.
Rest
接口算不算RPC
? 答:如果在client
端给远程的rest
封装好了service
之类的调用方法就算(比如说feign
),否则就不算。