Verticle实例

  故事发生到这里,相信大部分读者都已经了解了最基础的io.vertx.core.Vertx实例,接下来就让我们看看io.vertx.core.Verticle实例。最初的章节已经提到过Vert.x中的两个核心概念:VerticleEvent Bus,虽然之前我们也讲过和Verticle相关的东西,但是本章我们重新来认识它。

1. 关于翻译

  有关Verticle的话题我们从翻译说起,为什么呢?因为Vertical的含义是“垂直”,所以有些地方的机器翻译也将Verticle翻译成了“垂直”,是的,我们可以用垂直来理解它,那么这样就要从Vert.x中的Verticle的基本结构说起。网上大部分介绍Vert.x的文章使用的截图是这样的:

  在这些文章中,您会发现所有的Verticle都是竖着的,而且是一个倒置的矩形,姑且我们把这种结构当做Verticle被翻译成“垂直”的初衷;前文已经提到过,Verticle是Vert.x中的核心概念,更多的时候一个Verticle实例描述的是一个“线程”,目前您可以把它当做一个“线程”,等到本章完结后,您的脑子里应该就有一个更加深刻的印象了,那么这也是本书的初衷,抽丝剥茧地给读者介绍每一个基本的概念,并且提供可理解这些概念的“隐喻”。

1.1. Verticle的碎碎

  学习Verticle的基础是理解Actor模型,官方是这么定义的:

Vert.x comes with a simple, scalable, *actor-like *deployment and concurrency model out of the box that you can use to save you writing your own.

  Actor模型详细内容可参考后记《Actor模型1》,这种模型在Vert.x的编程中是可选的,我们并不严格要求您在编写Vert.x的应用时使用Actor模型,若您要使用Actor模型,那么您将编写的就是一个Verticle的集合。Vert.x中的Verticle实际上就充当了Actor模型中的Actor(有时候又称为Worker线程),这些Verticle是可以在Vert.x中通过Event Bus相互通信的,读者可以这样理解:Event Bus实际上提供了Vert.x中的Verticle之间相互通信的一种介质。它们之间的关系可以这样描述:

Actor       <--   通信         -->  Actor:(异步通信)
Verticle    <--   Event Bus   -->  Verticle:(事件驱动模型、异步通信)

  Verticle就是Actor模型中的Actor,而Event Bus就是Actor模型中两个Actor之间通信的一种实现机制。

1.2. Verticle的分类

  前文一直提到了Verticle的分类2,但我们都没有梳理过,这里先谈谈它的分类。前边章节我们已经介绍了Actor模型,那么这个章节我们需要对Actor给个定义:Vert.x中的Actor(Verticle组件)是有种类的。根据官方教程的描述,Verticle主要分为三种:

  • Standard:标准类型,我称之为“哨兵”,在Vert.x中,它运行的线程池称为Event Loop,它有一个典型特征就是负责“监听”客户端产生的事件,在该机制中,客户端所有的请求都可抽象成事件(Event)。您可以在编程过程中,将您的代码放心大胆地放到Verticle中执行,因为Vert.x中的Verticle是线程安全的,它的每一个处理事件的处理器(Handler)独占单个线程,您可以把您编写的代码当成单线程代码来处理,甚至于不需要去考虑线程同步的问题。
  • Worker:工作线程,我称之为“士兵”,在Vert.x中,这种类型的Verticle运行的线程池称为Worker Pool,它不会直接处理客户产生的事件,只会接收Event Bus中发生的事件并去处理,在该机制中,Event Bus中所有的消息都可抽象成事件(Event)。——有时候这样的Verticle可以用于处理后台任务,而官方的推荐是工作线程本身是为了后台一些阻塞式代码(访问文件系统、数据库、网络)设计的,所以对于这种任务场景,尽可能使用Worker类型的Verticle。
  • Multi-Threaded Worker:另一种工作线程,我称之为“神兵”,在Vert.x中,这种类型的Verticle依然是一种Worker,唯一不同的是它可以被多个线程同时执行,所以可以当做“升级的士兵”来对待,为此我们称为“神兵”。

  上述三种Verticle就是您目前手中所有的筹码,对的,您没听错,我们没有十八般武器一样的各种兵种,只有三种,在继续分析之前,我们先看看官方的一段警告(写给“神兵”的):

