HTTP 기반 백엔드 시스템에서 데드락 현상이 발생하는 경우는 드물지만, "자바 트러블 슈팅 이야기" 와 "자바 최적화" 책을 읽으면서 데드락 상태에서 원인을 진단하고 해결하는 방법에 대해서 공부한 겸 정리해보고자 한다.
사실 그동안 특정 API 호출했을 때 응답이 없거나 시스템 자체가 멈춰버렸을 때, 그리고 로그 파일에 단서가 보이지 않을때 만병통치약 처럼 보이는 "재시작" 을 자주 사용했었고 재시작 이후 증상이 없어진 것을 확인하면 그냥 뭔가 꼬여버렸겠거니 하면서 넘긴적이 많았다. (물론 그래도 증상이 계속 되면 어떻게든 찾으려 노력은 했지만..)
이번 글의 내용은 이러한 상황에서 "재시작" 이라는 "요행" 이 아니라 "원인" 을 진단해서 "해결" 하는 과정을 담았다.
우선 내용을 전개하기 전에 몇가지 전제해야하는 상황이 있다.
- 특정 API 호출 시 응답이 없음
- 해당 API 는 외부 시스템과 연계하는 코드가 없음
- 로그 파일엔 마땅한 단서가 남아있지 않음
- 애플리케이션이 실행중인 서버는 Ubuntu OS이며 scouter 가 설치되어 있음
여기서 4번은 크게 중요하지 않다. 어떤 OS 여도 상관은 없고, scouter가 설치되어 있지 않더라도 기타 호스트의 리소스를 모니터링 할 수 있는 도구가 있다면 그걸 사용해도 된다.
가장 먼저 해야할 일은 뭘까?
데드락이 원인이란걸 모르는 상황이기 때문에 우선 scouter 를 통해 리소스 사용량을 먼저 확인해보자.
지표를 보니 CPU, Memory 모두 응답을 늦출만한 수치는 아니다.
실습을 단순화 하기 위해 네트워크 관련 지표는 따져보지 않을 예정이다.
보통 메모리나 CPU 에 특이점이 없을 경우 스레드의 문제일 가능성이 높다.
스레드 덤프 생성 비용은 힙 덤프 생성 비용에 비해 공짜라고 볼 수 있기 때문에 우선 스레드 덤프를 생성해보자.
jstack 11952 > /jstack/thread_dump_20230906
위 명령어를 터미널에 입력하면 /jstack 디렉터리에 덤프 파일이 생성된다.
11952 는 PID 를 입력하면 되고 덤프 파일 경로나 이름은 마음대로 바꿔도 된다.
이제 위에서 생성한 스레드 덤프를 분석할 차례이다.
나는 가난한 개발자이기 때문에 무료 분석 툴을 사용할건데, 그 중에서 ThreadLogic 이라는 툴을 사용할 것이다.
자바로 만들어져있어서 ThreadLogic 깃헙에서 jar 파일만 다운로드 받아서 실행하면 된다. 아주 간단하다..
https://github.com/sparameswaran/threadlogic/releases
링크로 들어가서 jar파일을 다운로드 받고 실행해보자.
그럼 이런 화면이 뜨는데 당황하지 말고 좌측 상단의 CPU 칩 모양 버튼 (Open Logfile)을 클릭해서 위에서 생성한 덤프 파일을 열자. (혹은 File->Open)
덤프 파일을 열면 해당 덤프 파일을 분석해서 리포팅을 해주는데, 왼쪽 상단을 보면 스레드 덤프 파일 생성 당시의 모든 스레드에 대한 정보를 확인할 수 있는 메뉴들이 제공된다.
하나하나 눌러보면 대충 감이 올건데 여기서 나는 맨 아레이 있는 Thread Groups Summary -> Non-WLS Thread Groups 를 선택했다. (위에 있는 Threads 메뉴를 클릭해도 된다.)
해당 메뉴를 클릭하면 사진에서 오른쪽 화면과 같이 현재 주의가 필요하거나 문제가 있는 스레드들의 목록을 보여준다.
(만약 모든 스레드를 보고 싶다면 그 위에 있는 Minimum Health Level 을 IGNORE로 변경하면 된다. 이 설정은 전역 설정이다.)
이 스레드들 중에서 Scouter 관련 스레드를 제외한 Thread-7,8 이 FATAL 상태인것을 확인할 수 있다.
해당 두 스레드를 드래그하면 아래에 스레드 상태에 대한 설명과 스택 트레이스 정보를 보여주는데 이것만 봐도 사실 어느 코드에서 BLOCK 이 발생했는지 알 수 있다.
나의 경우 DemoApplication.java 파일의 30번째 줄과 47번째 줄에서 문제가 있다고 나와 있다.
그렇다. 나는 이 실습을 위해 Rest API 환경을 셋팅하기가 귀찮아서 애플리케이션을 실행하면 무조건 데드락이 걸리게끔 main 메소드에 구현해 놓았다 ㅜㅜ
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoApplication {
final static String R1 = "Hello Welcome to Scaler!";
final static String R2 = "Visit Scaler!";
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
// creating thread T1
Thread T1 = new Thread(){
// implementing run method
public void run(){
// thread T1 locking the R1 resource
synchronized (R1){
System.out.println("Thread T1 locked -> Resource R1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// thread T1 locking the R2 resource
synchronized (R2){
System.out.println("Thread T1 locked -> Resource R2");
}
}
}
};
// creating thread T2
Thread T2 = new Thread(){
// implementing run method
public void run(){
// thread T2 locking the R2 resource
synchronized (R2){
System.out.println("Thread T2 locked -> Resource R2");
// thread T2 locking the R1 resource
synchronized (R1){
System.out.println("Thread T1 locked -> Resource R1");
}
}
}
};
// starting both the threads
T1.start();
T2.start();
}
}
사실 이 예제도 GeeksForGeeks 의 데드락 강좌에서 가져온 것이다.
흐흐.. 나는야 게으름뱅이
물론 실무에서는 더 복잡한 환경과 각종 변수가 도사리고 있으니 이렇게 순탄하게 진단되진 않을 것 같지만.. 제자리에서 표적을 맞추는 사격 연습을 해놔야 전쟁통에 뭐라도 하지 않을까 싶다.
참고로 ThreadLogic 으로 진단할 수 있는 스레드 관련 문제는 데드락 말고도 매우 많으니 이것저것 눌러보고 실습해보자.
(다음 글은.. 메모리 릭 진단하는 과정을 쓸까 생각중이다.)