내용 목차
본 장에서는 EJB 클러스터링 개념과 주요 기능 설정 방법에 대하여 설명한다.
EJB의 Failover와 부하 분산(Load Balancing) 기능을 사용하기 위해서 각 Bean들은 여러 EJB 엔진에 디플로이되어 클러스터링을 형성해야 한다. 클러스터링은 컴포넌트 레벨(개별 Bean)에서 수행되고 Stateless/Stateful Session Bean과 Entity Bean에서 이용할 수 있다. Message Driven Bean(MDB)은 클러스터링 대상에 해당 되지 않는다.
JEUS EJB 클러스터링은 다음과 같은 2가지 기능을 한다.
다음 그림은 2가지 EJB 클러스터링의 주요 기능인 Failover와 부하 분산을 설명한다.
클러스터링을 원하는 모듈을 디플로이하면 Naming Server에 모두 같은 이름으로 바인드된다. 클라이언트는 그 하나의 이름으로 수행해도 부하 분산과 Failover가 가능하다. 따라서 같은 모듈이더라도 Naming Server에 바인딩할 이름을 다르게 하여 디플로이하면 그 모듈이 클러스터링되지 않음에 주의한다.
Stateful Session Bean의 경우 Failover를 위해 JEUS 분산 Session Manager를 사용한다. Session Manager는 EJB 엔진당 하나만 존재하기 때문에 클러스터링할 Bean의 클러스터링 범위가 각 Bean 별로 달라서는 안 된다. 예를 들어, Bean A는 EJB einge1과 EJB engine2로 묶고 Bean B는 EJB engine1과 engine3으로 묶었다면 Session Manager는 Bean A와 Bean B가 EJB engine1, EJB engine2, EJB engine3에 클러스터링되어 있다고 잘못 확인하게 된다.
다음은 EJB 클러스트링의 주요 기능에 대한 설명이다.
클라이언트가 lookup이나 injection에 의해 Bean A를 요청하면 Naming Server는 3개의 EJB 엔진에 존재하는 3개의 Bean 중 하나를 임의로 선택하여 반환한다. 클라이언트는 그 후부터는 선택된 EJB 엔진에서 비즈니스 인터페이스를 통하여 Bean과 일반적인 방법으로 연동한다.
이는 3개의 Bean들은 동일하게 같은 메소드 호출 요청을 받게 되고 잠재적으로 한 개의 엔진이 모든 요청에 대해 서비스하는 것보다 무려 3배의 시스템 성능 향상을 기대할 수 있다는 것을 의미한다(부하 분산의 경우에 발생하는 작은 자원소모를 계산하지 않을 때).
Failover는 하나의 EJB 서비스의 장애가 발생하더라도 정상적인 서비스를 제공하는 것을 의미한다(예: OS 장애, 네트워크 중단 또는 EJB 엔진 장애).
JEUS 시스템이 처리할 수 있는 장애 복구에는 다음의 2가지가 있다.
클라이언트의 요청이 도착했을 때 접근 불가한 Bean에게 요청을 보내지 않고, 처리되지 않은 요청을 다른 Bean에게 다시 보내는 방법
단순히 문제가 발생한 EJB 엔진을 제외하고 부하 분산 알고리즘을 실행한다. 새로운 클라이언트의 요청을 처리하기 위해서는 사용 가능한 엔진의 Bean을 선택한다.
JEUS에서는 Failover의 Rerouting이 클라이언트 측의 EJB Stub에서 처리된다. 이 Stub을 Active Stub이라 부르고 또는 표준 EJB 인터페이스를 둘러 싼 wrapper라고도 부른다. 이러한 wrapper를 사용할 때의 다른 점은 현재 연결되어 있는 Bean 또는 EJB 엔진이 문제가 있는지 판단할 수 있는 로직을 가지고 있는지 여부이다. 이러한 문제점이 발견되면, 기동 중인 Stub이 자동으로 JNDI 서버에 접속해서 작동하고 있는 EJB 엔진을 대신하여 새로운 Stub을 요청한다([그림 6.1]의 5번).
실행 중인 Bean이 어떤 이유로 런타임 오류를 가지고 있다면 진행 중인 요청을 다른 Bean에게 다시 보내는 방법
이러한 상황에서의 복구 방법은 한계가 있다. Bean이 요청을 처리하고 있을 때 문제가 발견되면 얼만큼 요청을 처리하고 있었는지도 모르고 문제가 발생했을 때 어떤 런타임 에러를 발생시켰는지도 모른다. 단순하게 클러스터에 존재하는 다른 Bean의 같은 메소드를 호출하는 것은 또 다른 부작용을 조장할 수 있기 때문에 자칫하면 위험한 결과를 가져올 수도 있다.
이 문제를 명백히 하기 위해서 DB 필드를 1씩 증가시키는 메소드를 가지는 Bean Instance A를 고려해보자. 1이 증가된 후에 바로 문제가 발생하는 경우 단순하게 다른 Bean “B”에 있는 같은 Business 메소드를 다시 호출하면 1이 다시 한 번 증가한다. 결과적으로 DB에 일괄적이지 못하고 잘못된 값을 전송하게 된다. 이런 경우 Idempotent 메소드를 이용하면 안전하게 복구될 수 있다. 자세한 내용은 “6.2.3. Idempotent 메소드를 통한 EJB 복구”를 참고한다.
위의 두 시나리오의 차이는 오류 상황이 발견되는 시점이다. 즉, Remote Business 메소드를 호출하기 전인가 아니면 Bean이 요청을 처리하고 있는 중인가의 예가 있을 수 있다.
Idempotent 메소드는 부작용이 없는 “getter” 메소드이다. 이는 메소드의 수행 중에 어떠한 상태(예: instance 변수, DB 필드 등)도 변경되지 않는 것을 보장한다.
따라서 “6.2.2. Failover(EJB 복구)”에서의 두 번째 복구 방법이 지닌 한계는 Idempotent 메소드로 극복할 수 있다. 그러나 Idempotent 메소드가 아니라면 역시 대책이 없다. 런타임 에러가 발생한 메소드를 다시 실행시키는 것보다는 Exception을 던지는 것이 차라리 낫다. 그러므로 Idempotent 메소드를 많이 사용할수록 EJB Failover는 더 잘 작동된다. 메소드가 Idempotent 메소드인지 아닌지 판단하는 공식은 없다. 그러므로 비즈니스 메소드의 상태를 정확히 식별하고 설정해야 한다.
Stateful Session Bean의 경우 세션의 백업을 위해서 JEUS Session Manager를 사용한다. 일반적으로 비즈니스 메소드 호출 단위로 세션의 상태가 변화하기 때문에 JEUS에서는 메소드 호출이 발생하고 결과가 리턴되는 시점마다 JEUS Session Manager에 세션 백업을 요청한다. 이러한 백업 작업을 다른 용어로 세션 복제(Session Replication)라고 한다.
JEUS Session Manager는 세션을 동기적(Sync) 또는 비동기적(Async)으로 복제할 수 있다. JEUS에서는 이를 복제 모드(Replication Mode)라고 한다.
동기적(Sync) 복제 모드
세션 백업이 완료될 때까지 Bean이 기다려야 한다. 따라서 Failover를 해야 할 때 항상 최신의 세션이 복제되어 있다는 장점이 있다. 하지만 Session Manager에서 네트워크 장애 등이 발생하여 타임아웃에 걸려 진행이 안 될 경우 Bean도 그만큼 기다려야 하는 문제가 있다.
비동기적(Async) 복제 모드
퍼포먼스 문제는 없어지지만 세션 복제가 바로 발생하지 않고 나중에 이루어지기 때문에 그 사이에 Failover를 한다면 최신의 세션을 얻을 수 없게 된다.
2가지 방법은 서로 장단점이 있으므로 JEUS에서는 Bean과 각 비즈니스 메소드 특성에 따라 사용자가 설정할 수 있다. 또한 세션 복제를 하지 않아도 되는 메소드가 있을 수 있으므로 이 역시 설정할 수 있다. 자세한 설정 방법은 “6.3. EJB 클러스터링 설정”을 참고한다.
JEUS에서는 클러스터링에 참여한 Stateful Session Bean이라면 기본적으로 동기적(Sync) 복제 모드로 세션 복제가 이루어진다. 사용자는 설정에 의해 이를 조정할 수 있다.
EJB 클러스터링은 Bean 클래스에 Annotation으로 설정하거나 jeus-ejb-dd.xml에 설정할 수 있다. 설정할 사항은 클러스터링으로 구성될 Bean, 그 Bean의 idempotent 메소드, 그리고 Bean 또는 각 메소드의 세션 복제 모드이다.
본 절에서는 예를 통해 Annotation과 Descriptors(xml)에 클러스터링을 설정하는 방법에 대해 설명한다.
클러스터링에 참여하는 Bean 클래스 또는 메소드에 다음과 같은 Annotation을 이용하여 설정한다.
[예 6.1] Annotation을 통한 클러스터링 설정 : <<CounterEJB.java>>
package ejb.basic.statelessSession; import javax.ejb.Stateful; import jeus.ejb.Clustered; import jeus.ejb.Replication; import jeus.ejb.ReplicationMode; @Stateful(name="counter", mappedName="COUNTER") @Clustered @Replication(ReplicationMode.SYNC) @CreateIdempotent public class CounterEJB implements Counter, CounterLocal { private int count = 0; public int increaseAndGet() { return ++count; } @Replication(ReplicationMode.NONE) public void doNothing(int a, String b) { } @Idempotent public int getResult() { return count; } @Idempotent @jeus.ejb.Replication(ReplicationMode.ASYNC) public int getResultAnother() { return count; } ... }
다음은 각 클래스별 설정에 대한 설명이다.
클래스 | 설명 |
---|---|
@jeus.ejb.Clustered | Bean의 클러스터링을 전체적으로 활성화 또는 비활성화시킨다. |
@jeus.ejb.Idempotent |
|
@jeus.ejb.CreateIdempo tent |
|
@jeus.ejb.Replication |
|
JEUS EJB 모듈 DD 파일(jeus-ejb-dd.xml)에는 클러스터링에 참여하는 각각의 Bean들을 위해서 <clustering> 태그 아래에 다음과 같은 설정을 적용할 수 있다.
[예 6.2] xml을 통한 클러스터링 설정 : <<jeus-ejb-dd.xml>>
<jeus-ejb-dd>
. . .
<beanlist>
. . .
<jeus-bean>
<ejb-name>counter</ejb-name>
<export-name>COUNTER</export-name>
. . .
<clustering>
<enable-clustering>true</enable-clustering>
<ejb-remote-idempotent-method>
<method-name>getResult</method-name>
</ejb-remote-idempotent-method>
<ejb-remote-idempotent-method>
<method-name>getResultAnother</method-name>
</ejb-remote-idempotent-method>
<create-idempotent>true</create-idempotent>
<replication>
<bean-mode>sync</bean-mode>
<methods>
<method>
<method-name>doNothing</method-name>
<method-params>
<method-param>int</method-param>
<method-param>java.lang.String</method-param>
</method-params>
<mode>none</mode>
</method>
<method>
<method-name>getResultAnother</method-name>
<method-params>
<method-param>void</method-param>
</method-params>
<mode>async</mode>
</method>
</methods>
</replication>
</clustering>
. . .
</jeus-bean>
. . .
</beanlist>
. . .
</jeus-ejb-dd>
<clustering>
다음은 설정 태그에 대한 설명이다.
태그 | 설명 |
---|---|
<enable-clustering> | Bean의 클러스터링을 전체적으로 활성화 또는 비활성화시킨다. |
<ejb-remote-idempotent-method> | Bean 메소드 중에 idempotent 메소드들을 선언한다(“6.3.1. Annotation을 통한 클러스터링 설정”의 @jeus.ejb.Idempotent 참조). |
<ejb-remote-idempotent-exclude-method> | Bean 메소드 중에 idempotent 메소드들로 선언한 것 중 제외하고 싶은 메소드를 선언한다. <idempotent-method>에 우선한다. |
<ejb-home-idempotent-method> | 2.x 스타일의 홈 인터페이스에 정의된 메소드 중에 idempotent 메소드들을 선언한다(위의 <ejb-remote-idempotent-method> 참조). |
<ejb-home-idempotent-exclude-method> | 2.x 스타일의 홈 인터페이스에 정의된 메소드 중에 idempotent 메소드들로 선언한 것 중 제외하고 싶은 메소드를 선언한다. <idempotent-method>에 우선한다. |
<create-idempotent> | Session Bean을 생성할 때 idempotent하게 할지 선언한다. |
<replication> | Bean 레벨의 세션 복제 모드 또는 메소드별 복제 모드를 설정한다. 자세한 내용은 “6.2.4. Session Replication”과 [예 6.2]를 참고한다. |
위에서 지정한 Bean 클러스터링이 작동하기 위해서는 다음의 내용을 주의해야 한다.
클러스터링에 참여하는 모든 Bean들은 <clustering> 하위의 모든 정보가 동일해야 한다.
클러스터링으로 구성하기 위해서는 원하는 Bean의 <export-name>을 모두 동일하게 설정한다.
Annotation뿐만 아니라 xml을 통해서도 클러스터링 설정이 가능하지만 Annotation 사용을 권장한다.
클러스터링 환경에서 Stateful Session Bean을 실행하기 원한다면 노드 클러스터링 설정과 Session Manager 설정을 추가로 해야 한다. 노드 클러스터링은 Failover될 때 클러스터링된 Naming Server에서 EJB를 Lookup하거나 Session Manager가 백업 Session Manager를 찾기 위해서 필요하다.
더불어 백업 Session Manager가 무엇인지 설정하여 자신이 서비스를 할 수 없을 때 어떤 Session Manager가 서비스할지 지정해주어야 한다. 본 절에서는 최소한의 설정만 설명한다.
자세한 Session Manager 백업 설정은 "JEUS Server 안내서"를 참고한다.
현재 클러스터링에 참여하는 모든 EJB 엔진을 설정하면 자동으로 백업을 선택해 주기 때문에 특정한 엔진을 백업으로 설정할 것이 아니라면 다음과 같이 설정해도 충분하다. 단, 클러스터링에 참여하는 모든 노드에 다음과 같이 JEUSMain.xml에 설정되어 있어야 한다.
[예 6.3] Stateful Session Bean의 클러스터링 설정 : <<JEUSMain.xml>>
<?xml version="1.0"?> <jeus-system xmlns="http://www.tmaxsoft.com/xml/ns/jeus"> <node> <name>johan</name> <engine-container> <name>container1</name> <base-port>10501</base-port> . . . <engine-command> <type>ejb</tyep> <name>engine1</name> </engine-command> . . . </engine-container> <engine-container> <name>container2</name> <base-port>10502</base-port> . . . <engine-command> <type>ejb</tyep> <name>engine2</name> </engine-command> . . . </engine-container> . . . <session-router-config> <session-router> <engine-name>johan_ejb_engine1</engine-name> </session-router> <session-router> <engine-name>johan_ejb_engine2</engine-name> </session-router> </session-router-config> . . . </node> <node> <name>johan1</name> <engine-container> <name>container3</name> <base-port>10503</base-port> . . . <engine-command> <type>ejb</tyep> <name>engine3</name> </engine-command> . . . </engine-container> <engine-container> <name>container4</name> <base-port>10504</base-port> . . . <engine-command> <type>ejb</tyep> <name>engine4</name> </engine-command> . . . </engine-container> . . . <session-router-config> <session-router> <engine-name>johan1_ejb_engine3</engine-name> </session-router> <session-router> <engine-name>johan1_ejb_engine4</engine-name> </session-router> </session-router-config> . . . </node> </jeus-system>
매번 클라이언트에서 EJB Reference(Stub)를 lookup하지 않고, lookup한 EJB Reference를 Cache하여 계속 사용하는 경우(Injection을 사용하는 경우도 포함), 살아 있는 다른 EJB End-point가 있음에도 불구하고 Failover가 되지 않는 경우가 있을 수 있다. 이는 Lookup하는 시점에 존재했던 EJB End-point 리스트에 해당하는 노드들이 모두 살아 있지 않고 그 이후에 구동된 새로운 노드만 존재하는 경우에 발생할 수 있다.
Failover를 해야하는 시점에서 내부적으로 Lookup을 수행하여 새로운 EJB Reference를 클라이언트에게 전달한다. 그러나 이 경우 사용 중인 EJB Reference가 Lookup될 때 디플로이되어 있던 노드들이 모두 다운되어 버리면, 현재 사용하고 있던 EJB Reference가 Lookup될 당시 디플로이되지는 않았지만 그 후에 디플로이되어 Failover하는 시점에서 서비스 가능한 새로운 EJB End-point가 존재해도 Failover가 되지 않는다.
여기서 기존 노드들이 모두 다운되었다는 것은 비정상 종료되거나, EJB End-point가 Undeploy되어서 기존의 모든 EJB End-point의 서비스가 불가능한 것을 의미한다. 또한 새로운 EJB End-point가 디플로이되는 경우는 뒤늦게(EJB Reference를 lookup하여 이미 사용 중일 때) 새로운 노드가 클러스터에 포함되거나, 뒤늦게 다운했던 노드를 재시작을 하였거나, Undeploy했던 EJB End-point를 뒤늦게 ReDeploy를 하는 경우가 포함된다.
2개의 노드로 Active/Backup 클러스터링을 하는 경우에 이런 현상이 발생할 수 있다. 또는 다음과 같은 시나리오에서 이런 경우가 발생할 수 있다.
예를 들어, A,B,C 노드에 편의상 EJB 엔진이 하나씩 있고 클라이언트가 처음 Lookup을 하여 계속 Cache를 하는 경우이다.
A,B,C에 모두 디플로이되어 있고 처음 Lookup한 결과 A의 EJB를 받는다.
A의 EJB를 사용하다 A가 비정상 종료되어 내부적으로 B의 EJB를 Lookup하여 계속 서비스를 받는다.
B의 EJB가 Undeploy되어서 C의 EJB를 Lookup을 사용한다.
이때 다시 B의 EJB가 디플로이되고, 곧이어 C가 비정상 종료되었다.
이 경우 C의 EJB Reference가 Lookup될 때 B의 EJB End-point는 Undeploy되어 있었고 C의 EJB Reference를 lookup한 다음에 디플로이되었으므로, C가 죽었을 때 B는 살아있었지만 Failover는 되지 않는다. 그러나 이때 B가 Deploy/Undeploy가 된것이 아니라 비정상 기동/종료가 되었다면 정상적으로 Failover가 된다. 비정상 종료의 경우에는 Failover 시점에서 다시 살아났는지 검사를 하기 때문에 가능하다.
이렇게 Failover가 제대로 안되는 상황이 발생하는 경우에는 다시 lookup을 하여 새로운 EJB Reference를 가져오면 새로운 End-point 목록을 가져오기 때문에 이런 문제를 피할 수 있다.