APM과 JFR은 JVM 안에서 일어나는 일을 잘 보여 주지만, 스레드가 왜 CPU를 받지 못하는지, 어떤 시스템 호출에서 오래 기다리는지, 패킷이 재전송되는지는 JVM 밖, 커널에서 일어난다. eBPF는 애플리케이션을 바꾸거나 재시작하지 않고 커널 안에 작은 관측 프로그램을 붙여 이런 정보를 낮은 비용으로 꺼내 준다. 이 글은 eBPF의 동작 방식과 Java에서 생기는 어려움(JIT 코드의 이름 찾기), 그리고 상황별로 쓸 도구를 정리한 것이다.

eBPF는 무엇을 하나
eBPF 프로그램은 커널 안의 정해진 지점에 붙어, 그 지점을 지날 때마다 실행되는 작은 코드다.
| 붙이는 지점 | 무엇을 보나 | 예 |
|---|---|---|
| tracepoint | 커널이 미리 정해 둔 안정적인 이벤트 | 시스템 호출 진입·종료, 스케줄러 전환 |
| kprobe | 커널 함수 진입·반환 | TCP 재전송, 블록 I/O 완료 |
| uprobe·USDT | 사용자 프로그램의 함수나 정적 프로브 | libc 함수, JVM 내부 프로브(빌드에 따라) |
| perf event | CPU 샘플링, 하드웨어 카운터 | 초당 99번 스택 샘플링 |
안전장치가 핵심이다. 프로그램은 커널에 올라가기 전에 검증기(verifier) 가 무한 루프나 잘못된 메모리 접근이 없는지 확인하고, 결과는 맵(map) 에 모아 사용자 공간 도구가 읽는다. 이벤트마다 데이터를 사용자 공간으로 복사하지 않고 커널 안에서 집계하기 때문에 운영 환경에서도 비용이 작다.

