Infinispan 集群管理器

本项目基于 Infinispan 实现了一个集群管理器。

这个集群管理器的实现由以下依赖引入:

<dependency>
 <groupId>io.vertx</groupId>
 <artifactId>vertx-infinispan</artifactId>
 <version>4.1.8</version>
</dependency>

Vert.x 集群管理器包含以下几项功能:

  • 发现并管理集群中的节点

  • 管理集群的 EventBus 地址订阅清单(这样就可以轻松得知集群中的哪些节点订阅了哪些 EventBus 地址)

  • 分布式 Map 支持

  • 分布式锁

  • 分布式计数器

Vert.x 集群器 并不 处理节点之间的通信。在 Vert.x 中,集群节点间通信是直接由 TCP 连接处理的。

使用集群管理器

如果通过命令行来使用 Vert.x,对应集群管理器的 jar 包(名为 vertx-infinispan-4.1.8.jar ) 应该在 Vert.x 中安装路径的 lib 目录中。

若您希望在集群中使用此集群管理器,只需要在您的Vert.x Maven 或 Gradle 工程中添加依赖: io.vertx:vertx-infinispan:4.1.8

如果(集群管理器的)jar 包在 classpath 中,Vert.x将自动检测到并将其作为集群管理器。 需要注意的是,要确保 Vert.x 的 classpath 中没有其它的集群管理器实现, 否则会使用错误的集群管理器。

编程内嵌 Vert.x 可以在创建 Vert.x 实例时, 通过代码显式配置 Vert.x 集群管理器,例如:

ClusterManager mgr = new InfinispanClusterManager();

VertxOptions options = new VertxOptions().setClusterManager(mgr);

Vertx.clusteredVertx(options, res -> {
  if (res.succeeded()) {
    Vertx vertx = res.result();
  } else {
    // 失败!
  }
});

配置集群管理器

默认的集群管理器配置可以通过 infinispan.xml 及/或 jgroups.xml 文件进行修改。 前者配置数据网格(data grid),后者配置组管理和集群成员发现。

您可以在 classpath 中替换这些配置文件(其中一个或两个)。 如果想在 fat jar 中内嵌自己的配置文件,这些文件必须在 fat jar 的根目录中。 如果这些配置文件是外部文件,则必须将其所在的 目录 添加至 classpath 中。 举个例子,如果使用 Vert.x 的 launcher 类启动应用,则 classpath 应该设置为:

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

# 如果 infinispan.xml 及/或 jgroups.xml 在 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

# 或从 classpath 中加载一个文件为自定义配置文件
java -Dvertx.infinispan.config=my/package/config/my-infinispan.xml -jar ... -cluster

集群管理器优先在 classpath 查找指定的配置文件,如果没有找到,则在文件系统中查找指定的配置文件。

如果设置了上述系统属性,则会覆盖 classpath 中的 infinispan.xmljgroups.xml 文件。

jgroup.xmlinfinispan.xml 分别是 JGroups 、 Infinispan 配置文件。在对应的官方可以网站可以详细的配置攻略。

重要
如果 classpath 中包含 jgroups.xml 文件,同时也设置了 vertx.jgroups.config 系统属性, 那么 Infinispan 配置中所有的 JGroups 的 stack-file 路径配置会被 jgroups.xml 中的覆盖。

在默认的 JGroups 配置中,节点发现使用组播,组管理使用 TCP 。 请确认您的网络支持组播。

有关如何配置或使用其他传输方式的完整文档, 请查询 Infinispan 或 JGroups 文档。

使用已有的 Infinispan 缓存管理器

构造集群管理器时,传入已有的 DefaultCacheManager 可以复用已有的缓存管理器。

ClusterManager mgr = new InfinispanClusterManager(cacheManager);

VertxOptions options = new VertxOptions().setClusterManager(mgr);

Vertx.clusteredVertx(options, res -> {
  if (res.succeeded()) {
    Vertx vertx = res.result();
  } else {
    // 失败!
  }
});

在这种情况下,Vert.x 并不是缓存管理器的所有者,因此不能在关闭 Vert.x 时停止 Infinispan 。

