design\project\Vertx(四)

186 阅读10分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第 2 天

Vertx 官方 DEMO 示栗(二)

Vertx 快速上手指北(二)

第二章: Vertx 中的 actors:Verticle(基本服务)

本章大纲:

  1. EventLoop 不可以被阻塞(blocked)
  2. Verticle 异步初始化
  3. 如何发布(deploy)和撤销发布(undeploy)服务 Verticle
  4. 如何应对需要运行阻塞任务或长时间运行的代码
  5. Verticle 究竟是什么?
    1. Verticel 以及其执行环境
    2. Context 对象
    3. 桥接 Vertx 和 非 Vertx 的线程模型
    4. 集群部署

个人总结:

  1. 本章概要:Vertx 程序由一个个独立的 “服务(Verticle)” 组成
  2. 本章是非常实用的一章,介绍了 新手 编写 Vertx 程序时 常犯的错误
    1. 新手将服务部署为 work 导致的困惑(博主擅自加塞,别问我为啥知道)
    2. 新手阻塞事件队列(EventLoop) 引发的血案
    3. 新手该如何执行阻塞代码(长时间任务)
    4. 新手发布服务成功后忘记调用 startPromise.complete() 引发的血案
    5. 新手如何发布,撤销发布服务
    6. 新手由于不熟悉 Verticle 执行环境,导致不打印异常引发的困惑
    7. 新手由于不熟悉 Verticle 执行环境,写过的那些废代码
    8. 新手集群部署指北

个人认为:为了实现异步响应式编程,为了可以水平扩展,Vertx 有了 Verticle

  • 你可以把 Verticle 看成是微服务中的一个独立运行的 “服务”
  • 这个服务有生命周期,可以被发布,被卸载
  • 这个 “服务” 很小,小到仅仅是一个实现 Verticle 接口的实现类
    • 那是比 “组件(一般由好几个类组成)” 还小的 “代码块”
    • 可以部署多份,多线程并发执行
    • 可以启动多个进程,在隔离环境中并发执行
    • 可以部署成集群,并行执行

image.png

特别 “细” 的,并行皮卡丘们,啊不 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 接口 😎
  • 博主试过了,真香
新手该如何执行阻塞代码(长时间任务)

其实比较简单:把整条河买下来,让那个喜欢截流的同学去别地玩去…… 😎(机智)

1.3.png

图:把喜欢截流的同学仍海里(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 实例可以访问正在处理事件的线程

1.4.png

图: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 上没有订阅者,那么将事件发送到该节点就没有意义

2.4.png

图: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 ? 这取决于您的实际运行环境……

算了不写了,剩下的下次再写,博主一会还要挨面 😀 背题去………………