자바 애플리케이션 CPU 부하 원인 분석을 위한 top 및 jstack 활용 가이드

프로세스 및 실행 스레드 식별

시스템의 CPU 사용률이 비정상적으로 상승했을 때, 우선 JDK 기반의 대상 프로세스를 식별해야 합니다.

# 전체 시스템 정보 확인 후 JAVA 프로세스 필터링
$ top -b -n 1 | grep -i java
# 출력 예시:
# PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
# 98765 devuser 20 0 15.2g 6.1g 1.2k S 210.5 8.2 45:12.33 java

위 명령어에서 목표 프로세스의 PID(예: 98765)와 높은 %CPU 값을 기록합니다. 이후 해당 프로세스 내부의 개별 스레드 수준으로 뷰를 전환하여 실제 부하를 발생시키는 작업을 찾습니다.

$ top -H -p 98765
# 키보드 입력: 'P'를 눌러 CPU 순서대로 정렬

출력 결과 중 상태가 R(Running)이며 %CPU가 압도적으로 높은 행의 PID를 주목합니다. 이 값은 후속 분석을 위해 별도로 메모해 두어야 합니다.

스레드 ID(hex) 변환 과정

Linux 커널의 네이티브 스레드 ID(Native Thread ID, 이하 TID)는 10진수로 관리되지만, JVM의 덤프 파일에서는 16진수 형식(NID)으로 표현됩니다. 정확한 매칭을 위해 아래 명령어로 변환합니다.

TARGET_TID=98765
printf "%x\\n" $TARGET_TID
# 변환 결과 예시: 17f59

이제 0x17f59 값이 jstack 출력물 검색 키워드가 됩니다.

덤프 수집 및 스택 레이어 분석

대상 프로세스에 대용량의 스레드 상태를 요청하는 명령어입니다. 프로세스가 완전히 응답하지 않는 경우 -F 플래그를 추가로 전달하여 강제 추출할 수 있습니다.

JSTACK_TARGET_PID=98765
jstack -l $JSTACK_TARGET_PID > app_thread_dump_$(date +%s).txt

생성된 텍스트 파일 내에서 nid=0x17f59 패턴을 찾아 해당 스레드의 호출 스택을 확인합니다.

"Worker-Thread-A" #8 prio=5 os_prio=0 tid=0x00007fa2c10a1000 nid=0x17f59 runnable [0x00007fa2c99f0000]
   java.lang.Thread.State: RUNNABLE
        at java.util.regex.Matcher.reset(Matcher.java:320)
        at java.util.regex.Matcher.<init>(Matcher.java:234)
        at com.myapp.utils.StringParser.validate(StringParser.java:88)

상세 해석:

  • RUNNABLE 상태: CPU 사이클을 적극적으로 점유하고 있으며 리소스 대기 중이 아님을 의미합니다.
  • 콜 스택: 정규표현식 검증 로직(StringParser.validate)에서 연산 집착이 발생했음을 시사합니다.
  • 해결 방향: 반복적으로 사용되는 정규식을 미리 컴파일해 보관하거나(Group Capture 최소화), 불필요한 탐색 로직을 제거하는 것이 효율적입니다.

주요 성능 병목 유형 및 대응 방안

계산 집중형 과부하 (Compute-Bound)

징후: 지속적 90% 이상 CPU 점유, 네트워크/디스크 대역폭은 정상 수준 유지.

코드 구조 예시:

public void heavyProcessing(List<DataItem> items) {
    while(true) { // 무조건적 루프
        double result = complexMathFormula(items.get(rand.nextInt()));
        if(result == Double.MAX_VALUE) break; // 매우 드문 조건만 탈출
    }
}

개선안: 알고리즘 점근 복잡도 개선(예: Brute-force → Dynamic Programming), 작업 큐를 활용한 분산 처리, 또는 제한된 배치 크기(Batch Size) 적용.

동기화 블록 경합 (Lock Contention)

징후: CPU 수치 대비 서비스 처리량(Throughput) 급감, 다중 스레드에서 동일 모니터링 객체 접근 시도.

덤프 특징:

