8.4. HA シングルトンの実装

概要

JBoss Enterprise Application Platform 5 では、HA シングルトンアーカイブは他のデプロイメントとは別に deploy-hasingleton/ ディレクトリーにデプロイされていました。これは自動デプロイメントが発生しないようにするためで、また確実に HASingletonDeployer サービスがデプロイメントを制御し、クラスターのマスターノードのみにアーカイブがデプロイされるようにするための処置でした。ホットデプロイメント機能がなかったため、再デプロイメントにはサーバーの再起動が必要でした。また、マスターノードに障害が発生し、他のノードがマスターとして引き継ぐ必要がある場合、シングルトンサービスはサービスを提供するためデプロイメントプロセス全体を実行する必要がありました。

JBoss Enterprise Application Platform 6 ではこれが変更になりました。SingletonService を使用してクラスターの各ノードに目的のサービスがインストールされますが、サービスは一度に 1 つのノード上でのみ起動されます。これにより、デプロイメントの要件が簡素化され、ノード間でシングルトンマスターサービスを移動するために必要な時間が最小限になります。

手順8.3 HA シングルトンサービスの実装

  1. HA シングルトンサービスアプリケーションを作成します。

    シングルトンサービスとしてデプロイされる SingletonService デコレーターでラッピングされたサービスの簡単な例は次のとおりです。
    1. シングルトンサービスを作成します。

      以下のリストは、シングルトンサービスの例です。
      package com.mycompany.hasingleton.service.ejb;
      
      import java.util.concurrent.atomic.AtomicBoolean;
      import java.util.logging.Logger;
      
      import org.jboss.as.server.ServerEnvironment;
      import org.jboss.msc.inject.Injector;
      import org.jboss.msc.service.Service;
      import org.jboss.msc.service.ServiceName;
      import org.jboss.msc.service.StartContext;
      import org.jboss.msc.service.StartException;
      import org.jboss.msc.service.StopContext;
      import org.jboss.msc.value.InjectedValue;
      
      /**
       * @author <a href="mailto:wfink@redhat.com">Wolf-Dieter Fink</a>
       */
      public class EnvironmentService implements Service<String> {
          private static final Logger LOGGER = Logger.getLogger(EnvironmentService.class.getCanonicalName());
          public static final ServiceName SINGLETON_SERVICE_NAME = ServiceName.JBOSS.append("quickstart", "ha", "singleton");
          /**
           * A flag whether the service is started.
           */
          private final AtomicBoolean started = new AtomicBoolean(false);
      
          private String nodeName;
      
          private final InjectedValue<ServerEnvironment> env = new InjectedValue<ServerEnvironment>();
      
          public Injector<ServerEnvironment> getEnvInjector() {
              return this.env;
          }
      
          /**
           * @return the name of the server node
           */
          public String getValue() throws IllegalStateException, IllegalArgumentException {
              if (!started.get()) {
                  throw new IllegalStateException("The service '" + this.getClass().getName() + "' is not ready!");
              }
              return this.nodeName;
          }
      
          public void start(StartContext arg0) throws StartException {
              if (!started.compareAndSet(false, true)) {
                  throw new StartException("The service is still started!");
              }
              LOGGER.info("Start service '" + this.getClass().getName() + "'");
              this.nodeName = this.env.getValue().getNodeName();
          }
      
          public void stop(StopContext arg0) {
              if (!started.compareAndSet(true, false)) {
                  LOGGER.warning("The service '" + this.getClass().getName() + "' is not active!");
              } else {
                  LOGGER.info("Stop service '" + this.getClass().getName() + "'");
              }
          }
      }
      
      
    2. サーバー起動時にサービスを SingletonService として起動するためにシングルトン EJB を作成します。

      以下のリストは、サーバー起動時に SingletonService を起動するシングルトン EJB の例です。
      package com.mycompany.hasingleton.service.ejb;
      
      import java.util.Collection;
      import java.util.EnumSet;
      
      import javax.annotation.PostConstruct;
      import javax.annotation.PreDestroy;
      import javax.ejb.Singleton;
      import javax.ejb.Startup;
      
      import org.jboss.as.clustering.singleton.SingletonService;
      import org.jboss.as.server.CurrentServiceContainer;
      import org.jboss.as.server.ServerEnvironment;
      import org.jboss.as.server.ServerEnvironmentService;
      import org.jboss.msc.service.AbstractServiceListener;
      import org.jboss.msc.service.ServiceController;
      import org.jboss.msc.service.ServiceController.Transition;
      import org.jboss.msc.service.ServiceListener;
      import org.slf4j.Logger;
      import org.slf4j.LoggerFactory;
      
      
      /**
       * A Singleton EJB to create the SingletonService during startup.
       * 
       * @author <a href="mailto:wfink@redhat.com">Wolf-Dieter Fink</a>
       */
      @Singleton
      @Startup
      public class StartupSingleton {
        private static final Logger LOGGER = LoggerFactory.getLogger(StartupSingleton.class);
      
        /**
         * Create the Service and wait until it is started.<br/>
         * Will log a message if the service will not start in 10sec. 
         */
        @PostConstruct
        protected void startup() {
          LOGGER.info("StartupSingleton will be initialized!");
      
          EnvironmentService service = new EnvironmentService();
          SingletonService<String> singleton = new SingletonService<String>(service, EnvironmentService.SINGLETON_SERVICE_NAME);
          // if there is a node where the Singleton should deployed the election policy might set,
          // otherwise the JGroups coordinator will start it
          //singleton.setElectionPolicy(new PreferredSingletonElectionPolicy(new NamePreference("node2/cluster"), new SimpleSingletonElectionPolicy()));
          ServiceController<String> controller = singleton.build(CurrentServiceContainer.getServiceContainer())
              .addDependency(ServerEnvironmentService.SERVICE_NAME, ServerEnvironment.class, service.getEnvInjector())
              .install();
      
          controller.setMode(ServiceController.Mode.ACTIVE);
          try {
            wait(controller, EnumSet.of(ServiceController.State.DOWN, ServiceController.State.STARTING), ServiceController.State.UP);
            LOGGER.info("StartupSingleton has started the Service");
          } catch (IllegalStateException e) {
            LOGGER.warn("Singleton Service {} not started, are you sure to start in a cluster (HA) environment?",EnvironmentService.SINGLETON_SERVICE_NAME);
          }
        }
      
        /**
         * Remove the service during undeploy or shutdown
         */
        @PreDestroy
        protected void destroy() {
          LOGGER.info("StartupSingleton will be removed!");
          ServiceController<?> controller = CurrentServiceContainer.getServiceContainer().getRequiredService(EnvironmentService.SINGLETON_SERVICE_NAME);
          controller.setMode(ServiceController.Mode.REMOVE);
          try {
            wait(controller, EnumSet.of(ServiceController.State.UP, ServiceController.State.STOPPING, ServiceController.State.DOWN), ServiceController.State.REMOVED);
          } catch (IllegalStateException e) {
            LOGGER.warn("Singleton Service {} has not be stopped correctly!",EnvironmentService.SINGLETON_SERVICE_NAME);
          }
        }
      
        private static <T> void wait(ServiceController<T> controller, Collection<ServiceController.State> expectedStates, ServiceController.State targetState) {
          if (controller.getState() != targetState) {
            ServiceListener<T> listener = new NotifyingServiceListener<T>();
            controller.addListener(listener);
            try {
              synchronized (controller) {
                int maxRetry = 2;
                while (expectedStates.contains(controller.getState()) && maxRetry > 0) {
                  LOGGER.info("Service controller state is {}, waiting for transition to {}", new Object[] {controller.getState(), targetState});
                  controller.wait(5000);
                  maxRetry--;
                }
              }
            } catch (InterruptedException e) {
              LOGGER.warn("Wait on startup is interrupted!");
              Thread.currentThread().interrupt();
            }
            controller.removeListener(listener);
            ServiceController.State state = controller.getState();
            LOGGER.info("Service controller state is now {}",state);
            if (state != targetState) {
              throw new IllegalStateException(String.format("Failed to wait for state to transition to %s.  Current state is %s", targetState, state), controller.getStartException());
            }
          }
        }
      
        private static class NotifyingServiceListener<T> extends AbstractServiceListener<T> {
          @Override
          public void transition(ServiceController<? extends T> controller, Transition transition) {
            synchronized (controller) {
              controller.notify();
            }
          }
        }
      }
      
      
    3. クライアントよりサービスへアクセスするためステートレスセッション Bean を作成します。

      以下は、クライアントからサービスにアクセスするステートレスセッション Bean の例です。
      package com.mycompany.hasingleton.service.ejb;
      
      import javax.ejb.Stateless;
      
      import org.jboss.as.server.CurrentServiceContainer;
      import org.jboss.msc.service.ServiceController;
      import org.slf4j.Logger;
      import org.slf4j.LoggerFactory;
      
      /**
       * A simple SLSB to access the internal SingletonService.
       * 
       * @author <a href="mailto:wfink@redhat.com">Wolf-Dieter Fink</a>
       */
      @Stateless
      public class ServiceAccessBean implements ServiceAccess {
          private static final Logger LOGGER = LoggerFactory.getLogger(ServiceAccessBean.class);
      
          public String getNodeNameOfService() {
              LOGGER.info("getNodeNameOfService() is called()");
              ServiceController<?> service = CurrentServiceContainer.getServiceContainer().getService(
                      EnvironmentService.SINGLETON_SERVICE_NAME);
              LOGGER.debug("SERVICE {}", service);
              if (service != null) {
                  return (String) service.getValue();
              } else {
                  throw new IllegalStateException("Service '" + EnvironmentService.SINGLETON_SERVICE_NAME + "' not found!");
              }
          }
      }
      
      
    4. SingletonService のビジネスロジックインターフェースを作成します。

      以下は、SingletonService に対するビジネスロジックインターフェースの例です。
      package com.mycompany.hasingleton.service.ejb;
      
      import javax.ejb.Remote;
      
      /**
       * Business interface to access the SingletonService via this EJB
       * 
       * @author <a href="mailto:wfink@redhat.com">Wolf-Dieter Fink</a>
       */
      @Remote
      public interface ServiceAccess {
          public abstract String getNodeNameOfService();
      } 
      
      
      
  2. クラスタリングが有効な状態で各 Jboss Enterprise Application Platform 6 インスタンスを起動します。

    クラスターを有効化する方法は、サーバーがスタンドアロンであるか管理ドメインで実行されているかによって異なります。
    1. 管理対象ドメインで実行されているサーバーに対してクラスタリングを有効にします。

      管理 CLI を使用してクラスタリングを有効にするか、設定ファイルを手動で編集できます。
      • 管理 CLI を使用してクラスタリングを有効にします。

        1. ドメインコントローラーを起動します。

        2. 使用しているオペレーティングシステムのコマンドプロンプトを開きます。

        3. ドメインコントローラーの IP アドレスまたは DNS 名を渡して管理 CLI に接続します。

          この例では、ドメインコントローラーの IP アドレスが 192.168.0.14 であることを前提とします。
          • Linux の場合は、コマンドラインで以下を入力します。
            $ EAP_HOME/bin/jboss-cli.sh --connect --controller=192.168.0.14
            $ Connected to domain controller at 192.168.0.14
            
          • Windows の場合は、コマンドラインで以下を入力します。
            C:\>EAP_HOME\bin\jboss-cli.bat --connect --controller=192.168.0.14
            C:\> Connected to domain controller at 192.168.0.14
            
        4. main-server サーバーグループを追加します。

          [domain@192.168.0.14:9999 /] /server-group=main-server-group:add(profile="ha",socket-binding-group="ha-sockets") 
          {
              "outcome" => "success",
              "result" => undefined,
              "server-groups" => undefined
          }
          
        5. server-one という名前のサーバーを作成し、main-server サーバーグループに追加します。

          [domain@192.168.0.14:9999 /]  /host=station14Host2/server-config=server-one:add(group=main-server-group,auto-start=false)
          {
              "outcome" => "success",
              "result" => undefined
          }
          
        6. main-server サーバーグループに対して JVM を設定します。

          [domain@192.168.0.14:9999 /] /server-group=main-server-group/jvm=default:add(heap-size=64m,max-heap-size=512m)
          {
              "outcome" => "success",
              "result" => undefined,
              "server-groups" => undefined
          }
          
        7. server-two という名前のサーバーを作成し、別のサーバーグループに置き、ポートオフセットを 100 に設定します。

          [domain@192.168.0.14:9999 /]  /host=station14Host2/server-config=server-two:add(group=distinct2,socket-binding-port-offset=100)
          {
              "outcome" => "success",
              "result" => undefined
          }
          
      • サーバー設定ファイルを手動で編集してクラスタリングを有効にします。

        1. JBoss Enterprise Application Platform サーバーを停止します。

          重要

          変更がサーバーの再起動後にも維持されるようにするには、サーバー設定ファイルの編集前にサーバーを停止する必要があります。
        2. domain.xml 設定ファイルを開いて編集します。

          ha プロファイルと ha-sockets ソケットバインディンググループを使用するサーバーグループを指定します。例は次のとおりです。
          <server-groups>
            <server-group name="main-server-group" profile="ha">
              <jvm name="default">
                <heap size="64m" max-size="512m"/>
              </jvm>
              <socket-binding-group ref="ha-sockets"/>
            </server-group>
          </server-groups>
          
          
        3. host.xml 設定ファイルを開いて編集します。

          以下のようにファイルを変更します。
          <servers>
            <server name="server-one" group="main-server-group" auto-start="false"/>
            <server name="server-two" group="distinct2">
              <socket-bindings port-offset="100"/>
            </server>
          <servers>
          
          
        4. サーバーを起動します。

          • Linux の場合は、EAP_HOME/bin/domain.sh と入力します。
          • Microsoft Windows の場合は、EAP_HOME\bin\domain.bat と入力します。
    2. スタンドアロンサーバーに対してクラスタリングを有効にします。

      スタンドアロンサーバーに対してクラスタリングを有効にするには、次のようにノード名と standalone-ha.xml 設定ファイルを使用してサーバーを起動します。
      • Linux の場合は、EAP_HOME/bin/standalone.sh --server-config=standalone-ha.xml -Djboss.node.name=UNIQUE_NODE_NAME と入力します。
      • Microsoft Windows の場合は、EAP_HOME\bin\standalone.bat --server-config=standalone-ha.xml -Djboss.node.name=UNIQUE_NODE_NAME と入力します。

    注記

    1 つのマシン上で複数のサーバーが実行されている時にポートの競合が発生しないようにするため、別のインターフェースでバインドするように各サーバーインスタンスに対して standalone-ha.xml ファイルを設定します。または、コマンドラインで -Djboss.socket.binding.port-offset=100 のような引数を使用し、ポートオフセットを持つ後続のサーバーインスタンスを開始して対応することも可能です 。
  3. アプリケーションをサーバーにデプロイします。

    Maven を使用してアプリケーションをデプロイする場合は、次の Maven コマンドを使用してデフォルトのポートで稼働しているサーバーへデプロイします。
    mvn clean install jboss-as:deploy
    追加のサーバーをデプロイするには、サーバー名とポート番号をコマンドラインに渡します。
    mvn clean package jboss-as:deploy -Ddeploy.hostname=localhost -Ddeploy.port=10099