RPC 原理

RPC 基本原理

RPC 是什么

RPC 是远程过程调用(Remote Procedure Call)的缩写形式。

RPC 的概念与技术早在 1981 年由 Nelson 提出。1984 年,BirrellNelson 把其用于支持异构型分布式系统间的通讯BirrellRPC 模型引入存根进程(stub ) 作为远程的本地代理,调用 RPC 运行时库来传输网络中的调用。StubRPC runtime 屏蔽了网络调用所涉及的许多细节,特别是,参数的编码/译码及网络通讯是由 stubRPC runtime 完成的,因此这一模式被各类 RPC 所采用。

什么叫 RPC 呢?

简单来说,就是“像调用本地方法一样调用远程方法”。如果我们的方法在本地,那么我们可以直接使用如下方式调用 UserService

1
2
UserService service = new UserService();
User user = service.findById(1);

那有时候我们的方法实现类不在本地,对于分布式场景来说这种情况是很常见的,例如我现在使用的电影服务,需要调用用户服务的实现类。我们有以下几种办法:

  • 将用户服务的实现完全搬一份过来;
  • 使用 okHttphttpclientrestTemplate 等工具自己远程去调用接口;
  • 使用 RPC 。

第一种方法显然不可取,而通过 okhttp 等工具需要我们自己来编写请求逻辑,且需要服务端提供相应的 API 接口才可。
使用 RPC 则只需要服务端提供实现类即可,不必要求有接口的开放。

RPC 的使用伪代码大致如下:(Rpcfx 代指我们的 RPC 框架)

1
2
UserService service = Rpcfx.create(UserService.class, url);
User user = service.findById(1);

那么,RPC 是怎么实现将本地调用的方法转为远程调用的呢?

RPC原理

RPC 的简化版原理如下图:(核心是代理机制)

RBrVB9.png

具体的调用流程为:

  • 本地代理存根: Stub
  • 本地序列化反序列化;
  • 网络通信;
  • 远程序列化反序列化;
  • 远程服务存根: Skeleton
  • 调用实际业务服务;
  • 原路返回服务结果;
  • 返回给本地调用方。

简单的说,就是当本地发起调用后,RPC 使用动态代理将调用拦截下来,将要调用哪个类、哪个方法、以及方法参数进行序列化。然后通过 HTTPTCP 协议发送到服务方,服务方通过服务查找 ,取出具体要执行方法的实例。或者通过反射的方式来构建实例。最后将调用结果返回给客户端。

当然, RPC 框架需要特别注意异常处理、重试等机制。因为无论调用结果成功或失败,都需要将信息给到客户端来进行处理

RPC 是如何设计的

共享

客户端与服务端需要共享什么东西?

由于 RPC 是基于接口的远程调用,客户端和服务端需要共享接口和实体的定义。注意,这里强调了是定义,而不是直接说的共享接口和实体。

对于 java 平台而言我们可以直接共享接口和定义。但是对于跨语言间的调用,系统之间就只能使用双方都能理解的接口和实体的描述文件来进行统一。比如 WebService 就通过 WSDL 来进行描述,各语言都能从中进行解析取到共享的内容。

序列化

选择序列化方式也是 RPC 非常重要的一个决策点,我们通常会要求序列化框架用于以下特点:

  • 序列化后的数据最好是易于人类阅读的;
  • 实现的复杂度是否足够低;
  • 序列化和反序列化的速度越快越好;
  • 序列化后的信息密度越大越好,也就是说,同样的一个结构化数据,序列化之后占用的存储空间越小越好;

但事实上,没有任何一种序列化能同时满足这些要求,基于二进制的序列化实现结果小、传输快但是不易懂。基于文本的 jsonxml 序列化很方便易懂,但是占用的内存势必更大。

不同的 RPC 框架在设计时都会偏向一些特定领域来进行实现,trade off 理论确实是万用的。

通信协议

基于 TCP 协议的话效率高,性能相对来说会更好些,但 TCP 也有自己的缺点,当遇到一些额外需求时,我们无法自己定义对象头,针对性的对某些调用进行处理,而 HTTP 则支持这样的操作,扩展性会更强一些。

服务注册与发现

在发送请求前,客户端需要知道服务提供方的地址,而这个地址通常使用注册中心的方式来进行管理。

注册中心需要负责服务的上线,通知客户端变动情况,下线处理等操作