"CacheUpdater-B" #12 prio=5 tid=0x... nid=0x8a waiting for monitor entry
   java.lang.Thread.State: BLOCKED (on object monitor)
        at com.app.cache.LocalCache.put(LocalCache.java:42)
        - waiting to lock <0x00000006c91a2b10> (a java.util.HashMap)

개선안: 전역 락 대신 세그먼트 단위 락(Segmented Lock) 적용, ReentrantReadWriteLock 도입, 아키텍처 변경을 통한 공유 상태 제거(Immutable Object, Read-Write Separation).

외부 서브시스템 대기 (I/O Blocking)

징후: 스레드는 활성(CPU 낮음)이나 응답 시간(TTFB)이 지연됨.

덤프 특징:

"SQLExec-Pool-03" #25 daemon prio=5 tid=0x... nid=0xb1 waiting on condition
   java.lang.Thread.State: TIMED_WAITING (parking)
        at sun.misc.Unsafe.park(Native Method)
        at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:215)
        at com.app.db.QueryExecutor.fetch(QueryExecutor.java:110)

개선안: 데이터베이스 쿼리 로그 확인, 커넥션 풀 재구성(Connection Pool Tune-up), 비동기 프록시(CompletableFuture, Armeria Async)를 통한 블로킹 코드 분리.

진단 작업 자동화 스크립트

운영 환경에서 신속하게 반응하기 위해 Bash 기반으로 핵심 파라미터를 추출하는 스크립트를 구성했습니다.

#!/bin/bash
set -e

PROC_NAME="java"
CPU_THRESHOLD=75.0
RESULT_FILE="diagnostics_$$.txt"

# 1. CPU 점유율 기준 상위 JAVA 프로세스 PID 수집
TARGET_JAVA_PID=$(ps -eo pid,%cpu,comm --sort=-%cpu | grep "$PROC_NAME" | head -1 | awk '{print $1}')
echo "[INFO] Target PID: $TARGET_JAVA_PID" >> $RESULT_FILE

# 2. 프로세스 내 최고 CPU 스레드 ID(TID) 추출
TOP_WORKER_TID=$(ps -o pid,tid,%cpu --sort=-%cpu -C java | awk '$3 > '"$CPU_THRESHOLD"' {print $2}' | head -1)
if [ -z "$TOP_WORKER_TID" ]; then
  echo "[WARN] No threads exceeded threshold." >> $RESULT_FILE
else
  HEX_NID=$(printf "%x" $TOP_WORKER_TID)
  echo "[CRITICAL] High CPU Worker TID: $TOP_WORKER_TID (Hex: 0x$HEX_NID)" >> $RESULT_FILE

  # 3. 현재 순간 상태 덤프 생성
  DUMP_TS=$(date +%Y%m%d_%H%M%S)
  jstack -l $TARGET_JAVA_PID > jstack_dump_${DUMP_TS}.txt
  sed -i "/nid=0x$HEX_NID/,/^$/p" jstack_dump_${DUMP_TS}.txt >> $RESULT_FILE
fi

echo "Analysis complete. Logs saved to $RESULT_FILE"

심층 분석 운영 원칙

  • 시점 적절성: 부하 피크 구간 직전과 정상 운영 시점을 각각 촬영하여 스택 깊이(Stack Depth) 및 락 대기열(Queue Length) 비교 수행.
  • 메모리 연계 분석: CPU 부하가 갑자기 소멸하며 메모리 사용량이 폭증했다면 Stop-The-World(STW) Full GC 여파일 가능성이 높습니다. 이때 jstat -gcutilHeapDump를 교차 검증해야 합니다.
  • 가공 오류 구별: JIT 컴파일러가 코드를 기계어로 전환하는 동안 일시적 spikes가 발생할 수 있습니다. 이는 설계 결함이 아니라 JVM의 내부 최적화 메커니즘이므로 무시하는 것이 올바릅니다.
  • 컨텍스트 스위칭 과다 감지: iowaitcontext switches 수치가 높은 경우, 단순 애플리케이션 버그보다는 OS 레벨의 스케줄링 또는 하드웨어 한계에 대한 검토가 선행되어야 합니다.

태그: Java Performance jstack TOP CPU Profiling Thread Dump

9월 3일 08:47에 게시됨