원문 : https://petabridge.com/blog/akkadotnet-what-is-an-actor
Akka.NET: 액터란 무엇인가요?
읽는 데 20분
제가 Akka.NET 과 액터 모델 에 대해 쓸 때 , 제가 의미하는 것은 다음과 같습니다.
저는 액터 개념을 소개하는 모든 100레벨 블로그 게시물과 비디오에서 이 멍청한 농담을 남용할 가능성이 높으므로 양해 부탁드립니다.
진지하게 말해서, 행위자 는 광범위한 개념입니다. 제가 쉽게 정의한 행위자 모델에 대한 정의는 다음과 같습니다.
"액터"는 실제로 시스템 내 인간 참여자를 비유적으로 표현한 것입니다. 액터는 인간과 마찬가지로 메시지를 교환하여 서로 소통합니다. 인간처럼 액터도 메시지 사이에서 작업을 수행할 수 있습니다.
수백 명의 고객이 1-800 번호로 전화를 걸어 여러 명의 고객 서비스 상담원 중 한 명과 동시에 대화를 나누는 콜센터를 생각해 보세요.
이러한 유형의 상호작용은 액터를 사용하여 모델링할 수 있습니다.
Actor 모델에서는 모든 것이 Actor입니다. 객체 지향 프로그래밍(OOP)에서 모든 것이 "객체"인 것과 마찬가지입니다. C#에서 작업할 때는 클래스와 객체를 사용하여 문제 도메인을 모델링해야 합니다. Akka.NET에서 작업할 때는 actor와 message를 사용하여 문제 도메인을 모델링합니다 .
UntypedActorAkka.NET에서 가장 기본적인 모습이 어떤 것인지 예를 들어보겠습니다 .
using System;
using Akka.Actor;
namespace ActorsSendingMessages
{
/// <summary>
/// See http://akkadotnet.github.io/wiki/Actors for more details about
/// Akka.NET actors
/// </summary>
public class BasicActor : UntypedActor
{
protected override void PreStart()
{
}
protected override void PreRestart(Exception reason, object message)
{
}
protected override void OnReceive(object message)
{
//handle messages here
}
protected override void PostStop()
{
}
protected override void PostRestart(Exception reason)
{
}
}
}
Akka.NET에는 다양한 유형의 Actor가 있지만, 모두 .에서 파생됩니다 UntypedActor.
왜 Akka.NET을 사용해야 하나요?
Akka.NET을 사용해야 하는 이유는 무엇이며, Akka.NET은 귀하와 귀사의 어떤 문제를 해결해 줄 수 있습니까?
여기를 클릭하여 메일링 목록에 가입하고 "왜 Akka.NET을 사용해야 하나요?" 백서를 다운로드하세요.
메시지란 무엇인가요?
위의 정의 에서 다음과 같은 방법을 눈여겨보셨을 겁니다 BasicActor.
protected override void OnReceive(object message)
{
//handle messages here
}
이 OnReceive메서드는 모든 액터가 메시지를 수신하는 곳입니다. Akka.NET에서 "메시지"는 C# 입니다 . 메시지는 , 또는 와 같은 사용자 정의 클래스 object의 인스턴스일 수 있습니다 .stringintOfficeStaff.RequestMoreCoffee
액터는 일반적으로 몇 가지 특정 유형 의 메시지만 처리하도록 프로그래밍되지만, 액터가 처리할 수 없는 메시지를 수신하더라도 아무런 문제가 발생하지 않습니다. 일반적으로 액터는 해당 메시지를 "처리되지 않음"으로 기록하고 계속 진행합니다.
메시지는 변경할 수 없습니다
개발자로서 정말 똑똑해 보이게 해줄 재밌는 유행어를 소개합니다. 바로 '불변성'입니다 !
그렇다면 "변경 불가능한" 객체란 무엇일까요?
변경 불가능한 객체란 객체가 생성된 후에는 상태(즉, 메모리의 내용)를 수정할 수 없는 객체를 말합니다.
.NET 개발자라면 클래스를 사용해 보셨을 겁니다 . .NET에서는 가 변경 불가능한 객체라는string 사실을 알고 계셨나요 ?string
예를 하나 들어보겠습니다.
var myStr = "Hi!".ToLowerInvariant().Replace("!", "?");
이 예에서는 다음이 순서대로 발생합니다.
- .NET은
const stringHi!”라는 내용으로 a를 할당한 다음 ToLowerInvariant()호출되면 원래 "Hi!" 문자열을 복사 하고 복사본을 모두 소문자로 수정한 다음 수정된 복사본(이제 "hi!"로 읽힘)을 반환합니다..Replace(string,string)호출되면 "hi!" 문자열을 복사하여ToLowerInvariant복사본을 수정 하고 대체!하여?수정된 복사본을 반환하며 최종 결과는 "hi?"로 읽힙니다.
원본은 string변경할 수 없으므로 이러한 모든 작업은 변경 사항을 적용하기 전에 문자열의 사본을 만들어야 했습니다.
변경 불가능한 메시지는 본질적으로 스레드로부터 안전합니다. 어떤 스레드도 변경 불가능한 메시지의 내용을 수정할 수 없으므로, 원본 메시지를 수신하는 두 번째 스레드는 이전 스레드가 예측할 수 없는 방식으로 상태를 변경하는 것에 대해 걱정할 필요가 없습니다.
따라서 Akka.NET에서는 모든 메시지가 변경 불가능하므로 스레드로부터 안전합니다. 이것이 수천 개의 Akka.NET 액터가 동기화 메커니즘이나 기타 이상한 기능 없이 동시에 메시지를 처리할 수 있는 이유 중 하나입니다. 변경 불가능한 메시지는 이러한 필요성을 없애주기 때문입니다.
액터 행동 101
그렇다면 우리는 액터와 메시지에 어떤 구성 요소가 들어가는지 대략적으로 알고 있습니다. 이게 실제로 어떻게 작동할까요?
액터들은 메시지 전달을 통해 소통합니다
객체 지향 프로그래밍(OOP)에서 객체는 함수 호출을 통해 서로 통신합니다. 절차적 프로그래밍에서도 마찬가지입니다. 클래스 A는 클래스 B의 함수를 호출하고, 그 함수가 반환될 때까지 기다린 후 나머지 작업을 진행합니다.
Akka.NET과 Actor 모델에서 Actor는 메시지를 보내어 서로 통신합니다.
그러면 이 아이디어의 어떤 점이 그렇게 급진적일까요?
우선, 메시지 전달은 비동기적입니다 . 즉, 메시지를 보낸 행위자는 수신 행위자가 보낸 사람의 메시지를 처리하는 동안 다른 작업을 계속 수행할 수 있습니다.
따라서 실제로 한 액터가 다른 액터와 하는 모든 상호작용은 기본적으로 비동기적으로 진행됩니다.
극적인 변화이긴 하지만, 또 다른 큰 변화가 있습니다...
모든 "함수 호출"이 메시지, 즉 객체의 개별 인스턴스로 대체되었기 때문에, 행위자는 함수 호출의 기록을 저장할 수 있으며 심지어 일부 함수 호출 처리를 나중으로 연기할 수도 있습니다!
액터를 사용하여 Microsoft Word에서 실행 취소 버튼과 같은 것을 만드는 것이 얼마나 쉬운지 상상해 보세요. 기본적으로 문서에 적용된 모든 변경 사항을 나타내는 메시지가 있습니다. 이러한 변경 사항 중 하나를 실행 취소하려면 UndoActor 의 메시지 모음에서 메시지를 꺼내 Word 문서의 현재 상태를 관리하는 다른 액터에게 해당 변경 사항을 푸시하기만 하면 됩니다. 이는 실제로 매우 강력한 개념입니다.
액터들은 액터에게 직접 메시지를 보내는 것이 아니라 액터 참조에 메시지를 보냅니다.
또 다른 유행어를 기대하시나요? 위치 투명성 . 액터들이 바로 그겁니다.
위치 투명성이 의미하는 바는, 액터에게 메시지를 보낼 때마다 수백 대의 컴퓨터에 걸쳐 있는 액터 시스템 내에서 액터가 어디에 있는지 알 필요가 없다는 것입니다. 액터의 주소만 알면 됩니다.
누군가의 휴대폰 번호로 전화를 거는 것과 비슷하다고 생각해 보세요. 친구 밥이 미국 워싱턴주 시애틀에 있다는 사실조차 알 필요 없이, 밥의 휴대폰 번호만 누르면 나머지는 통신사에서 알아서 처리해 줍니다.
액터의 작동 방식도 똑같습니다. Akka.NET에서 모든 액터는 다음 부분을 포함하는 주소를 갖습니다.
- 프로토콜 - 웹에서 HTTP와 HTTPS를 사용할 수 있는 것처럼 Akka.NET은 프로세스 간 통신을 위해 여러 전송 프로토콜을 지원합니다. 단일 프로세스 액터 시스템의 기본 프로토콜은 입니다 . 원격 또는 클러스터링을 사용하는 경우 일반적으로 또는
akka://와 같은 소켓 전송을 사용하여 노드 간 통신을 수행합니다.akka.tcp://akka.udp:// - ActorSystem -
ActorSystemAkka.NET의 모든 인스턴스는 시작 시 이름을 지정해야 하며, 해당 이름은 분산 .에 참여하는 여러 프로세스나 머신에서 공유될 수 있습니다ActorSystem. - 주소 - 원격 기능을 사용하지 않는 경우, 의 주소 부분은
ActorPath생략할 수 있습니다. 하지만 이는 액터 시스템 간 원격 통신에 사용되는 특정 IP 주소/도메인 이름 및 포트 정보를 전달하는 데 사용됩니다. - 경로 - 특정 주소에 있는 특정 액터로 가는 경로입니다. 웹사이트의 URL 경로와 같은 구조이며, 모든 사용자 정의 액터는
/user/루트 액터에서 파생됩니다.
따라서 Actor에게 메시지를 보내려면 주소로 메시지를 보내면 됩니다.
//local actor
var actorRef = MyActorSystem.Selection("/user/myActor");
actorRef.Tell("HI!");
//remote actor
var remoteActorRef = MyActorSystem.Selection("akka.tcp://MyActorSystem@localhost:1001/user/myActor");
remoteActorRef.Tell("HI!");
원격 액터와 로컬 액터에게 메시지를 보내는 것은 똑같아 보입니다. 이것이 바로 위치 투명성이 의미하는 바입니다.
ActorReference에 전송된 모든 메시지는 Actor에게 속한 "사서함"에 저장됩니다.
액터에게 메시지를 보내면 해당 메시지가 액터의 OnReceive메서드로 직접 전달되지 않습니다.
메시지는 일반 데이터 구조처럼 FIFO(Queue<T> 선입선출) 방식으로 정렬된 "메일박스"에 저장됩니다 . 메일박스의 역할은 매우 간단합니다. 메시지를 수신하고 액터가 처리할 준비가 될 때까지 보관하는 것입니다.
액터가 메시지를 처리할 준비가 되면 사서함은 메시지를 액터의 OnReceive메서드로 푸시하고 액터의 메시지 처리 메서드를 실행합니다.
액터는 한 번에 하나의 메시지만 처리합니다.
번호표를 받고 줄을 서세요, 친구!
글쎄요, 사실은 그렇지 않아요.
Akka.NET의 액터가 보장하는 것 중 하나는 메시지를 처리할 때 액터의 컨텍스트와 내부 상태가 항상 스레드로부터 안전하다는 것입니다.
이것이 사실인 이유는 다음과 같습니다.
- 메시지는 변경할 수 없으므로 각 메시지의 내용은 본질적으로 스레드로부터 안전합니다.
- 메시지는 직렬로 처리되므로 액터의 내부 상태와 컨텍스트에 대한 변경 사항을 여러 스레드에서 동기화할 필요가 없습니다. 즉, 단일 스레드 환경에서와 마찬가지로 처리할 수 있습니다.
따라서 행위자는 OnReceive메서드가 종료될 때까지 두 번째 메시지 처리를 시작할 수 없습니다. 메서드가 종료되면 사서함은 다음으로 사용 가능한 메시지를 OnReceive메서드에 푸시합니다.
액터는 내부 상태를 가질 수 있습니다
다른 C# 클래스와 마찬가지로 액터는 자체 속성과 필드를 가질 수 있습니다.
BasicActor액터가 재시작되면 액터 인스턴스( 이 경우 내 클래스의 인스턴스 )가 파괴되고 다시 생성됩니다.
새로운 인스턴스가 BasicActor생성되고, 이에 전달된 모든 생성자 인수 Props(나중에 더 자세히 설명하겠지만, 지금은 Props액터를 생성하기 위한 레시피라고 생각하세요)가 새 인스턴스로 다시 전달됩니다.
이 내용을 언급하는 이유는 다음 섹션에서 액터의 수명 주기에 대해 이야기할 것이고, 예상치 못한 문제가 발생할 때마다 Akka.NET이 모든 액터 클래스를 초기 상태로 재부팅할 수 있다는 것을 기억하는 것이 중요하기 때문입니다.
액터들은 명확하게 정의된 라이프 사이클을 가지고 있습니다
액터가 사서함에서 메시지 처리를 시작하려면 먼저 액터 시스템에 의해 인스턴스화되고 수명 주기를 거쳐야 합니다.
액터는 생성되어 시작되고, 이후 대부분의 시간을 메시지를 수신하는 데 사용합니다. 더 이상 액터가 필요하지 않으면 액터를 종료하거나 "중지"할 수 있습니다.
액터가 실수로 충돌하는 경우(즉, 처리할 수 없는 을 발생시키는 경우
Exception), 액터의 감독자는 액터의 수명 주기를 처음부터 자동으로 다시 시작합니다. 액터의 사서함에 남아 있는 메시지는 손실되지 않습니다.
사실 저는 이 라이프사이클을 구현하는 모든 Akka.NET 메서드를 BasicActor이전 샘플에서 그림으로 설명했습니다. 살펴보겠습니다.
Actor's constructor- 샘플에서는 기본 생성자를 사용했기 때문에 이를 설명하지 않았지만BasicActor, C#의 다른 클래스와 마찬가지로 액터의 생성자에 인수를 전달할 수 있습니다.PreStart- 이 로직은 액터가 메시지 수신을 시작하기 전에 실행되며, 초기화 로직을 배치하기에 좋은 위치입니다. 재시작 시에도 호출됩니다.PreRestart- 액터가 실수로 실패하면(즉, 처리할 수 없는 오류를 발생시키는 경우Exception) 액터의 부모가 액터를 다시 시작합니다.PostStop- 액터가 중지되어 더 이상 메시지를 수신하지 않을 때 호출됩니다. 여기에 정리 로직을 포함하기 좋습니다.PostStop액터 재시작 시에는 호출되지 않고, 액터를 의도적으로 종료할 때만 호출됩니다.PostRestartPreRestart- . 이후 재시작 시 호출됩니다PreStart. . 이 함수는 Akka.NET에서 제공하는 기능 외에도 액터 충돌을 유발한 오류에 대한 추가 보고나 진단을 수행하기에 좋은 곳입니다.
앞서 보여드린 라이프사이클에 Akka.NET 메서드가 어떻게 적용되는지는 다음과 같습니다.
모든 액터에게는 부모가 있고, 어떤 액터에게는 자녀가 있습니다.
사람과 마찬가지로 액터에게도 부모가 있습니다. 그리고 어떤 액터에게는 조부모, 형제 자매, 자녀가 있습니다.
뭐야? 이게 무슨 뜻이야?
즉, 모든 액터는 다른 액터에 의해 생성되어야 합니다. 따라서 다음과 같은 코드를 작성하면:
var actorRef = MyActorSystem.ActorOf(Props.Create<BasicActor>(), "myActor");
actorRef.Tell("HI!");
우리는 실제로 루트 액터에 새로운 자식 액터를 만들고 있습니다 /user/. 즉, 새로 만든 액터의 경로는 실제로 .입니다 /user/myActor/.
마찬가지로, 이것 내부에서 실행할 때 BasicActor다른 자식 행위자를 생성할 수 있습니다.
protected override void OnReceive(object message)
{
var childActor = Context.ActorOf(Props.Create<BasicChildActor>(), "child1");
childActor.Tell("Hi!");
}
이 경우childActor경로는 다음과 같습니다/user/myActor/child1/.
이제… 이것이 왜 중요한지 알아보겠습니다.
부모는 자녀를 감독합니다
액터의 라이프사이클에 대한 섹션에서 "감독자에 의해 액터가 재시작된다"는 개념을 언급했습니다. 제가 말하고자 하는 것은 모든 부모 액터가 자녀에게서 " 도와주세요, 저 망가졌어요! "라는 특별한 메시지를 받는다는 것입니다.
SuperviserStrategy부모 객체에는 자식 액터의 실패 처리 방법을 결정하는 기본 객체(또는 사용자 지정 객체 제공 가능)가 제공됩니다 . 부모 객체는 다음 세 가지 결정 중 하나를 내릴 수 있습니다.
- 실패한 액터를 다시 시작합니다 . 이는 자식이 짧은 시간(예: 60초) 동안 반복적으로 실패하지 않는 한 부모가 기본적으로 수행하는 작업입니다.
- 실패한 행위자를 중지하여 영구적으로 종료합니다. 또는
- 감독을 조부모 액터에게 확대합니다 .
Restart또는 지시가 내려 지면 Stop영향을 받는 액터의 모든 자식도 재시작되거나 종료됩니다. 즉, 실패했던 액터 가계도 전체를 한 번에 재부팅할 수 있습니다.
이는 신뢰성과 자가 복구 시스템에 대한 매우 강력한 개념으로, 앞으로 더 자세히 살펴보겠습니다.
잠깐만요... 이 모든 것이 사실이라면 어떻게 액터들이 대규모로 동시에 작업할 수 있나요?
액터는 한 번에 하나의 메시지만 처리할 수 있다고요? 엄청 느리지 않나요?
좋은 질문입니다. 이러한 액터들을 동시 처리가 가능한 거대 기계로 만드는 방법에 대한 자세한 설명은 다음과 같습니다.
액터들은 싸다
Akka.NET 웹사이트 의 벤치마크 결과에 따르면 , 1GB RAM에 250만 개의 액터를 저장할 수 있습니다. 그 이후로 상당한 메모리 최적화를 진행했기 때문에 이제는 훨씬 더 많은 액터를 메모리에 저장할 수 있을 것으로 예상됩니다.
하지만 요점은 액터를 고용하는 데 비용이 많이 들지 않고, 많은 수의 액터를 활용할 수 있다는 것입니다.
액터들의 확장
액터 비용이 저렴하다면, 액터 한 개를 사용하는 것과 액터 천 개를 사용하는 것의 메모리 오버헤드가 미미하다는 것을 의미합니다. 그리고…
한 액터는 한 번에 하나의 메시지만 처리할 수 있습니다. 하지만 천 명의 액터는 한 번에 천 개의 메시지를 처리할 수 있습니다. 바로 이러한 방식으로 액터 시스템의 동시 처리 잠재력을 최대한 활용할 수 있습니다. 바로 여러 액터를 확장하여 확장하는 것입니다.
다시 말해, 액터를 여러 개 사용하면 최대의 효과를 얻을 수 있습니다. 전체 애플리케이션을 단일 액터만 사용하도록 작성하면 엄청나게 느려질 것입니다.
따라서 액터 모델의 동시 처리 기능을 최대한 활용하려면 액터 시스템을 확장 가능하도록 설계하세요.
액터들은 게으르다
그렇다면 행위자가 처리할 메시지가 없다면 어떻게 될까요?
네, 우편함 안에서 자는 우편배달부입니다. 혹시 궁금해하시는지 모르겠지만요.
처리할 메시지가 없으면 액터는 아무 작업도 하지 않습니다. 실제로 처리할 메시지가 있는 다른 모든 액터가 스레드와 서버의 처리 능력을 사용하게 됩니다.
메시지가 없는 액터는 스레드나 다른 컴퓨팅 리소스를 사용하지 않습니다. 이들은 반응형입니다 . 즉, 도착하는 메시지로 깨어날 때까지 기다립니다.
이것이 액터가 매우 저렴하고 확장성이 뛰어난 이유 중 하나입니다.
Akka.NET 및 .NET에서의 분산 컴퓨팅과 관련된 다른 주제에 대해 자세히 알아보고 싶으시다면 아래 메일링 목록에 가입하세요!
이 글이 마음에 드셨다면, 팔로워들과 공유하시거나 트위터 에서 저희를 팔로우해주세요 !
2015년 1월 25일 Aaron Stannard 가 작성함
Phobos를 사용하여 Akka.NET 애플리케이션을 관찰하고 모니터링하세요
Phobos가 OpenTelemetry를 사용하여 Akka.NET 애플리케이션을 자동으로 계측할 수 있다는 사실을 알고 계셨나요 ?










