Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

...

이를 통해 클러스터 시스템의 확장성과 유연성을 크게 높일 수 있습니다.

클러스터 내에서 Point To Point 전송

draw.io Board Diagram
bordertrue
diagramNamecluster
simpleViewerfalse
width
linksauto
tbstyletop
lboxtrue
diagramWidth621
revision1

클러스터 내에서는 PubSub을 브로드캐스트 방식을 사용하지 않고도 , 정확하게 구독한 객체에게 전송이 가능하며

이것이 가능한 이유중에 하나는 클러스터내에서 P2P 전송을 지원하며 물리적 주소를 알 필요없이 Key와 같은

논리적으로 생성한 주소만으로 원격지의 객체에게 이벤트 전송을 할수있습니다.


클러스터 테스트 코드

Code Block
themeEmacs
// A와 B는 다른 포트로 구성된 Node입니다.
testKitA = ActorTestKit.create("ClusterSystem",cluster1)
testKitB = ActorTestKit.create("ClusterSystem",cluster2)

    @Test
    fun testClusterRouter(){
		//테스트를 위한 준비코드
        val probeA = testKitB.createTestProbe<Receptionist.Listing>()
        val probeB = testKitB.createTestProbe<HelloActorAResponse>()

        var list = testKitB.system().receptionist()
        list.tell(Receptionist.find(ClusterHelloActorA.ClusterHelloActorAKey, probeA.ref))
        val listing = probeA.receiveMessage() 
		

		//실제 활용되는 코드
        val router = listing.getServiceInstances(ClusterHelloActorA.ClusterHelloActorAKey)

        router.forEach {
            it.tell(HelloA("Hello", probeB.ref))
            probeB.expectMessage(HelloAResponse("Kotlin"))
        }
    }
  • val router : 위치 투명성이 있는 객체로 클러스터를 접근하는 논리적인 키만 알고 있으면 
    • ClusterHelloActorA.ClusterHelloActorAKey 라는 논리적 주소만으로, B 시스템에서 A 시스템에 구성된 액터에게 이벤트를 보내고 응답을 받는 샘플입니다.



클러스터 액터정의

