2.2 更多关于verticles的细节
关于编写和部署 Verticle,还有更多需要了解的事情:
- 当事件循环被阻塞时会发生什么?
- 在存在异步初始化工作的情况下,如何推迟生命周期完成的通知?
- 你如何部署和取消部署 Verticle?
- 你如何传递配置数据?
我们将使用非常简单但重点突出的示例来介绍这些主题中的每一个。
2.2.1 阻塞和事件循环
处理程序回调从事件循环线程运行。 重要的是,在事件循环上运行的代码需要尽可能少的时间,这样事件循环线程才能在处理的事件数量上具有更高的吞吐量。 这就是为什么不应在事件循环上发生长时间运行或阻塞的 I/O 操作的原因。
话虽如此,发现阻塞代码可能并不总是那么容易,尤其是在使用第三方库时。 Vert.x 提供了一个检查器,用于检测事件循环何时被阻塞太久。
为了说明这一点,让我们看看当我们在事件处理程序回调中引入无限循环时会发生什么。
package chapter2.blocker;
import io.vertx.core.AbstractVerticle;
import io.vertx.core.Vertx;
public class BlockEventLoop extends AbstractVerticle {
@Override
public void start() {
vertx.setTimer(1000, id -> {
while (true);
});
}
public static void main(String[] args) {
Vertx vertx = Vertx.vertx();
vertx.deployVerticle(new BlockEventLoop());
}
}
清单 2.5 中的代码定义了一个一秒计时器,处理程序回调进入一个无限循环。
清单 2.6 显示了运行清单 2.5 中的代码时的典型日志输出。 如您所见,在事件循环线程运行无限循环时开始出现警告,因此不可用于处理其他事件。经过一些迭代(默认为5秒)后,警告通过堆栈跟踪转储得到了丰富,因此您可以清楚地识别代码中的罪魁祸首。注意,这只是一个警告。事件循环线程检查器不能杀死花费太长时间来完成其任务的处理程序。
当然,有时您需要使用阻塞或长时间运行的代码,Vert.x 提供了在不阻塞事件循环的情况下运行此类代码的解决方案。 这是第 2.3 节的主题。
配置 Vert.x 阻塞线程检查器
默认情况下,阻塞线程检查器抱怨之前的时间限制是2秒,但可以将其配置为不同的值。 有些环境,例如嵌入式设备,处理能力较慢,增加它们的线程检查器阈值是正常的。
您可以使用系统属性来更改设置:
-Dvertx.options.blockedThreadCheckInterval=5000
将时间间隔更改为5秒。-Dvertx.threadChecks=false
禁用线程检查器。
🏷注意: 请注意,此配置是全局的,不能在每个verticle的基础上进行微调。
2.2.2 生命周期事件的异步通知
到目前为止,我们已经查看了具有 start()
生命周期方法的示例。 这些方法中的约定是,除非该方法抛出异常,否则 Verticle 已成功完成其 start
生命周期事件处理。 这同样适用于 stop()
方法。
然而,有一个问题:start
或 stop
方法中的某些操作可能是异步的,因此它们可能会在对 start()
或 stop()
的调用返回后完成。
让我们看看如何正确地通知调用者延迟成功或失败。 一个很好的例子是启动 HTTP 服务器,这是一个非阻塞操作。
package chapter2.future;
import io.vertx.core.AbstractVerticle;
import io.vertx.core.Future;
import io.vertx.core.Promise;
import io.vertx.core.Vertx;
public class SomeVerticle extends AbstractVerticle {
@Override
public void start(Promise<Void> promise) { // <1>
vertx.createHttpServer()
.requestHandler(req -> req.response().end("Ok"))
.listen(8080, ar -> {
if (ar.succeeded()) { // <2>
promise.complete(); // <3>
} else {
promise.fail(ar.cause()); // <4>
}
});
}
public static void main(String[] args) {
Vertx vertx = Vertx.vertx();
vertx.deployVerticle(new SomeVerticle());
}
}
<1>: Promise 的类型为 void,因为 Vert.x 只对部署完成感兴趣,没有任何价值可携带。
<2>: 支持异步结果的listen变体指示操作是否失败。
<3>:
complete()
用于将 Promise 标记为已完成(当 Promise 不是 void 类型时,可以传递一个值).<4>: 如果listen操作失败,我们将 Promise 标记为失败并传播错误。
清单 2.7 显示了一个示例,其中 Verticle 在启动时报告异步通知。 这很重要,因为启动 HTTP 服务器可能会失败。 事实上,TCP 端口可能被另一个进程使用,在这种情况下 HTTP 服务器无法启动,因此 Verticle 没有成功部署。 为了报告异步通知,我们使用 listen
方法的变体,并在操作完成时调用回调。
AbstractVerticle
中的 start
和 stop
方法支持带有 io.vertx.core.Promise
类型参数的变体。 顾名思义,Vert.x 的 Promise
是对 futures 和 Promise 模型的改编,用于处理异步结果。 promise
用于写入异步结果,而 future
用于查看异步结果。 给定一个 Promise
对象,您可以调用 future()
方法来获取 io.vertx.core.Future
类型的future 。
在清单 2.7 中,Promise
对象被设置为在 Verticle 成功完成其 start
或 stop
生命周期时完成。 如果出现错误,Promise
对象将失败,并出现描述错误的异常,并且 Verticle 部署失败。
为了更好地理解这里发生了什么,图 2.2 展示了 Verticle、Vert.x 对象和负责调用 start
方法的内部 Vert.x 部署程序对象之间的交互。 我们可以检查部署程序是否等待承诺完成以了解部署是否成功,即使在对 start
方法的调用返回之后也是如此。 相比之下,图 2.3 显示了不使用接受 Promise
对象的 start
变体时的交互。 部署者无法收到错误通知。
💡提示: 使用接受回调来通知错误的异步方法变体是一种很好的健壮性实践,例如清单 2.7 中的
listen
方法。 如果它允许我减少代码示例的冗长,我不会在本书的其余部分总是这样做。
2.2.3 部署 verticles
到目前为止,我们一直在从嵌入在单个 Verticle 类中的main
方法中部署 Verticle。
Verticle 总是通过 Vertx
对象部署(和取消部署)。 您可以通过任何方法执行此操作,但部署由 Verticle 组成的应用程序的典型方法如下:
- 部署一个“主” verticle.
- “主” verticle 部署其他 verticles。
- 被部署的verticles又可以部署更多的verticles 。
🏷注意: 请注意,虽然这听起来是分层的,但 Vert.x 并没有正式的"父/子" Verticle 概念。
为了说明这一点,让我们定义一些 Verticle。
package chapter2.deploy;
import io.vertx.core.AbstractVerticle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class EmptyVerticle extends AbstractVerticle {
private final Logger logger = LoggerFactory.getLogger(EmptyVerticle.class);
@Override
public void start() {
logger.info("Start");
}
@Override
public void stop() {
logger.info("Stop");
}
}
清单 2.8 定义了一个简单的 Verticle。 除了在启动和停止时记录日志之外,它没有做任何有趣的事情。
package chapter2.deploy;
import io.vertx.core.AbstractVerticle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Deployer extends AbstractVerticle {
private final Logger logger = LoggerFactory.getLogger(Deployer.class);
@Override
public void start() {
long delay = 1000;
for (int i = 0; i < 50; i++) {
vertx.setTimer(delay, id -> deploy()); // <1>
delay = delay + 1000;
}
}
private void deploy() {
vertx.deployVerticle(new EmptyVerticle(), ar -> { // <2>
if (ar.succeeded()) {
String id = ar.result();
logger.info("Successfully deployed {}", id);
vertx.setTimer(5000, tid -> undeployLater(id)); // <3>
} else {
logger.error("Error while deploying", ar.cause());
}
});
}
private void undeployLater(String id) {
vertx.undeploy(id, ar -> { // <4>
if (ar.succeeded()) {
logger.info("{} was undeployed", id);
} else {
logger.error("{} could not be undeployed", id);
}
});
}
}
<1>: 我们每秒部署一个 EmptyVerticle 的新实例。
<2>: 部署一个 Verticle 是一个异步操作,并且有一个支持异步结果的 deploy 方法的变体。
<3>: 我们将在五秒后解除 Verticle 部署。
<4>: 取消部署与部署非常相似。
清单 2.9 定义了一个 Verticle,它部署了来自 清单 2.8 的 50 个 EmptyVerticle
类的实例。 计时器的使用允许我们将每个部署间隔一秒。 deploy
方法使用另一个计时器在部署 Verticle 5 秒后取消部署它。 部署为一个verticle分配一个唯一的标识符字符串,以后可以将其用于取消部署。
最后但同样重要的是,Deployer
verticle 本身可以从主方法和类部署,如清单 2.10 所示。 运行此示例会产生类似于以下清单中的日志条目。
您可以从 vert.x-eventloop-thread-0
线程查看日志条目; 它们对应于 Deployer
verticle。 然后您可以从 EmptyVerticle
实例中查看生命周期日志事件; 他们使用其他事件循环线程。
有趣的是,我们从 Deployer 部署了 50 个 Verticle,但线程可能比日志中出现的 Verticle 少。 默认情况下,Vert.x 创建的事件循环线程数量是 CPU 内核的两倍。 如果您有 8 个内核,那么 Vert.x 应用程序有 16 个事件循环。 将 Verticle 分配给事件循环是以循环方式完成的。
这给我们上了一堂有趣的课: 一个Verticle总是使用同一个事件循环线程,事件循环线程被多个Verticle共享。这种设计导致运行应用程序的线程数量是可预测的。
💡提示: 可以调整应该可用的事件循环的数量,但无法手动将给定的 Verticle 分配给特定的事件循环。 这在实践中永远不会成为问题,但在最坏的情况下,您始终可以计划 Verticle 的部署顺序。
2.2.4 传递配置数据
应用程序代码通常需要配置数据。 一个很好的例子是连接到数据库服务器的代码:它通常需要主机名、TCP 端口、登录名和密码。 由于值从一种部署配置更改为另一种,因此需要从配置 API 访问此类配置。
Vert.x verticles 可以在部署时传递此类配置数据。 您将在本书后面看到可以使用一些更高级的配置形式,但是 Vert.x 核心 API 已经提供了一个非常有用的通用 API。
配置需要作为 JSON 数据传递,使用由 io.vertx.core.json 包
中的 JsonObject
和 JsonArray
类具体化的 Vert.x JSON API。
package chapter2.opts;
import io.vertx.core.AbstractVerticle;
import io.vertx.core.DeploymentOptions;
import io.vertx.core.Vertx;
import io.vertx.core.json.JsonObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class SampleVerticle extends AbstractVerticle {
private final Logger logger = LoggerFactory.getLogger(SampleVerticle.class);
@Override
public void start() {
logger.info("n = {}", config().getInteger("n", -1)); // <1>
}
public static void main(String[] args) {
Vertx vertx = Vertx.vertx();
for (int n = 0; n < 4; n++) {
JsonObject conf = new JsonObject().put("n", n); // <2>
DeploymentOptions opts = new DeploymentOptions()
.setConfig(conf) // <3>
.setInstances(n); // <4>
vertx.deployVerticle("chapter2.opts.SampleVerticle", opts); // <5>
}
}
}
<1>: config() 返回 JsonObject 配置实例,访问器方法支持可选的默认值。 这里,如果 JSON 对象中没有“n”键,则返回 -1。
<2>: 我们创建一个 JSON 对象并为键“n”放置一个整数值。
<3>: DeploymentOption 允许对 Verticle 进行更多控制,包括传递配置数据。
<4>: 我们可以一次部署多个实例。
<5>: 由于我们部署了多个实例,我们需要使用它的完全限定类名(FQCN)而不是使用 new 运算符来指向 Verticle。 对于仅部署一个实例,您可以选择使用 new 或使用 FQCN 创建的实例。
清单 2.12 展示了一个部署多个 Verticle 并传递配置数据的示例。 运行该示例给出了清单 2.13 中的输出,您可以检查配置数据的不同值。