1.5.Verticle生命周期

    本章承接前一章,谈谈未完成的剧情。上一章节讲到了主流程中,Vertx发布Verticle实例的细节,那么在发布之后,就进入了Verticle的内部代码,即start/stop两个核心生命周期,本章节的故事则是围绕Verticle的两个生命周期展开。Verticle的start方法将在Vertx实例调用deployVerticle的API后被触发。

1. Verticle的编写

    在书写Verticle^1的过程中,我们会用到一个新类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中有它的地方就意味着**“异步”**,综上所述:

  最开始的代码演示了如何同步启动/停止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());
      }
    });
  }
}

    异步启动/停止的使用场景分析:结合官方教程,简单分析一下异步启动的必要性。异步启动和停止两个方法本身为了解决这样一种问题,您想要在这两个生命周期中定义自己的逻辑:

  1. 这个代码逻辑本身包含了异步代码,如上边示例中异步调用了server.listen方法;

  2. 这个代码可能包含了一些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在启动过程中的状态一致性。我们期望的结果是当HttpServerlisten动作完成后才标志着Verticle的启动完成,那么改变前的代码就可以达到这个效果,这里的主要原因是我们使用了HttpServer的异步listen的API,所以为了确保状态的一致性,使用Verticle组件中的异步start就成为了一种必然。

    当然,您如果可以忍受在listen在Verticle的启动之后完成的话,那么您可以使用这样的写法,可是如果出现一种极端的情况:您写好的Verticle不论从日志还是其他参数都看到的是启动完成,而HttpServerlisten过程失败了(可能端口被占用,可能其他),那么这种情况下,从您想要的初衷,这个HttpServer并没有启动完成,那么这个时候Verticle的状态和HttpServer的状态就出现了明显的不一致。——之所以说这是一种极端情况是因为这种情况可能发生概率并不大,但是为了从程序的严谨上考虑,建议这样的代码还是不要出现,因为Vert.x中本来就有解决这种异步调用的解决方案。

2. 关于DeploymentID

    在Vert.x中,每个Verticle组件在发布过后会有一个唯一标识符——称为DeploymentID,该标识符的格式是UUID^2的格式如: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并不是自动触发。

[^3]: 《Runtime.addShutdownHook用法》http://kim-miao.iteye.com/blog/1662550, 作者:kim_miao

最后更新于