Dispatcher는 ActorSystem 내에서 실행되는 모든 코드를 스케줄링 합니다.
각 Actor의 처리량과 시간 점유율을 조정하여 각자에게 공정한 리소스를 제공합니다.
구성 변경을 하지 않는한 일반적으로 시나리오에 맞게 최적화 된 각 프레임워크가 가지고 있는 ThreadPOOL을 각각 활용합니다.
- JAVA의 경우 JVM에서 제공하는 스레드풀 전략을 사용
- .net의 경우 .net CLR이 제공하는 스레드풀 전략을 사용
Dispatcher 설정기능을 제공함으로 , 액터그룹별로 다른 성능목표를 가지고 , 성능전략을 세울수가 있습니다.
AKKA를 사용하기 위해서는 스레드를 직접 생성하고 복잡한 스레드모델을 작성할 필요가 없다라고 앞에서
설명을 하였지만 동시성 메시지 처리를 잘하기 위해서 Dispatcher가 어떠한 옵션을 제공하는가?를
이해하기위해서는 멀티스레드를 구현의 관점이 아닌 사용 관점에서 학습할 필요가 있습니다.
C#이 지원하는 비동기 동시처리 프로그래밍에서 ASYNC ,AWAIT,TASK등의 지원으로 ( .net 45+)
닷넷프레임워크 내에서도 직접 스레드 생성하는것을 지양합니다.
동시성 처리 변천사 : 멀티스레드프로그래밍 → 비동기프로그램밍 → 동시성 프로그래밍
관련 개발 트렌드는, 언어에 상관없이 발전을하였며 추상화되어서 사용법의 차이이며
라이브러리 자체가 기존 메카니즘을 포함하고 있거나, 연동되어 같이 사용가능합니다.
포함의 의미는, 동시성 프로그래밍을 다루면 멀티스레드가 포함된다란 의미 입니다.
반대로 멀티스레드 프로그래밍만 제공되는 어떠한 프레임워크에서는
문법적으로 비동기 프로그래밍지원이 안될수도 있습니다.
Dispatcher
custom-dispatcher { type = Dispatcher throughput = 100 }
.NET ThreadPool 을 통해 작동이되며 대부분의 경우 이것만으로 충분합니다.
몇개의 스레드를 둘것인가? 보다 몇개의 메시지를 동시에 처리하게 할것인가? 에대한 설정입니다.
TaskDispatcher
custom-task-dispatcher { type = TaskDispatcher throughput = 100 }
TPL 인프라를 사용합니다. 어떠한 Task가 병렬적으로 처리가 되어 멀티코어에 최적화되어
작동되게 합니다. 일반적으로 사용될 일이 없으나
TPL인프라를 꼭 사용하여 극단적 튜닝이 필요할시 활용할수가 있습니다.
- TPL에서 메시지 전송순서가 보장되는지 확인할것
PinnedDispatcher
custom-dedicated-dispatcher { type = PinnedDispatcher }
AKKA가 준비한 ,단일 전용 스레드만 사용합니다. 어떠한 목적을 두고 준비된 Dispatcher는 아니며
서비스 메시지 처리와, 시스템 성능 메시지처리를 분리한다고 했을시
성능확인을 위해 그리많은 메시지처리가 되지 않고, 서비스 메시지처리와 분리하여 작동시키고자 할때 활용될수 있습니다.
ForkJoinDispatcher
my-dispatcher { type = ForkJoinDispatcher throughput = 100 throughput-deadline-time = 0ms dedicated-thread-pool { thread-count = 3 deadlock-timeout = 3s threadtype = background } }
대부분 Dispatcher는 목적에맞게 최적화된 각자의 스레드풀 메카니즘이 있고 신경쓸필요없으나
ForJoinDispatcher의 경우 전용 스레드풀을 생성하여, 스레드수를 직접 할수있는 유일한 Dispatcher입니다.
이 스케줄러를 이용할시, 세밀한 튜닝 전략으로 일부 액터를 분리할수가 있습니다.
또한 기존 플래폼이 가진, 비동기 프로그래밍과 휼륭하게 연동될수 있습니다.
지난 몇년간 멀티스레딩 프로그래밍을 통해 개발을 했고 문제를 풀려고 하였습니다.
하지만,5년간 운영하면서 어떠한 한계에 부딪쳤습니다. 스레드문제처리에대해 개발내에 숙련이 유지되지 않는다는점이며
단일기기에서 충분한 사용자처리 성능을 이끌수있으나, 그속에서도 대부분의 운영 로그가 스레드에관한것이고 이것을 해결하는데 골칫거리였습니다.
실제 서비스에관련된 로그가 20%라고 하면 TCP처리및 스레드에관련된 로그가 80%이상이였습니다.
로그의 복잡성은, 개발코드의 복잡성을 반영하며 문제파악및 확장이 어려워집니다.
더욱이 현재 느끼는 심각한문제는, 그렇게 쌓인 팀라이브러리가 AKKA에서 고민하는 추상화된 스레드모델에(디스페쳐) 한참 못미치고
거의 쓰일데가 없다란 것입니다.
오히려 어떠한 동시성을 위한 추상적 레벨의 라이브러리를통해, 제공하는 옵션을 어떻게 이해하고 사용해야하는가?
멀티스레드및 비동기프로그래밍을 어떻게 이해해야하고, 다시 학습하게 되는 계기가 되었습니다.
AKKA에서는 아래와 같은 스레드 옵션을 지원합니다.
thread-count
- dispatcher 가 사용할 스레드수를 지정합니다.deadlock-timeout
- 기본적으로 지정없으면 무한적으로 실행될수 있습니다. 너무 낮은값을 지정하면 작업 손실이 되니 신중하게 값을 설정합니다.threadtype
-background
,foreground
. 두 옵션이 있으며 OS또는 플래폼 특성적이니 how .NET handles 을 참고합니다.
SynchronizedDispatcher
synchronized-dispatcher { type = "SynchronizedDispatcher" throughput = 10 } private void Form1_Load(object sender, System.EventArgs e) { system.ActorOf(Props.Create<UIWorker>().WithDispatcher("synchronized-dispatcher"), "ui-worker"); }
SynchronizedDispatcher는 SynchronizationContext 를 사용하며 Actor가 UI 업데이트를 한다고하면
UI를 업데이트할수있는 전용 Dispatcher 입니다. Actor는 UI Thread내에서 작동되는 녀석이 아니며
Actor와 별개로 대부분 타 스레드가 UI 객체를 변경하고자 할때 문제가 생깁니다. ( WPF,WINFORM 모두 동일)
이러한 문제에 대한 단순한 처리는 UI 스레드내에서 어떠한 작업을 진행하는것인데, 그러면 UI가 멈춰서 좋은 UX를 제공하기 불가합니다.
관리툴 목적으로, 네이티브한 UI 를 만드는 경우 활용할수 있습니다.
TEST1-액터내 블락킹처리
public class ReActor : ReceiveActor { private ILoggingAdapter log = Context.GetLogger(); public ReActor() { Receive<string>(message => { if(message == "slow") //slow메시지를 받으면, 지연시킵니다.(테스트 지연코드) { Task.Delay(500).Wait(); //처리결과를 기다리는 블락킹 코드사용은 권장되지 않음 } string reply = string.Format("I'am {0} RE:{1}", Self.Path, message); log.Info(reply); Sender.Tell(reply); }); } }
var actor1 = actorSystem.ActorOf(Props.Create<ReActor>().WithDispatcher("my-dispatcher"), "my-actor1"); var actor2 = actorSystem.ActorOf(Props.Create<ReActor>().WithDispatcher("my-dispatcher"), "my-actor2"); var actor3 = actorSystem.ActorOf(Props.Create<ReActor>().WithDispatcher("my-dispatcher"), "my-actor3"); for (int i=0; i < 100; i++) { actor1.Tell("slow"); actor2.Tell("slow"); actor3.Tell("slow"); } Console.WriteLine("completed - send all");
Akka의 로그는 기본적으로 고유 스레드 아이디를 표시해줍니다.
아래내용을 펼쳐 옵션에따라 어떻게 결과가 다른지 확인이 가능합니다.
스레드수를 늘려 동시성을 처리하는것은 좋은 아이디어가 아닙니다. Dispatcher의 Thread수 조절에 따라
어떻게 작동되는지 확인하기위한 샘플이며, Actor내의 어떠한 Task도 비동기처리가 권장됩니다.
대부분 사용할일이 없지만, 어떠한 액터가 메시지를 받으면 블락킹이되고 오래걸리는경우 (일반적으로 외부 RestAPI)
이와같은 전략을 통해 스레드수분리가 가능합니다. ( 기본 메시지처리기에 부하를 분리)
적용2-ACTOR내에서 비동기처리
RestAPI를 대신하여 호출해주는 Actor가 필요하다고 가정해봅시다.
설계시 다음과 같은 요구사항을 가집니다.
- 호출한 순서로 완료 처리되는게아닌, 호출완료된순으로 완료 처리가 되어야하며 순서는 크게 중요하지 않음
- Actor를 통해 RestAPI 결과값을 비동기적으로 전송이되어야함
- 성능을 위해 동시에 실행되거나 병렬로 실행되어야함
C#의 비동기 프로그래밍의 힘을 빌려 Task에 완료된 비동기처리를 Pipe로 연결하여
이러한 문제를 풀어보겠습니다. 또한 성능 옵션설정은 위에서 설명한 디스패쳐를 통해 튜닝전략을 옵션을통해
이룩해냅니다.
이러한 모델은, 어떠한 작업이 언제끝날지 모르는 외부장치 의존이있을시(DB,RESTAPI)
연동시 활용될수 있습니다.
public class DelayReply { public string message; } public class ReActor : ReceiveActor { private ILoggingAdapter log = Context.GetLogger(); public ReActor() { string myPath = Self.Path.ToString(); Receive<string>(message => { Handle(message); }); Receive<DelayReply>(message => { Handle(message); }); } public void Handle(string str) //In:샘플은 String이지만, 어떠한 Request를 의미하는게 좋습니다 { Task.Run(async () => { await Task.Delay(1000); //어떠한 값을 기다림 DelayReply reply = new DelayReply(); reply.message = str; return reply; }).PipeTo(Self); //처리완료 결과가 파이프를 통해 DelayReply 처리기로 넘깁니다. } public void Handle(DelayReply data) //Out:어떠한 Result/Response임을 명시하는게 좋습니다. { string logtrace = string.Format("I'am {0} RE:{1}", Self.Path, data.message); log.Info(data.message); Sender.Tell(data); } }
var tasks = new List<Task>(); tasks.Add(actorA.Ask("request", TimeSpan.FromSeconds(1))); tasks.Add(actorB.Ask("another request", TimeSpan.FromSeconds(5))); Task.WhenAll(tasks).PipeTo(actorC, Self);
액터의 리소스사용
- 사용 자원증가에 따른 최대 한계측정
스레드의 비용
항목 | 비용 | 상세 |
---|---|---|
커널메모리 | 약 1KB | 스레드 데이터와 속성들을 저장합니다. 이 데이터들은 페이징 될 수 없습니다. |
스택영역 | 512KB(일반 스레드), 8MB(OS X 메인 스레드), 1MB(iOS 메인 스레드) | 일반 스레드의 스택 영역은 최소 16KB이고, 4KB의 배수여야 합니다. 해당 메모리는 스레드가 생성될 때 할당되지만, 실제 사용되기 전까지 페이지가 생성되지는 않습니다. |
생성 시간 | 약 90ms | 스레드 생성 요청부터 스레드 루틴이 시작될 때 까지의 시간 |
스레드는 스택영역에 생성되기때문에 생성만 되어도 기본적으로 큰 메모리를 차지하게됩니다.
채널처리라는 도메인처리를 단일장비에서 1000개가 필요하다고 가정해봅시다.
- 1000 * 1mb = 약 1gb
채널하나당 스레드로 할당하게되면 아무런 일을 하지 않고도 1gb의 메모리를 차지하게 됩니다.
물론 스레드풀을 이용하여 스레드를 더 적게 사용할수 있도록 설계할수도 있습니다. 하지만 이것은 도메인 프로그래밍이 아니라 시스템 프로그래밍이 되게 됩니다.
도메인 로직속에 시스템 프로그래밍이 결정적으로 함께 있어야함을 의미하며 멀티스레딩 프로그래밍이란 다루기 어려운 시스템 프로그래밍을 깊이 있게 학습해야합니다.
반면 액터의 경우 스택영역이 아닌 힙영역에 생성되며 200만개의 액터가 사용하는 메모리는 고작 1GB에 불가합니다.
물론 이것이 200만개의 액터가 모두 동시에 진행할수 있다란것을 보장하는것은 아닙니다.
액터를 실행하는 스케줄러는 Dispacher에서 튜닝을 할수 있으며 스레드수를 조절할수 있습니다.
스레드 사용전략을 도메인 모델을 가지고 있는 액터모델과 완벽하게 분리할수 있음을 의미하며 실행레벨에 정의할수도 있지만
설정화로 완벽하게 분리하여 빌드없이 튜닝옵션으로 실행단계에 결정할수도 있습니다.