请注意,可通过如下配置以定制 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>

打包可执行的 fat-jar 包

Infinispan 在运行时使用 Java SPI 机制(ServiceLoader) 发现一些类/接口的实现。

构建可执行的超级 JAR(也称为 fat-JAR )包时,必须在构建工具中配置将服务描述文件打包。

如果您使用 Maven 及 Maven Shade 插件,插件应该配置如下:

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

如果您使用 Gradle 及 Gradle Shadow 插件:

shadowJar {
 mergeServiceFiles()
}

适配 Kubernetes

在 Kubernetes 上,JGroups 的节点发现配置可选择使用 Kubernetes API (KUBE_PING) 或 DNS (DNS_PING)。 本文将使用 DNS 发现。

首先通过以下系统属性配置 JVM 强制使用 IPv4:

-Djava.net.preferIPv4Stack=true

然后设置系统属性 vertx.jgroups.configdefault-configs/default-jgroups-kubernetes.xml。 JGroups 的 stack-file 在 infinispan-core 的JAR包中,并已为 Kubernetes 做了预配置。

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

同时设置 JGroups DNS 查询以便发现集群成员。

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

其中 MY-SERVICE-DNS-NAME 的取值必须是一个 Kubernetes 无头 服务(Headless Service) 名,JGroups 会用该名称来标识所有集群成员。 无头服务的创建配置可参考下面代码:

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 标签选择的集群成员

  3. 设置为true,则可以在不干涉就绪探针(readiness probe)逻辑的前提下,发现集群成员

最后,属于集群的所有 Kubernetes 部署需要增加 cluster=clustered-app 标签:

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

滚动更新

Infinispan 团队 建议 在滚动更新期间逐一更换 Pod。

为此,我们必须将 Kubernetes 配置为:

  • 不要同时启动多个新 Pod

  • 在滚动更新过程中,不可用的 Pod 不能多于一个

spec:
 strategy:
   type: Rolling
   rollingParams:
     updatePeriodSeconds: 10
     intervalSeconds: 20
     timeoutSeconds: 600
     maxUnavailable: 1 (1)
     maxSurge: 1 (2)
  1. 在升级过程中允许 不可用的最大 Pod 数

  2. 允许超过预期创建数量的最大 Pod 数(译者注:即,实际创建的 Pod 数量 ≤ 预期 Pod 数量 + maxSurge)

同样地,Pod 的就绪探针(readiness probe)必须考虑集群状态。 请参阅 集群管理 章节,了解如何使用 Vert.x 健康检查 实现准备情况探针。

适配 Docker Compose

确认 JVM 在启动时 设置了下面的配置:

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

通过上述两项系统配置,JGroups 才能正确地选择 Docker 创建的虚拟网络接口。

集群故障排查

如果默认的组播配置不能正常运行,通常有以下原因:

机器禁用组播

通常来说,OSX 默认禁用组播。 请自行Google一下如何启用组播。

使用错误的网络接口

如果机器上有多个网络接口(也有可能是在运行 VPN 的情况下), 那么 JGroups 很有可能使用错误的网络接口。

为了确保 JGroups 使用正确的网络接口,在配置文件中将 bind_addr 设置为指定IP地址。 例如:

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

如果您直接使用了内置的 jgroups.xml 配置文件,也可以通过设置 jgroups.bind.address 系统属性来指定 JGroups 的网络接口:

-Djgroups.bind.address=192.168.1.20

Vert.x 运行在集群模式时,必须确保 Vert.x 获取到正确的网络接口。 在 Vert.x 命令行模式下,可以通过 cluster-host 选项指定集群的网络接口:

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

其中 your-ip-address 与 JGroups 配置中指定的IP地址一致。

若使用编码的方式启动 Vert.x,可以通过 .setHost(java.lang.String) 设置集群的网络接口。

使用VPN

使用VPN是上述问题的变种。 VPN 软件工作时通常会创建虚拟网络接口,但往往不支持组播。 在 VPN 环境中,如果 JGroups 与 Vert.x 没有配置正确的话, 将会选择 VPN 创建的网络接口,而不是正确的网络接口。