Multi-threaded worker verticles are an advanced feature and most applications will have no need for them. Because of the concurrency in these verticles you have to be very careful to keep the verticle in a consistent state using standard Java techniques for multi-threaded programming.

  在Vert.x中,不论哪一种Verticle实际上都扮演了Actor模型中的Actor角色,每一个Verticle实例就是一个Actor线程,一般在应用程序中,通常使用的是Standard/Worker两种,就像官方教程所说,一般只有在特殊场景中会使用到Multi-Threaded Worker这种类型的Verticle组件——唯有局势危机,才会“神兵”天降

  对Standard和Worker的定位,分享几个心得:

  • 最基础的用法:Standard的Verticle组件用于接收请求,Worker的Verticle组件用于执行阻塞任务(访问数据库、文件系统、网络),二者使用Event Bus通信。
  • 如果是少量的阻塞任务,可以考虑使用内联的方式(vertx中的executeBlocking方法)替代Worker。
  • 而前端拦截器和前端过滤器有几种做法:
    • 使用API Web Contract做基础数据验证和数据合约验证。
    • 使用Vertx Web做前端验证和前端过滤,设置Router中的order绑定优先级高的Handler。
    • 参考Zero中支持JSR340的做法,自己开发基于Vert.x的过滤器/拦截器。
  • Worker中的方法一般是做阻塞任务,在发布过程,它和Standard类型的Verticle可以不对等(instances参数不相同)。
  • 在Event Bus中传输数据时,必要的时候自己开发自定义的Codec来实现数据传输。
  • 在Standard组件发布时需要执行阻塞任务(如访问etcd配置中心、访问h2元数据服务器),最好将代码放到executeBlocking中,防止Event Loop阻塞。

  当然以上只是在书写Standard和Worker类型的Verticle组件时的一些小心得,后续的很多章节会针对这些心得阐述并告诉读者为什么,这些心得不是法典,只是一些曾经踩过的坑,您也可以不按照这些推荐心得去开发。

2. 一个 or 一堆?

  Vert.x提供了一个事件驱动、纯异步编程框架,在这种框架中,我们所做的每一件事都是面向“线程”,从实际效果看来是面向一个Verticle组件,这里先回顾一下前文的主代码:

package io.vertx.up._01.lanucher;

public class MainLauncher {

    public static void main(final String[] args) {
        // 哪种模式?
        final boolean isClustered = false;
        final Launcher launcher = isClustered ? new ClusterLauncher() :
                new SingleLauncher();
        // 设置Options
        launcher.start(vertx -> {
            // 执行Vertx相关后续逻辑
            // TODO: 主逻辑
        });
    }
}

  这里只讨论主代码是因为支线剧本我们已经设计好了,如果不理解上述主代码做了什么可参考:1.3.Vertx实例。接下来需要定义一个Verticle组件:

package io.vertx.up._01.verticles;

import io.vertx.core.AbstractVerticle;

public class MyFirstVerticle extends AbstractVerticle {

    @Override
    public void start() {
        System.out.println(Thread.currentThread().getName() +
                ": Hello Verticle : " +
                Thread.currentThread().getId());
    }
}

  上述代码是我们写的第一个Verticle组件,Vert.x中书写Verticle有下边几个步骤:

  1. io.vertx.core.AbstractVerticle继承。
  2. 重写它的public void start()方法放核心代码。
  3. 选择重写它的public void stop()方法。
  4. 选择重写异步模式start/stop两个方法。

  上边第四个步骤可能会让读者有些误解,为什么要刻意指出异步模式重写两个同名方法?这里先看看AbstractVerticlestart/stop的方法签名,关于start/stop的细节我们将在生命周期章节来解析:

    // 同步模式的 start/stop 方法
    public void start() throws Exception {
        // TODO: 大多数情况下只会重写这个方法作为主要启动逻辑
    }
    public void stop() throws Exception {
    }
    // 异步模式的 start/stop 方法
    @Override
    public void start(Future<Void> startFuture) throws Exception {
        start();
        startFuture.complete();
    }
    @Override
    public void stop(Future<Void> stopFuture) throws Exception {
        stop();
        stopFuture.complete();
    }

  其实上述Verticle的代码会衍生一个问题:我们写的Verticle究竟是一个线程还是一个组件?这也就是本章说的“一个 or 一堆”的争议,结合前文提到的特殊参数:workerinstances,我更倾向于理解成我们写了“一堆”,为什么?您可以将您的主代码稍作修改:

        // 设置Options
        launcher.start(vertx -> {
            // 执行Vertx相关后续逻辑
            vertx.deployVerticle("io.vertx.up._01.verticles.MyFirstVerticle",
                    new DeploymentOptions().setInstances(5));
        });

  那么您将看到下边的输出:

vert.x-eventloop-thread-0: Hello Verticle : 14
vert.x-eventloop-thread-1: Hello Verticle : 15
vert.x-eventloop-thread-4: Hello Verticle : 18
vert.x-eventloop-thread-3: Hello Verticle : 17
vert.x-eventloop-thread-2: Hello Verticle : 16

  也就是说实际上Vertx实例发布了MyFirstVerticle这一类的5个Verticle实例,这就是为什么我倾向于理解成“一堆”的原因,实际上我们使用Java语言定义的Verticle表示:相同类型的某一类Verticle组件;Vertx实例在发布这一类Verticle组件时,它可以通过instances参数来指定这一类的Verticle组件要发布多少个,而这里发布的一个Verticle组件才会对应到“线程”。还有一个小细节相信读者可以看到:上边的例子中的Verticle组件使用的线程名称前缀为:vert.x-eventloop-thread,也就是说:

  • 这些Verticle实例都是运行在Event Loop线程池中的。
  • 这些线程名称的后缀和线程的ID没有必然联系,vert.x-eventloop-thread-0的ID是14
  • 总共发布了5个Verticle实例,每个Verticle实例独占一个线程;

  除了在DeploymentOptions中设置Verticle实例数量,还有没有其他办法呢?如果您把主代码修改成:

        // 设置Options
        launcher.start(vertx -> {
            // 执行Vertx相关后续逻辑
            vertx.deployVerticle(new MyFirstVerticle(),
                    new DeploymentOptions().setInstances(5));
        });

  那么您将收到如下错误信息:

Exception in thread "main" java.lang.IllegalArgumentException: \
    Can't specify > 1 instances for already created verticle

  是的,在Vert.x中,如果您已经创建好了Verticle后,就不可再对它的数量进行调整(instances)了,那么这个方法是不是就鸡肋了?当然不是,让我们一一解答这个问题!

3. deployVerticle

  这个章节,我们就是“创始者”——因为我们要来看Verticle组件的诞生,在主代码中,我们已经拿到了Vertx实例的引用,那么接下来的步骤就是发布Verticle实例,所以接下来我们深入解析Vertx中的发布系列的API。

  // 实例方式
  @GenIgnore
  void deployVerticle(Verticle verticle);
  @GenIgnore
  void deployVerticle(Verticle verticle, 
                      Handler<AsyncResult<String>> completionHandler);
  @GenIgnore
  void deployVerticle(Verticle verticle, DeploymentOptions options);
  @GenIgnore
  void deployVerticle(Verticle verticle, DeploymentOptions options, 
                      Handler<AsyncResult<String>> completionHandler);

  // 类反射方式
  @GenIgnore
  void deployVerticle(Class<? extends Verticle> verticleClass, DeploymentOptions options);
  @GenIgnore
  void deployVerticle(Class<? extends Verticle> verticleClass, DeploymentOptions options, 
                      Handler<AsyncResult<String>> completionHandler);

  // 函数方式
  @GenIgnore
  void deployVerticle(Supplier<Verticle> verticleSupplier, DeploymentOptions options);
  @GenIgnore
  void deployVerticle(Supplier<Verticle> verticleSupplier, DeploymentOptions options, 
                      Handler<AsyncResult<String>> completionHandler);

  // 类全名方式
  void deployVerticle(String name);
  void deployVerticle(String name, 
                      Handler<AsyncResult<String>> completionHandler);
  void deployVerticle(String name, DeploymentOptions options);
  void deployVerticle(String name, DeploymentOptions options, 
                      Handler<AsyncResult<String>> completionHandler);

  deployVerticle在Vert.x中有十二个重载方法,按照参数表和传入标识(这里的传入标识就是上边的注释部分)可对应下边的表格统计:

Verticle实例(对象) 类反射 函数 类名反射
同步:直接发布 支持 X X 支持
同步:Option发布 支持 支持 支持 支持
异步:直接发布 支持 X X 支持
异步:Option发布 支持 支持 支持 支持

  这样您就可以记住Vertx实例对应的deployVerticle方法的作用了,从参数签名上看,包含了下边参数的都属于异步

Handler<AsyncResult<String>> completionHandler

  这个时候返回的值(java.lang.String类型)是Verticle实例发布后的标识,称为DeploymentID,它是一个UUID的字符串。回顾一下前文,和Future相关的代码都是异步代码,为什么?实际上,从源代码中看Future的定义就清楚了:

public interface Future<T> extends AsyncResult<T>, Handler<AsyncResult<T>> {...}

「思」那么为什么在创建好的Verticle组件中无法设置instances参数呢?   每一个创建的Verticle实例和线程是一一绑定的关系,它和instances的语义是拥有“因果”关系的,这是一个“先生鸡还是先生蛋”的问题。是因为Vertx先设置了将要发布的Verticle实例的数量,然后再创建了Verticle实例,并将该实例和Event Loop线程池中的线程绑定,而不是在创建之后才设置这个参数;也就是说一旦Verticle组件创建过后,instances的设置会变得没有逻辑,这也解释上边的代码会什么会抛出异常:Can't specify > 1 instances for already created verticle

