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

Hazelcast 集群管理器 (vertx-hazelcast)

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

它是 Vert.x 命令行工具默认采用的集群管理器,但由于 Vert.x 的集群管理器设计完全可插拔,因此你也可以随时替换为其他实现。

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

  • Maven(在 pom.xml 中):

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

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

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

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

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

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

  • 分布式锁(Distributed Locks)

  • 分布式计数器(Distributed Counters)

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

使用 Hazelcast 集群管理器

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

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

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

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

ClusterManager mgr = new HazelcastClusterManager();

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

配置 Hazelcast 集群管理器

通过 XML 配置文件配置

集群管理器默认加载打包在 jar 内部的 default-cluster.xml 配置文件。

若需覆盖该默认配置,可以在类路径根目录下提供自定义的 cluster.xml 文件。 若将应用打包为可执行 Fat Jar,cluster.xml 必须位于 Fat Jar 的根路径下;若使用外部配置文件,必须将包含该文件的 目录 追加到类路径中。例如,配合 Vert.x 启动器使用时配置如下:

# 如果 cluster.xml 位于当前工作目录:
java -jar ... -cp . -cluster
vertx run MyVerticle -cp . -cluster

# 如果 cluster.xml 位于 conf 目录:
java -jar ... -cp conf -cluster

另一种更灵活的覆盖方式是通过系统属性 vertx.hazelcast.config 指定外部配置文件路径:

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

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

当系统属性 vertx.hazelcast.config 存在时,其优先级高于类路径下的任何 cluster.xml;若根据该系统属性加载失败,系统将平滑降级至 cluster.xml 或 Hazelcast 默认配置。

Vert.x 不支持 使用 Hazelcast 原生的 -Dhazelcast.config 系统属性,请勿使用该参数。

XML 配置文件的属性细节在 Hazelcast 官方手册中有全面详尽的说明。

以编程方式配置

你也可以完全通过代码构造 Hazelcast 配置:

Config hazelcastConfig = new Config();

// 此处配置具体属性(省略)

ClusterManager mgr = new HazelcastClusterManager(hazelcastConfig);

Vertx
  .builder()
  .withClusterManager(mgr)
  .buildClustered()
  .onComplete(res -> {
  if (res.succeeded()) {
    Vertx vertx = res.result();
  } else {
    // 启动失败!
  }
});

有时在既有 XML 配置的基础上仅微调部分字段会非常便利,例如动态修改集群名称:

Config hazelcastConfig = ConfigUtil.loadConfig();

hazelcastConfig.setClusterName("my-cluster-name");

ClusterManager mgr = new HazelcastClusterManager(hazelcastConfig);

Vertx
  .builder()
  .withClusterManager(mgr)
  .buildClustered()
  .onComplete(res -> {
  if (res.succeeded()) {
    Vertx vertx = res.result();
  } else {
    // 启动失败!
  }
});

ConfigUtil#loadConfig 负责解析 XML 配置文件并将其转换为 Hazelcast 的 Config 对象,解析优先级依次为:

  1. vertx.hazelcast.config 系统属性指定的文件路径(若存在)

  2. 类路径下的 cluster.xml 文件(若存在)

  3. 默认内置的 default-cluster.xml

节点发现方式

Hazelcast 支持多种节点发现机制。 默认配置使用组播(Multicast),因此你的局域网环境必须支持并开启组播广播。

如需使用 TCP-IP、云厂商发现等其他方式,请参阅 Hazelcast 官方文档。

通过系统属性覆盖本地与公网发布地址

有时集群节点绑定的内部监听地址与其他成员节点无法直接互通(例如处于多网络平面或云环境安全组隔离)。可以通过以下系统属性分别指定本地绑定地址与对外广播的公网/路由地址:

-Dhazelcast.local.localAddress=172.16.5.131 -Dhazelcast.local.publicAddress=104.198.78.81

复用既有的外部 Hazelcast 集群

你可以传入一个现存的 HazelcastInstance 实例以复用已有集群:

ClusterManager mgr = new HazelcastClusterManager(hazelcastInstance);
Vertx
  .builder()
  .withClusterManager(mgr)
  .buildClustered()
  .onComplete(res -> {
  if (res.succeeded()) {
    Vertx vertx = res.result();
  } else {
    // 启动失败!
  }
});

在此场景下,Vert.x 并不拥有该 Hazelcast 集群实例的生命周期主权,因此在 Vert.x 关闭时不会停机 Hazelcast 实例。

请注意:传入的自定义 Hazelcast 实例必须包含以下核心拓扑定义:

Vert.x 所需的最小 Hazelcast 拓扑配置
<member-attributes>
  <attribute name="__vertx.nodeId">unique-identifier</attribute>
</member-attributes>

<multimap name="__vertx.subs">
  <backup-count>1</backup-count>
  <value-collection-type>SET</value-collection-type>
</multimap>

<map name="__vertx.haInfo">
  <backup-count>1</backup-count>
</map>

<map name="__vertx.nodeInfo">
  <backup-count>1</backup-count>
</map>
__vertx.nodeId 被 Vert.x 用作节点在集群内的唯一标识。必须确保不同成员节点配置了不同的唯一值。
本模块不支持 Hazelcast Client 或 Smart Client 模式。
必须确保 Hazelcast 优先于 Vert.x 启动并在 Vert.x 之后关闭;此外应禁用 Hazelcast 自带的 JVM Shutdown Hook(可通过配置或系统属性关闭)。

调整故障节点踢出超时时间

默认情况下,若 Hazelcast 连续 60 秒未收到某节点的心跳,该节点将被判定为下线并移出集群。 可以通过系统属性 hazelcast.max.no.heartbeat.seconds 调整该心跳阈值:

