Vert.x 技术内幕 | 异步RPC实现原理

 

 
compile group: 'io.vertx', name: 'vertx-core', version: '3.5.1'
compile group: 'io.vertx', name: 'vertx-web', version: '3.5.1'
compile group: 'io.vertx', name: 'vertx-service-proxy', version: '3.5.1'
compile group: 'io.vertx', name: 'vertx-codegen', version: '3.5.1'
compile group: 'io.vertx', name: 'vertx-web-client', version: '3.5.1'
compile group: 'io.vertx', name: 'vertx-rx-java2', version: '3.5.1'

 

task rxgen(type: JavaCompile, group: 'build') { // codegen
    source = sourceSets.main.java
    classpath = configurations.compile + configurations.compileOnly
    destinationDir = project.file('src/main/rxgen')
    options.compilerArgs = [
            "-proc:only",
            "-processor", "io.vertx.codegen.CodeGenProcessor",
            "-Acodegen.output=${project.projectDir}/src/main"
    ]
}

compileJava {
    targetCompatibility = 1.8
    sourceCompatibility = 1.8

    dependsOn rxgen
}

sourceSets {
    main {
        java {
            srcDirs += 'src/main/rxgen'
        }
    }
}

 

 

Vert.x 技术内幕 | 异步RPC实现原理

 

经常有一些开发者在group中问到,如何利用Vert x进行RPC通信。其实,Vert x提供了一个组件 —— Vert x Service Proxy ,专门用于进

经常有一些开发者在group中问到,如何利用Vert.x进行RPC通信。其实,Vert.x提供了一个组件 —— Vert.x Service Proxy ,专门用于进行异步RPC通信(通过Event Bus)。Vert.x Service Proxy会自动生成代理类进行消息的包装与解码、发送与接收以及超时处理,可以为我们省掉不少代码。之前我在Vert.x Blueprint中已经详细讲解了 Vert.x Service Proxy 的使用,大家可以参考Vert.x Kue 文档 中的相关部分。本篇文章中我们将探索一下通过 Vert.x Service Proxy 生成的代理类进行异步RPC的原理,对应的Vert.x版本为 3.3.2 。

传统的RPC想必大家都不陌生,但是传统的RPC有个缺陷:传统的RPC都是阻塞型的,当调用者远程调用服务时需要阻塞着等待调用结果,这与Vert.x的异步开发模式相违背;而且,传统的RPC未对容错而设计。

因此,Vert.x提供了Service Proxy用于进行异步RPC,其底层依托Clustered Event Bus进行通信。我们只需要按照规范编写我们的服务接口(一般称为Event Bus服务),并加上 @ProxyGen 注解,Vert.x就会自动为我们生成相应的代理类在底层处理RPC。有了Service Proxy,我们只需给异步方法提供一个回调函数 Handler> ,在调用结果发送过来的时候会自动调用绑定的回调函数进行相关的处理,这样就与Vert.x的异步开发模式相符了。由于 AsyncResult 本身就是为容错而设计的(两个状态),因此这里的RPC也具有了容错性。

原理简介

假设有一个Event Bus服务接口:

@ProxyGen
@VertxGen
public interface SomeService {

 String SERVICE_ADDRESS = "service.example";

static SomeService createService(Vertx vertx, JsonObject config) {
return new SomeServiceImpl(vertx, config);
 }

static SomeService createProxy(Vertx vertx) {
return ProxyHelper.createProxy(SomeService.class, vertx, SERVICE_ADDRESS);
 }

@Fluent
SomeService process(String id, Handler> resultHandler);

}

这里定义了一个异步方法 process ,其异步调用返回的结果是 AsyncResult 类型的。由于异步RPC底层通过Clustered Event Bus进行通信,我们需要给器指定一个通信地址 SERVICE_ADDRESS 。 @Fluent 注解代表此方法返回自身,便于进行组合。我们同时还提供了两个辅助方法: createService 方法用于创建服务实例,而 createProxy方法则通过 ProxyHelper 辅助类创建服务代理实例。