所以,如果您的应用运行在 VPN 环境中,请参考上述章节, 设置正确的网络接口。

组播不可用

在某些情况下,由于特殊的运行环境,可能无法使用组播。 在这种情况下,应该配置为其他协议,例如配置 TCPPING 以使用 TCP 套接字,或配置 S3_PING 以使用亚马逊 EC2。

有关其他可用的 JGroups 发现协议及其如何配置的更多信息,请查阅 JGroups文档

使用IPv6的问题

如果在 IPv6 地址配置遇到困难,可以通过设置系统属性 java.net.preferIPv4Stack 强制使用 IPv4:

-Djava.net.preferIPv4Stack=true

开启日志

在排除故障时,开启 Infinispan 和 JGroups 日志很有帮助,可以观察是否组成了集群。 使用默认的 JUL 日志时,在 classpath 中添加 vertx-default-jul-logging.properties 文件可开启 Infinispan 和 JGroups 日志。 这是一个标准 java.util.logging(JUL) 配置文件。 具体配置如下:

org.infinispan.level=INFO
org.jgroups.level=INFO

以及

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

Infinispan 日志配置

Infinispan 依赖与 JBoss Logging 。JBoss Logging 是一个与多种日志框架的桥接器。

请将日志框架实现的jar包放入 classpath 中,JBoss Logging 能够自动检测到并使用。

如果在 classpath 有多种日志框架,可以通过设置系统变量 org.jboss.logging.provider 来指定具体的实现。 例如:

-Dorg.jboss.logging.provider=log4j2

更多配置信息请参考 JBoss日志指南

JGroups 日志配置

JGroups 默认采用 JDK Logging 实现。同时也支持 log4j 与 log4j2 ,只要相应的 jar 包 在 classpath 中。

如果想查阅更详细的信息,或实现自己的日志后端,请参考 JGroups日志文档

SharedData 扩展

AsyncMap 内容流

InfinispanAsyncMap API支持将 AsyncMap 的键、值及 Entry 作为流进行读取。 如果您需要遍历读取很大的 AsyncMap 并进行批量处理,这将很有帮助。

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 集群的成员。 因此,Vert.x 使用 Infinispan 集群管理器时,应遵循 Infinispan 的管理准则。

首先介绍下再平衡(Rebalancing)和脑裂。

再平衡(Rebalancing)

每个 Vert.x 节点都包含部分集群数据,包括:EventBus 订阅,异步 Map,分布式计数器等等。

当有节点加入或离开集群时,Infinispan 会在新的集群拓扑中重新平衡分配(rebalance)缓存条目。 换句话说,它可以移动数据以适应新的集群拓扑。 此过程可能需要一些时间,具体取决于集群数据量和节点数量。

脑裂

在理想环境中,不会出现网络设备故障。 实际上,集群迟早会被分成多个小组,彼此之间不可见。

Infinispan 能够将节点合并回单个集群。 但是,就像数据分区迁移一样,此过程可能需要一些时间。 在集群变回可用之前,某些 EventBus 的消费者可能无法获取到消息。 否则,重新部署故障的 Verticle 过程中无法保证高可用。

注意

很难(或者说基本不可能)区分脑裂和:

  • 长时间的GC暂停 (导致错过了心跳检查),

  • 部署新版本应用时,同时强制关闭了很多节点

建议

考虑到上面讨论的常见集群问题,建议遵循下述的最佳实践。

优雅地关闭

应该避免强行停止集群成员节点(例如,对节点进程使用 kill -9 )。

当然,进程崩溃是不可避免的,但是优雅地关闭进程有助于其余节点更快地恢复稳定状态。

逐个添加或移除节点

滚动更新新版本应用时,或扩大/缩小集群时,应该一个接一个地添加或移除节点。

逐个停止节点可避免集群误以为发生了脑裂。 逐个添加节点可以进行干净的增量数据分区迁移。

可以使用 Vert.x 运行状况检查 来验证集群安全性:

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

完成集群创建后,可以通过 Vert.x Web 路由 Handler 编写的HTTP程序进行健康检查:

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