
평화롭던 어느날… 서비스 장애는 언제나 갑자기 찾아온다!
서비스 장애의 원인을 찾아보니 주문 서버가 죽어있는게 원인이였다.
이전부터 종종 주문 서버가 죽는 증상이 있었으나, APM을 따로 달지 않아 죽어도 원인 파악이 힘든 부분이 있었다. 그리하여 Elastic APM을 달아두고 몇 일간 모니터링 한 결과 죽은 원인을 추측할 수 있었다.

thread count가 계속 증가하면서 사용 memory가 같이 증가하였고, 그로 인해 WAS가 뻗는 것으로 추론할수 있었다.
일단 스레드 분석을 위해서 스레드 덤프를 확인해 봤다.
$ jps
12051 order
$ jstack 12051 > dump.txt
"pool-60-thread-1" #277 prio-5 os_prio=31 tid=0x000000012cc3d800 nid=0x28907 waiting on condition [0x00000002b16aa000]
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park (Native Method)
- parking to wait for <0x000000066d7beee0> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
at java.util.concurrent.locks. LockSupport.park(LockSupport.java:175)
at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await (AbstractQueuedSynchronizer.java:2044)
at java.util.concurrent. ScheduledThreadPoolExecutor$DelayedWorkQueue.take (ScheduledThreadPoolExecutor.java:1081)
at java.util.concurrent. ScheduledThreadPoolExecutor$DelayedWorkQueue. take (ScheduledThreadPoolExecutor.java:809)
at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1074)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1134)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:750)
Locked ownable synchronizers:
- None
WAITING 상태인 thread가 굉장히 많이 생성되어 있다...
하지만 thread-name-prefix가 지정되어 있음에도 thread pool도 기본 이름으로 되어있고, Stack trace를 보더라도 아무런 힌트를 얻을 수 없는 상황…
spring:
task:
execution:
pool:
max-size: 16
core-size: 16
thread-name-prefix: order-executor
일단 Thread pool을 생성하는 곳이 있는지 프로젝트를 무작정 검색해보기로 했다.
키워드는 ThreadPoolExcutor인것 같으니 코드상에서 ExcutorService를 생성하는 곳이 있는지 검색해본 결과…


바로 용의자를 찾을 수 있었다!

ExcutorService를 사용한 후엔 자원 반납을 위해 shutdown 해줘야 하는데 해주지 않아 발생하는 것으로 파악된다.
용의자를 찾았으니 일단 무작정 같은 코드를 작성해 테스트하기로 했다.
코드는 다음과 같다.
void test (int maxSize) throws InterruptedException, ExecutionException {
final ExecutorService executors = Executors.newWorkStealingPool();
final ArrayList<FutureTask<Integer>> taskList = new ArrayList<>();
List<Integer> listOfNumbers = IntStream.rangeClosed(1, maxSize)
.boxed().collect(Collectors.toList());
for (int mappingResult : listOfNumbers) {
final FutureTask<Integer> task = new FutureTask<>(() -> {
return mappingResult;
});
taskList.add(task);
executors.submit(task).get();
}
}
thread pool 분석에는 이전부터 애용했던 VisualVM을 사용했다.
그리고 대망의 테스트 결과…


정상적(?)으로 thread pool이 잘 생성되고 있는걸 확인할수 있었다.
정확한 확인을 위해 intellij debug를 사용해서 다시 한 번 확인했다.

해당 코드는 shutdown을 추가하여 자원을 반납하는 것으로 마무리!
void test (int maxSize) throws InterruptedException, ExecutionException {
final ExecutorService executors = Executors.newWorkStealingPool();
final ArrayList<FutureTask<Integer>> taskList = new ArrayList<>();
List<Integer> listOfNumbers = IntStream.rangeClosed(1, maxSize)
.boxed().collect(Collectors.toList());
for (int mappingResult : listOfNumbers) {
final FutureTask<Integer> task = new FutureTask<>(() -> {
return mappingResult;
});
taskList.add(task);
executors.submit(task).get();
}
executors.shutdown();
}
Thread Count가 계속 쌓이는 원인도, 관련된 처리도 비교적 간단히 찾을 수 있었고, 그로 인해서 스레드 관련 공부도 할수 있었던 기회였어서 꽤 즐겁게 작업할수 있었던것 같다. (물론 더이상의 장애는 사절이다)
