이장을 통해 알게되는것
- 액터의 성능 전략을 다르게 할수있으며, 코드의 변경없이 설정(튜닝)만으로 가능
- 성능을 위한 스레드풀 선택
- 성능을 위한 라우터 선택
액터의 메시징 동시성 처리를 위해서, 내부적으로 작성된 플랫폼에 최적화된 스레드풀을 활용하지만 경우에 따라 메시지마다 스레드풀의 옵션을 다르게 해야할 필요가 있습니다.
멀티스레드 프로그래밍을 직접하였다면, 이것을 모두 구현해야하지만 AKKA 에서는 Dispatcher라는 컨셉을 통해 이러한 스레드풀의 성능 전략을 옵션화하여
액터별/라우터별로 성능전력을 각각 지정할수가 있습니다.
일반적으로 라우터는 스케일 아웃(분산처리 성능업)과 관련이 있고, Dispatcher 전략은 스케일업(단일 노드 성능업)과 관련이 있습니다.
스레드풀 전략을 다르게 사용해야하는이유
스레드 생성에는 많은 비용이 발생하며, 최대 생성 개수가 그렇게 넉넉하지 않으며 생성에 걸리는 시간도 부담이 될수 있습니다.
매작업마다 스레드를 생성하여 처리하고 종료하는것은 아주 비효율적입니다.
이러한 문제를 해결하기위해 스레드풀이란 컨셉이 생겼으며 원리는 간단합니다. 미리 생성된 스레드를 준비시켜놓고
작업해야할 Task를 공유된 큐를 통해 전달하고 준비된 스레드에게 작업을 분배함으로 동시에 처리하는듯한 효과를 연출하는것입니다.
이러한 이점이 있음에도 불구하고, 성능이 다른 Task를 동일한 스레드풀을 공유하는것은 비효율적일수 있습니다.
극단적인 튜닝의 세계에서는 분리하는것이 좋습니다. ( 일반적으로 같이 사용해도 무방합니다.)
메시지 중에도 처리가 빠른것,느린것이 존재 할수 있습니다.
- 메시지TypeA : 메시지를 받으면 DB Update하느라 10초가 소요되는 것
- 메시지TypeB: 메시지를 받으면 1초 이내에 수행이 모두됨
DB 업데이트 속도에따른 처리량은 어떻게 할수 없다고 하더라도, 같은 풀에서 관리하면
TypeA가 모든 스레드를 점유 하여, 의존없는 TypeB의 처리량이 현저히 감소하기 때문의 균등한 처리기회를 주기위해 분리를 해야합니다.
다음과 같이 설정된 톨게이트가 있다고 해봅시다.
- 하이패스 게이트 7개 / 1개
- 현금 정산 게이트 3개 / 10개
하이패스 초기 도입시는, 하이패스 통과 게이트가 작아서 찾기 어려웠지만, 최근에는 하이패스가 많이 보급되어 현금정산 게이트찾기가 어렵습니다.
정산이 필요한 느린 게이트를 위해 스레드를 많이 확보해둘 필요는없습니다. 느린 게이트때문에 하이패스 게이트가 느려질 필요가 없다란것이 중요하며
보급률을 고려하여, 유연하게 조정또한 가능 해야합니다. 여기에는 비용과 효율이라는 복합적인 문제가 뒤따릅니다.
현금정산 속도를 하이패스와 동일한 속도를 맞추기위해 불필요하게 스레드를 확보할 필요가 없다란것입니다.
그래서 각각의 전용 게이트(스레드풀) 를 이용하여 각각 성능 최적화의 옵션을 찾는것이 중요합니다.
도메인 구현로직을 전혀 변경하지 않고 유연하게 성능 전략 변경이 가능한것이 Akka Dispatcher의 핵심입니다.
구현 라우팅,액터 스레드풀 변경
# 기본 스레드 풀 사용 custom-dispatcher { type = Dispatcher throughput = 100 } # TPL 사용 custom-task-dispatcher { type = TaskDispatcher throughput = 100 } # fork-join 스레드풀 사용 custom-fork-join-dispatcher { type = ForkJoinDispatcher throughput = 100 dedicated-thread-pool { thread-count = 3 deadlock-timeout = 3s threadtype = background } } AkkaLoad.RegisterActor( "printer", actorSystem.ActorOf(actorSystem.ActorOf(Props.Create<PrinterActor>() .WithDispatcher("custom-dispatcher") .WithRouter(FromConfig.Instance).WithDispatcher("custom-task-dispatcher"), //.WithRouter(new RoundRobinPool(5)), "printer-pool" ));
- throughput : 스레드 를 N개 사용하겠다란 의미가 아니며, N개 동시처리가능하며 ,N개 입출력에 최적화된 스레드풀을 활용하겠다란 의미입니다.
- Dispatcher : 닷넷 스레드풀 을 이용하며 대부분 이것만으로 충분합니다.
- TaskDispatcher : 닷넷 TPL 을 활용하는 전략
- ForkJoinDispatcher : 분할 정복 알고리즘 이 적용된 스레드풀 로직 - Fork And Join 컨셉
성능과 관련된 용어
- DeadLock : 특정 Task에 Lock이 해제가 안되어, 이것을 공유하는 나머지 모든 Task가 실행대기에 빠져 교착상태가 되어 아무것도 실행되지 않는경우
- LiveLock : 작업 우선순위를 서로에게 양보하여 데드락과 똑같이 누구도 실행되지 않는경우
- Starvation : 우선순위가 높은 Task만 실행되어, 낮은 Task가 실행이 거의 되지 않는 경우
- Race Condition : 우선순위 경쟁에 의해 의도하지 않는 작업순서가 바뀌어 로직 에러가 발생하는경우
참고 :Terminology
더자세한 정보 :
- https://getakka.net/articles/actors/dispatchers.html
- https://getakka.net/articles/configuration/akka.html
- https://medium.com/akka-for-newbies/dispatchers-5c123e35acaf
- 코드 변경 : https://github.com/psmon/AkkaForNetCore/commit/1e95db3e2286e013662ffedb713cc00a9eca8025