分布式集群管理
你当前正在浏览历史版本 Vert.x 5.0.12 的文档。 访问本页面的 最新版本。

Infinispan 集群管理器 (vertx-infinispan)

该组件为 Vert.x 提供了基于 Infinispan 数据网格的集群管理器(Cluster Manager)实现。

在项目构建描述文件中添加以下依赖:

  • Maven(在 pom.xml 中):

<dependency>
  <groupId>io.vertx</groupId>
  <artifactId>vertx-infinispan</artifactId>
  <version>5.0.12</version>
</dependency>
  • Gradle(在 build.gradle 文件中):

compile 'io.vertx:vertx-infinispan:5.0.12'

在 Vert.x 中,集群管理器承担以下核心协作职责:

  • 集群中 Vert.x 节点的自动发现与成员拓扑维护

  • 维护全集群范围的 Event Bus 主题订阅者映射表(以便获知哪些节点对哪些 Event Bus 地址感兴趣)

  • 分布式映射(Distributed Map)支持

  • 分布式锁(Distributed Locks)

  • 分布式计数器(Distributed Counters)

请注意:集群管理器并不负责节点间的 Event Bus 实际消息传输;节点间的数据通信由 Vert.x 直接通过底层的 TCP 连接通道高效完成。

使用 Infinispan 集群管理器

若使用 Vert.x 命令行工具启动应用,需确保将对应的 jar 包(如 vertx-infinispan-5.0.12.jar)放置在 Vert.x 安装目录下的 lib 目录中。

在基于 Maven 或 Gradle 的工程中,只需直接引入依赖 io.vertx:vertx-infinispan:5.0.12 即可。

当该 jar 包存在于应用的类路径(Classpath)中时,Vert.x 在启动集群模式时将自动检测并将其激活为默认集群管理器。 请确保类路径中未引入其他集群管理器实现,否则 Vert.x 可能会误选其他集群组件。

若通过代码方式嵌入式启动 Vert.x,可以在配置选项中显式指定该集群管理器:

ClusterManager mgr = new InfinispanClusterManager();

Vertx.builder()
  .withClusterManager(mgr)
  .buildClustered().onComplete(res -> {
  if (res.succeeded()) {
    Vertx vertx = res.result();
    // 集群 Vert.x 实例就绪
  } else {
    // 启动失败!
  }
});

配置 Infinispan 集群管理器

默认的集群管理器配置可以通过 infinispan.xml 和/或 jgroups.xml 文件进行定制覆盖。 前者用于配置 Infinispan 数据网格(Data Grid)拓扑与缓存策略,后者用于配置底层 JGroups 的组成员管理与节点发现通道。

你可以将其中一个或两个文件放置在类路径根目录下。 若将应用打包为可执行 Fat Jar,配置文件必须位于 Fat Jar 的根路径下;若使用外部配置文件,必须将包含该文件的 目录 追加到类路径中。例如,配合 Vert.x 启动器使用时配置如下:

# 如果 infinispan.xml 和/或 jgroups.xml 位于当前目录:
java -jar my-app.jar -cp . -cluster

# 如果配置文件位于 conf 目录:
java -jar my-app.jar -cp conf -cluster

另一种更灵活的覆盖方式是通过系统属性指定文件路径:vertx.infinispan.config 与/或 vertx.jgroups.config。

# 加载位于文件系统中的外部绝对/相对路径配置:
java -Dvertx.infinispan.config=./config/my-infinispan.xml -jar ... -cluster

# 或者加载类路径中指定包路径下的配置:
java -Dvertx.infinispan.config=my/package/config/my-infinispan.xml -jar ... -cluster

集群管理器将优先在类路径中检索该文件,未命中时平滑降级至文件系统查找。

当系统属性存在时,其优先级高于类路径下的任何 infinispan.xml 或 jgroups.xml。

这些 XML 文件的底层配置规范与属性结构在 Infinispan 和 JGroups 官方文档中有详尽说明。

若类路径中存在 jgroups.xml 文件,或显式配置了 vertx.jgroups.config 系统属性,它将覆盖在 Infinispan 配置文件中定义的任何 JGroups stack-file 引用路径。

默认的 JGroups 配置采用组播(Multicast)进行节点发现,采用 TCP 进行组管理通信。请确保局域网开启了组播支持。

有关如何采用其他传输协议或定制组播通道,请参阅 Infinispan 与 JGroups 官方手册。

复用既有的 Infinispan CacheManager

你可以在构造集群管理器时传入一个既有的 DefaultCacheManager 实例,以复用应用中已初始化的缓存管理器:

ClusterManager mgr = new InfinispanClusterManager(cacheManager);

Vertx.builder()
  .withClusterManager(mgr)
  .buildClustered().onComplete(res -> {
  if (res.succeeded()) {
    Vertx vertx = res.result();
    // 集群 Vert.x 实例就绪
  } else {
    // 启动失败!
  }
});

在此场景下,Vert.x 并不拥有该 CacheManager 的生命周期主权,因此在 Vert.x 实例关闭时不会将其强行停止。

