장애 허용시스템

장애에 대응하기 위해서는 크게 두가지 방법이 있습니다.

  • 시스템및 서비스가 절대로 장애가 발생하지 않아서 아무것도 하지않는 경우
  • 반대로 모든 장애에 대해 대응책을 세우는것입니다.

전자는 거의 불가능하고, 후자또한 어떠한 장치로 모든것에 대응하는 전략을 세우는것은 아주 어렵고

서비스코드에 썩임으로 서비스코드의 흐름을 복잡하게 만들수가 있습니다.


AKKA자체로 이러한 장애에대한 대응을 자동으로 할수 있는것은 아닙니다.

단지 ,이러한 대응을 일괄적이고 유연한 방법을 제시하고 개발자는 다양한 전략중

하나를 선택하고 그것을 서비스코드와 분리하여 설계할수 있는 우아한 장치를 제공해줍니다.



기능에따른 장애처리 전략 분리

OneForOneAllForOne

하나가 망가져도 나머지가 그 역활을 하는경우

  • 그냥 놔둔다
  • c1죽은 기능을 살리려노력한다.

하나라도 망가지면 완전한 역활을 못하는경우

  • c3죽은 기능을 살리려 노력한다.
  • b3 하위 전체를 리셋한다.

AKKA 장애처리 모델은 크게 두가지 전략을 선택하여 설계에 반영할수 있습니다.

그러면 이와같은 장애처리에대한 정의가 어떻게 분리되고 이용되는지 코드를 통해서 살펴보겠습니다.


장애처리 정책정의

import akka.actor.OneForOneStrategy;
import akka.actor.SupervisorStrategy;
import akka.japi.pf.DeciderBuilder;
import scala.concurrent.duration.Duration;
import java.util.concurrent.TimeUnit;

//복구정책을 정의합니다.
public class Policy {	
	public static SupervisorStrategy strategy1 = new OneForOneStrategy(10, Duration.create(1, TimeUnit.MINUTES),		//복구시도제한 : 1분이내에 10번만 시도한다
		      DeciderBuilder
		           //다양한 예외에대한 가이드를 서비스코드와 분리할수가 있습니다.
		          .match(ArithmeticException.class, e -> SupervisorStrategy.resume())
		          .match(NullPointerException.class, e -> SupervisorStrategy.restart())
		          .match(IllegalArgumentException.class, e -> SupervisorStrategy.stop())
		          .matchAny(o -> SupervisorStrategy.escalate())
		          .build());
}


부모(감독자) 액터 정의

import java.util.Optional;

import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;

import akka.actor.AbstractActor;
import akka.actor.SupervisorStrategy;
import akka.actor.Props;

import akka.event.Logging;
import akka.event.LoggingAdapter;

@Component("Supervisor")
@Scope("prototype")
public class Supervisor extends AbstractActor{
    private final LoggingAdapter log = Logging.getLogger(getContext().system(), "Supervisor");
    
	@Override
    public SupervisorStrategy supervisorStrategy() {
		//자신의 아이가 문제가 생길때 복구전략
      return Policy.strategy1;
    }
	
    @Override
    public Receive createReceive() {
      return receiveBuilder()
    	//부모가 아이를 책임지는구조로, 생성도 부모만 할수가 있습니다.(여기서는 생성의기능만 존재)
        .match(Props.class, props -> {
          getSender().tell(getContext().actorOf(props), getSelf());
        })
        .build();
    }    
}



아이(실제기능) 액터 정의

import org.springframework.context.annotation.Scope;

import org.springframework.stereotype.Component;

import akka.actor.AbstractActor;
import akka.event.Logging;
import akka.event.LoggingAdapter;


@Component("BadChild")
@Scope("prototype")
public class BadChild extends AbstractActor {
	//우리가 생성한 클래스는 항상 착하지 않다.
	private final LoggingAdapter log = Logging.getLogger(getContext().system(), "BadChild");	
	int state = 0;
  
	@Override
	public Receive createReceive() {
		return receiveBuilder()
			.match(Exception.class, exception -> {
				log.error(exception.toString());
				/*
				전통적인 익센셥처리의 문제는, 익센셥은 스택구조이기때문에
				자기자신에게 발생한 익센셥을 해결하지못하는데 있고
				서비스코드에 처리방법이 썩임으로 예외처리코드가 서비스코드를 덮는데 있다.
				Akka의 예외처리는 예외가발생하도록 놔두고, 예외처리정책을 우아하게 분리할수가 있다.
				이것이 Let it Crash 전략이다.
				*/
				throw exception;
			})
			.match(Integer.class, i -> state = i)
			.matchEquals("get", s -> getSender().tell(state, getSelf()))
			.build();
	}
}


상황별 복구테스트코드

	protected void supervisorTest(ActorSystem system,SpringExtension ext) throws Exception {
		new TestKit(system) {{
			Props superprops = ext.props("Supervisor");
			ActorRef supervisor = system.actorOf(superprops, "supervisor");
			ActorRef probe = getRef();
			
			//나쁜아이를 허용하는 전략으로 부모(감독)를 통해 아이를 생성합니다.
			final CompletableFuture<Object> future = PatternsCS.ask(supervisor, ext.props("BadChild") , 5000).toCompletableFuture();
			ActorRef badChild = (ActorRef)future.get();
			
			//나쁜아이의 상태를 42로 만든다...
			badChild.tell(42, ActorRef.noSender());
			
			//나쁜아이의 상태를 물어보고, probe에 전달하도록한다
			badChild.tell("get", probe);
			
			//prove에 저장된 메시지가 42인지 확인한다.
			expectMsgEquals(42);
			
			// ArithmeticException 예외는 그냥 진행하도록 정의를 하였다.
			badChild.tell( new ArithmeticException(), ActorRef.noSender() );
			
			//아이가 살아있는지 확인...
			badChild.tell("get", probe);
			expectMsgEquals(42);
			
			// NullPointerException 예외는 아이를 다시 초기화하도록 정의하였다.
			badChild.tell( new NullPointerException(), ActorRef.noSender() );
			
			//아이가 초기화 되었는지 확인 ( 초기상태 0)
			badChild.tell("get", probe);
			expectMsgEquals(0);
			
			//액터를 확인하기위한 Util 액터로, 죽은지 여부를 확인할수가 있습니다.
			TestProbe probe2 = new TestProbe(system);
			probe2.watch(badChild);
			
			// IllegalArgumentException 익센셥이 발생하면 아이를 보내기로 결정하였다.
			badChild.tell( new IllegalArgumentException(), ActorRef.noSender()  );
									
			// 아이가 사라진지 체크...
			probe2.expectMsgClass(Terminated.class);
			
		}};		
	}

AkkaTest는 실제로 다양한 장애를 유발하여, Test코드를 통해서 어느정도 장애처리 모델에대한 검증이 가능합니다.

전통적인 Exception처리방법은 스택구조이기 때문에 아이에게 책임을 전가하면서 부모가 시도한 정보를 알수가 없어서 (예를들면 파일기록기는 Db접속정보를 알수가 없습니다.)

예외처리가 서비스코드와 뒤엉켜...심지어 서비스코드보다 더많은 코드량을 작성해야 하는 상황도 생기기도 합니다.

Akka에서는 그냥 액터를 죽게두고(Let It Crash) 감독자에의해 우아하게 복구할수 있는 다양한 장치를 제공합니다.

장애를 허용하고 복구한다는것은 실제는 어려운 주제입니다. 



참고url: 여기 샘플은 SpringBoot과 연동하여 재구성되었습니다.



  • No labels
Write a comment…