VisualStudio/C#서버

[C#서버] Akka.net과 Actor모델 Part.3

usingsystem 2024. 9. 30. 13:45
728x90

주요내용

  • 분산 메세지 전달과 Router
  • Pool Router
  • ActorSelect와 Route 비교
  • HOCON을 사용한 Router 설정
  • 비동기 actor간 메시지 전달 PipeTo와 ReceiveAsync
  • 비동기 호출 단순화 akka.Interfaced
  • 액터 메세지 수신 교착 상태 방지와 ReceiveTimeout

1. 분산 메세지 전달 Router

라우터는 다른 actor 그룹으로 메시지를 전달하는 메시징 허브 역할을 하는 특별한 종류의 actor 입니다. 라우터의 목적은 실제 작업을 수행할 배우들(즉, 라우티)을 통해 작업(메시지 스트림)을 분배하고 균형을 맞추는 것입니다.

라우터는 actor이지만 기존 actor와 다르게 한 번에 여러 메시지를 처리할 수 있습니다. 라우터의 목적은 메시지를 처리하는 것이 아니라, 다른 배우에게 전달하는 것입이기 때문에 가능합니다.

라우터는 대량의 데이터 스트림을 쉽게 관리 가능합니다.

 

라우터에는 두 가지 유형이 있습니다. 풀 라우터와 그룹 라우터입니다.

풀 라우터 (Pool Router)

  • 작업자 배우(라우티)를 생성하고 관리합니다.
  • 사용자가 라우터에 인스턴스 수를 제공하면 라우터가 작업자 배우를 자동으로 생성합니다.

그룹 라우터 (Group Router)

  • 이미 생성된 라우티에 메시지를 전달하는 역할만 합니다.
  • 라우터 생성 시 라우티의 ActorPaths를 지정하여 사용합니다

라우터 구성 방식

  • 절차적 구성 : 코드에서 직접 라우터를 구성하는 방법입니다.
  • HOCON 구성 : HOCON을 사용하여 라우터 구성

라우팅 전략에는 2가지가 존재한다.

1. 특정 상황 메세지 

  특정 요구사항이나 상황에 따라 동작하며, 예를 들어 모든 라우티에게 동일한 작업을 요구할 때 유용합니다.

  • Broadcast 이 라우팅 전략에서는 라우터가 수신하는 모든 메시지를 모든 라우티에 전달합니다.
  • Random 랜덤 라우팅 전략에서는 라우터가 메시지를 받을 때마다 무작위로 선택된 라우티에 메시지를 전달합니다.
  • ConsistentHash 라우팅 전략에서는 라우터가 일관된 해싱을 사용하여 메시지의 데이터를 기반으로 라우티를 선택합니다.

2. 부하 분산 라우팅 

  업을 라우티 간에 고르게 분산시켜 시스템의 효율성을 높입니다.

  • RoundRobin: 라우티를 순환적으로 선택하여 메시지를 전송합니다. 부하가 비교적 균일하고, 모든 작업자가 비슷한 성능을 가질 때 유용합니다.
  • SmallestMailbox (풀 라우터 전용) : 각 라우티의 메일박스에 있는 메시지 수를 기준으로 라우티를 선택합니다. 가장 적은 메시지를 보유한 라우티에 새로운 메시지를 전달합니다. 각 라우티의 메시지 큐가 과도하게 쌓이지 않도록 할 수 있습니다.
  • TailChopping: 이 전략은 메시지를 첫 번째 라우티에 전달한 후, 일정 시간 대기하여 응답을 기다립니다. 만약 첫 번째 라우티가 응답하지 않으면, 다음 라우티로 메시지를 전달합니다. 응답 시간이 중요한 시스템에서 사용됩니다. 예를 들어, 빠른 데이터 조회가 필요한 경우에 유용합니다.
  • ResizableRouter (풀 라우터 전용) : 이 전략은 풀 라우터가 자동으로 라우티의 수를 조정할 수 있게 해줍니다. 시스템의 부하에 따라 라우티의 수를 늘리거나 줄이는 방식입니다. 사용자 수가 변동이 큰 서비스(예: 온라인 게임, 대규모 이벤트)에서 효과적입니다.
  • ScatterGatherFirstCompleted: 모든 라우티에 메시지를 전송하고, 가장 먼저 응답한 라우티의 응답만을 받아서 원래 발신자에게 전달합니다. 규모 데이터 처리나, 신속한 결정이 필요한 상황에서 유용합니다. 예를 들어, 실시간 서비스에서 여러 소스에서 정보를 수집할 때 사용할 수 있습니다.

라우트 메시지

  • Broadcast 메시지: 비 방송 라우터에게 전달하여 모든 라우티에 메시지를 전달하도록 만듭니다.
router.Tell(new Broadcast("ㅎㅇㅎㅇ"));
  • GetRoutees 메시지: 라우터의 라우티 목록을 반환합니다. 주로 디버깅에 사용됩니다.
router.Tell(new GetRoutees());
  • PoisonPill 메시지: 수신한 배우를 즉시 종료합니다.
router.Tell(PoisonPill.Instance);

ActorSelect와 Route비교

특성 ACTORSELECT ROUTE
정의 특정 패턴에 맞는 모든 배우를 선택하는 방법 메시지를 특정 라우티에 전달하는 방법
사용 목적 여러 배우 중에서 특정 조건에 맞는 배우를 찾기 위해 메시지를 여러 라우티 간에 효율적으로 분산하기 위해
구현 방식 ActorPaths 또는 ActorSelection을 사용하여 선택 라우터가 지정된 라우티에게 메시지를 전달
성능 전체 배우 목록을 확인해야 하므로 상대적으로 느림 라우터를 통해 직접 메시지를 전달하여 빠름
유지 관리 동적으로 변경되는 배우의 상태를 반영하기 어려움 라우터가 상태를 관리하여 동적인 부하 분산 가능
응답 방식 응답이 개별 배우에서 오며, 라우터를 우회함 응답이 라우터를 통해 돌아오며, 라우터가 중재함

2. Group Router 사용예제

액터를 직접적으로 생성하지 않는다. 그렇기 때문에 자동으로 액터를 생성하고 감독가능하며 동적으로 크기를 조정하고 자식 액터의 에러가 발생해도 시스템의 방식이 일관되게 유지되는 등 다양한 장점이 있는 pool router를 주로사용한다.

특성 BroadcastGroup GroupRouter
목적 모든 라우티에 메시지를 전달 미리 정의된 라우티 중 하나에게 메시지를 전달
메시지 전달 방식 모든 라우티에 동일한 메시지를 전달 선택된 하나의 라우티에게만 메시지를 전달
사용 사례 상태 변경 알림, 긴급 알림 데이터베이스 쿼리 요청, 부하 분산

BroadcastGroup 예시

using Akka.Actor;
using Akka.Routing;
using System.Collections.Generic;

public class Worker : ReceiveActor
{
    public Worker()
    {
        // 메시지를 수신할 때 처리할 로직
        Receive<string>(msg => HandleMessage(msg));
    }

    private void HandleMessage(string msg)
    {
        // 수신한 메시지를 출력
        Console.WriteLine($"Worker {Self.Path.Name} received: {msg}");
    }
}

class Program
{
    static void Main(string[] args)
    {
        var system = ActorSystem.Create("MyActorSystem");

        // 여러 작업자 생성
        var workers = new List<IActorRef>
        {
            system.ActorOf(Props.Create<Worker>(), "worker1"),
            system.ActorOf(Props.Create<Worker>(), "worker2"),
            system.ActorOf(Props.Create<Worker>(), "worker3")
        };

        // BroadcastGroup 생성
        var broadcastGroup = system.ActorOf(Props.Empty.WithRouter(new BroadcastGroup(workers)));

        // 모든 작업자에게 메시지 전송
        broadcastGroup.Tell("Hello, everyone!");

        // 시스템 종료
        Console.ReadLine();
        system.Terminate();
    }
}

 

3. Pool Router 사용예제

액터를 여러 개의 인스턴스로 생성하고, 이러한 인스턴스들 사이에 메시지를 분산시키는 데 사용됩니다.

풀 라우터를 설정할 때 생성할 액터 인스턴스수를 지정하는 매개변수로 NrOfInstances가 있습니다.

풀 라우터와 NrOfInstances는 함께 사용되어 Akka.NET에서 효율적인 액터 관리를 가능하게 합니다. 풀 라우터는 메시지를 처리할 액터 인스턴스를 관리하고, NrOfInstances는 이 인스턴스의 수를 정의하여 시스템의 부하 분산과 성능을 최적화합니다.

var router = Context.ActorOf(Props.Empty.WithRouter(
    new RoundRobinPool(5) // NrOfInstances 5개의 액터 인스턴스를 생성하여 라우팅
));

 

풀라우팅 감시

풀 라우터는 자신의 경로(routee)로 사용되는 액터들을 직접 생성하여 자식 액터로 관리합니다. 이 의미는 풀 라우터가 이러한 자식 액터들을 자동으로 감독(Supervise)하고 상태를 감시(DeathWatch)한다는 것입니다. 기본적으로 발생하는 오류는 부모 액터로 전파되어, 풀 라우터가 재시작되고 자식 액터들도 함께 재시작됩니다. 이는 시스템의 안정성을 높이고, 오류 발생 시 일관된 처리를 가능하게 합니다. 필요에 따라 감독 전략을 설정하여 이러한 동작을 커스터마이즈할 수 있습니다.

풀 라우터의 기본 동작은 오류가 발생했을 때 다음과 같은 순서로 진행됩니다:

  • 에러 전파: 자식 액터(routee)에서 오류가 발생하면, 이 에러는 풀 라우터의 부모 액터로 전달됩니다.
  • 재시작 지시: 부모 액터는 풀 라우터에 대해 재시작 지시를 발행합니다.
  • 재시작: 풀 라우터는 스스로를 재시작하고, 그 후 자식 액터들을 재시작합니다.

풀라우팅 사용법

1. Smallest Mailbox Router

SmallestMailbox 라우터는 각 액터의 메일박스(즉, 메시지 큐)에 있는 메시지 수를 기반으로 라우팅합니다. 이 라우터는 현재 대기 중인 메시지가 가장 적은 액터에게 메시지를 전송하여 부하를 균등하게 분산하는 전략입니다. 각 액터의 메시지 큐 상태를 고려하여 최적의 라우팅 경로를 선택함으로써, 시스템의 응답성과 처리 성능을 향상시킵니다. 특정 액터에 과부하를 방지하고, 메시지 처리를 효율적으로 할 수 있습니다.

  • 부하 분산: 액터의 메일박스에 있는 메시지 수를 모니터링하여, 메시지를 보낼 때 대기 중인 메시지가 가장 적은 액터를 선택합니다. 이를 통해 모든 액터가 균일하게 부하를 분산받고, 특정 액터에 과부하가 걸리는 것을 방지합니다.
  • 성능 최적화: 메시지가 가장 적은 액터에 전달되므로, 그 액터가 대기 중인 메시지를 빠르게 처리할 수 있습니다. 이는 전체 시스템의 응답성을 높이고, 병목 현상을 줄이는 데 기여합니다.
var router = Context.ActorOf(Props.Empty.WithRouter(
    new SmallestMailboxPool(5) // 5개의 액터 인스턴스를 생성하고, 큐가 가장 작은 액터에 메시지를 전송
));

2. Resizable Router

Resizable Router는 필요에 따라 액터의 수를 동적으로 늘리거나 줄일 수 있는 라우터입니다. 시스템의 부하 변화에 유연하게 대응할 수 있도록 설계되어 있습니다. 시스템의 부하 변화에 유연하게 대응할 수 있도록 설계되어 있으며, 액터의 수를 동적으로 조정하여 리소스를 효율적으로 관리할 수 있습니다. 

  • 부하 변화 대응: 예를 들어, 요청량이 급증할 경우 추가 액터를 생성하여 처리 능력을 향상시키고, 부하가 감소할 때는 사용하지 않는 액터를 줄임으로써 리소스를 절약할 수 있습니다. 이는 클라우드 환경과 같은 동적인 리소스 관리에 적합합니다.
var router = Context.ActorOf(Props.Empty.WithRouter(
    new ResizablePool(5) // 초기 5개의 액터 인스턴스를 생성하고, 필요에 따라 수를 조정
));

4. 풀 라우터와 그룹 라우터의 차이점

풀 라우터는 라우트를 생성하고 관리하여 자신이 생성한 액터 인스턴스의 부모가 되며, 이를 통해 생명주기 관리와 상태 모니터링이 가능합니다. 그룹 라우터는 외부에서 주어진 경로를 기반으로 하여 신뢰성이 낮고 고정된 라우트를 관리합니다. 이러한 차이로 인해 풀 라우터는 일반적으로 더 안전하고 효율적인 선택이 됩니다.

특징 풀 라우터 (Pool Router) 그룹 라우터 (Group Router)
라우트 관리 풀 라우터는 자신의 라우트(액터 인스턴스)를 생성하고 관리합니다. 라우트의 부모가 됩니다. 외부에서 주어진 ActorPaths를 기반으로 라우트를 관리합니다. 라우트의 부모가 아닙니다.
신뢰성 액터 인스턴스를 직접 관리하여 더 안전하고 신뢰할 수 있습니다. 라우트의 상태를 알지 못해 신뢰성이 떨어집니다.
라우트 수동 조정 라우트 풀의 크기를 동적으로 조정할 수 있습니다. 액터를 추가하거나 제거할 수 있습니다. 설정된 이후 라우트의 수가 고정되며 조정할 수 없습니다.
라우트 이름 지정 라우트의 이름을 제어할 수 없으며, 라우터를 통해서만 통신할 수 있습니다. 액터의 경로를 알고 있어 직접적으로 특정 액터와 통신할 수 있습니다.

5. Hocon을 사용한 Routers설정

HOCON(Human-Optimized Config Object Notation)은 Akka.NET의 다양한 설정을 쉽게 관리할 수 있는 구성 형식입니다. 라우터를 설정할 때 HOCON을 사용하면 코드와 구성의 분리를 통해 가독성을 높일 수 있습니다. HOCON을 사용하면 원격 배포와 액터의 행동을 코드 수정 없이 구성 변경만으로 조정할 수 있어, 유연성과 유지보수성을 크게 향상시킵니다.

 

그룹라우터인 broadcastPool을 사용할 때 nr-of-instances가 무시되지만 설정이 무엇인지 명확히 할 수 있는 유용한 주석이된다. 또한 풀라우터가 있을 경우 코드에서 변경없이 사용할 수있다. 기준을 만드는 느낌.

akka {
	actor{
	  deployment{
	   /myRouter{
	      router = broadcast-pool
	      nr-of-instances = 3
	    }
	  }
   }
}

액터를 생성할 때 WithRouter 메서드를 사용해야 라우터가 설정됩니다. 만약 구성 파일에서 정의한 라우터를 사용하고 싶다면 FromConfig 클래스를 활용할 수 있습니다. 이 클래스는 Akka.NET에게 해당 액터에 대한 라우터 사양을 구성에서 찾도록 지시합니다. 예를 들어, FromConfig.Instance를 사용하면 구성에서 정의한 라우터를 그대로 사용할 수 있습니다. 이를 통해 코드 수정 없이도 구성 파일만으로 액터의 동작을 쉽게 변경할 수 있습니다. 즉 액터 라우터 설정의 재사용성

akka.actor.deployment {
/router1 {
    router = round-robin-pool
        resizer {
            enabled = on
            lower-bound = 2
            upper-bound = 3
        }
    }
}

Hocon을 사용한 설정 사용방법

FromConfig.Instance는 Akka.NET에서 라우터를 정의할 때 HOCON 설정에서 라우터 정의를 가져오는 데 사용되는 특수한 객체입니다. 이를 사용하면 코드에서 라우터를 명시적으로 정의하지 않고도 HOCON 파일에 설정된 대로 라우터를 인스턴스화할 수 있습니다.

akka {
  actor {
    deployment {
      /router1 {
        router = consistent-hashing-pool
        nr-of-instances = 3
        virtual-nodes-factor = 17
      }
    }
  }
}
var router1 = MyActorSystem.ActorOf(Props.Create(() => new FooActor()).WithRouter(FromConfig.Instance), "router1");

 

6. 비동기 actor간 메시지 전달 ( PipeTo와 ReceiveAsync )

1. ReceiveAsync

동기 작업을 현재 액터에서 처리하면서 송신자에게 결과를 직접 전달할 때 유용하지만, 액터 메일박스의 차단으로 인해 상태 관리가 어려울 수 있습니다.

public MyActor()
{
    ReceiveAsync<SomeMessage>(async some => {
        // 비동기 작업을 안전하게 사용할 수 있습니다.
        await SomeAsyncIO(some.Data);
        Sender.Tell(new EverythingIsAllOK());
    });
}

2. PipeTo

PipeTo는 Akka.NET에서 메시지를 한 액터에서 다른 액터로 전달할 때 사용하는 메서드입니다. 비동기 작업의 결과를 다른 액터로 안전하게 전달하고, 액터 간의 메시지 흐름을 명확하게 유지하는 데 유리합니다.

public class WorkerActor : ReceiveActor
{
    public WorkerActor()
    {
        Receive<string>(message =>
        {
            // 비동기 작업 수행
            var resultTask = PerformWorkAsync(message);
            // 결과를 다른 액터에게 전달
            resultTask.PipeTo(Sender);
        });
    }

    private async Task<string> PerformWorkAsync(string input)
    {
        await Task.Delay(1000); // 1초 대기
        return $"Processed: {input}";
    }
}

// 결과를 처리할 액터
public class ResultActor : ReceiveActor
{
    public ResultActor()
    {
        Receive<string>(result =>
        {
            Console.WriteLine(result);
        });
    }
}

3. akka.interfaced

인터페이스를 통한 비동기 호출을 단순화하고, 타입 안전성을 제공하는 데 초점이 있습니다. 액터 외부에서 액터의 메서드를 비동기적으로 호출하고 그 응답을 기다릴 때 유용합니다.

Akka.Interfaced의 메서드는 기본적으로 Task나 Task<T>로 반환되어 비동기 처리를 수행합니다. 호출하는 측에서는 await 키워드를 통해 액터의 응답을 기다리거나, 비동기 호출을 이어갈 수 있습니다.

public interface ICalculator : IInterfacedActor
{
    Task<int> Add(int a, int b);
}
public class CalculatorActor : InterfacedActor, ICalculator
{
    public Task<int> Add(int a, int b)
    {
        return Task.FromResult(a + b);
    }
}

//액터호출
var actorRef = actorSystem.ActorOf<CalculatorActor>();
var calculator = actorRef.Cast<ICalculator>();

// 비동기적으로 호출
int sum = await calculator.Add(5, 3);
Console.WriteLine($"Sum: {sum}");  // 출력: Sum: 8

PipeTo vs ReceiveAsync

PipeTo 패턴이 액터 내부에서 비동기 작업을 수행하는 데 더 선호됩니다. 그 이유는 PipeTo 패턴은 무엇이 발생하고 있는지를 명확하게 나타내기 때문에, 코드의 가독성이 높아집니다. 이는 코드 유지 보수 시 도움이 됩니다. ReceiveAsync에서 await를 사용하면 액터의 메일박스가 계속해서 차단되며, 이는 액터가 "한 번에 하나의 메시지"를 처리한다는 보장을 준수하는 데 도움이 됩니다. 즉, PipeTo를 사용하면 액터의 상태를 안전하게 유지할 수 있습니다.

 

즉 ReceiveAsync보다 PipeTo가 더 선호되는 이유는 비동기 작업의 흐름을 더 명확하게 하고, 액터의 스레드 안전성을 보다 잘 보장하기 때문입니다.

특징 PipeTo ReceiveAsync
사용 목적 비동기 작업의 결과를 다른 액터에 전달할 때 사용 비동기 메시지 처리 및 송신자에게 결과를 직접 전달할 때 사용
가독성 명시적이며 코드의 흐름을 쉽게 이해할 수 있음 상대적으로 덜 명시적일 수 있으며, 복잡한 흐름에서는 이해하기 어려울 수 있음
액터 메일박스 관리 액터의 메일박스가 차단되지 않으며, 메시지 처리를 안전하게 보장 await로 인해 메일박스가 차단될 수 있으며, "한 번에 하나의 메시지" 보장을 유지함
비동기 작업 결과 처리 다른 액터로 결과를 전달 현재 액터에서 결과를 처리하고 송신자에게 직접 전달
스레드 전환 비동기 작업의 결과를 다른 스레드에서 처리할 수 있음 비동기 작업이 같은 스레드에서 처리되므로 상태를 쉽게 유지할 수 있음
예외 처리 비동기 작업의 결과가 다른 액터로 전달되기 때문에 예외 처리가 분리될 수 있음 비동기 작업 중 발생하는 예외는 현재 액터에서 처리해야 함
유지 관리 결과를 다른 액터로 전달하기 때문에 더 유연함 송신자에게 직접 결과를 전달하므로 덜 유연함

PipeTo vs Akka.Interfaced

 

Akka.Interfaced외부에서 액터와의 비동기 상호작용을 단순화하고, 인터페이스 기반 호출을 통해 타입 안전성과 코드 가독성을 높이는 데 적합합니다.

PipeTo액터가 비동기 작업의 결과를 직접 메시지로 수신하고 처리하는 데 유용하며, 비동기 작업과 병행해 다른 메시지 처리를 가능하게 하는 등 액터 모델의 특성을 잘 활용할 수 있습니

 

특징 Akka.Interfaced PipeTo
주요 목적 인터페이스 기반으로 액터와의 상호작용을 단순화 비동기 작업의 결과를 액터에게 직접 전달하여 처리
사용 사례 외부에서 액터의 메서드를 비동기 호출하고 응답을 기다릴 때 사용 액터 내부에서 비동기 작업의 결과를 메시지로 수신하여 처리할 때 사용
비동기 처리 방식 Task 또는 Task<T> 반환을 통해 비동기 응답을 직접 대기 작업 결과를 PipeTo(Self) 또는 PipeTo(다른 액터)로 전달
코드 표현 방식 await를 통해 일반 메서드 호출처럼 비동기 응답 대기 액터가 메시지 수신자로서 결과를 받고, 비동기 작업 중에도 다른 메시지 처리 가능
주요 장점 - 타입 안전성을 제공 - 액터가 비동기 작업 결과를 자연스럽게 수신 및 처리
- 가독성 높은 메서드 호출 방식 - 병렬 처리 가능
적합한 상황 액터의 비동기 메서드를 외부에서 호출하며 타입 검사 및 간결한 코드가 필요할 때 액터가 비동기 작업의 결과를 기다리지 않고 다른 작업을 병행하며 결과를 처리할 때
타입 안전성 인터페이스를 통해 타입 안전성이 높음 일반 메시지 전달 방식으로, 타입 검사는 없음
응답 처리 위치 외부 호출자 (외부에서 액터의 응답을 기다림) 액터 자신 또는 다른 액터 (결과를 수신하여 메시지로 처리함)

7. 교착 상태 방지와 ReceiveTimeout

ReceiveTimeout은 액터가 메시지를 수신하지 않을 때 자동으로 타임아웃을 설정할 수 있는 기능입니다. 이 기능을 사용하면 액터가 일정 시간 동안 메시지를 받지 않으면 자동으로 타임아웃 이벤트를 발생시켜 특정 작업을 수행할 수 있습니다.

 

시간 초과가 발생하고 다른 메시지가 해당 ReceiveTimeout메시지보다 먼저 actor 사서함에 도착할 수 있습니다. 이 경우 다른 메시지가 해당 메시지보다 먼저 처리되어 ReceiveTimeout무효화됩니다.

 

타임아웃 설정: 액터가 Context.SetReceiveTimeout(TimeSpan)을 호출하여 타임아웃을 설정합니다. 이 경우, 액터는 지정된 시간 동안 메시지를 수신하지 않을 경우 ReceiveTimeout 메시지를 수신하게 됩니다.

// ReceiveTimeout을 5초로 설정
Context.SetReceiveTimeout(TimeSpan.FromSeconds(5));
// TimeOut 취소
Context.SetReceiveTimeout(null);
//5초 동안 메세지를 받지 못 한다면 자기자신에게 ReceiveTimeout 메세지
Receive<ReceiveTimeout>(timeout =>
{
});

 

 

 

 

 

 

 

 

 

 

 

 

 

참조

https://github.com/petabridge/akka-bootcamp/blob/master/src/Unit-3/README.md

 

akka-bootcamp/src/Unit-3/README.md at master · petabridge/akka-bootcamp

Self-paced training course to learn Akka.NET fundamentals from scratch - petabridge/akka-bootcamp

github.com

 

728x90