常见的 RPC 框架有哪些

  • 非常经典的 WebService,基于 HTTP 协议 和 XML 序列化,跨平台的 RPC,非常完善,缺点是较为复杂,性能一般。(XML 描述比较冗余,解析比较消耗资源,HTTP 的效率较 TCP 也相对更低);
  • dubbo,在 RPC 基础上增加了服务治理,功能非常强大,目前还衍生出了 dubbo-go
  • FeignSpringCloud 体系基于 RestTemplate 实现的 RPC,只支持 java
  • HessianHessian 自定义了一套二进制序列化化协议,在此基础上实现了 RPC,属于轻量级,跨平台类型;
  • gRPC , golang 开发的新一代 RPC
  • Thrift

一个简单的 RPC 实现

这里通过一个基于 HTTP + JSON 序列化 + Java 动态代理 + Spring Bean 查找(指的是,服务启动时将实现类注册为 Bean ,接收到请求时通过 getBean 获取实现类)

此实现没有路由、重试、负载均衡、集群相关的实现,仅用于加深理解,框架的整体调用流程图如下:

WpJHD1.png

定义消费者的请求体

1
2
3
4
5
6
7
@Getter
@Setter
public class RpcfxRequest {
private String serviceClass;//接口的全限定名
private String method;// 方法名
private Object[] params;// 方法参数
}

定义服务提供者返回的响应体

1
2
3
4
5
6
7
@Getter
@Setter
public class RpcfxResponse {
private Object result; //请求结果
private boolean status;//状态码
private Exception exception;//异常信息
}

定义客户端创建代理类的入口 Rpcfx.create

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
49
50
51
52
public final class Rpcfx {

static {
ParserConfig.getGlobalInstance().addAccept("io.demo");
}

public static <T> T create(final Class<T> serviceClass, final String url) {

return (T) Proxy.newProxyInstance(Rpcfx.class.getClassLoader(), new Class[]{serviceClass}, new RpcfxInvocationHandler(serviceClass, url));

}

public static class RpcfxInvocationHandler implements InvocationHandler {

public static final MediaType JSONTYPE = MediaType.get("application/json; charset=utf-8");

private final Class<?> serviceClass;
private final String url;

public <T> RpcfxInvocationHandler(Class<T> serviceClass, String url) {
this.serviceClass = serviceClass;
this.url = url;
}

@Override
public Object invoke(Object proxy, Method method, Object[] params) throws Throwable {

RpcfxRequest request = new RpcfxRequest();
request.setServiceClass(this.serviceClass.getName());
request.setMethod(method.getName());
request.setParams(params);
// 发送请求
RpcfxResponse response = post(request, url);
// 反序列化结果
return JSON.parse(response.getResult().toString());
}

private RpcfxResponse post(RpcfxRequest req, String url) throws IOException {
String reqJson = JSON.toJSONString(req);
System.out.println("req json: "+reqJson);

OkHttpClient client = new OkHttpClient();
final Request request = new Request.Builder()
.url(url)
.post(RequestBody.create(JSONTYPE, reqJson))
.build();
String respJson = client.newCall(request).execute().body().string();
System.out.println("resp json: "+respJson);
return JSON.parseObject(respJson, RpcfxResponse.class);
}
}
}

定义服务端查找实现类及调用的入口:

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
public class RpcfxInvoker {

private RpcfxResolver resolver;

public RpcfxInvoker(RpcfxResolver resolver){
this.resolver = resolver;
}

public RpcfxResponse invoke(RpcfxRequest request) {
RpcfxResponse response = new RpcfxResponse();
String serviceClass = request.getServiceClass();
// applicationContext.getBean(serviceClass);
Object service = resolver.resolve(serviceClass);

try {
// 反射调用
Method method = resolveMethodFromClass(service.getClass(), request.getMethod());
Object result = method.invoke(service, request.getParams());

response.setResult(JSON.toJSONString(result, SerializerFeature.WriteClassName));
response.setStatus(true);
return response;
} catch ( IllegalAccessException | InvocationTargetException e) {
// 异常
e.printStackTrace();
response.setException(e);
response.setStatus(false);
return response;
}
}

private Method resolveMethodFromClass(Class<?> klass, String methodName) {
return Arrays.stream(klass.getMethods()).filter(m -> methodName.equals(m.getName())).findFirst().get();
}

}

服务端的实现类

1
2
3
4
5
6
7
public class UserServiceImpl implements UserService {

@Override
public User findById(int id) {
return new User(id, "test" + System.currentTimeMillis());
}
}

服务端设置:

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.RPCHTTP 不能进行比较,两者完全是不同的东西,我们只能说 RPC 可以使用 HTTP 来作为调用双方的通信协议;
  • 3.Rest 接口算不算 RPC ? 答:如果在 client 端给远程的 rest 封装好了 service 之类的调用方法就算(比如说 feign),否则就不算。
0%