3.1. 源码流程

  在接触Verticle时,和发布相关的配置类是:io.vertx.core.DeploymentOptions,这个类用来描述被发布的Verticle的一些配置项,如果使用的是命令式启动,那么这个实例会根据传入的配置数据自动构造,反之若是编程方式,那么就会遇到Vert.x中对于DeploymentOptions的一些限制,接下来让读者知道使用编程方式如何去发布Verticle组件(也许等到您习惯了用Vert.x做相对比较复杂和庞大的系统时,您会爱上编程的方式去启动Vertx实例),对Verticle的发布流程有更深入的认识。

  最初我打算用例子来阐述该问题,后来发现最简单的方式应该是直接使用代码流程图,经过源代码的分析后再引入例子,那么读者更容易透过本质去知道发布过程的所有细节。由于图的详细内容过于复杂,所以使用符号来标记每个节点,先参考下边的阅读规则:

  • V:代表参数为Verticle实例类型。
  • C:代表参数为Class<? extends Verticle>类型。
  • F:代表参数为Supplier<Verticle>类型。
  • S:代表参数为String类型。
  • O:代表参数DeploymentOptions。
  • A:代表参数Handler<AsyncResult<String>>。

  Vert.x中的整个deployVerticle的代码流程图如下(图中省略方法名deployVerticle):

3.2. 限制点

  从上图可以知道,本文提到的限制点就是DeploymentOptions的异常会引起Error的地方,即上图中的Error-XXX部分,先看看上边标记的每个Error代表的含义:

Error代号 异常信息 含义
001 Can't specify > 1 instances for already created verticle 对于已经创建好的Verticle实例,不可再变更它的实例数量,instances参数不能再改变。
002 Can't specify < 1 instances for deploy Verticle在发布时,它的instances参数必须大于1。
003 If multi-threaded then must be worker too 如果要启用Multi-Threaded类型的Verticle,那么这个Verticle必须是一个Worker类型。
004 Can't specify extraClasspath for already created verticle 对于已经创建好的Verticle实例,不可再变更它的extraClasspath参数。
005 Can't specify isolationGroup for already created verticle 对于已经创建好的Verticle实例,不可再变更它的isolationGroup参数。
006 Can't specify isolatedClasses for already created verticle 对于已经创建好的Verticle实例,不可再变更它的isolatedClasses参数。

3.3. 发布流程总结

  根据3.1和3.2的阐述,最后通过对整个发布流程的解读来总结一下Verticle实例的发布过程。

  • 从发布流程的大分类上可以知道,主要分为两种方式:函数方式(F,O,A)和类名称方式(S,O,A),并且从源代码中可以知道只有类名称方式会调用HAManager中的方法开启HA高可用功能(这一点虽然没有明确验证过,不过从源代码流程上可以看到确实如此,所以如果要开启高可用功能,推荐使用类名方式发布Verticle实例)。
  • 所有带有Error-前缀的流程中,都会对DeploymentOptions的配置项进行验证,我将这种验证称为发布流程的限制点,参考3.2的表格可以知道发布流程中的限制点在哪儿(所有限制点都会抛出异常IllegalArgumentException)。
  • 最终执行发布流程的类是io.vertx.core.impl.DeploymentManager,该类有两个核心方法:doDeployVerticledoDeploy,前者称为:协调者方法,基本上这个方法不会做一些实质性的发布动作,主要是将所有传入的参数进行组织和整理;而后者称为:执行者方法,这个方法才是发布的核心主代码。
  • 图中隐藏了关于VerticleFactory的获取流程,该流程位于doDeploy方法内部,使用工厂模式发布Verticle组件的详细流程在后续的实战过程中再逐一解读。

4. 本章小结

  本章主要深入Verticle组件去理解Vert.x中的Verticle实例的本质,主要包含了以下核心知识点:

  • Verticle的基础知识(概念、分类、作用)
  • Actor模型的基础知识
  • Vertx实例中的deployVerticle发布专用API详情
  • Verticle的发布流程细节解读

  当Vertx实例调用了deployVerticle方法后,Verticle将在发布流程中执行发布过程,Verticle实例有两个核心生命周期start/stop,这生命周期中的start方法将在deploy过程中执行,接下来的章节我们将去继续探索Verticle实例,从另外一个角度来理解Verticle组件。

1. 「Actor模型」https://www.origin-x.cn/zero-up/5-vertx-land/zbr-004-actormo-xing.html
2. https://vertx.io/docs/vertx-core/java/#_verticle_types

results matching ""

    No results matching ""