<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-infinispan</artifactId>
<version>5.2.1</version>
</dependency>
Infinispan 集群管理器 (vertx-infinispan)
该组件为 Vert.x 提供了基于 Infinispan 数据网格的集群管理器(Cluster Manager)实现。
在项目构建描述文件中添加以下依赖:
-
Maven(在
pom.xml中):
-
Gradle(在
build.gradle文件中):
compile 'io.vertx:vertx-infinispan:5.2.1'
在 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.2.1.jar)放置在 Vert.x 安装目录下的 lib 目录中。
在基于 Maven 或 Gradle 的工程中,只需直接引入依赖 io.vertx:vertx-infinispan:5.2.1 即可。
当该 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 官方文档。
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));