6.4. HA 单例服务

群集单例服务也称为高可用性(HA)单例,是在群集的多个节点上部署的服务。该服务仅在其中一个节点上提供。运行单例服务的节点通常称为 master 节点。

master 节点出现故障或关闭时,将从剩余的节点中选择另一个 master,并在新 master 上重新启动该服务。除了一个 master 已停止并且另一个主机尚未接管时的简短间隔之外,该服务由一个(仅一个)节点提供。

HA Singleton ServiceBuilder API

JBoss EAP 7 引入了一个新的公共 API,用于构建单例服务,显著简化流程。

SingletonServiceConfigurator 实施会安装其服务,以便异步启动,防止 Modular Service Container(MSC)死锁。

HA Singleton Service Election 策略

如果哪个节点应启动 HA 单例,您可以在 ServiceActivator 类中设置选择策略。

JBoss EAP 提供两种选择策略:

  • 简单选择策略

    简单的选择策略会根据相对年龄选择 master 节点。所需的年龄在 location 属性中配置,这是可用节点列表中的索引,其中:

    • 位置 = 0 - 代表最旧的节点.这是默认值。
    • 位置 = 1 - 是指第 2 个最早的位置,以此类推。

    位置也可能为负数,表示最年轻的节点。

    • 位置 = -1 - 是指最年轻的节点.
    • 位置 = -2 - 是指第二大节点,以此类推。
  • 随机选择策略

    随机选择策略选择随机成员作为单例服务的提供商。

HA 单例服务首选项

HA 单例服务选择策略可以选择性地指定一个或多个首选服务器。此首选服务器在可用时将成为该策略下所有单例应用的主服务器。

您可以通过节点名称或通过出站套接字绑定名称来定义首选项。

注意

节点首选项总是优先于选择策略的结果。

默认情况下,JBoss EAP 高可用性配置提供一个简单的选择策略,名为 default,无首选服务器。您可以通过创建自定义策略和定义首选服务器来设置首选项。

仲裁

当存在网络分区时,单例服务可能出现潜在问题。在这种情况下,也称脑裂情景,节点子集无法互相通信。每组服务器都将另一组中的服务器视为故障,并作为存活的群集继续工作。这可能会导致数据一致性问题。

JBoss EAP 允许您在选择策略中指定仲裁,以防止出现脑裂情形。在进行单例供应商选择前,仲裁指定要存在的最少节点数。

典型的部署场景使用 N/2 + 1 的仲裁,其中 N 是预期的集群大小。这个值可以在运行时更新,并且会立即影响任何活跃的单例服务。

HA Singleton Service Election Listener

选择新的主单例服务提供商后,会触发任何注册的 SingletonElectionListener,向群集的每个成员通知新主提供程序。以下示例演示了 SingletonElectionListener 的用法:

public class MySingletonElectionListener implements SingletonElectionListener {
    @Override
    public void elected(List<Node> candidates, Node primary) {
        // ...
    }
}

public class MyServiceActivator implements ServiceActivator {
    @Override
    public void activate(ServiceActivatorContext context) {
        String containerName = "foo";
        SingletonElectionPolicy policy = new MySingletonElectionPolicy();
        SingletonElectionListener listener = new MySingletonElectionListener();
        int quorum = 3;
        ServiceName name = ServiceName.parse("my.service.name");
        // Use a SingletonServiceConfiguratorFactory backed by default cache of "foo" container
        Supplier<SingletonServiceConfiguratorFactory> factory = new ActiveServiceSupplier<SingletonServiceConfiguratorFactory>(context.getServiceRegistry(), ServiceName.parse(SingletonDefaultCacheRequirement.SINGLETON_SERVICE_CONFIGURATOR_FACTORY.resolve(containerName)));
        ServiceBuilder<?> builder = factory.get().createSingletonServiceConfigurator(name)
            .electionListener(listener)
            .electionPolicy(policy)
            .requireQuorum(quorum)
            .build(context.getServiceTarget());
        Service service = new MyService();
        builder.setInstance(service).install();
    }
}

创建 HA 单例服务应用

以下是作为集群范围的单例创建和部署应用所需的步骤的缩写示例。本例演示了一个查询服务,它会定期查询单例服务来获取运行它的节点的名称。

