Akka는 기본적으로 탑레벨 아키텍쳐의 설계방식으로 Actor를 위치시키고 접근을 하며 (최상위부터 생성하여 구조화하는 방식 )

다음과같은 특성이 있습니다.

  • 액터접근 : 트리구조이지만, FullName을 알고있을시 최하위 자식노드에게 직접 리모트 메시지전송이 가능합니다.
  • 액터접근 :  someActor의 자식에게 모두 메시지를 보내고 싶으면 someActor/* 를 할수 있습니다.
  • 라우팅화 가능 : 분산처리를 위해 자식노드에게 다양한 라우팅을 지정할수 있습니다. 순차적으로 분산처리 해야한다고하면 라운드 로빈으로 자식 액터 배치가 가능합니다.
  • 리모트 액터 : 모든게 메시징를 통해서만 명령이 전달되며 로컬과 리모트의 차이가 없습니다.
  • 클러스터 : 로컬과 리모트의 차이가 없다란것은 로컬로 개발된 액터가 약간의 코드수정으로 단일지점 병목없는 클러스터화 가능합니다.

Actor의 기본 생성과 Child Actor 생성

기본 액터생성
    public class MyActor : ReceiveActor
    {
        private ILoggingAdapter log = Context.GetLogger();

        public MyActor()
        {
            Receive<string>(message => {
                if (message == "createChild")
                {
                    Context.ActorOf<MyActor>("myChild");
                    Sender.Tell("Create Child Succed:myChild");
                }
                else
                {
                    Sender.Tell("RE:" + message);
                }
            });

            Receive<SomeMessage>(message => {
                Sender.Tell("RE:" + message.message);
            });
        }

    }


	IActorRef myActor = actorSystem.ActorOf<MyActor>("myactor");
	myActor.tell("createChild");


	


액터접근

Actor접근법
var parentActor= system.ActorSelection("user/myactor");
var childActor= system.ActorSelection("user/myactor/myChild");

잘설계된 RestAPI의 endpoint 와 접근법이 유사합니다.

어떠한 의미에서 REST-API 설계도 Top-Level 아키텍쳐를 따른다고 볼수 있습니다. ( 상위에서 하위기능으로 분류)

작동방식에서의 차이는 REST-API는 무상태를 지향하지만, 액터의 경우 상태가 있는 서비스 설계에 더 유리합니다.


액터성능

로컬에서만 사용될때는 메모리를 사용하며 , 리모트 전송이 발생할때는 고성능 TCP프로토콜을 활용합니다.

로컬에서는 주로 멀티스레드를 관리하는 Dispatcher에 의해 액터의 성능이 결정될수 있으며

리모트에서는 고성능 TCP모듈및 , 원격컴퓨터간 데이터복원을 위한 직렬화에 영향을 받을수 있으며

컴포넌트 형태로 이것은 모두 코드변경없이 가장 빠른모듈로 손쉽게 교체가 가능합니다.


액터의 소멸과 생명주기

 구조적인 설계로, 부모의 액터를 정지시키면 자식의 액터를 모두 종류후 마지막에 부모 액터가 종료가됩니다.

상속을 받은 OOP Class의 소멸 순서와도 유사합니다. 

부모가 자식을 관리감독할수 있으며, 장애복구전략에 따라 자식액터를 재생성 할수 있습니다.



Actor 중지
    public class StartStopActor1 : ReceiveActor
    {
        private ILoggingAdapter log = Context.GetLogger();

        public StartStopActor1()
        {
            Receive<string>(message => {
                if (message == "stop")
                {
                    Context.Stop(Self);
                }                
            });            
        }

        protected override void PreStart()
        {
            log.Info("first started");
            Context.ActorOf<StartStopActor2>("second");
        }

        protected override void PostStop()
        {
            log.Info("first stopped");
        }

    }

    public class StartStopActor2 : ReceiveActor
    {
        private ILoggingAdapter log = Context.GetLogger();

        public StartStopActor2()
        {
            Receive<string>(message => {                
            });            
        }

        protected override void PreStart()
        {
            log.Info("second started");

        }

        protected override void PostStop()
        {
            log.Info("second stopped");
        }

    }


	IActorRef myActor = actorSystem.ActorOf<StartStopActor1 >("myactor");
	myActor.Tell("stop");

//OutPut
//first started
//second started
//second stopped
//first stopped


Supervisor을 통한 장애허용시스템

Child 액터가 장애발생시, 다양한 복구전략( 3초간 30초동안시도)을 통해 Child액터의 복구가 가능합니다.

var supervisor = BackoffSupervisor.Props(
                    Backoff.OnFailure(
                        childProps,
                        childName: "supervised-actor",
                        minBackoff: TimeSpan.FromSeconds(3),
                        maxBackoff: TimeSpan.FromSeconds(30),
                        randomFactor: 0.2)
                    );



장애허용 시스템은 시스템을 구성하는 부품의 일부가 결함이있을때 정상혹은 부분적으로 작동가능한 시스템이다.

예외처리방식에서 사용하는 복구방법과 어떻게 다른지? 액터의 감독자(SupervisorStrategy)기능을 통해 살펴보자

Link : 장애허용시스템

예외처리 시스템

기존 예외처리 시스템의 모습입니다. 함수는 호출을 할수록 접시처럼 쌓이게 되고 정상 호출결과 반환은

접시를 하나씩 빼내어 전달이 되기때문에 함수호출 자체는 스택구조로 볼수있습니다.

예외발생도 마찬가지로, 스택구조를 사용하여 예외발생정보가 호출역순으로 전달됩니다.

클래스B에서 예외를 잡아내는 코드가 없으면, 클래스 A로 점프합니다. -실제는 역순으로 진행됨


기존 예외처리를 사용하여, 재시도와같은 복구처리 기능을 넣으려고하면 다음과같은 어려움이 있을수 있습니다.

예외발생은 호출자에게  그 책임을 계속 떠넘기는데 있으며 예외를 처리하는 지점에는 접시가 이미 제거되어

복구를 위한 충분한정보가 없을수 있습니다. (별다른 처리가 없다면 Try에서 생성한 객체를 Catch에서 참조할수 없습니다.)

재시도를 위해 중요한 정보를 예외발생시 호출자에게 계속 전달하는것은 설계의 원칙뿐만 아니라

보안설계원칙을 어길수 있습니다.  만약 클래스 A조차 예외를 못잡아내면 재시도를 위한 중요한 정보가

최종 호출자에게 전달및 책임이 전가될수 있습니다. 


예외처리기가 에러로그를 우아하게 남기거나, 구조적으로 처리하는데는 도움이되며 여전히 중요한 개발방법중에하나입니다.

다음 링크는,예외 처리에 대한 좋은 참고자료입니다.  임도형의 예외처리 가이드 (오래전,자바가이드를 해주신 선배개발자님)


Actor 결함 허용 시스템

액터 예외처리 특징은 예외 발생시, 호출자에게 책임을 전가하지 않습니다.

액터에서 예외처리는 문제를 일으킨 아이의 부모가 책임을 지며, 부모는 아이의 생성정보를 알고 있기때문에

복구를 위한 충분한 정보가 있으며 다양한 복구 플랜을 세울수 있습니다.

부모는 결함의 심각성에 따라 3가지 복구플랜을 선택할수 있으며 그대로 두거나(Resume) , 다시 시작시작하거나(Restart), 제거합니다(Stop)

이것은 Supervisor(감독자)에의한 장애복구로 불리며 모든 부모액터는 감독자 역활을 할수 있습니다.

복구전략으로는 크게 두가지중 하나를 선택할수 있습니다.

  • OneForOne : 문제를 일으킨 아이만 복구합니다. / 하위기능은 자기자신만을 위해 존재하며,나머지에 영향이 없거나 미비합니다. 문제있는 아이만 복구플랜을 선택할수 있습니다.
  • AllForOne : 한아이를 위해 나머지가 존재합니다. / 연대 책임이며 중요한 아이가 잘못되면 전체가 작동하리란 보증을 못하기때문에  연관된 노드 모두 복구플랜을 선택할수 있습니다. 


액터 구현체

using System;
using Akka.Actor;
using Akka.Event;

namespace AkkaNetCore.Actors.Study
{
    public class Child : ReceiveActor
    {
        private int state = 0;

        private readonly ILoggingAdapter logger = Context.GetLogger();

        public Child()
        {
            ReceiveAsync<object>(async message =>
            {
                switch (message)
                {
                    case Exception ex:
                        throw ex;
                    case int x:
                        state = x;
                        break;
                    case "get":
                        Sender.Tell(state);
                        break;
                }
            });
        }
    }

    public class Supervisor : ReceiveActor
    {
        private readonly ILoggingAdapter logger = Context.GetLogger();

        public Supervisor()
        {
            ReceiveAsync<Props>(async p =>
            {
                var child = Context.ActorOf(p); // create child
                Sender.Tell(child); // send back reference to child actor
            });
        }

        protected override SupervisorStrategy SupervisorStrategy()
        {
            return new OneForOneStrategy(
                maxNrOfRetries: 10,
                withinTimeRange: TimeSpan.FromMinutes(1),
                localOnlyDecider: ex =>
                {
                    switch (ex)
                    {
                        case ArithmeticException ae:
                            return Directive.Resume;
                        case NullReferenceException nre:
                            return Directive.Restart;
                        case ArgumentException are:
                            return Directive.Stop;
                        default:
                            return Directive.Escalate;
                    }
                });
        }
    }
}


유닛테스트

using System;
using Akka.Actor;
using AkkaNetCore.Actors.Study;
using Xunit;
using Xunit.Abstractions;

namespace AkkaNetCoreTest.Actors
{
    public class SupervisorTest : TestKitXunit
    {
        IActorRef supervisor;

        public SupervisorTest(ITestOutputHelper output) : base(output)
        {
            Setup();
        }

        public void Setup()
        {            
            supervisor = Sys.ActorOf<Supervisor>("supervisor");
        }

        [Fact(DisplayName = "ArithmeticException 무시하며,NullReferenceException 재시작,나머지 예외는 중단한다.")]
        public void Test1()
        {
            supervisor.Tell(Props.Create<Child>());
            var child = ExpectMsg<IActorRef>();

            //특정상태로 셋팅 하고 확인
            child.Tell(42); // set state to 42
            child.Tell("get");
            ExpectMsg(42);
            
            child.Tell(new ArithmeticException());      // Directive.Resume
            child.Tell("get");
            ExpectMsg(42);

            //재시작 되었기때문에,Child의 상태값이 0이되었다.
            child.Tell(new NullReferenceException());   //Directive.Restart
            child.Tell("get");
            ExpectMsg(0);

            //Watch를 하면 액터 종료메시지를 받을수 있다.(아카 모니터링기능)
            Watch(child);
            child.Tell(new ArgumentException());        //Directive.Stop
            var message1 = ExpectMsg<Terminated>();
            Assert.Equal(message1.ActorRef,child);

            supervisor.Tell(Props.Create<Child>()); // create new child
            var child2 = ExpectMsg<IActorRef>();
            Watch(child2);
            child2.Tell("get"); // verify it is alive
            ExpectMsg(0);

            child2.Tell(new Exception("CRASH"));
            var message2 = ExpectMsg<Terminated>();            
            Assert.Equal(message2.ActorRef, child2);
            Assert.Equal(true, message2.ExistenceConfirmed);
        }
    }
}





  • No labels