假设服务提供端A注册了一个 SomeService 类型的服务代理,服务调用端B需要通过异步RPC调用服务的 process 方法,此时调用端B可以利用 ProxyHelper 获取服务实例并进行服务调用。B中获取的服务其实是一个 服务代理类 ,而真正的服务实例在A处。何为服务代理?服务代理可以帮助我们向服务提供端发送调用请求,并且响应调用结果。那么如何发送调用请求呢?相信大家能想到,是调用端B将调用参数和方法名称等必要信息包装成集群消息( ClusteredMessage ),然后通过 send 方法将请求通过Clustered Event Bus发送至服务提供端A处(需要提供此服务的通信地址)。A在注册服务的时候会创建一个 MessageConsumer 监听此服务的地址来响应调用请求。当接收到调用请求的时候,A会在本地调用方法,并将结果回复至调用端。所以异步RPC本质上其实是一个基于 代理模式 的 Request/Response 消息模式。

用时序图来描述一下上述过程:

Vert.x 技术内幕

引入

以之前的 SomeService 接口为例,我们可以在集群中的一个节点上注册服务实例:

SomeService service = SomeService.createService(vertx, config);
ProxyHelper.registerService(SomeService.class, vertx, service, SomeService.SERVICE_ADDRESS);

然后在另一个节点上获取此服务实例的代理,并进行服务调用。调用的时候看起来就像在本地调用(LPC)一样,其实是进行了RPC通信:

SomeService proxyService = SomeService.createProxy(vertx);

// invoke the service
proxyService.process("fuck", ar -> {
// process the result...
});

其实,这里获取到的 proxyService 实例的真正类型是Vert.x自动生成的服务代理类 SomeServiceVertxEBProxy 类,里面封装了通过Event Bus进行通信的逻辑。我们首先来讲一下Service Proxy生成代理类的命名规范。

代理类命名规范

Vert.x Service Proxy在生成代理类时遵循一定的规范。假设有一Event Bus服务接口 SomeService ,Vert.x会自动为其生成代理类以及代理处理器:

  • 代理类的命名规范为 接口名 + VertxEBProxy 。比如 SomeService接口对应的代理类名称为 SomeServiceVertxEBProxy
  • 代理类会继承原始的服务接口并实现所有方法的代理逻辑
  • 代理处理器的命名规范为 接口名 + VertxProxyHandler 。比如 SomeService 接口对应的代理处理器名称为 SomeServiceVertxProxyHandler
  • 代理处理器会继承 ProxyHandler 抽象类

ProxyHelper 辅助类中注册服务以及创建代理都是遵循了这个规范。

在Event Bus上注册服务

我们通过 ProxyHelper 辅助类中的 registerService 方法来向Event Bus上注册Event Bus服务,来看其具体实现:

public static  MessageConsumer registerService(Class clazz, Vertx vertx, T service, String address,
 boolean topLevel,
 long timeoutSeconds) {
 String handlerClassName = clazz.getName() + "VertxProxyHandler";
 Class handlerClass = loadClass(handlerClassName, clazz);
 Constructor constructor = getConstructor(handlerClass, Vertx.class, clazz, boolean.class, long.class);
 Object instance = createInstance(constructor, vertx, service, topLevel, timeoutSeconds);
 ProxyHandler handler = (ProxyHandler) instance;
return handler.registerHandler(address);
}

首先根据约定生成对应的代理 Handler 的名称,然后通过类加载器加载对应的 Handler 类,再通过反射来创建代理 Handler 的实例,最后调用 handler 的 registerHandler 方法注册服务地址。

registerHandler 方法的实现在Vert.x生成的各个代理处理器中。以之前的 SomeService 为例,我们来看一下其对应的代理处理器 SomeServiceVertxProxyHandler 实现。首先是注册并订阅地址的 registerHandler 方法:

public MessageConsumer registerHandler(String address) {
 MessageConsumer consumer = vertx.eventBus().consumer(address).handler(this);
this.setConsumer(consumer);
return consumer;
}

registerHandler 方法的实现非常简单,就是通过 consumer 方法在 address 地址上绑定了 SomeServiceVertxProxyHandler 自身。那么 SomeServiceVertxProxyHandler 是如何处理来自服务调用端的服务调用请求,并将调用结果返回到请求端呢?在回答这个问题之前,我们先来看看代理端(调用端)是如何发送服务调用请求的,这就要看对应的服务代理类的实现了。

服务调用

我们来看一下服务调用端是如何发出服务调用请求的消息的。之前已经介绍过,服务调用端是通过Event Bus的 send 方法发送调用请求的,并且会提供一个 replyHandler 来等待方法调用的结果。调用的方法名称会存放在消息中名为 action 的header中。以之前 SomeService 的代理类 SomeServiceVertxEBProxy 中 process 方法的请求为例:

public SomeService process(String id, Handler> resultHandler) {
if (closed) {
 resultHandler.handle(Future.failedFuture(new IllegalStateException("Proxy is closed")));
return this;
 }
 JsonObject _json = new JsonObject();
 _json.put("id", id);
 DeliveryOptions _deliveryOptions = (_options != null) ? new DeliveryOptions(_options) : new DeliveryOptions();
 _deliveryOptions.addHeader("action", "process");
 _vertx.eventBus().send(_address, _json, _deliveryOptions, res -> {
if (res.failed()) {
 resultHandler.handle(Future.failedFuture(res.cause()));
 } else {
 resultHandler.handle(Future.succeededFuture(res.result().body()));
 }
 });
return this;
}

可以看到代理类把此方法传入的参数都放到一个 JsonObject 中了,并将要调用的方法名称存放在消息中名为 action 的header中。代理方法通过 send 方法将包装好的消息发送至之前注册的服务地址处,并且绑定 replyHandler 等待调用结果,然后使用我们传入到 process 方法中的 resultHandler 对结果进行处理。是不是很简单呢?

服务提供端的调用逻辑

调用请求发出之后,我们的服务提供端就会收到调用请求消息,然后执行 SomeServiceVertxProxyHandler 中的处理逻辑:

public void handle(Message msg) {
try {
 JsonObject json = msg.body();
 String action = msg.headers().get("action");
if (action == null) {
throw new IllegalStateException("action not specified");
 }
 accessed();
switch (action) {
case "process": {
 service.process((java.lang.String)json.getValue("id"), createHandler(msg));
break;
 }
default: {
throw new IllegalStateException("Invalid action: " + action);
 }
 }
 } catch (Throwable t) {
 msg.reply(new ServiceException(500, t.getMessage()));
throw t;
 }
}

handle 方法首先从消息header中获取方法名称,如果获取不到则调用失败;接着 handle 方法会调用 accessed 方法记录最后调用服务的时间戳,这是为了实现超时的逻辑,后面我们会讲。接着 handle 方法会根据方法名称分派对应的逻辑,在“真正”的服务实例上调用方法。注意异步RPC的过程本质是 Request/Response 模式,因此这里的异步结果处理函数 resultHandler 应该将调用结果发送回调用端。此 resultHandler 是通过 createHandler 方法生成的,逻辑很清晰:

private  Handler> createHandler(Message msg) {
return res -> {
if (res.failed()) {
if (res.cause() instanceof ServiceException) {
 msg.reply(res.cause());
 } else {
 msg.reply(new ServiceException(-1, res.cause().getMessage()));
 }
 } else {
if (res.result() != null && res.result().getClass().isEnum()) {
 msg.reply(((Enum) res.result()).name());
 } else {
 msg.reply(res.result());
 }
 }
 };
}

这样,一旦在服务提供端的调用过程完成时,调用结果就会被发送回调用端。这样调用端就可以调用结果执行真正的处理逻辑了。