Java에서 생기는 어려움: JIT 코드의 이름
eBPF로 CPU 스택을 샘플링하면 C로 된 JVM 함수는 이름이 보이지만, JIT 컴파일된 Java 메서드는 메모리 주소만 남는다. 두 가지를 준비해야 Java 메서드 이름이 붙는다.
- 프레임 포인터: JIT 코드가 프레임 포인터를 보존해야 커널이 스택을 끝까지 따라간다.
- perf 맵 파일: JIT 코드 주소와 메서드 이름의 대응표를
/tmp/perf-<pid>.map으로 내보낸다.
# 시작 옵션: 프레임 포인터 보존(약간의 성능 비용이 있으니 측정 후 결정)
java -XX:+PreserveFramePointer -jar app.jar
# 실행 중 perf 맵 파일 생성 (JDK 17 이상의 jcmd)
jcmd <pid> Compiler.perfmap
JIT 코드는 계속 다시 컴파일되므로 맵 파일은 수집 직전에 다시 만든다. 이 과정이 번거로우면 JVM 내부 정보를 함께 쓰는 async-profiler가 Java CPU 프로파일링에는 더 편하다. eBPF는 그 프로파일러가 보지 못하는 커널 쪽 대기를 볼 때 강하다.
상황별로 쓸 도구
아래는 bcc 도구 모음의 이름이다(배포판에 따라 -bpfcc가 붙는다). 모두 -p <pid>로 대상 프로세스를 좁힐 수 있다.
| 질문 | 도구 | 보는 것 |
|---|---|---|
| CPU를 어디서 쓰나 | profile -F 99 -p <pid> -f 30 |
스택 샘플링 → 플레임 그래프 |
| CPU를 안 쓰는데 왜 느린가 | offcputime -p <pid> -f 30 |
스레드가 잠들어 있던 스택과 시간 |
| CPU를 받으려고 얼마나 기다리나 | runqlat -p <pid> |
실행 대기열 지연 분포 |
| 어떤 파일을 여나, 실패하나 | opensnoop -p <pid> |
파일 열기와 오류 |
| 디스크가 느린가 | biolatency |
블록 I/O 지연 분포 |
| 네트워크 재전송이 있나 | tcpretrans |
재전송된 TCP 연결 |
-f(접힌 스택 출력) 결과는 FlameGraph 스크립트로 SVG 플레임 그래프를 만든다. 가장 유용한 조합은 CPU 플레임 그래프와 Off-CPU 플레임 그래프를 나란히 보는 것이다. 응답이 느린데 CPU 그래프에 아무것도 없다면, 시간은 Off-CPU(락, I/O, 네트워크 대기) 쪽에 있다.
사례로 보는 질문 흐름
"P99가 가끔 2초로 튄다"는 문제를 eBPF로 좁혀 가는 순서다.
runqlat로 실행 대기열 지연을 본다. 수십 ms 대기가 보이면 CPU 한도(컨테이너 throttling)나 다른 프로세스와의 경쟁을 의심한다.offcputime으로 느린 시간대의 Off-CPU 스택을 본다.futex(락 대기)가 크면 애플리케이션 락,epoll·read가 크면 외부 I/O다.- 네트워크 쪽이면
tcpretrans로 재전송을, 디스크 쪽이면biolatency로 I/O 지연을 확인한다. - JVM 쪽 원인(GC 멈춤, 세이프포인트)은 GC 로그와 JFR로 교차 확인한다.
운영 환경에서 쓰기 전에
- 권한: root나
CAP_BPF·CAP_PERFMON같은 권한이 필요하다. 운영 서버에서 누가 언제 실행할지 절차를 정한다. - 커널 버전: 최신 기능(BTF, CO-RE)은 비교적 최근 커널이 필요하다. 공공 기관의 오래된 커널에서는 일부 도구가 동작하지 않을 수 있다.
- 컨테이너: 호스트 커널을 공유하므로 관측은 보통 노드 단위로 한다. PID 네임스페이스 때문에 컨테이너 안 PID와 호스트 PID가 다르다.
- 비용: 이벤트가 아주 잦은 지점(모든 함수 호출 등)에 붙이면 비용이 커진다. 샘플링이나 집계 위주로 쓰고 짧게 실행한다.
- 상시 관측: 매번 손으로 실행하지 않고 eBPF 기반 지속 프로파일링 도구를 클러스터에 깔아 두는 방법도 있다.
자주 하는 실수
- Java 스택에 주소만 보이고 포기한다. 프레임 포인터와 perf 맵을 준비하면 이름이 붙는다.
- CPU 프로파일만 본다. 느린 시간의 대부분은 Off-CPU 대기에 있다.
- 컨테이너 PID로 호스트에서 찾는다. 네임스페이스가 달라 다른 프로세스를 보게 된다.
- 잦은 이벤트에 오래 붙여 둔다. 관측 자체가 부하가 된다.
- JVM 지표와 따로 본다. GC 로그·JFR과 시간축을 맞춰야 원인이 이어진다.
점검표
- 운영 서버의 커널 버전과 eBPF 도구 동작 여부를 확인했다
- 실행 권한과 절차(누가, 언제, 얼마 동안)가 정해져 있다
- Java 프로세스에 프레임 포인터 보존 여부를 결정했다
- 수집 직전에 perf 맵 파일을 만든다
- CPU와 Off-CPU 플레임 그래프를 함께 본다
- 컨테이너 환경에서 호스트 PID를 확인하는 방법을 안다
- eBPF 결과를 GC 로그·JFR과 같은 시간축으로 비교한다
eBPF는 JVM이 볼 수 없는 커널 쪽 시간을 보여 준다. Java 메서드 이름을 붙이는 준비만 해 두면, "CPU는 놀고 있는데 왜 느린가"라는 질문에 스택으로 답할 수 있다.
댓글 0개
아직 댓글이 없습니다. 첫 댓글을 남겨보세요.