请注意:自定义传入的 Infinispan 容器必须预先声明以下核心缓存拓扑:

<cache-container default-cache="distributed-cache">
  <distributed-cache name="distributed-cache"/>
  <replicated-cache name="__vertx.subs"/>
  <replicated-cache name="__vertx.haInfo"/>
  <replicated-cache name="__vertx.nodeInfo"/>
  <distributed-cache-configuration name="__vertx.distributed.cache.configuration"/>
</cache-container>

打包可执行 Uber/Fat JAR

Infinispan 依赖 Java 原生的 ServiceLoader SPI 机制在运行时动态发现类实现。在构建可执行 Fat/Uber Jar 时,构建工具必须配置合并 SPI 描述文件(META-INF/services/)。

若使用 Maven 及 Maven Shade Plugin,配置如下:

<configuration>
  <transformers>
    <transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
    <!-- ... -->
  </transformers>
  <!-- ... -->
</configuration>

若使用 Gradle 及 Gradle Shadow Plugin:

shadowJar {
  mergeServiceFiles()
}

此外,Infinispan 依赖 多版本兼容 JAR (Multi-Release JAR Files) 特性以提供针对不同 Java 发行版优化的字节码。因此,可执行 Fat Jar 的 MANIFEST.MF 文件中必须包含如下声明:

Multi-Release: true

Kubernetes 云原生容器环境配置

在 Kubernetes 环境下,底层 JGroups 可以配置为通过 Kubernetes API (KUBE_PING) 或 DNS (DNS_PING) 进行 Pod 发现。本文推荐并采用基于 DNS 的发现方案。

首先,通过系统属性强制 JVM 优先使用 IPv4 协议栈:

-Djava.net.preferIPv4Stack=true

接着,将系统属性 vertx.jgroups.config 指向 default-configs/default-jgroups-kubernetes.xml。该 JGroups 栈配置文件内嵌在 infinispan-core jar 中,专为 Kubernetes 优化预设:

-Dvertx.jgroups.config=default-configs/default-jgroups-kubernetes.xml

请注意:从 Infinispan 16 开始,该内置文件路径变更为 org/infinispan/configuration/default-jgroups-kubernetes.xml:

-Dvertx.jgroups.config=org/infinispan/configuration/default-jgroups-kubernetes.xml

同时,配置 JGroups DNS 查询域名以发现集群 Pod:

-Djgroups.dns.query=MY-SERVICE-DNS-NAME

其中 MY-SERVICE-DNS-NAME 必须对应一个 Kubernetes 无头服务 (Headless Service) 的 DNS 名称。JGroups 借此识别集群中的所有 Pod 成员。Headless Service 定义示例如下:

apiVersion: v1
kind: Service
metadata:
  name: clustered-app
spec:
  selector:
    cluster: clustered-app (2)
  ports:
    - name: jgroups
      port: 7800 (1)
      protocol: TCP
  publishNotReadyAddresses: true (3)
  clusterIP: None
1 JGroups 底层 TCP 通信端口
2 匹配所有带有 cluster: clustered-app 标签的集群 Pod 成员
3 必须设为 true,确保未通过 Readiness 就绪检查的 Pod 也能被 JGroups 提前发现组网

最后,将 cluster: clustered-app 标签打到所有参与集群的 Deployment Pod 模板中:

apiVersion: apps/v1
kind: Deployment
spec:
  template:
    metadata:
      labels:
        cluster: clustered-app

滚动更新策略 (Rolling Updates)

在进行 Kubernetes 滚动更新时,Infinispan 官方团队 强烈推荐 逐个(One by One)替换 Pod 实例。

为此,Kubernetes 部署策略应约束为:

  • 同一时刻最多只启动 1 个新 Pod

  • 整个过程最多允许 1 个 Pod 处于不可用状态

spec:
  strategy:
    type: Rolling
    rollingParams:
      updatePeriodSeconds: 10
      intervalSeconds: 20
      timeoutSeconds: 600
      maxUnavailable: 1 (1)
      maxSurge: 1 (2)
1 更新期间允许不可用的 Pod 数量上限
2 允许超出目标副本数的最大并发创建 Pod 数量

此外,Pod 的就绪探针(Readiness Probe)必须紧密结合集群健康度。请参阅 集群运维指南 了解如何结合 Vert.x Health Checks 实现动态就绪探针。

Docker Compose 环境配置

确保在启动容器的 JVM 时追加以下系统参数:

-Djava.net.preferIPv4Stack=true -Djgroups.bind.address=NON_LOOPBACK

该参数指示 JGroups 自动识别并绑定由 Docker 容器网络创建的私有虚拟网卡。

集群故障排查指南

如果默认的组播发现无法正常组网,请按以下常见原因排查:

1. 主机未开启组播

在 macOS 等工作站环境中,组播通常默认处于未启用状态。请根据操作系统指引开启组播路由。

2. 绑定了错误的网络网卡

如果机器上存在多个网卡接口(例如安装了虚拟机或 VPN),JGroups 可能会绑定错误的网卡。

可以通过在 XML 配置中的 bind_addr 属性显式指定 IP 地址:

<TCP bind_addr="192.168.1.20" ... />
<MPING bind_addr="192.168.1.20" ... />

或者直接通过系统属性 jgroups.bind.address 进行全局覆盖:

-Djgroups.bind.address=192.168.1.20

在集群模式下运行 Vert.x 时,还必须确保 Vert.x Event Bus 同样绑定到正确的网络接口。在命令行中可以通过 cluster-host 参数显式指定:

vertx run myverticle.js -cluster -cluster-host your-ip-address

其中 your-ip-address 应与 JGroups 配置中的 IP 保持一致。代码中可调用 options.getEventBusOptions().setHost(ip) 进行设置。

3. VPN 软件网络干扰

VPN 软件通常会创建不支持组播的虚拟网络接口。若启用了 VPN 且未显式指定正确的物理网卡,系统可能会错误选择 VPN 接口导致无法通信。解决方法是按照上一小节显式指定物理局域网 IP。

4. 生产环境不支持组播时的替代方案

在公有云容器等生产环境中,组播通常被网络基础设施直接屏蔽。此时应切换为对应的静态或云端发现协议,例如使用 TCPPING 通过 TCP Socket 静态列表组网,或在 AWS EC2 上使用 S3_PING。更多详情请参阅 JGroups 官方文档。

5. IPv6 兼容性问题

如果遇到 IPv6 主机地址解析与绑定异常,可以通过系统属性强制退回 IPv4:

-Djava.net.preferIPv4Stack=true

6. 开启调试日志

若使用默认的 JUL 日志,可以在类路径中添加 vertx-default-jul-logging.properties 文件并配置:

org.infinispan.level=INFO
org.jgroups.level=INFO
java.util.logging.ConsoleHandler.level=INFO
java.util.logging.FileHandler.level=INFO

Infinispan 日志框架

Infinispan 底层基于 JBoss Logging 抽象层,可无缝桥接 Log4j2、SLF4J 等多种主流日志实现。向类路径添加对应日志 jar 即可自动识别。若存在多个日志后端,可通过系统属性显式指定:

-Dorg.jboss.logging.provider=log4j2

更多详情请参阅 JBoss Logging 手册。

JGroups 日志框架

JGroups 默认采用 JDK 原生日志,并在类路径中发现 Log4j 或 Log4j2 时自动适配。详细配置请参阅 JGroups 日志文档。

SharedData 扩展能力

AsyncMap 流式读取

InfinispanAsyncMap API 支持将 Map 中的 Key、Value 或 Entry 作为反应式流(ReadStream)提取,这在大批量数据全量处理时非常有用:

InfinispanAsyncMap<K, V> infinispanAsyncMap = InfinispanAsyncMap.unwrap(asyncMap);
ReadStream<K> keyStream = infinispanAsyncMap.keyStream();
ReadStream<V> valueStream = infinispanAsyncMap.valueStream();
ReadStream<Map.Entry<K, V>> entryReadStream = infinispanAsyncMap.entryStream();

集群运维与高可用治理

Infinispan 集群管理器将 Vert.x 节点作为 Infinispan 数据网格中的成员协同运行。因此,集群运维需要理解数据重平衡与脑裂现象。

数据重平衡 (Rebalancing)

每个 Vert.x 节点均持有集群共享数据的一部分切片(Event Bus 订阅映射、异步 Map 数据、集群计数器等)。 当有成员加入或脱离集群时,Infinispan 会在新成员集合间重新平衡(Rebalance)缓存分区。重平衡耗时取决于集群节点数及数据体量。

脑裂现象 (Split-Brain Syndrome)

在复杂的网络基础设施中,网络分区故障在所难免,此时集群可能会被割裂为彼此失联的若干独立孤岛。 Infinispan 具备在网络恢复后自动将各孤岛重新合并为统一样群的能力。但在此恢复期间,部分 Event Bus 消费者可能暂时收不到消息,高可用(HA)故障转移也可能暂时受阻。

在工程实践中,网络分区与以下异常现象高度相似,极易混淆: - 长时间 JVM GC 停顿(STW 导致心跳超时被踢出) - 部署新版本时一次性粗暴强杀大量节点

生产实践推荐

优雅停机 (Graceful Shutdown)

严禁在生产中强制强杀进程(如直接 kill -9)。虽然崩溃不可避免,但优雅停机能够主动通知其余节点接管数据分区,使集群以最快速度稳定收敛。

逐节点平滑更替 (One Node After The Other)

在滚动更新、集群扩缩容时,必须 逐个(One by One) 增删节点。 逐个下线节点可避免集群误判为发生网络分区,逐个上线则允许轻量级递增执行数据重平衡。

可以通过结合 Vert.x Health Checks 实现集群健康状态检查:

Handler<Promise<Status>> procedure = ClusterHealthCheck.createProcedure(vertx, true);
HealthChecks checks = HealthChecks.create(vertx).register("cluster-health", procedure);

然后通过 Vert.x Web 暴露为 HTTP 就绪端点供容器探针拉取:

Router router = Router.router(vertx);
router.get("/readiness").handler(HealthCheckHandler.createWithHealthChecks(checks));