1.4.Verticle实例
最后更新于
最后更新于
故事发生到这里,相信大部分读者都已经了解了最基础的io.vertx.core.Vertx
实例,接下来就让我们看看io.vertx.core.Verticle
实例。最初的章节已经提到过Vert.x中的两个核心概念:Verticle
和Event Bus
,虽然之前我们也讲过和Verticle相关的东西,但是本章我们重新来认识它。
有关Verticle的话题我们从翻译说起,为什么呢?因为Vertical的含义是“垂直”,所以有些地方的机器翻译也将Verticle翻译成了“垂直”,是的,我们可以用垂直来理解它,那么这样就要从Vert.x中的Verticle的基本结构说起。网上大部分介绍Vert.x的文章使用的截图是这样的:
在这些文章中,您会发现所有的Verticle都是竖着的,而且是一个倒置的矩形,姑且我们把这种结构当做Verticle被翻译成“垂直”的初衷;前文已经提到过,Verticle是Vert.x中的核心概念,更多的时候一个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.
这种模型在Vert.x的编程中是可选的,我们并不严格要求您在编写Vert.x的应用时使用Actor模型,若您要使用Actor模型,那么您将编写的就是一个Verticle的集合。Vert.x中的Verticle实际上就充当了Actor模型中的Actor(有时候又称为Worker线程),这些Verticle是可以在Vert.x中通过Event Bus相互通信的,读者可以这样理解:Event Bus实际上提供了Vert.x中的Verticle之间相互通信的一种介质。它们之间的关系可以这样描述:
Verticle就是Actor模型中的Actor,而Event Bus就是Actor模型中两个Actor之间通信的一种实现机制。
##「Actor模型」
提到Actor模型就不得不提CSP模型(Communicating Sequential Processes)模型,本小节主要介绍Actor模型,同时对比一下Actor模型和CSP模型的一些小区别,希望读者从概念上先理解Actor模型,关于Actor模型的更底层的理论,读者可以参考《并发之痛Thread、Gorouting、Actor^1》,这里不再累赘。
在Actor并发模型中,它的主角是Actor,实际上它类似于一种Worker线程,它们之间通过消息相互通信(在Vert.x中,它们之间的消息介质就是Event Bus),这些消息的发送是异步的,可并行。
Actor模型可用上述图来描述,且满足一定的原理:
所有Actor的状态由Actor自身维护,且外部不可访问它,除非它自己暴露了访问自身状态的接口;
Actor和Actor之间的通信使用的是消息;
一个Actor可发送消息给另外一个Actor,也可以响应另外一个Actor发过来的消息,同时它可以改变自身的内部状态;
Actor可能阻塞自己(就是内部调用阻塞IO的代码,但Actor不应该阻塞它运行的线程)。
Actor模型的代表:Akka/Erlang、以及我们将要学习的Vert.x都是使用的Actor模型。
另外一种流行的模型是CSP模型,也就是Channel模型,在这种模型中,在Worker和Worker之间引入了一种新的机制,称为Channel,这种模型的Worker线程相互之间是通过Channel进行消息发布、以及监听,而且Worker之间相互透明:它既不知道消息发送给谁,也不知道自己消费的消息是哪一个Worker发出的。
Go语言就是使用的这种CSP模型,它利用协程Goroutine和通道Channel实现:
Go协程Goroutine:它是一种轻量级线程(并非操作系统的线程,而是将一个操作系统线程分段使用,通过调度器实现协作式调度。)它是一种绿色线程,又可称为微线程,它和Corouting协程的区别在于:当它发现阻塞后会启动新的微线程。
通道Channel:类似Unix的Pipe,用于协程之间通讯和同步,虽然协程自身之间已经解耦,但Worker和Channel之间依然存在耦合。
这里不解释并发(Concurrency)和并行(Parallelism)的区别,但需要理解三个基本概念:进程、线程、协程^3。
进程出现的目的是为了更好地利用CPU资源,使得并发称为可能。比如任务A和B,当A处理IO操作,CPU默默等待A,然后执行B,这样实际上就浪费了CPU资源,进程的出现使得A和B在“等待”过程可以进行任务切换,比如A进程和B进程分别执行这两个任务,进程中保存了各自的任务状态,包括需要的内存、硬盘、CPU等资源,操作系统则通过进程来标识任务,调度资源。——简单说进程是系统资源分配的最小单位。
线程的出现则是降低上下文切换的消耗,提高系统的并发,并且突破一个进程只能干一件事的缺陷,使得进程内实现并发。按照上述进程合理利用CPU资源的思路,线程的出现就是合理利用进程资源,多个进程同时执行时,由于它们可相互通信协作,而在CPU分配资源时进程和进程之间不停进行上下文切换,这种切换是很耗资源的。而线程是进程内的机制,它们共享了进程的大部分资源,且参与了CPU的调度,而自身又保留了自己的栈、寄存器等——这个时候进程就充当了线程的容器。
协程又往下走了一级,它可在线程内实现调度,避免了内核级别的上下文切换造成的性能损失,进而去突破线程在IO上的性能瓶颈。当程序遇到大规模的并发连接时,若在进程内以线程作为最小处理单元,实际上其开销还是不小,之中最耗资源的依旧是上下文切换,为了减少这种上下文切换,所以让线程内部实现自己的调度,而不去内核级别做上下文切换。
**「思」**实际上,从进程、到线程、到协程,都是为了最大化地利用计算机本身的资源不断进步的一个概念,读者会发现其思路是一致的:
1.进程是为了提高CPU的利用率,减少CPU资源调度过程中的上下文切换,它是操作系统中的最小单位。 2.线程则是为了提高进程内的资源利用率,突破进程本身的缺陷,在进程内部实现并发,它是进程内的最小单位。 3.协程(又称微线程)往下再走一级,提高线程内的资源利用率,减少上下文切换的开销,突破线程本身的缺陷,它是线程内的最小单位。 总地来说,主要是从提高资源利用率、减少上下文(进程、线程、协程的上下文不一样)切换的开销、减少IO的阻塞或者更加高效处理阻塞,同时将每一个级别的使用并行化。
最后需要说明的一点是,线程不一定比进程快,而协程不一定比线程快,它们只是理论上的一种资源利用的突破,而具体的业务场景有时候还是会影响到三者的效率。至于在协程之后会不会出现另外一种X程,这个只能拭目以待,毕竟这个过程是一步一步突破而来的。
前文一直提到了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组件时的一些小心得,后续的很多章节会针对这些心得阐述并告诉读者为什么,这些心得不是法典,只是一些曾经踩过的坑,您也可以不按照这些推荐心得去开发。
Vert.x提供了一个事件驱动、纯异步编程框架,在这种框架中,我们所做的每一件事都是面向“线程”,从实际效果看来是面向一个Verticle组件,这里先回顾一下前文的主代码:
这里只讨论主代码是因为支线剧本我们已经设计好了,如果不理解上述主代码做了什么可参考:1.3.Vertx实例。接下来需要定义一个Verticle组件:
上述代码是我们写的第一个Verticle组件,Vert.x中书写Verticle有下边几个步骤:
从io.vertx.core.AbstractVerticle
继承。
重写它的public void start()
方法放核心代码。
选择重写它的public void stop()
方法。
选择重写异步模式的start/stop
两个方法。
上边第四个步骤可能会让读者有些误解,为什么要刻意指出异步模式重写两个同名方法?这里先看看AbstractVerticle
的start/stop
的方法签名,关于start/stop
的细节我们将在生命周期章节来解析:
其实上述Verticle的代码会衍生一个问题:我们写的Verticle究竟是一个线程还是一个组件?这也就是本章说的“一个 or 一堆”的争议,结合前文提到的特殊参数:worker
和instances
,我更倾向于理解成我们写了“一堆”,为什么?您可以将您的主代码稍作修改:
那么您将看到下边的输出:
也就是说实际上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实例数量,还有没有其他办法呢?如果您把主代码修改成:
那么您将收到如下错误信息:
是的,在Vert.x中,如果您已经创建好了Verticle后,就不可再对它的数量进行调整(instances)了,那么这个方法是不是就鸡肋了?当然不是,让我们一一解答这个问题!
这个章节,我们就是“创始者”——因为我们要来看Verticle组件的诞生,在主代码中,我们已经拿到了Vertx实例的引用,那么接下来的步骤就是发布Verticle实例,所以接下来我们深入解析Vertx中的发布系列的API。
deployVerticle
在Vert.x中有十二个重载方法,按照参数表和传入标识(这里的传入标识就是上边的注释部分)可对应下边的表格统计:
这样您就可以记住Vertx
实例对应的deployVerticle
方法的作用了,从参数签名上看,包含了下边参数的都属于异步:
这个时候返回的值(java.lang.String
类型)是Verticle实例发布后的标识,称为DeploymentID,它是一个UUID的字符串。回顾一下前文,和Future
相关的代码都是异步代码,为什么?实际上,从源代码中看Future
的定义就清楚了:
**「思」**那么为什么在创建好的Verticle组件中无法设置
instances
参数呢? 每一个创建的Verticle实例和线程是一一绑定的关系,它和instances
的语义是拥有“因果”关系的,这是一个“先生鸡还是先生蛋”的问题。是因为Vertx先设置了将要发布的Verticle实例的数量,然后再创建了Verticle实例,并将该实例和Event Loop
线程池中的线程绑定,而不是在创建之后才设置这个参数;也就是说一旦Verticle组件创建过后,instances
的设置会变得没有逻辑,这也解释上边的代码会什么会抛出异常:Can't specify > 1 instances for already created verticle
。
在接触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):
从上图可以知道,本文提到的限制点就是DeploymentOptions
的异常会引起Error的地方,即上图中的Error-XXX
部分,先看看上边标记的每个Error代表的含义:
根据3.1和3.2的阐述,最后通过对整个发布流程的解读来总结一下Verticle实例的发布过程。
从发布流程的大分类上可以知道,主要分为两种方式:函数方式(F,O,A)
和类名称方式(S,O,A)
,并且从源代码中可以知道只有类名称方式会调用HAManager
中的方法开启HA高可用功能(这一点虽然没有明确验证过,不过从源代码流程上可以看到确实如此,所以如果要开启高可用功能,推荐使用类名方式发布Verticle实例)。
所有带有Error-
前缀的流程中,都会对DeploymentOptions
的配置项进行验证,我将这种验证称为发布流程的限制点,参考3.2的表格可以知道发布流程中的限制点在哪儿(所有限制点都会抛出异常IllegalArgumentException
)。
最终执行发布流程的类是io.vertx.core.impl.DeploymentManager
,该类有两个核心方法:doDeployVerticle
和doDeploy
,前者称为:协调者方法,基本上这个方法不会做一些实质性的发布动作,主要是将所有传入的参数进行组织和整理;而后者称为:执行者方法,这个方法才是发布的核心主代码。
图中隐藏了关于VerticleFactory的获取流程,该流程位于doDeploy
方法内部,使用工厂模式发布Verticle组件的详细流程在后续的实战过程中再逐一解读。
本章主要深入Verticle组件去理解Vert.x中的Verticle实例的本质,主要包含了以下核心知识点:
Verticle的基础知识(概念、分类、作用)
Actor模型的基础知识
Vertx
实例中的deployVerticle
发布专用API详情
Verticle的发布流程细节解读
当Vertx实例调用了deployVerticle
方法后,Verticle将在发布流程中执行发布过程,Verticle实例有两个核心生命周期start/stop
,这生命周期中的start
方法将在deploy
过程中执行,接下来的章节我们将去继续探索Verticle实例,从另外一个角度来理解Verticle组件。
Verticle实例(对象) | 类反射 | 函数 | 类名反射 | |
---|---|---|---|---|
Error代号 | 异常信息 | 含义 |
---|---|---|
同步:直接发布
支持
X
X
支持
同步:Option发布
支持
支持
支持
支持
异步:直接发布
支持
X
X
支持
异步:Option发布
支持
支持
支持
支持
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参数。