장애에 대응하기 위해서는 크게 두가지 방법이 있습니다.
- 시스템및 서비스가 절대로 장애가 발생하지 않아서 아무것도 하지않는 경우
- 반대로 모든 장애에 대해 대응책을 세우는것입니다.
전자는 거의 불가능하고, 후자또한 어떠한 장치로 모든것에 대응하는 전략을 세우는것은 아주 어렵고
서비스코드에 썩임으로 서비스코드의 흐름을 복잡하게 만들수가 있습니다.
AKKA자체로 이러한 장애에대한 대응을 자동으로 할수 있는것은 아닙니다.
단지 ,이러한 대응을 일괄적이고 유연한 방법을 제시하고 개발자는 다양한 전략중
하나를 선택하여 그것을 서비스코드와 분리하여 설계할수 있는 장치만 제공을 해줍니다.
장애 대응 설계
액터 장애처리모델은 기존 예외처리모델과 비교하여, 서비스코드와 예외상황을 완전하게
분리합니다. 장애처리 코드가 서비스 코드에 썩이지 않음으로 서비스의 흐름을 방해하지 않습니다.
장애처리에대한 플랜은 부모가 모니터링하고 책임지는 구조로 그것을 직접 설계에 반영해야합니다.
다음과 같이, 객체에대한 장애 대응 플랜을 세웠다고 가정해봅시다.
- 복구시도는 1분이내에 10번만 시도될수 있다. (복구 플랜)
- 널참조로인한 예외는 관련 작동객체(액체) 재시작하여 복구한다. (재시작 플랜)
- 인자값에 관련한 예외 발생시, 해당 작동객체를 Stop시킨다. ( 중단 플랜 )
Test Plan 은 아래와같습니다.
- 생성 - 테스트에 사용할 액터를 생성한다. 부모 : supervisor , 자식 : child1
- 예외 - child1액터에게 널 예외 유발시킨다.
- 장애 - child1액터 crash 및 복구확인
- 체크 - child1에게 메시지보내어 작동중인지 확인 - 복구가 되었음으로 전송가능
- 예외 - child1액터에게 인자값 예외 유발시킨다.
- 장애 - child1가 crash가 된것 확인
- 체크 - child1에게 메시지를 보내 작동중인지 확인 - crash임으로 전송불가
구현및 실행코드
public class Supervisor : UntypedActor { private readonly ILoggingAdapter log = Context.GetLogger(); //장애처리 룰을 정의합니다.(서비스 코드와 완전분리) protected override SupervisorStrategy SupervisorStrategy() { return new OneForOneStrategy( maxNrOfRetries: 10, //최고 시도횟수 withinTimeRange: TimeSpan.FromMinutes(1), //최고 시도 시간 localOnlyDecider: ex => { // Exception별 복구전략 if( ex is ArithmeticException ) return Directive.Resume; else if( ex is NullReferenceException) return Directive.Restart; else if (ex is ArgumentException) return Directive.Stop; else return Directive.Escalate; }); } protected override void OnReceive(object message) { if (message is string) { string msg = message as string; if ( msg.Contains("create-") ) { string childName = msg.Split('-')[1]; log.Info("CreateChildActor:" + childName); var childActor = Context.ActorOf<Child>(childName); Context.Watch(childActor); Sender.Tell(childActor); } } } } public class Child : UntypedActor { private int state = 0; private readonly ILoggingAdapter log = Context.GetLogger(); protected override void OnReceive(object message) { if( message is Exception) //가상의 예외가 발생했다고 가정하고,Let it Crash( 죽게놔둠 ) { throw message as Exception; } else if (message is int) // 서비스코드는 예외에대한 어떠한 키워드도 사용할 필요가 없습니다. { state = (int)message; } else if (message is string) { switch (message as string) { case "get": log.Info("Active Check"); //메시지를 받을수 있는지 체크 Sender.Tell(state); break; } } } protected override void PreRestart(Exception reason, object message) { log.Info("Actor Restart - reason:" + reason.GetType().Name ); //액터 재 시작된이 야기된 이유를 , 재시작시 알수 있습니다. } }
using (ActorSystem actorSystem = ActorSystem.Create("ServiceA")) { IActorRef supervisor = actorSystem.ActorOf<Supervisor>("supervisor"); IActorRef child = supervisor.Ask("create-child1").Result as IActorRef; child.Tell(new NullReferenceException() ); //강제로 예외발생... Task.Delay(1000).Wait(); //복구를 기다려준다. child.Tell("get"); child.Tell(new ArgumentException() ); //강제로 예외발생... Task.Delay(1000).Wait(); //복구를 기다려준다. child.Tell("get"); Task.Delay(3000).Wait(); //종료 지연시간 }
결함 처리가 다소 이질적일수 있습니다.
기존 예외처리는, 예외 발생에대해 복구를 하기보다, 책임을 전가하면서 빠르고 우아하게
상위코드로 흐름을 변경할수 있다란 특징이 있으며 절대로 죽지 않게하고, 로그를 처리할때는 유용할수 있습니다.
기존 예외처리에 재시도 처리를 하고자 한다면 다음과 같은 문제가 있을수 있습니다.
호출자에게 예외를 던지면, 대부분 호출자는 어떻게 처리해야할지 모름
다음과 같이, 로컬에 파일이 추가 되면 DB까지 자동으로 적는 FileWatcher가 있다고 가정해봅시다.
- A : 새파일이 추가되면 FileWatcher가 감지하고 , 스레드가 작동됨
- B: 파일 리드기는 파일을 Row단위로 읽어서 로그 Write에게 정보를 넘김
- C: 로그 Witer기는 Row단위로 정보를 받아서 DB에 적재하려고함
- D: DBWrite는 DB에 연결을 하여, Row단위로 적재를함
진행Type | 예외발생시 | |
---|---|---|
A | FileWatcher | |
예외처리 | 여기서 예외잡지못하면 스레드 종료됨 | |
B | FileReader | |
예외처리기 C | 어디까지 기록했는지 모름 | |
C | LogWriter | |
예외처리D | DBWrite를 재시도할수있는 정보가 없음 | |
D | DBWriter |
기존 예외 처리 모델의 문제는, 호출자에게 책임을 계속 떠 넘기는데 있습니다.
호출구조상 Stack의 윗부분에는, 에러핸들링 처리를 위한 충분한 정보가 없습니다.
즉 D과정에 DbBrockenConnectionException 이 발생하고 이과정에서
예외가 발생하여 C의 예외처리기에 갔다고 했을시, C가가진 예외처보및 LogWrite기는
기존 DB접속에대한 정보가 없어서 재시도할 충분한 정보가 없습니다. 물론 피호출자에게 에러처리를 위한 정보를 계속 전달할수도 있습니다.
하지만 예외 처리를 위해 LogWrite기에서 DB재시도를 위해 DBWriter를 재생성할수도 있으나, DB 정보접속을 전달하는것은 설계원칙을 깰수가 있습니다.
LogWriter기는 DB의 접속정보를 가지고 있지 말아야합니다.
책임을 지지않는 전역 Catch
과거 예외 핸들링이 지원안되었을시, retun -1,switch(error) 이러한 코드로 일관성없는 에러처리를 위한 로직을 서비스 코드와 혼재하였고
이것을 개선하고자 예외 처리모델이 나왔습니다. 예외를 발생시키고 , 어느곳에 집결을하여 예외 핸들링을 할수가 있어서
조금더 구조적인 예외 핸들링이 가능해졌습니다.
Exception을 세부적으로 설계하고 구조적으로 Catch를 하여 일관성을 유지하는것 그것이 예외처리 모델에 기본이였습니다.
AKKA에서는 부모가 자식(정확하게는 호출자)에게 예외를 전가시키지 않는 관리감독 전략을 통해 에러핸들링을
서비스 코드에서 분리하고, 최대한 서비스코드에서 에러처리를 분리하여 상위 부모에서 해결하려는 전략을 사용합니다.
생성에 관여한 부모액터가 가장 적절한 복구전략을 세울수 있기때문입니다.
물론 액터는 트리형태의 구조적인 형태로 구성이 되기때문에, 이러한 관리자 감독에 의한 에러처리가 가능하며
모든경우 다 사용할수 있는 방법이 아니기때문에 익셉션처리 프로그래밍은 여전히 중요하게 사용되고 있으며,
액터의 복구전략과 예외처리 프로그래밍을 비교하는것은 예외처리 프로그래밍을 더 고찰할수 있게합니다. - 예외 처리 가이드
예외란?
예외는 복구를 위한 수단이라기보다, 에러처리에대한 일관성 없는 상태를 방지하고자 함수호출을
빠르게 거슬러 올라가기위한 수단이다.
Let It Crash?
부적절하게 안죽게 하려고, 처리하지도 못하는 예외를 서비스코드에 썩지말고
그냥 죽게두고, 부모의 복구 플랜을 통해 장애처리를 서비스코드와 분리하는 전략
그리고 전략에따라, 백업서비스가 있다고하면 그냥 죽게놔두는것이 유용한 전략
참고: http://wiki.c2.com/?LetItCrash