created at 2023-03-04
인증서버의 속도를 높이기 위해 기존 단일 스레드로 작업하던 부분을 아래의 configuration 을 통해 10개~100개의 스레드로 동시성을 가져가보았습니다.
미리 결론을 말씀드리면 성능은 개선되지 않았습니다. 이유는 곧 말씀드리겠습니다.
Thread Pool 설정
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean(name = "taskExecutor")
public Executor taskExecutor(){
ThreadPoolTaskExecutor t = new ThreadPoolTaskExecutor();
t.setCorePoolSize(10); // 최소 스레드 개수
t.setQueueCapacity(10); // 중간 LinkedQueue 크기
t.setMaxPoolSize(100); // 최대 사용가능한 스레드 개수
t.setThreadNamePrefix("auth-thread-");
t.setWaitForTasksToCompleteOnShutdown(true); // MaxPoolSize를 넘겨도 대기
t.setAwaitTerminationSeconds(60); // 대기시간
t.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); // 대기 시간을 넘겨 reject되면, 호출한 Thread에서 reject된 task를 대신 실행
t.initialize();
return t;
}
}
@Service
@Asnyc <-- ADDED
public class UserService {
...
}
저는 가장 많이 사용되는 ThreadPoolTaskExecutor 를 사용하였습니다. 얘는 아래와 같이 Task 를 스케줄링하게 됩니다.
- Task는
CorePoolSize
만큼 새로운 스레드에서 실행됩니다.- CorePoolSize : 최소 스레드 개수를 설정하는 부분입니다.
CorePoolSize
수 만큼 새로운 스레드로 Task가 실행되면, 이후QueueCapacity
크기 만큼 큐에 Task가 저장됩니다.- QueueCapacity : 큐의 크기입니다. default로 LinkedQueue가 생성됩니다.
- Task가
QueueCapacity
크기를 초과할 경우, Task는MaxPoolSize
만큼 새로운 스레드를 늘리며 실행됩니다.- MaxPoolSize : 최대 스레드 개수를 설정하는 부분입니다.
- 만약
MaxPoolSize
을 초과하는 Task 가 실행된다면,RejectExecutionException
에러를 던지며 셧다운 됩니다.
이 떄, 셧다운 되기전 RejectExecutionException
에러를 핸들링할 수 있습니다. 그래서 Task가 빠짐없이 실행되어야 할 경우, 아래와 같이 설정할 수 있습니다.
- setWaitForTasksToCompleteOnShutdown : MaxPoolSize를 넘겨도 Task를 대기시킵니다.
- setAwaitTerminationSeconds : 대기시간을 설정할 수 있습니다.
- setRejectedExecutionHandler : 대기시간을 넘겨 reject 되어도 호출한 Thread에서 task를 실행할 수 있도록 설정합니다.
(CPU/MEM 사용률) 멀티 스레딩 이전 VS 이후
싱글 스레딩
멀티 스레딩 이전 싱글 스레드로 구현된 docker 컨테이너 서비스들의 cpu/memory 사용률을 확인해볼까요?
제 노트북에서 여러 컨테이너를 사용함에 따라 잔여 CPU 리소스는 고작 0.05%. 이미 한계까지 사용하고 있었네요.
10K HTTP request 의 총 RTT 시간 : 30초
멀티 스레딩
10K HTTP request 의 총 RTT 시간 : 30초
Single VS Multiple Threads
보시다시피 10K 의 HTTP request에 걸린 총 시간은 30초로 동일합니다. 또한 CPU 사용률 또한 별 차이가 없죠(이미 극한까지 사용하고 있었기 때문에…)
결론은, 멀티 스레딩은 CPU에 충분한 resource가 존재해야됩니다! 만약 리소스가 없다면, 동시성 부분에서야 이점이 있겠지만 task가 빠짐없이 실행되어야 한다면 결론적으로는 성능은 싱글과 동일하거나 낮습니다(Context-Switch 비용 증가).
즉, 아무래도 저는 현재 성능을 개선시키기 위해서 device 자체를 수평/수직적으로 붙여서 확장시켜야 할 것 같습니다.