携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第 2 天
Vertx 官方 DEMO 示栗(二)
Vertx 快速上手指北(二)
第二章: Vertx 中的 actors:Verticle(基本服务)
本章大纲:
- EventLoop 不可以被阻塞(blocked)
- Verticle 异步初始化
- 如何发布(deploy)和撤销发布(undeploy)服务 Verticle
- 如何应对需要运行阻塞任务或长时间运行的代码
- Verticle 究竟是什么?
- Verticel 以及其执行环境
- Context 对象
- 桥接 Vertx 和 非 Vertx 的线程模型
- 集群部署
个人总结:
- 本章概要:Vertx 程序由一个个独立的 “服务(Verticle)” 组成
- 本章是非常实用的一章,介绍了 新手 编写 Vertx 程序时 常犯的错误
- 新手将服务部署为 work 导致的困惑(博主擅自加塞,别问我为啥知道)
- 新手阻塞事件队列(EventLoop) 引发的血案
- 新手该如何执行阻塞代码(长时间任务)
- 新手发布服务成功后忘记调用 startPromise.complete() 引发的血案
- 新手如何发布,撤销发布服务
- 新手由于不熟悉 Verticle 执行环境,导致不打印异常引发的困惑
- 新手由于不熟悉 Verticle 执行环境,写过的那些废代码
- 新手集群部署指北
个人认为:为了实现异步响应式编程,为了可以水平扩展,Vertx 有了 Verticle
- 你可以把 Verticle 看成是微服务中的一个独立运行的 “服务”
- 这个服务有生命周期,可以被发布,被卸载
- 这个 “服务” 很小,小到仅仅是一个实现 Verticle 接口的实现类
- 那是比 “组件(一般由好几个类组成)” 还小的 “代码块”
- 可以部署多份,多线程并发执行
- 可以启动多个进程,在隔离环境中并发执行
- 可以部署成集群,并行执行
特别 “细” 的,并行皮卡丘们,啊不 Verticle 们(吱吱吱~)
关于日志
VertX 使用 Netty 默认的 Logback 配置。我们可以通过创建 src/main/resources/logback.xml 来控制日志数量
<!-- 通过修改 Netty 的日志级别,即可修改 Vertx 的日志 -->
<logger name="io.netty" level="warn" />
然而博主配置了之后,并没有什么鸟用……………………
新手将服务部署为 work 导致的困惑
博主刚上手 Vertx 那会,微微瞄了一眼 Vertx,自信的说到:
Vertx 不就是基于 Netty 做的封装么?
这个我 “会”:
Netty 的 Boss Group == Vertx 的 EventLoop
Netty 的 Work Group == Vertx 的 Worker
- 错误示栗
- 错误示栗
- 错误示栗
// 错误示栗
// 错误示栗
// 错误示栗
public static void main(String[] args) {
Vertx vertx = Vertx.vertx();
SimpleVerticle verticle = new SimpleVerticle();
// 机智如博主已经用上了多线程(将所有 Verticle 部署成 Worker)提升性能啊,这谁不知道(错误示栗)
vertx.deploy(verticle, new DeployOptions().setWorker(true));
}
然后自信的把 Vertx 的 EventLoop 线程池设置为 2,worker 线程池配置超大,然后将所有 Verticle 部署成 Worker
- 然后发生了 生无可恋般的 Bug 和各种并发问题(纯属瞎搞 ☹)
- 然后性能还差,全设置成 worker,由于频繁的线程切换,性能只剩下 1 / 3 到 1 / 5
Boss Group 和 EventLoop 虽然都是 React 和 多路复用,但,好像没啥可比性:
- Vertx 的 EventLoop 负责处理通讯,传输层的概念
- Netty 的 BossGroup 负责处理通信,链路层的概念
官方吐槽:
有的人会在 worker 线程中再开启多线程,这进一步提升并发处理能力。看上去很高级的用法。但许多人使用时遇到了并发问题。我们不推荐这么用。我们鼓励用户简单地调整 worker 线程池的 大小以匹配工作负载,而不是在 worker 线程池中使用多线程
新手阻塞事件队列(EventLoop) 引发的血案
EventLoop 被阻塞?
你可以想象一下,有一天你和你的好朋友划着皮划艇正在快乐的玩耍
突然,有人在上游截流(阻塞)了
然后,当你的皮划艇沉底以后。上游突然又开闸放水了(阻塞期间累积的流量)
这时候你只能无奈的表示:😫 喝饱了
/**
* Handler 和 Callback 运行在 EventLoop 线程上。
* 需要注意的是
* 运行在 EventLoop 上的代码需要尽可能少,以便获得更高的吞吐量。
*
* 但是发现阻塞的代码并不总是那么容易,尤其是在使用第三方库时
* VertX 提供了一个检查器
* 可以检测 EventLoop 何时被阻塞太久。
*/
public class BlockEventLoop extends AbstractVerticle {
@Override
public void start() {
// 当 EventLoop 线程阻塞时,警告开始出现,因为 EventLoop 不能用于处理其他事件
// 经过一些迭代(默认情况下为5秒)后,开始打印堆栈跟踪信息
//
// 因为发现阻塞的代码并不总是那么容易,所以 Vertx 内置了这一警告机制
//
// 如果您正在断点调试,可以无视这些信息
// 可以通过添加如下参数配置(但不建议您关闭此功能)
//-Dvertx.options.blockedThreadCheckInterval=5000 每 5 秒检查一次
//-Dvertx.threadChecks=false 关闭检查
vertx.setTimer(1000, id -> {
while (true);
});
}
public static void main(String[] args) {
Vertx vertx = Vertx.vertx();
vertx.deployVerticle(new BlockEventLoop());
}
}
有时需要运行阻塞任务或长时间运行的代码,VertX 提供了运行此类代码而不阻塞 EventLoop 的解决方案
新手发布服务成功后忘记调用 startPromise.complete() 引发的血案
忘记调用 startPromise.complete() ?
你可以想象一下,追了 3 年的老婆,是个火星人,领不到结婚证 🥺????
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> 重写 AbstractVerticle 父类的 start 方法
<2> 如果初始化成功
<3> 调用 promise.complete(); 方法,完成发布
<4> 否则调用 promise.fail(cause); 方法,标记发布异常
新手如何发布,撤销发布服务
问题:
// Vertx 不会帮你管理这些 ID
final String[] myId = {""};
vertx.deployVerticle("my.Verticle", new DeploymentOptions().setInstances(2), (h) -> {
if (h.succeeded()) {
// This is deployment ID for both. Store it in some holder, because Java
myId[0] = h.result();
}
else {
System.out.println("CAUSE " + h.cause());
}
});
// ...
vertx.undeploy(myId[0])
解答:
- 多方百度,大多都是继承 AbstractVerticle ,用 MyAbstractVerticle 缓存 myId 来解决
- 其实(来自官方文档的提示),最优解是自己实现 Verticle 接口 😎
- 博主试过了,真香
新手该如何执行阻塞代码(长时间任务)
其实比较简单:把整条河买下来,让那个喜欢截流的同学去别地玩去…… 😎(机智)
图:把喜欢截流的同学仍海里(worker 线程池)去
/**
* executeBlocking
*/
public class Offload extends AbstractVerticle {
private final Logger logger = LoggerFactory.getLogger(Offload.class);
@Override
public void start() {
vertx.setPeriodic(5000, id -> {
logger.info("Tick");
// 执行长时间任务
vertx.executeBlocking(this::blockingCode, this::resultHandler);
// false: 并行执行,true:串行执行
// vertx.executeBlocking(this::blockingCode, false, this::resultHandler);
});
}
/**
* 此部分代码将在 worker 线程池中执行
* @param promise
*/
private void blockingCode(Promise<String> promise) {
logger.info("Blocking code running");
try {
Thread.sleep(4000);
logger.info("Done!");
promise.complete("Ok!");
} catch (InterruptedException e) {
promise.fail(e);
}
}
/**
* 执行完成之后,结果将发回给 eventLoop 线程
* 此方法在 EventLoop 线程池中执行
* @param ar
*/
private void resultHandler(AsyncResult<String> ar) {
if (ar.succeeded()) {
logger.info("Blocking code result: {}", ar.result());
} else {
logger.error("Woops", ar.cause());
}
}
public static void main(String[] args) {
Vertx vertx = Vertx.vertx();
vertx.deployVerticle(new Offload());
}
}
新手由于不熟悉 Verticle 执行环境,导致的各种困惑
Vertx 是单线程的
你可以愉快的使用堆内存,不会有并发问题(仅限在 EventLoop 中)
/**
* Verticle 可以有自己的私有状态
* 当接收事件时可以更新,可以部署其他 Verticle,还可以通过消息传递进行通信
*
* 两个事件处理程序:
* 一个用于每 5 秒执行一次的周期性任务
* 一个用于处理 HTTP 请求
* 主方法实例化一个全局 VertX 实例,并部署一个 Vertivle 的实例。
*
* Java 中定义一个 Verticle 通常继承 AbstractVerticle
* 你也可以选择继承 Verticle
* AbstractVerticle 提供了默认的 Vert 相关的事件处理、配置和执行管
*
* 通过执行:
* 我们发现,所有的 Http 请求和 定时任务 都在 vert.x-eventloop-thread-0 上执行
* 这样做没有线程安全问题,counter 我们不需要使用 java.util.concurrent.AtomicLong
* Vertx 类本身提供的方法是线程安全的,可以在多线程环境下使用
*
*/
public class HelloVerticle extends AbstractVerticle {
private final Logger logger = LoggerFactory.getLogger(HelloVerticle.class);
// 私有内部状态
private long counter = 1;
@Override
public void start() {
// 每 5 秒输出日志
vertx.setPeriodic(5000, id -> {
logger.info("tick");
});
// http 服务器,返回 hello
vertx.createHttpServer()
.requestHandler(req -> {
logger.info("Request #{} from {}", counter++, req.remoteAddress().host());
req.response().end("Hello!");
})
.listen(8080);
logger.info("Open http://localhost:8080/");
}
public static void main(String[] args) {
// 创建全局 vertx 对象
Vertx vertx = Vertx.vertx();
// 最简单的部署方法
vertx.deployVerticle(new HelloVerticle());
}
}
新手由于不熟悉 Verticle 执行环境,导致不打印异常引发的困惑
Vertx vertx = Vertx.vertx();
Context ctx = vertx.getOrCreateContext();
ctx.put("foo", "bar");
// 注册异常处理类,打印异常
ctx.exceptionHandler(t -> {
if ("Tada".equals(t.getMessage())) {
logger.info("Got a _Tada_ exception");
} else {
logger.error("Woops", t);
}
});
新手由于不熟悉 Verticle 执行环境,写过的那些废代码
给 Verticle 设置很多的配置属性 ?
使用 opts.setConfig(conf) 快捷又高效,还能使用 全限定名部署,便于之后集群化扩展
public class SampleVerticle extends AbstractVerticle {
private final Logger logger = LoggerFactory.getLogger(SampleVerticle.class);
@Override
public void start() {
logger.info("n = {}", config().getInteger("n", -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);
DeploymentOptions opts = new DeploymentOptions()
.setConfig(conf)
.setInstances(n);
vertx.deployVerticle("chapter2.opts.SampleVerticle", opts);
}
}
}
善用 Vertx 的 Context 对象
一个 Verticle 对象本质上是两个对象的组合而成
- Vertx 的 vertx 对象
- vertx 实例被多个 Verticle 共享,并且每个 JVM 进程通常只有一个 Vertx 实例
- VertX 所属上下文 Context 的 ctx 实例,用于分发事件
- context 实例可以访问正在处理事件的线程
图:Vertx 的线程模型
- 看不懂?很好理解啊 ( ̄︶ ̄*))
- 计时器(Timer)、数据库驱动程序(Database)、HTTP服务器等发出事件 Events
- 这些事件 Events,通常是由其他线程触发
- 这些事件 Events,在 context 中执行处理
- context 允许我们在 verticle 的 EventLoop 线程上回调处理程序
- 然后,所以,就有了混合线程模型呀…………(很简单的)
/**
* 混合线程模型
* 在第三方库的线程中,使用 context.runOnContext 实现让代码在 EvenLoop 线程上执行
*/
public class MixedThreading extends AbstractVerticle {
private final Logger logger = LoggerFactory.getLogger(MixedThreading.class);
@Override
public void start() {
Context context = vertx.getOrCreateContext();
new Thread(() -> {
try {
run(context);
} catch (InterruptedException e) {
logger.error("Woops", e);
}
}).start();
}
private void run(Context context) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(1);
logger.info("I am in a non-Vert.x thread");
// 这些代码将会在 EventLoop 线程上执行
context.runOnContext(v -> {
logger.info("I am on the event-loop");
vertx.setTimer(1000, id -> {
logger.info("This is the final countdown");
latch.countDown();
});
});
logger.info("Waiting on the countdown latch...");
latch.await();
logger.info("Bye!");
}
public static void main(String[] args) {
Vertx vertx = Vertx.vertx();
vertx.deployVerticle(new MixedThreading());
}
}
没了,我自个也不懂,真的……
新手集群部署指北
Vertx 的集群管理器确保节点可以通过事件总线交换消息。且具备以下特性:
- Group 关系和服务发现:允许发现新节点、维护当前节点列表以及检测节点下线
- 共享数据:允许在集群范围内维护 map lock 和 counters,以便所有节点共享相同的值。分布式锁可用于节点之间的服务协调。
- subscribe topology (拓扑) 允许每个节点都能知道其他节点的事件总线订阅情况。这对于通过分布式事件总线高效地调度消息非常有用。如果一个节点在目标 a.b.c 上没有订阅者,那么将事件发送到该节点就没有意义
图:Vertx 秃头的集群管理器
Vertx 有好几种 ClusterManager 实现
比较有名的有:Hazelcast, Infinispan, Apache Ignite, and Apache ZooKeeper 等等
- Hazelcast:Hazelcast是基于内存的数据网格开源项目,轻量化和简单易用是Hazelcast的设计目标。Hazelcast以Jar包的方式发布(
通过博主实际运行测试,大概消耗300M(可压缩到 100M)物理内存,JVM 内存 40M 左右)持久化需要 enterprise 版本支持 - Infinispan:开源的数据网格平台,在分布式模式下,Infinispan可以将集群缓存起来并公开大容量的堆内存,快速轻量级核心(
通过博主实测,本地启动,内存在562M,JVM 内存100M) - Apache Ignite:
Apache Ignite是一款以内存为中心的分布式数据库、缓存和处理平台。可以在PB级数据中,以内存级的速度进行事务性、分析性以及流式负载的处理。使用springBoot开发,支持完整的sql(内存需求10 G,博主未测,主要不是因为笔记本内存不够…………) - ZooKeeper:必须要集群,内存消耗甚大(ZK 单机版太不稳定,一定要
集群,大概几百M *至少 3 个节点博主想了想……还是别测了……) - Hazelcast 是 VertX 默认的集群管理器
- 应该使用什么 ClusterManager ? 这取决于您的实际运行环境……
算了不写了,剩下的下次再写,博主一会还要挨面 😀 背题去………………