-Dhazelcast.max.no.heartbeat.seconds=5

设置后,无心跳达到 5 秒即可触发故障节点剔除。

更多详情请参阅 Hazelcast 系统属性官方说明。

集群故障排查指南

如果默认的组播配置无法组网,请按以下原因排查:

1. 主机未开启组播

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

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

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

可以通过在 XML 配置的 interfaces 元素中显式指定 IP 地址(必须将 enabled 设置为 true):

<interfaces enabled="true">
  <interface>192.168.1.20</interface>
</interfaces>

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

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

其中 your-ip-address 应与 Hazelcast 配置中的 IP 保持一致。

3. VPN 软件网络干扰

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

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

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

5. 开启调试日志

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

com.hazelcast.level=INFO
java.util.logging.ConsoleHandler.level=INFO
java.util.logging.FileHandler.level=INFO

Hazelcast 日志框架

Hazelcast 默认采用 JDK 原生日志。若需重定向到其他日志门面(如 SLF4J),可以设置系统属性:

-Dhazelcast.logging.type=slf4j

覆盖 Hazelcast 驱动版本

你可以选用自定义版本的 Hazelcast 驱动。操作步骤为:

  • 在应用类路径中引入目标版本的依赖

  • 若构建 Fat Jar,需在构建脚本中排除传递依赖并显式指定新版本

Maven 示例:

<dependency>
  <groupId>com.hazelcast</groupId>
  <artifactId>hazelcast</artifactId>
  <version>ENTER_YOUR_VERSION_HERE</version>
</dependency>
<dependency>
  <groupId>io.vertx</groupId>
  <artifactId>vertx-hazelcast</artifactId>
  <version>5.0.12</version>
</dependency>

Gradle 示例:

dependencies {
 compile ("io.vertx:vertx-hazelcast:5.0.12"){
   exclude group: 'com.hazelcast', module: 'hazelcast'
 }
 compile "com.hazelcast:hazelcast:ENTER_YOUR_VERSION_HERE"
}

Kubernetes 云原生容器环境配置

首要步骤是在 Hazelcast 配置中声明自动发现插件(通过自定义 cluster.xml 或代码均可,参阅 配置 Hazelcast 集群管理器)。

Hazelcast 支持两种 Kubernetes 发现模式:Kubernetes API 模式与 DNS Lookup 域名解析模式。本文推荐使用基于 DNS 解析的发现方式。

在网络配置中启用 Kubernetes 插件:

<hazelcast>
  <network>
    <join>
      <auto-detection enabled="false"/> (1)
      <multicast enabled="false"/> (2)
      <kubernetes enabled="true"> (3)
        <service-dns>MY-SERVICE-DNS-NAME</service-dns> (4)
      </kubernetes>
    </join>
  </network>
</hazelcast>
1 禁用自动检测插件
2 禁用局域网组播发现
3 激活 Kubernetes 发现插件
4 Service 的 DNS 域名,通常形如 MY-SERVICE-NAME.MY-NAMESPACE.svc.cluster.local

上述配置仅包含网络发现片段,实际配置中必须同时包含 最小必需拓扑配置。

MY-SERVICE-DNS-NAME 对应一个 Kubernetes Headless Service 的域名,示例如下:

apiVersion: v1
kind: Service
metadata:
  namespace: MY-NAMESPACE
  name: MY-SERVICE-NAME
spec:
  type: ClusterIP
  clusterIP: None
  selector:
    component: MY-SERVICE-NAME (1)
  ports:
    - name: hazelcast
      port: 5701
      protocol: TCP
1 通过标签选择集群 Pod 成员

最后,在 Deployment 的 Pod 模板中打上对应的标签:

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  namespace: MY-NAMESPACE
spec:
  template:
    metadata:
      labels:
        component: MY-SERVICE-NAME

更多细节请查阅 Kubernetes Auto Discovery 官方文档。

滚动更新策略 (Rolling Updates)

在进行滚动更新时,推荐 逐个(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)必须紧密结合集群健康状态。详情请参阅 集群运维指南。

集群运维与高可用治理

Hazelcast 集群管理器将 Vert.x 节点作为 Hazelcast 集群中的节点协同运作,因此需遵循数据分区与脑裂防范准则。

数据分区迁移 (Data Partitioning)

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

脑裂现象 (Split-Brain Syndrome)

在复杂的网络基础设施中,网络分区故障在所难免,此时集群可能会被割裂为彼此失联的独立孤岛。 Hazelcast 具备在网络恢复后自动将各孤岛重新合并为统一集群的能力。但在此恢复期间,部分 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);
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));

采用轻量成员节点 (Lite Members)

为了将 Vert.x 集群在拓扑变更时的等待耗时降至最低,你可以引入外部专属的数据存储节点,并将 Vert.x 节点全部声明为 Lite Members(轻量成员)。

Lite Members 像常规成员一样完全参与集群通信与协调,但它们 不持有任何数据分区。因此在增删 Vert.x 节点时,Hazelcast 完全不需要执行数据分区迁移!

必须预先启动外部数据存储节点,因为 Hazelcast 不允许创建仅由 Lite Members 构成的纯计算集群。

Vert.x 节点可以在 XML 配置中声明为轻量成员:

<lite-member enabled="true"/>

也可以通过代码动态配置:

Config hazelcastConfig = ConfigUtil.loadConfig()
  .setLiteMember(true);

ClusterManager mgr = new HazelcastClusterManager(hazelcastConfig);

Vertx
  .builder()
  .withClusterManager(mgr)
  .buildClustered()
  .onComplete(res -> {
  if (res.succeeded()) {
    Vertx vertx = res.result();
  } else {
    // 启动失败!
  }
});