要查看单例行为,您必须将应用部署到至少两台服务器。无论单例服务是否在同一节点上运行,还是远程获取该值,都是透明的。

  1. 创建 SingletonService 类。getValue() 方法由查询服务调用,提供有关在其上运行的节点的信息。

    class SingletonService implements Service {
        private Logger LOG = Logger.getLogger(this.getClass());
        private Node node;
    
        private Supplier<Group> groupSupplier;
        private Consumer<Node> nodeConsumer;
    
        SingletonService(Supplier<Group> groupSupplier, Consumer<Node> nodeConsumer) {
           this.groupSupplier = groupSupplier;
           this.nodeConsumer = nodeConsumer;
        }
    
        @Override
        public void start(StartContext context) {
            this.node = this.groupSupplier.get().getLocalMember();
    
            this.nodeConsumer.accept(this.node);
    
            LOG.infof("Singleton service is started on node '%s'.", this.node);
        }
    
        @Override
        public void stop(StopContext context) {
            LOG.infof("Singleton service is stopping on node '%s'.", this.node);
    
            this.node = null;
        }
    }
  2. 创建查询服务。它调用单例服务的 getValue() 方法以获取运行它的节点的名称,然后将结果写入服务器日志。

    class QueryingService implements Service {
        private Logger LOG = Logger.getLogger(this.getClass());
        private ScheduledExecutorService executor;
    
        @Override
        public void start(StartContext context) throws {
            LOG.info("Querying service is starting.");
    
            executor = Executors.newSingleThreadScheduledExecutor();
            executor.scheduleAtFixedRate(() -> {
    
            Supplier<Node> node = new PassiveServiceSupplier<>(context.getController().getServiceContainer(), SingletonServiceActivator.SINGLETON_SERVICE_NAME);
             if (node.get() != null) {
                 LOG.infof("Singleton service is running on this (%s) node.", node.get());
             } else {
                 LOG.infof("Singleton service is not running on this node.");
             }
    
            }, 5, 5, TimeUnit.SECONDS);
        }
    
        @Override
        public void stop(StopContext context) {
            LOG.info("Querying service is stopping.");
    
            executor.shutdown();
        }
    }
  3. 实施 SingletonServiceActivator 类,以构建和安装单例服务和查询服务。

    public class SingletonServiceActivator implements ServiceActivator {
    
        private final Logger LOG = Logger.getLogger(SingletonServiceActivator.class);
    
        static final ServiceName SINGLETON_SERVICE_NAME =
                ServiceName.parse("org.jboss.as.quickstarts.ha.singleton.service");
        private static final ServiceName QUERYING_SERVICE_NAME =
                ServiceName.parse("org.jboss.as.quickstarts.ha.singleton.service.querying");
    
        @Override
        public void activate(ServiceActivatorContext serviceActivatorContext) {
            SingletonPolicy policy = new ActiveServiceSupplier<SingletonPolicy>(
                    serviceActivatorContext.getServiceRegistry(),
                    ServiceName.parse(SingletonDefaultRequirement.POLICY.getName())).get();
    
            ServiceTarget target = serviceActivatorContext.getServiceTarget();
            ServiceBuilder<?> builder = policy.createSingletonServiceConfigurator(SINGLETON_SERVICE_NAME).build(target);
            Consumer<Node> member = builder.provides(SINGLETON_SERVICE_NAME);
            Supplier<Group> group = builder.requires(ServiceName.parse("org.wildfly.clustering.default-group"));
            builder.setInstance(new SingletonService(group, member)).install();
    
            serviceActivatorContext.getServiceTarget()
                    .addService(QUERYING_SERVICE_NAME, new QueryingService())
                    .setInitialMode(ServiceController.Mode.ACTIVE)
                    .install();
    
            serviceActivatorContext.getServiceTarget().addService(QUERYING_SERVICE_NAME).setInstance(new QueryingService()).install();
    
            LOG.info("Singleton and querying services activated.");
        }
    }
  4. META-INF/services/ 目录中创建一个名为 org.jboss.msc.service.ServiceActivator 的文件,其中包含 ServiceActivator 类的名称,例如 org.jboss.as.quickstarts.ha.singleton.service.SingletonServiceActivator

请参阅 JBoss EAP 随附的 ha-singleton-service 快速入门,以获取完整的工作示例。此快速入门还提供了第二个示例,它演示了安装有备份服务的单例服务。备份服务在所有不选择运行单例服务的节点中运行。最后,此快速入门还演示了如何配置几个不同的选择策略。