超时处理

Vert.x自动生成的代理处理器内都封装了一个简单的超时处理逻辑,它是通过定时器定时检查最后的调用时间实现的。逻辑比较简单,直接放上相关逻辑:

public SomeServiceVertxProxyHandler(Vertx vertx, SomeService service, boolean topLevel, long timeoutSeconds) {
// 前面代码略。。。
if (timeoutSeconds != -1 && !topLevel) {
long period = timeoutSeconds * 1000 / 2;
if (period > 10000) {
 period = 10000;
 }
this.timerID = vertx.setPeriodic(period, this::checkTimedOut);
 } else {
this.timerID = -1;
 }
 accessed();
}

private void checkTimedOut(long id) {
long now = System.nanoTime();
if (now - lastAccessed > timeoutSeconds * 1000000000) {
 close();
 }
}

一旦超时,就自动调用 close 方法终止定时器,注销响应服务调用请求的consumer并关闭代理。

代码是如何生成的?

大家可能会很好奇,这些服务代理类是怎么生成出来的?其实,这都是Vert.x Codegen的功劳。Vert.x Codegen的本质是一个 注解处理器(APT),它可以扫描源码中是否包含要处理的注解,检查规范后根据响应的模板生成对应的代码,这就是注解处理器的作用(注解处理器于JDK 1.6引入)。为了让Codegen正确地生成代码,我们需要配置编译参数来确保注解处理器能够正常的工作,具体的可以参考 Vert.x Codegen的文档 (之前里面缺了Gradle相关的实例,我给补上了)。

Vert.x Codegen使用MVEL2作为生成代码的模板,扩展名为 *.templ ,比如代理类和代理处理器的模板就位于 vert-x3/vertx-service-proxy 中,配置文件类似于这样:

{
 "name": "Proxy",
 "generators": [
 {
 "kind": "proxy",
 "fileName": "ifaceFQCN + 'VertxEBProxy.java'",
 "templateFileName": "serviceproxy/template/proxygen.templ"
 },{
 "kind": "proxy",
 "fileName": "ifaceFQCN + 'VertxProxyHandler.java'",
 "templateFileName": "serviceproxy/template/handlergen.templ"
 }
 ]
}

具体的代码生成逻辑还要涉及APT及MVEL2的知识,这里就不展开讲了,有兴趣的朋友可以研究研究Vert.x Codegen的源码。

优点与缺点

Vert.x提供的这种Async RPC有着许多优点:

  • 通过Clustered Event Bus传输消息,不需引入其它额外的组件
  • 自动生成代理类及代理处理器,可以帮助我们做消息封装、传输、编码解码以及超时处理等问题,省掉不少冗余代码,让我们可以以LPC的方式进行RPC通信
  • 多语言支持(Polyglot support)。这是Vert.x的一大亮点。只要加上 @VertxGen 注解并在编译期依赖中加上对应语言的依赖(如 vertx-lang-ruby ),Vert.x Codegen就会自动处理注解并生成对应语言的服务代理(通过调用Java版本的服务代理实现)。这样Async RPC可以真正地做到不限language

当然Vert.x要求我们的服务接口必须是 基于回调的 ,这样写起来可能会不优雅。还好 @VertxGen 注解支持生成Rx版本的服务类,因此只要加上 vertx-rx-java 依赖,Codegen就能生成对应的Rx风格的服务类(异步方法返回 Observable ),这样我们就能以更reactive的风格来构建应用了,岂不美哉?

当然,为了考虑多语言支持的兼容性,Vert.x在传递消息的时候依然使用了传统的JSON,这样传输效率可能不如Protobuf高,但是不一定成为瓶颈。(看业务情况。真正的瓶颈一般还是在DB上)

总结:这异步调用是基于eventbus,但eventbus官网解释不保证消息的可达性,因此不建议

你可能感兴趣的:(java)