[monthler] 10초 이상 걸리는 로직 강제종료
문제 상황
최근에 db connection timeout 상황이 많이 나왔고, 오래걸리는 로직을 서버에서 자체적으로 종료시키자는 논의가 나왔습니다. 추가로 10초 이상 걸리면 프론트로 시스템 오류 메시지를 던지도록 했습니다.
해결 방법
우선적으로 ELB 의 세션타임아웃을 고민해봤으나, 타임아웃이 발생한다고 하더라도 서버 내부에서는 로직이 계속 실행된다는 문제가 있습니다. 원하는 흐름은 단순히 “연결종료” 뿐만 아니라 “실행종료” 이기 때문입니다.
따라서 쓰레드가 로직을 실행할 때, 그 쓰레드의 실행시간을 측정해야 합니다. 그러기 위해서는 실행 쓰레드를 분리할 수밖에 없다고 생각했습니다. 그래서 Future 를 통해 내부에 사용되는 스레드를 구분해주었습니다.
스레드 구분은 AOP 레벨에서 *Controller
클래스를 호출할 때 발생되는데, 그러면 Filter, Interceptor 에서 넣었던 ThreadLocal 의 정보가 전파되지 않습니다. 또한 OSIV 트랜잭션도 전파되지 않기 때문에 두 부분도 함께 전달해야 합니다. 이러한 부분은 직접 넣어주는 방식으로 구현했는데, 이러한 구현으로 성능 상으로 떨어지거나 레이턴시가 증가하는 경향이 있는지는 나중에 로드 테스트로 확인해봐야 합니다.
코드 레벨
@Aspect
@Component
class TimeoutAop(
//1
@Qualifier("futureTaskExecutor")
private val futureTaskExecutor: ThreadPoolTaskExecutor,
//2
private val transactionTemplate: TransactionTemplate
) {
@Pointcut("execution(* kr.monthler.api.controller.*.*(..))")
fun controllerPointcut() {}
@Around("controllerPointcut()")
fun timeoutAdvice(pjp: ProceedingJoinPoint): Any? {
//3
val callable = Callable {
transactionTemplate.execute {
pjp.proceed()
}
}
//4
val contextAwareCallable = ContextAwareCallable(callable)
val future: Future<Any> = futureTaskExecutor.submit(contextAwareCallable)
return try {
//5
future.get(10000, TimeUnit.MILLISECONDS)
} catch (e: TimeoutException) {
//6
future.cancel(true)
//7
throw BaseException(MESSAGES.RESPONSE_TIMEOUT)
}
}
}
class ContextAwareCallable<V>(private val delegate: Callable<V>) : Callable<V> {
private val requestAttributes: RequestAttributes? = RequestContextHolder.getRequestAttributes()
private val securityContext = SecurityContextHolder.getContext()
override fun call(): V {
RequestContextHolder.setRequestAttributes(requestAttributes)
SecurityContextHolder.setContext(securityContext)
return delegate.call()
}
}
futureTaskExecutor
는 내부에서 실행될 스레드풀로, 최대 200개입니다.transactionTemplate
은 트랙잭션을 전파하기 위해 사용합니다.pjp.proceed()
를 실행할 Callable 입니다.ContextAwareCallable
클래스로 감싸서 필요한 정보를 주입합니다. 이 프로젝트에서는 RequestAttributes 와 SecurityContextHolder 가 스레드로컬로 필요합니다.- future.get 으로 스레드를 실행합니다. 10초를 타임아웃으로 둡니다.
- TimeoutException 이 발생하면 future 을 취소합니다.
- 미리 정의된
BaseException(MESSAGES.RESPONSE_TIMEOUT)
을 던집니다.
이렇게 해서 내부에서 10초 이상 걸리면 자동으로 실행을 종료하고 에러를 반환합니다. 다만 스레드가 2배로 사용된다는 점이 마음에 걸리는데, 로드 테스트와 함께 나중에 개선포인트로 생각해야 겠습니다.
댓글남기기