Code Block
themeEmacs
/** HelloActor 클래스 */
class ClusterHelloActorA private constructor(
    private val context: ActorContext<HelloActorACommand>,
) : AbstractBehavior<HelloActorACommand>(context) {

    companion object {

        var ClusterHelloActorAKey:ServiceKey<HelloActorACommand> = ServiceKey.create(HelloActorACommand::class.java, "ClusterHelloActorA")

        fun create(): Behavior<HelloActorACommand> {
            return Behaviors.setup { context -> ClusterHelloActorA(context) }
        }
    }

    init {
        //Router
        context.system.receptionist().tell(Receptionist.register(ClusterHelloActorAKey, context.self))

    }
  • 로컬액터에서 클러스터 액터로 배치되는 액터로 변식하기 위해서는 단지 자신의 액터가 생성될시 Key를 등록하면 됩니다.


특정 Role이 부여된 액터배치

Code Block
themeEmacs
val clusterA = Cluster.get(testKitA.system())
// Role에 따라 작동하는 Actor 구분생성
if (clusterA.selfMember().hasRole("helloA")) {
	actorA = testKitA.spawn(ClusterHelloActorA.create(), "actorA")
}
  • 클러스터는 설정을 통해 Role이 부여되며 , 부여된 Role에서만 우리가 설계한 액터를 배치할수 있습니다.
  • 기능단위 마이크로 서비스가 꼭 저장소로 분리되고 CI가 별도 구성되어 관리포인트가 증가하는것을 방지할수 있으며 응집력있는 코드를 유지할수 있는것이 Akka Cluster의 특징입니다. 
    • 상담히 진보적인 아이디어로 MSA로 인해 코드가 분산되고 관리되어야하는 포인트를 증가하는것을 방지할수 있습니다. ( MSA의 대표적인 단점을 해결 )


가장 작은단위의 전송인 PointToPoint 전송을 기본으로 지원하게되면  Topic단위 또는 이벤트 버스를 구현할때도 최소 전송단위가 존재하기때문에 나머지는 논리적인 구성을 효과적으로 할수 있게됩니다.


Pub/Sub 시스템에서 상태없는 서비스와 상태있는 액터 모델을 사용했을 때의 메시지 전송 빈도 효율을 비교해 보겠습니다. 특히, 이벤트 생성 시마다 브로드캐스트가 필요하지 않도록 효율적으로 효율적 설계하는 방법도 다뤄보겠습니다.

1. 상태없는 서비스 기반 Pub/Sub

...

따라서 이벤트 발생 빈도가 높거나 구독자가 많은 시스템에서는 상태있는 액터 모델을 사용하는 것이 효율적이며, 브로드캐스트 빈도를 줄여 네트워크와 시스템 리소스를 절약할 수 있습니다.

상태없는 서비스의 PUBSUB

최종 사용자가 무엇을 구독하였는지 서버에서 상태관리가 되지 않기때문에 PUB/SUB을 이용하더라도 결국 

모든 이벤트를 분산하는것이 아닌 모든 이벤트를 분산된 노드에게 전체 전송해야하기때문에 올바른 의미에서 분산처리 시스템으로 볼수 없습니다.

...



가정 및 설정 추가

  • 중간 장치 10대: 각 장치는 필터링을 통해 수신한 이벤트 중 일부만 최종 사용자에게 전송할 수 있습니다.
  • 상태없는 Pub/Sub: 모든 이벤트가 모든 사용자에게 전송되므로, 중간 장치들은 단순히 이벤트를 전달하는 역할만 수행한다고 가정합니다.
  • 상태있는 액터 클러스터Akka 분산 Pub/Sub: 중간 장치가 이벤트 필터링을 추가적으로 수행하여, 각 장치가 받은 이벤트 중 10%만 최종 사용자에게 전송한다고 가정합니다.

이를 바탕으로 이벤트가 발생할 때마다 중간 장치에서 최종 사용자까지의 네트워크 전송 수를 다시 계산해 보겠습니다.

네트워크 이벤트 전송 수 계산

  1. 상태없는 Pub/Sub:

    • 모든 이벤트가 모든 사용자에게 전송되므로, 중간 장치를 거쳐 최종 사용자까지의 네트워크 전송 수가 단순히 사용자 수에 비례하여 증가합니다.
    • 네트워크 전송 수 = (이벤트 수 × 사용자 수) + (중간 장치 수 × 이벤트 수)
    • 전송 수=(이벤트 수×10,000)+(10×이벤트 수)\text{전송 수} = (\text{이벤트 수} \times 10,000) + (10 \times \text{이벤트 수})전송 =(이벤트 ×10,000)+(10×이벤트 )
  2. 상태있는 액터 클러스터: 액터가 아니더라도 BroadCast를 사용하지 않고 중간 필터가능

    • 각 이벤트의 10%만 중간 장치를 통해 최종 사용자에게 전송됨.
    • 네트워크 전송 수 = (이벤트 수 × 사용자 수 × 0.1) + (중간 장치 수 × 이벤트 수)
    • 전송 수=(이벤트 수×10,000×0.1)+(10×이벤트 수)\text{전송 수} = (\text{이벤트 수} \times 10,000 \times 0.1) + (10 \times \text{이벤트 수})전송 =(이벤트 ×10,000×0.1)+(10×이벤트 )
  3. Akka 분산 Pub/Sub:

    • 이벤트의 5%만 최종 사용자에게 전송되며, 중간 장치는 이벤트의 10%를 필터링 후 최종 사용자에게 전달함.
    • 네트워크 전송 수 = (이벤트 수 × 사용자 수 × 0.05 × 0.1) + (중간 장치 수 × 이벤트 수)
    • 전송 수=(이벤트 수×10,000×0.05×0.1)+(10×이벤트 수)\text{전송 수} = (\text{이벤트 수} \times 10,000 \times 0.05 \times 0.1) + (10 \times \text{이벤트 수})전송 =(이벤트 ×10,000×0.05×0.1)+(10×이벤트 )

이를 바탕으로 단위별 비교 표를 작성하겠습니다.

이벤트 수상태없는 Pub/Sub 전송 수상태있는 액터 클러스터 전송 수Akka 분산 Pub/Sub 전송 수
110,0101,01051
10100,10010,100510
1001,001,000101,0005,100
1,00010,010,0001,010,00051,000
10,000100,100,00010,100,000510,000
100,0001,001,000,000101,000,0005,100,000

분석 및 결론

  • 상태없는 Pub/Sub는 여전히 모든 이벤트를 모든 사용자에게 브로드캐스트하기 때문에 네트워크 트래픽이 매우 높습니다. 중간 장치가 있더라도, 단순한 중계 역할만 하기 때문에 네트워크 전송 수에 큰 영향을 주지 않습니다.
  • 상태있는 액터 클러스터는 상태 기반 필터링을 통해 전체 이벤트의 약 10%만 최종 사용자에게 전달되므로, 상태없는 Pub/Sub에 비해 네트워크 전송 수가 크게 줄어듭니다.
  • Akka 분산 Pub/Sub는 필터링과 구독 그룹을 통해 추가적으로 10배의 네트워크 트래픽 절감 효과가 있으며, 중간 장치에서도 필터링이 적용되므로 네트워크 전송 수가 가장 낮습니다.

이와 같은 구성에서는 Akka 분산 Pub/Sub 방식이 가장 효율적이며, 특히 이벤트 발생 수가 많거나 네트워크 부하가 높은 환경에서 큰 절감 효과를 제공합니다.


다음장에서는 전송지점 어딘가에 브로드 캐스트가 항상 존재해야하는 전통적인 StateLess PubSub에서 상태기반의 분산처리 가능한 PubSub 을 알아보겠습니다.

...

클러스터 내에서 Point To Point 전송

...

클러스터 내에서는 PubSub을 브로드캐스트 방식을 사용하지 않고도 , 정확하게 구독한 객체에게 전송이 가능하며

이것이 가능한 이유중에 하나는 클러스터내에서 P2P 전송을 지원하며 물리적 주소를 알 필요없이 Key와 같은

논리적으로 생성한 주소만으로 원격지의 객체에게 이벤트 전송을 할수있습니다.

클러스터 테스트 코드

Code Block
themeEmacs


// A와 B는 다른 포트로 구성된 Node입니다.
testKitA = ActorTestKit.create("ClusterSystem",cluster1)
testKitB = ActorTestKit.create("ClusterSystem",cluster2)

    @Test
    fun testClusterRouter(){
		//테스트를 위한 준비코드
        val probeA = testKitB.createTestProbe<Receptionist.Listing>()
        val probeB = testKitB.createTestProbe<HelloActorAResponse>()

        var list = testKitB.system().receptionist()
        list.tell(Receptionist.find(ClusterHelloActorA.ClusterHelloActorAKey, probeA.ref))
        val listing = probeA.receiveMessage() 
		

		//실제 활용되는 코드
        val router = listing.getServiceInstances(ClusterHelloActorA.ClusterHelloActorAKey)

        router.forEach {
            it.tell(HelloA("Hello", probeB.ref))
            probeB.expectMessage(HelloAResponse("Kotlin"))
        }
    }
  • val router : 클러스터 내에서 해당 키를 가진 액터를 모두 탐색해냅니다.ClusterHelloActorA.ClusterHelloActorAKey 라는 논리적 주소만으로, B 시스템에서 A 시스템에 구성된 액터에게 이벤트를 보내고 응답을 받는 샘플입니다.