홈 › 리눅스 운영 › 09 / 13

장애 대응 (jstack·jmap·OOM killer·FD)

섹션 7진행 0 / 13

4. 응용 변형 예제

4.1 덤프 수집 스크립트

장애 때 손으로 치지 않도록 08 레슨 형식으로 묶습니다.

bash
#!/usr/bin/env bash
set -euo pipefail
PID=${1:?JVM PID}; OUT=/var/log/myapp/incident/$(date +%Y%m%d_%H%M%S); mkdir -p "$OUT"
for i in 1 2 3; do jstack "$PID" > "$OUT/td$i.txt"; sleep 5; done
top -H -p "$PID" -b -n 1 | head -n 20 > "$OUT/top-h.txt"
jstat -gcutil "$PID" 1000 5 > "$OUT/gc.txt"
jmap -histo:live "$PID" | head -n 40 > "$OUT/histo.txt"
ls /proc/"$PID"/fd | wc -l > "$OUT/fd.txt"
echo "saved: $OUT"

힙 덤프는 앱을 멈추므로 자동에서 빼고 판단 후 수동으로 찍습니다. 이 스크립트를 실행한 뒤 재시작하는 것이 운영 원칙입니다.

4.2 커넥션 풀 고갈

HikariPool-1 - Connection is not available, request timed out 로그가 나올 때입니다.

bash
grep -c 'HikariPool.*getConnection' td1.txt              # 커넥션 기다리는 스레드
grep -B2 -A15 'HikariProxyConnection' td1.txt | grep 'at com.shop' | sort | uniq -c | sort -rn | head
ss -tn state established '( dport = :1521 )' | wc -l    # 실제 DB 연결 수

두 번째 줄이 커넥션을 오래 잡고 있는 앱 코드를 빈도순으로 보여 줍니다. 대개 트랜잭션 안에서 외부 API 를 부르는 코드입니다.

4.3 GC 로그 켜기

재발에 대비해 GC 로그를 항상 남깁니다. JDK 9 이상 통합 로깅 형식입니다.

bash
JAVA_OPTS="$JAVA_OPTS -Xlog:gc*:file=/var/log/myapp/gc.log:time,uptime:filecount=5,filesize=20m"
grep 'Pause Full' /var/log/myapp/gc.log | tail -n 5

filecount·filesize 로 스스로 백업(압축·삭제)하므로 06 레슨의 logrotate 대상에서 뺍니다. Full GC 줄의 ms 가 초 단위로 커지면 힙 부족 경고입니다.

4.4 Native Memory 확인

힙은 여유 있는데 RSS 가 계속 늘면 힙 밖 메모리입니다. 스레드 스택, 메타스페이스, 직접 버퍼가 후보입니다.

bash
JAVA_OPTS="$JAVA_OPTS -XX:NativeMemoryTracking=summary"   # 시작 시 켜야 함
jcmd 12345 VM.native_memory summary | grep -E 'Thread|Class|Internal|committed'

Thread 가 크면 스레드 수가 많은 것이고 -Xss 나 풀 크기를 줄입니다. 켜면 5~10% 오버헤드가 있어 진단 기간에만 씁니다.

4.5 응답 없음인데 CPU 도 낮을 때

CPU 가 낮고 BLOCKED 도 없으면 외부 대기입니다. 스택에서 소켓 읽기를 찾습니다.

bash
grep -B1 -A6 'SocketInputStream.read\|socketRead' td1.txt | grep -E '^"|at com.shop' | head -n 20
ss -tnp | grep 'pid=12345' | awk '{print $5}' | sort | uniq -c | sort -rn | head -n 3

같은 원격 주소로 연결이 몰려 있으면 그 외부 서비스가 느린 것입니다. 실무 확장 11 레슨의 타임아웃과 서킷 브레이커가 이 상황의 대책입니다.