- Authors

- Name
- Youngju Kim
- @fjvbn20031
- 들어가며
- 기존 관측 도구의 한계와 eBPF 관측성
- bpftrace 문법 5분 입문
- BCC 도구상자 투어
- 지연 히스토그램 읽는 법
- CPU 프로파일링과 플레임그래프
- 컨테이너와 쿠버네티스 환경에서
- USDT와 uprobe로 애플리케이션 추적
- 실전 트러블슈팅 시나리오 3가지
- 오버헤드 관리와 안전 수칙
- 운영 체크리스트
- 마치며
- 참고 자료
들어가며
새벽 2시, "API 응답이 간헐적으로 3초씩 걸린다"는 알림이 울립니다. APM 대시보드를 열어 보지만 애플리케이션 코드 안에서는 아무것도 느리지 않습니다. CPU 사용률도, 메모리도 정상입니다. 문제는 애플리케이션과 하드웨어 사이 어딘가, 즉 커널이라는 블랙박스 안에 있습니다.
이 블랙박스를 여는 가장 강력한 도구가 eBPF 기반 관측성입니다. 지난 글에서 eBPF의 기초(프로그램, 맵, Verifier)를 다뤘다면, 이번 글은 철저히 실전입니다. bpftrace 원라이너로 5분 안에 가설을 검증하고, BCC 도구상자로 디스크/네트워크/스케줄러를 파헤치고, 플레임그래프로 CPU를 해부한 뒤, 실제 장애 시나리오 3가지를 단계별로 추적해 보겠습니다.
기존 관측 도구의 한계와 eBPF 관측성
전통적인 관측 도구는 각자 사각지대가 있습니다.
| 도구 계열 | 잘하는 것 | 한계 |
|---|---|---|
| 메트릭 (Prometheus 등) | 추세, 알림 | 집계된 평균 뒤에 숨은 개별 이벤트를 못 봄 |
| 로그 | 애플리케이션이 기록하기로 한 것 | 기록하지 않은 일은 존재하지 않는 것처럼 보임 |
| APM 트레이싱 | 요청 단위 분해 | 계측 코드 삽입 필요, 커널 구간은 공백 |
| strace / ltrace | 시스템 콜 전수 관찰 | ptrace 기반이라 대상 프로세스가 수십 배 느려짐 |
| perf | CPU 프로파일링 | 이벤트 덤프 후 후처리 방식, 커스텀 로직 부족 |
eBPF 관측성의 차별점은 세 가지입니다.
- 코드 수정과 재배포가 필요 없습니다. 이미 실행 중인 프로세스와 커널을 그 자리에서 관찰합니다.
- 오버헤드가 낮습니다. 이벤트가 커널 안에서 필터링/집계되고 필요한 결과만 유저 공간으로 넘어옵니다. strace가 컨텍스트 스위치 폭탄을 만드는 것과 대조적입니다.
- 커널과 애플리케이션을 같은 도구로 봅니다. 시스템 콜, 디스크 IO, 스케줄러, TCP 재전송부터 유저 함수 지연까지 한 줄로 연결됩니다.
전체 그림을 ASCII로 그리면 다음과 같습니다.
관측 대상 계층과 eBPF 훅
+------------------------------------------------------------+
| 애플리케이션 <── uprobe / USDT (함수 지연, GC 이벤트) |
+------------------------------------------------------------+
| 라이브러리(libc) <── uprobe (malloc, pthread 등) |
+------------------------------------------------------------+
| 시스템 콜 경계 <── tracepoint syscalls:* (open, read...) |
+------------------------------------------------------------+
| 커널 서브시스템 |
| VFS/파일시스템 <── kprobe/kfunc (vfs_read 등) |
| 블록 계층 <── tracepoint block:* (IO 지연) |
| 네트워크 스택 <── kprobe tcp_* / tracepoint sock:* |
| 스케줄러 <── tracepoint sched:* (런큐 대기) |
+------------------------------------------------------------+
| 하드웨어 이벤트 <── perf_event (CPU 사이클, 캐시 미스) |
+------------------------------------------------------------+
bpftrace 문법 5분 입문
bpftrace는 awk를 닮은 DSL입니다. 기본 구조는 "프로브 / 필터 / 액션"입니다.
probe /filter/ { action }
probe : 어디에 붙일 것인가 (tracepoint, kprobe, uprobe, profile...)
filter : 어떤 이벤트만 볼 것인가 (pid == 1234 등, 생략 가능)
action : 무엇을 할 것인가 (출력, 맵에 집계)
자주 쓰는 내장 변수와 함수만 알면 바로 시작할 수 있습니다.
| 요소 | 의미 |
|---|---|
| pid, tid, comm | 프로세스 ID, 스레드 ID, 명령어 이름 |
| args | tracepoint 인자 구조체 |
| retval | kretprobe/uretprobe의 리턴값 |
| nsecs | 나노초 타임스탬프 |
| kstack, ustack | 커널/유저 스택 트레이스 |
| count(), sum(), avg() | 집계 함수 |
| hist(), lhist() | log2/선형 히스토그램 |
실전 원라이너 10선
- 파일 오픈 추적 — 누가 어떤 파일을 여는가:
sudo bpftrace -e 'tracepoint:syscalls:sys_enter_openat
{ printf("%-16s %s\n", comm, str(args->filename)); }'
- 프로세스 실행 추적 — 시스템에서 실행되는 모든 명령:
sudo bpftrace -e 'tracepoint:syscalls:sys_enter_execve
{ printf("%-16s -> %s\n", comm, str(args->filename)); }'
- 시스템 콜 횟수 톱 — 어떤 프로세스가 어떤 시스콜을 많이 부르는가:
sudo bpftrace -e 'tracepoint:raw_syscalls:sys_enter
{ @[comm] = count(); }'
- read 시스콜 지연 히스토그램 — 시스콜 단위 지연 분포:
sudo bpftrace -e '
tracepoint:syscalls:sys_enter_read { @start[tid] = nsecs; }
tracepoint:syscalls:sys_exit_read /@start[tid]/
{ @usecs = hist((nsecs - @start[tid]) / 1000); delete(@start[tid]); }'
- 새 TCP 연결 추적 — 어떤 프로세스가 어디로 연결하는가:
sudo bpftrace -e 'kprobe:tcp_connect
{ printf("%-16s pid=%d\n", comm, pid); }'
- 블록 IO 크기 분포 — 디스크 요청 크기 히스토그램:
sudo bpftrace -e 'tracepoint:block:block_rq_issue
{ @bytes = hist(args->bytes); }'
- 프로세스별 디스크 IO 바이트 합계:
sudo bpftrace -e 'tracepoint:block:block_rq_issue
{ @[comm] = sum(args->bytes); }'
- CPU 99Hz 샘플링 — 커널 스택 기준 어디서 시간을 쓰는가:
sudo bpftrace -e 'profile:hz:99 { @[kstack] = count(); }'
- 페이지 폴트 많은 프로세스:
sudo bpftrace -e 'software:page-faults:1 { @[comm] = count(); }'
- 시그널 추적 — 누가 누구에게 kill을 보내는가:
sudo bpftrace -e 'tracepoint:syscalls:sys_enter_kill
{ printf("%s (pid %d) -> pid %d, sig %d\n",
comm, pid, args->pid, args->sig); }'
이 10개만 손에 익어도 "추측" 대신 "측정"으로 대화를 시작할 수 있습니다. 사용 가능한 프로브 목록은 다음으로 탐색합니다.
sudo bpftrace -l 'tracepoint:syscalls:*' | head -20
sudo bpftrace -lv 'tracepoint:syscalls:sys_enter_openat' # 인자 구조 확인
BCC 도구상자 투어
bpftrace가 즉석 질문용이라면, BCC 도구 모음(그리고 libbpf로 재작성된 bcc/libbpf-tools)은 잘 다듬어진 기성 도구입니다. 대부분의 배포판에서 bpfcc-tools 또는 bcc-tools 패키지로 설치할 수 있습니다.
상황별 도구 선택 테이블
| 증상/질문 | 도구 | 보여 주는 것 |
|---|---|---|
| 무슨 프로세스가 계속 뜨는가 | execsnoop | 모든 execve와 인자 |
| 어떤 파일을 여는가 | opensnoop | open 호출과 결과 코드 |
| 디스크가 느린가 | biolatency | 블록 IO 지연 히스토그램 |
| 어떤 IO가 느린가 | biosnoop | 개별 IO의 지연/크기/프로세스 |
| TCP 연결 수명은 | tcplife | 연결별 수명, 송수신 바이트 |
| 누가 연결을 만드는가 | tcpconnect | 능동적 connect 추적 |
| 누가 연결을 받는가 | tcpaccept | 수동적 accept 추적 |
| 재전송이 있는가 | tcpretrans | TCP 재전송 이벤트 |
| 시스콜이 느린가 | syscount | 시스콜별 횟수/지연 |
| 런큐 대기가 긴가 | runqlat | 스케줄러 런큐 대기 히스토그램 |
| 캐시 미스가 많은가 | cachestat | 페이지 캐시 히트/미스 |
| 파일시스템이 느린가 | ext4slower 등 | 임계값 이상 느린 FS 연산 |
전형적인 사용 예시는 다음과 같습니다.
# 10ms보다 느린 ext4 연산만 출력
sudo ext4slower-bpfcc 10
# 1초 간격으로 블록 IO 지연 히스토그램 출력
sudo biolatency-bpfcc -m 1
# 컨테이너 포함 전체 TCP 세션 수명 관찰
sudo tcplife-bpfcc
60초 진단 루틴
장애 초동 대응에서 제가 쓰는 순서입니다.
sudo execsnoop-bpfcc # 예상 밖 프로세스 폭주 여부 (cron, 헬스체크 등)
sudo runqlat-bpfcc 5 1 # CPU 경합: 런큐 대기 분포
sudo biolatency-bpfcc 5 1 # 디스크: IO 지연 분포
sudo tcpretrans-bpfcc # 네트워크: 재전송 발생 여부
sudo syscount-bpfcc -L -d 5 # 시스콜 지연 톱
5개 명령, 1분이면 문제의 계층(CPU/디스크/네트워크/시스콜)을 좁힐 수 있습니다.
지연 히스토그램 읽는 법
eBPF 도구의 출력 중 가장 정보량이 많은 것이 log2 히스토그램입니다. biolatency의 출력 예시를 보겠습니다.
usecs : count distribution
128 -> 255 : 1402 |************************** |
256 -> 511 : 2012 |****************************************|
512 -> 1023 : 803 |*************** |
1024 -> 2047 : 95 |* |
...
65536 -> 131071 : 41 | |
131072 -> 262143 : 17 | |
읽는 포인트는 세 가지입니다.
- 본체(mode)의 위치: 대부분의 IO가 256~511 마이크로초에 모여 있습니다. 이것이 장치의 "정상" 특성입니다.
- 꼬리(tail)의 존재: 13만~26만 마이크로초(130ms 이상) 구간에 17건이 있습니다. 평균에는 거의 안 보이지만 p99.9 사용자 경험을 망치는 주범입니다.
- 봉우리가 2개인가(bimodal): 봉우리가 둘이면 "캐시 히트 vs 미스", "로컬 vs 원격", "정상 경로 vs 재시도"처럼 서로 다른 두 경로가 섞여 있다는 강력한 신호입니다. 평균값은 두 봉우리 사이의, 실제로는 아무도 경험하지 않는 값을 가리키게 됩니다.
요약하면, 평균을 버리고 분포를 보라는 것이 eBPF 관측성의 핵심 교훈입니다.
CPU 프로파일링과 플레임그래프
perf 기반 vs eBPF 기반
| 항목 | perf record | eBPF (profile 프로브 / profile-bpfcc) |
|---|---|---|
| 수집 방식 | 샘플을 파일로 덤프 후 후처리 | 커널 내 맵에서 스택별 집계 |
| 데이터량 | perf.data가 수 GB까지 커질 수 있음 | 집계 결과만 전달되어 작음 |
| 오버헤드 | 디스크 쓰기 비용 포함 | 일반적으로 더 낮음 |
| 유연성 | 표준화된 워크플로, 폭넓은 PMU 지원 | 조건부 수집 등 커스텀 로직 가능 |
99Hz로 전체 CPU 스택을 샘플링해 플레임그래프를 만드는 두 경로입니다.
# 경로 A: perf 기반
sudo perf record -F 99 -a -g -- sleep 30
sudo perf script | stackcollapse-perf.pl | flamegraph.pl > cpu.svg
# 경로 B: eBPF 기반 (커널 내 집계, 접힌 포맷 바로 출력)
sudo profile-bpfcc -F 99 -af 30 > out.folded
flamegraph.pl out.folded > cpu.svg
플레임그래프 읽기는 단순합니다. 가로 폭이 CPU 시간 점유율이고, 위로 갈수록 콜스택이 깊어집니다. 가장 넓은 "고원"을 찾고, 그 함수가 왜 호출되는지 아래로 따라 내려가면 됩니다. 유저 스택까지 보려면 대상 프로세스가 프레임 포인터를 보존하도록 빌드되어 있거나(-fno-omit-frame-pointer), 최근 도구처럼 DWARF/BTF 기반 언와인딩을 지원해야 합니다.
오프CPU 분석
CPU 플레임그래프는 "도는 동안"만 보여 줍니다. 그런데 지연의 대부분은 기다리는 시간(디스크, 락, 네트워크)에서 옵니다. 이때는 오프CPU 분석을 씁니다.
# 스레드가 CPU를 내려놓은(블록된) 시간을 스택별로 집계
sudo offcputime-bpfcc -df 30 > offcpu.folded
flamegraph.pl --colors=io offcpu.folded > offcpu.svg
CPU 플레임그래프에서 안 보이던 "락 대기 80%" 같은 진실이 오프CPU 그래프에서 드러나는 경우가 많습니다. 온CPU와 오프CPU를 한 쌍으로 보는 습관을 권합니다.
컨테이너와 쿠버네티스 환경에서
원리: eBPF는 노드 레벨 기술
eBPF 프로그램은 커널에 부착되므로, 한 노드의 모든 컨테이너를 한 번에 관찰합니다. 컨테이너는 결국 cgroup + 네임스페이스로 격리된 프로세스이기 때문에, PID나 cgroup ID로 필터링하면 특정 Pod만 골라 볼 수 있습니다. 노드에서 직접 실행할 때의 전형적인 방법은 다음과 같습니다.
# 특정 컨테이너의 PID를 찾아 그 프로세스만 필터
PID=$(crictl inspect --output go-template \
--template '{{.info.pid}}' CONTAINER_ID)
sudo bpftrace -e 'tracepoint:syscalls:sys_enter_openat /pid == '$PID'/
{ printf("%s\n", str(args->filename)); }'
데몬셋으로 디버그 도구 이미지를 깔아 두거나, kubectl debug node 기능으로 노드에 임시 파드를 띄워 작업하는 방식이 일반적입니다.
생태계 도구 (아는 범위에서)
| 도구 | 접근 방식 | 특징 |
|---|---|---|
| Pixie | 클러스터 내 eBPF 에이전트 | 프로토콜 자동 캡처(HTTP, DB 등), 스크립트 기반 조회 |
| Parca | eBPF 연속 프로파일링 | 항상 켜 두는 저오버헤드 프로파일러, 시계열 플레임그래프 |
| Grafana Beyla | eBPF 자동 계측 | 코드 수정 없이 HTTP/gRPC RED 메트릭과 트레이스 생성 |
| Cilium Hubble | CNI 통합 관측 | 네트워크 플로우 가시성, 서비스 맵 |
| Inspektor Gadget | BCC류 도구의 k8s 포장 | kubectl 플러그인으로 노드 도구를 Pod 단위로 실행 |
공통 패턴은 "사이드카가 아니라 노드 에이전트"라는 점입니다. eBPF 덕분에 파드마다 프록시나 에이전트를 주입하지 않고도 노드당 하나의 데몬셋으로 전체를 관찰합니다.
USDT와 uprobe로 애플리케이션 추적
커널만이 아니라 유저 공간도 같은 방식으로 추적할 수 있습니다.
uprobe는 바이너리의 임의 함수에 부착합니다. 예를 들어 특정 C/C++/Go/Rust 바이너리의 함수 지연을 잴 수 있습니다.
# 바이너리에서 추적 가능한 심볼 찾기
sudo bpftrace -l 'uprobe:/usr/local/bin/myapp:*' | grep -i order
# 특정 함수의 지연 히스토그램
sudo bpftrace -e '
uprobe:/usr/local/bin/myapp:process_order { @start[tid] = nsecs; }
uretprobe:/usr/local/bin/myapp:process_order /@start[tid]/
{ @latency_us = hist((nsecs - @start[tid]) / 1000);
delete(@start[tid]); }'
USDT(User Statically-Defined Tracing)는 애플리케이션이 미리 심어 둔 안정적인 트레이스 포인트입니다. PostgreSQL, JVM, Python, Node.js 등이 USDT를 제공합니다.
# 프로세스의 USDT 프로브 나열
sudo bpftrace -lp $(pgrep -o postgres) 'usdt:*' | head
# PostgreSQL 쿼리 시작 추적 (USDT 예)
sudo bpftrace -e 'usdt:/usr/lib/postgresql/16/bin/postgres:query__start
{ printf("query: %s\n", str(arg0)); }'
주의할 점은 두 가지입니다. uprobe는 함수가 인라인되면 잡히지 않고, 심볼이 스트립된 바이너리에서는 심볼 테이블이 필요합니다. 또한 호출 빈도가 매우 높은 유저 함수에 uprobe를 걸면 트랩 비용이 누적되므로 빈도를 먼저 확인하는 것이 안전합니다.
실전 트러블슈팅 시나리오 3가지
시나리오 1: 디스크 IO 급증
증상: 특정 시간대마다 디스크 사용률이 치솟고 서비스 지연이 동반됩니다.
# 1단계: 지연 분포 확인 — 정말 디스크가 느린가, 얼마나?
sudo biolatency-bpfcc -m 5 3
# 2단계: 범인 프로세스 — 누가 IO를 만들고 있나?
sudo biotop-bpfcc 5
# 3단계: 개별 IO 관찰 — 어떤 파일/패턴인가?
sudo biosnoop-bpfcc | head -50
# 4단계: 파일 레벨로 — 어떤 파일에 읽기/쓰기가 집중되나?
sudo filetop-bpfcc -C 5
# 5단계: 쓰기 유발자 추적 — 페이지 캐시 플러시인가, 직접 쓰기인가?
sudo bpftrace -e 'tracepoint:block:block_rq_issue
{ @[comm, args->rwbs] = count(); }'
전형적인 결론 예시: 백업 잡이나 로그 로테이션이 같은 디스크에서 순차 쓰기를 일으켜 DB의 랜덤 읽기 지연 꼬리를 키우는 패턴입니다. 해결은 IO 스케줄링 클래스 분리(ionice), cgroup io.max 제한, 또는 볼륨 분리입니다.
시나리오 2: 간헐적 지연 (p99 스파이크)
증상: 평균 응답은 정상인데 p99가 주기적으로 튑니다. APM에는 단서가 없습니다.
# 1단계: 계층 식별 — CPU 대기인가?
sudo runqlat-bpfcc 5 12 # 1분간 12회: 스파이크 시점과 겹치는지
# 2단계: CPU 도둑 찾기 — 누가 갑자기 CPU를 쓰나?
sudo profile-bpfcc -F 99 -af 30 > spike.folded
# 3단계: 블로킹 확인 — 오프CPU 시간은 어디서?
sudo offcputime-bpfcc -df 30 -p $(pgrep -o myapp) > offcpu.folded
# 4단계: GC/뮤텍스 가설 검증 (예: futex 대기 시간 분포)
sudo bpftrace -e '
tracepoint:syscalls:sys_enter_futex { @start[tid] = nsecs; }
tracepoint:syscalls:sys_exit_futex /@start[tid]/
{ @futex_ms = hist((nsecs - @start[tid]) / 1000000);
delete(@start[tid]); }'
전형적인 결론 예시: cron으로 도는 메트릭 수집기가 1분마다 CPU를 점유해 런큐 대기가 늘어나는 경우, 또는 특정 락의 컨보이 현상. runqlat의 히스토그램이 스파이크 시점에만 오른쪽으로 밀린다면 CPU 경합 쪽 가설이 강해집니다.
시나리오 3: 커넥션 누수
증상: 시간이 지날수록 파일 디스크립터가 증가하고, 결국 too many open files로 장애가 납니다.
# 1단계: 연결의 수명 관찰 — 닫히지 않는 연결이 있는가?
sudo tcplife-bpfcc -p $(pgrep -o myapp)
# 2단계: 생성 vs 종료 균형 — 어느 쪽이 새는가?
sudo bpftrace -e '
kprobe:tcp_connect { @open[comm] = count(); }
kprobe:tcp_close { @close[comm] = count(); }
interval:s:10 { print(@open); print(@close);
clear(@open); clear(@close); }'
# 3단계: 누수 지점의 스택 — 어디서 만들어진 연결이 안 닫히나?
sudo bpftrace -e 'kprobe:tcp_connect /comm == "myapp"/
{ @[ustack] = count(); }'
# 4단계: FD 증가 일반 확인 (소켓이 아닐 수도 있으므로)
sudo opensnoop-bpfcc -p $(pgrep -o myapp) -d 30
전형적인 결론 예시: 에러 경로에서 HTTP 클라이언트의 응답 바디를 닫지 않아 커넥션 풀 밖의 연결이 누적되는 패턴입니다. 3단계의 유저 스택이 누수를 일으키는 정확한 코드 경로를 가리켜 줍니다.
오버헤드 관리와 안전 수칙
운영 장비에서 eBPF 도구를 돌릴 때의 원칙입니다.
- 빈도를 먼저 추정합니다. 프로브를 걸기 전에 그 이벤트가 초당 몇 번 발생하는지 카운트만 먼저 재 봅니다.
sudo bpftrace -e 'kprobe:vfs_read { @ = count(); }
interval:s:1 { print(@); clear(@); }'
- 커널 안에서 집계하고, 출력은 요약만 받습니다. printf로 이벤트마다 출력하는 방식은 이벤트가 많으면 그 자체가 부하가 됩니다. count/hist 집계를 기본으로 합니다.
- 시간을 제한합니다. 대부분의 BCC 도구는 횟수/시간 인자를 받습니다. 무기한 실행을 피하고, 측정이 끝나면 즉시 종료합니다.
- 필터를 최대한 앞에 둡니다. PID, cgroup, 포트 등으로 커널 사이드에서 먼저 거릅니다.
- 카나리부터 시작합니다. 전체 플릿에 한 번에 적용하지 않고 한 대에서 오버헤드를 측정한 뒤 확대합니다.
- 맵 메모리를 의식합니다. 키 카디널리티가 폭발할 수 있는 집계(예: 스택 트레이스 키)는 맵 크기 한도를 확인합니다.
- 권한을 분리합니다. 풀 root 대신 CAP_BPF + CAP_PERFMON으로 충분한 도구가 많습니다. 자세한 내용은 기초 편을 참고하세요.
운영 체크리스트
- 노드에 bpftrace와 bcc 도구가 설치되어 있고 버전이 커널과 호환된다
- 커널에 BTF가 활성화되어 있다 (/sys/kernel/btf/vmlinux 존재)
- 장애 초동용 60초 진단 루틴이 런북에 정리되어 있다
- 주요 서비스 바이너리가 프레임 포인터를 보존하도록 빌드되어 있다
- 프로파일링 산출물(플레임그래프)을 보관/공유하는 위치가 정해져 있다
- 컨테이너 환경이라면 노드 접근 또는 Inspektor Gadget류 도구 경로가 준비되어 있다
- eBPF 도구 실행 권한 정책(누가, 어떤 권한으로)이 정의되어 있다
- 고빈도 프로브에 대한 가드레일(빈도 추정 먼저)이 팀 내 합의되어 있다
마치며
eBPF 관측성의 본질은 도구 목록이 아니라 질문하는 방식의 전환입니다. "아마 디스크겠지"라고 추측하는 대신, biolatency 히스토그램으로 30초 만에 확인합니다. "GC 때문일 거야"라고 말하는 대신, 오프CPU 플레임그래프로 증명합니다. 커널이 더 이상 블랙박스가 아니게 되면, 장애 대응의 대화 전체가 데이터 위에서 이루어집니다.
기초 편에서 다룬 프로그램/맵/Verifier의 원리를 알고 있다면, 이 글의 모든 도구가 "tracepoint에 붙어 percpu 맵에 집계하고 ringbuf로 넘기는 프로그램"으로 읽히기 시작할 것입니다. 다음 글에서는 같은 기술이 보안으로 향합니다. Tetragon과 Falco, BPF LSM으로 관측을 넘어 탐지와 차단까지 가 보겠습니다.