Verticle生命周期
本章承接前一章,谈谈未完成的剧情。上一章节讲到了主流程中,Vertx发布Verticle实例的细节,那么在发布之后,就进入了Verticle的内部代码,即start/stop
两个核心生命周期,本章节的故事则是围绕Verticle的两个生命周期展开。Verticle的start
方法将在Vertx实例调用deployVerticle
的API后被触发。
1. Verticle的编写
在书写Verticle1的过程中,我们会用到一个新类io.vertx.core.AbstractVerticle
,我们编写的所有的Verticle都是从这个类继承过来的,最简单的一个Verticle代码如下:
public class MyVerticle extends AbstractVerticle {
// 启动Verticle时被调用的方法:Deploy
public void start() {
}
// 停止Verticle时被调用的方法:Undeploy
public void stop() {
}
}
上边的代码编写了一个最简单的Verticle组件,最直接的编写自定义逻辑的方式就是重写抽象类中的start/stop
两个方法,这两个方法分别对应到Verticle的核心生命周期,在继续下边内容之前我们去AbstractVerticle
这个类中逛逛:其实这个类的源代码很简单,去掉注释部分如下:
public abstract class AbstractVerticle implements Verticle {
protected Vertx vertx;
protected Context context;
@Override
public Vertx getVertx() {
return vertx;
}
@Override
public void init(Vertx vertx, Context context) {
this.vertx = vertx;
this.context = context;
}
public String deploymentID() {
return context.deploymentID();
}
public JsonObject config() {
return context.config();
}
public List<String> processArgs() {
return context.processArgs();
}
@Override
public void start(Future<Void> startFuture) throws Exception {
start();
startFuture.complete();
}
@Override
public void stop(Future<Void> stopFuture) throws Exception {
stop();
stopFuture.complete();
}
public void start() throws Exception {
}
public void stop() throws Exception {
}
}
上述代码可以看到,AbstractVerticle
中定义了四个核心的生命周期方法:两个同步、两个异步、两个开始、两个结束,读者先不要去纠结io.vertx.core.Future
类是做什么用的,只需要知道在Vert.x中有它的地方就意味着“异步”,综上所述:
同步 | 异步 | |
---|---|---|
开始 | start() | start(Future\ |
结束 | stop() | stop(Future\ |
最开始的代码演示了如何同步启动/停止Verticle组件,那么接下来就看看如何异步地启动/停止Verticle组件。
public class MyVerticle extends AbstractVerticle {
private HttpServeer server;
public void start(Future<Void> startFuture) {
server = vertx.createHttpServer().requestHandler(req -> {
req.response()
.putHeader("content-type", "text/plain")
.end("Hello from Vert.x!");
});
// Server的listen方法本身是一个异步动作,绑定了一个异步回调
server.listen(8080, res -> {
if (res.succeeded()) {
startFuture.complete();
} else {
startFuture.fail(res.cause());
}
});
}
}
异步启动/停止的使用场景分析:结合官方教程,简单分析一下异步启动的必要性。异步启动和停止两个方法本身为了解决这样一种问题,您想要在这两个生命周期中定义自己的逻辑:
- 这个代码逻辑本身包含了异步代码,如上边示例中异步调用了
server.listen
方法; - 这个代码可能包含了一些Block的动作,如访问IO装置;
根据Vert.x官方提供的黄金法则,您不能在Verticle中引入直接的阻塞代码,这样的引入有可鞥会直接阻塞Event Loop,这个是不被允许的,在这样的情况下,您需要使用另外的手段来解决这种“阻塞”问题,那么异步的start/stop
就是您的选择。这里做个假设,如果上边的代码为:
public class MyVerticle extends AbstractVerticle {
private HttpServeer server;
public void start() {
server = vertx.createHttpServer().requestHandler(req -> {
req.response()
.putHeader("content-type", "text/plain")
.end("Hello from Vert.x!");
});
// Now bind the server:
server.listen(8080, res -> {
if (res.succeeded()) {
System.out.println("Successed");
} else {
System.out.println("Failure");
}
});
}
}
上边代码会发生什么?
从上图可以看到,在Verticle的start方法执行完成后,HttpServer的listen动作有可能还没完成,也就是说这样的方式导致了listen
的完成动作在start
完成动作之后,——这样没有办法维持Verticle在启动过程中的状态一致性。我们期望的结果是当HttpServer
的listen
动作完成后才标志着Verticle的启动完成,那么改变前的代码就可以达到这个效果,这里的主要原因是我们使用了HttpServer
的异步listen
的API,所以为了确保状态的一致性,使用Verticle组件中的异步start
就成为了一种必然。
当然,您如果可以忍受在listen
在Verticle的启动之后完成的话,那么您可以使用这样的写法,可是如果出现一种极端的情况:您写好的Verticle不论从日志还是其他参数都看到的是启动完成,而HttpServer
在listen
过程失败了(可能端口被占用,可能其他),那么这种情况下,从您想要的初衷,这个HttpServer
并没有启动完成,那么这个时候Verticle的状态和HttpServer的状态就出现了明显的不一致。——之所以说这是一种极端情况是因为这种情况可能发生概率并不大,但是为了从程序的严谨上考虑,建议这样的代码还是不要出现,因为Vert.x中本来就有解决这种异步调用的解决方案。
2. 关于DeploymentID
在Vert.x中,每个Verticle组件在发布过后会有一个唯一标识符——称为DeploymentID,该标识符的格式是UUID2的格式如:a8cdd717-49f8-452d-8d07-8263cae99a26
,Vert.x在发布(Deploy)时会触发Verticle组件的start
方法,进入启动的生命周期,而在撤销(Undeploy)时会触发Verticle组件的stop
方法,进入停止的生命周期,冒看之下DeploymentID在启动过程没有使用到,但在停止的过程中往往会用到,参考一下Vertx实例的undeploy
系列方法。
/**
* Undeploy a verticle deployment.
* <p>
* The actual undeployment happens asynchronously and may not complete
* until after the method has returned.
*
* @param deploymentID the deployment ID
*/
void undeploy(String deploymentID);
/**
* Like {@link #undeploy(String) } but the completionHandler will be notified
* when the undeployment is complete.
*
* @param deploymentID the deployment ID
* @param completionHandler a handler which will be notified when the undeployment is complete
*/
void undeploy(String deploymentID, Handler<AsyncResult<Void>> completionHandler);
Vertx实例中只有两个undeploy
方法,这两个方法的第一个参数都是DeploymentID,所以如果要处理“停止”这个生命周期的一些细节,比如使用自定义代码,那么在编程方式发布/撤销时需要考虑将发布过程的DeploymentID记录下来,这样才可以针对对应的Verticle实例执行后续操作。
「注」这里的DeploymentID实际上关联的也不是一个Verticle实例(单线程),而是某一类(即您所编写的Verticle类),也就是说DeploymentID还不会涉及到线程这个级别,而是前文提到的“一堆”。
3. 例子:生命周期控制
接下来通过zero中对Deploy/Undeploy
的应用解析一下处理Verticle完整生命周期的代码
3.1. 开发Verticle
package io.vertx.up._01.verticles;
import io.vertx.core.AbstractVerticle;
public class LifeVerticle extends AbstractVerticle {
@Override
public void start() {
System.out.println(Thread.currentThread().getName() +
": Start : " +
Thread.currentThread().getId()
+ ", Did: " + this.deploymentID());
}
@Override
public void stop() {
System.out.println(Thread.currentThread().getName() +
": Stop : " +
Thread.currentThread().getId()
+ ", Did: " + this.deploymentID());
}
}
开发一个Verticle如上,使用打印语句的主要目的是可直接监控当前这个Verticle组件的生命周期。
3.2. 主程序
package io.vertx.up._01.life;
import io.vertx.core.DeploymentOptions;
import io.vertx.up._01.lanucher.Launcher;
import io.vertx.up._01.lanucher.SingleLauncher;
import io.vertx.up._01.verticles.LifeVerticle;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
public class LifeCycle {
private static final ConcurrentMap<String, String> IDS = new ConcurrentHashMap<>();
public static void main(final String[] args) {
// 选择单点模式
final Launcher launcher = new SingleLauncher();
launcher.start(vertx -> {
// 发布
vertx.deployVerticle(LifeVerticle::new, new DeploymentOptions().setInstances(10), res -> {
if (res.succeeded()) {
IDS.put(res.result(), res.result());
}
});
vertx.deployVerticle(LifeVerticle::new, new DeploymentOptions().setInstances(3), res -> {
if (res.succeeded()) {
IDS.put(res.result(), res.result());
}
});
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
// 撤销
IDS.keySet().forEach(item -> vertx.undeploy(item, res -> {
System.out.println("Successfully undeploy the item: " + item);
}));
}));
});
}
}
3.3. 运行
直接运行该程序,然后将该程序关闭,点击IDE中的停止或在命令行中Ctrl + C
的中断模式,您将可以看到下边输出:
# public void start()方法
vert.x-eventloop-thread-0: Start : 14, Did: be38fa6e-80af-4ce0-8509-0a2ccfb6026c
vert.x-eventloop-thread-1: Start : 15, Did: be38fa6e-80af-4ce0-8509-0a2ccfb6026c
vert.x-eventloop-thread-12: Start : 26, Did: e5b0d620-4cc3-4709-8574-19a17ddeba5e
vert.x-eventloop-thread-11: Start : 25, Did: e5b0d620-4cc3-4709-8574-19a17ddeba5e
vert.x-eventloop-thread-10: Start : 24, Did: e5b0d620-4cc3-4709-8574-19a17ddeba5e
vert.x-eventloop-thread-9: Start : 23, Did: be38fa6e-80af-4ce0-8509-0a2ccfb6026c
vert.x-eventloop-thread-8: Start : 22, Did: be38fa6e-80af-4ce0-8509-0a2ccfb6026c
vert.x-eventloop-thread-7: Start : 21, Did: be38fa6e-80af-4ce0-8509-0a2ccfb6026c
vert.x-eventloop-thread-6: Start : 20, Did: be38fa6e-80af-4ce0-8509-0a2ccfb6026c
vert.x-eventloop-thread-5: Start : 19, Did: be38fa6e-80af-4ce0-8509-0a2ccfb6026c
vert.x-eventloop-thread-4: Start : 18, Did: be38fa6e-80af-4ce0-8509-0a2ccfb6026c
vert.x-eventloop-thread-3: Start : 17, Did: be38fa6e-80af-4ce0-8509-0a2ccfb6026c
vert.x-eventloop-thread-2: Start : 16, Did: be38fa6e-80af-4ce0-8509-0a2ccfb6026c
# public void stop()方法
vert.x-eventloop-thread-0: Stop : 14, Did: be38fa6e-80af-4ce0-8509-0a2ccfb6026c
vert.x-eventloop-thread-2: Stop : 16, Did: be38fa6e-80af-4ce0-8509-0a2ccfb6026c
vert.x-eventloop-thread-12: Stop : 26, Did: e5b0d620-4cc3-4709-8574-19a17ddeba5e
vert.x-eventloop-thread-4: Stop : 18, Did: be38fa6e-80af-4ce0-8509-0a2ccfb6026c
vert.x-eventloop-thread-3: Stop : 17, Did: be38fa6e-80af-4ce0-8509-0a2ccfb6026c
vert.x-eventloop-thread-6: Stop : 20, Did: be38fa6e-80af-4ce0-8509-0a2ccfb6026c
vert.x-eventloop-thread-10: Stop : 24, Did: e5b0d620-4cc3-4709-8574-19a17ddeba5e
vert.x-eventloop-thread-8: Stop : 22, Did: be38fa6e-80af-4ce0-8509-0a2ccfb6026c
vert.x-eventloop-thread-9: Stop : 23, Did: be38fa6e-80af-4ce0-8509-0a2ccfb6026c
vert.x-eventloop-thread-5: Stop : 19, Did: be38fa6e-80af-4ce0-8509-0a2ccfb6026c
vert.x-eventloop-thread-1: Stop : 15, Did: be38fa6e-80af-4ce0-8509-0a2ccfb6026c
vert.x-eventloop-thread-11: Stop : 25, Did: e5b0d620-4cc3-4709-8574-19a17ddeba5e
vert.x-eventloop-thread-7: Stop : 21, Did: be38fa6e-80af-4ce0-8509-0a2ccfb6026c
# undeploy方法的回调
Successfully undeploy the item: e5b0d620-4cc3-4709-8574-19a17ddeba5e
Successfully undeploy the item: be38fa6e-80af-4ce0-8509-0a2ccfb6026c
3.4. 分析
在总结本小节之前先看看上边用到的一个特殊的JVM函数:Runtime.getRuntime().addShutdownHook
3函数,该函数参数为一个java.lang.Thread
,它的方法签名如下:
public void addShutdownHook(Thread hook) ...
该方法用于在JVM中增加一个关闭的钩子,当程序正常退出,系统调用System.exit方法
或者虚拟机被关闭时就会执行该线程中的代码。其中shutdownHook是一个已经初始化但是并没启动的线程,当JVM关闭时,会执行系统中已经设置的所有通过addShutdownHook
添加的钩子,等到系统执行完这些钩子中的代码后,JVM才会正式关闭,所以可以使用该方法在JVM关闭时进行内存清理、资源回收等工作。
为什么我们需要该方法?Vert.x中的stop
方法不会主动触发,它只有在您调用了undeploy
过后才会触发,所以对于Vert.x中的”停止“部分,必须设置关闭时的代码,从实际使用看来,该代码放到shutdownHook
是最有效的,因为该代码的触发点比较合适——最理想的状况就是在系统关闭之前调用所有Verticle组件的stop
方法来清理相关资源,这些清理动作包括:
- 关闭未关闭完全的连接池。
- 在微服务模式下更新当前服务的状态(若使用ZooKeeper或Etcd3作为配置中心,需要善后)。
- 向其他节点发出消息提交当前节点的心跳信息,告诉其他节点当前节点将会停止。
当然清理动作不仅仅是以上的信息,我这边只枚举了一部分。
最后根据日志读者可以简单分析并且彻底理解DeploymentID的用途:
DeploymentID
是UUID格式,主要和每一次Deploy
行为绑定,因为一次发布可能关联某一类Verticle的多个实例。- 同一类Verticle(自己编写的Java类)可发布多次,每次发布会产生新的DeploymentID,应该说每个线程会独占一个
DeployementID
,这也是为什么反复在强调Verticle究竟是一类还是一个的原因。 stop
方法在调用了undeploy
过后被触发,而且在完成之前打印了所有的日志。
最后,在stop中一定要注意代码本身是异步还是同步,如果使用了异步方法,有可能代码还没执行JVM就关闭了导致关闭状态的不同步,所以在该代码中做好数据同步的完善工作是有必要的,不过这不是一个开发的问题,相反这是架构师应该考虑的点。
4.小结
本章节主要针对Verticle生命周期的细节进行讲解,通过一个完整的例子(zero中的关闭部分远比例子中的复杂)来告诉读者如何设计以及开发Vert.x中的Verticle去触发stop
生命周期,使得整个程序变得完整。曾经我遇到过一个朋友,当时一直问我为什么stop
的生命周期没触发,当时给我演示时它直接点了IDE中的stop按钮,而当时点击该按钮后stop
并没有执行。回到上边使用的手段,因为这个IDE在stop按钮点击时,只有ShutdownHook部分的代码可捕捉到这个行为,而Verticle中的stop
生命周期是没有办法捕捉该行为的,所以它的stop
并不是自动触发。
1. https://vertx.io/docs/vertx-core/java/#_verticles ↩
2. 《UUID》https://baike.baidu.com/item/UUID/5921266?fr=aladdin ↩
3. 《Runtime.addShutdownHook用法》http://kim-miao.iteye.com/blog/1662550, 作者:kim_miao ↩