프로덕션에서 “왜 이 요청이 느린가”를 추적하다 보면 애플리케이션 로그만으로는 부족한 순간이 옵니다. 커넥션이 어디서 끊겼는지, 어떤 시스템콜에서 블로킹됐는지는 커널의 관점에서만 정확히 답할 수 있습니다. 전통적으로 이런 관측은 strace·tcpdump로 부분적으로만 가능했고, 오버헤드가 커서 상시 붙이기 어려웠습니다.
eBPF(extended Berkeley Packet Filter)는 이 지형을 바꿨습니다. 검증된 샌드박스 바이트코드를 커널 안에서 안전하게 실행해 네트워크 패킷·시스템콜·스케줄러 이벤트를 낮은 오버헤드로 추적합니다. 이 글에서는 실행 모델부터 시스템콜·네트워크 추적, XDP 패킷 처리, 프로덕션 도구 연동까지 단계별로 살펴봅니다.
eBPF가 커널 안에서 안전하게 도는 원리
eBPF 프로그램은 유저 공간에서 컴파일된 바이트코드가 커널로 로드되는 구조입니다. 커널은 이 코드를 그냥 실행하지 않고, 먼저 검증기(verifier)가 정적 분석으로 무한 루프나 초기화되지 않은 메모리·커널 메모리 임의 접근이 없는지를 증명하며, 통과하지 못하면 로드 자체가 거부됩니다.
검증을 통과한 프로그램은 JIT 컴파일되어 네이티브 기계어로 변환된 뒤 훅 포인트에 부착됩니다. 대표적인 훅은 kprobe(커널 함수 진입/반환), tracepoint(안정적 커널 이벤트), XDP(드라이버 최말단), uprobe(유저 공간 함수)입니다. 데이터 교환은 eBPF 맵(map)으로 이뤄져, 이벤트 처리는 커널에서 초고속으로, 집계·전송은 유저 공간에서 유연하게 나뉩니다.
- 안전성: 검증기가 크래시·무한 루프를 원천 차단해 프로덕션 상시 부착이 현실적입니다.
- 낮은 오버헤드: 커널 안에서 실행되어
strace의 ptrace 방식보다 수십 배 가볍습니다. - 동적 부착: 커널 재컴파일이나 모듈 로드 없이 실행 중인 시스템에 붙였다 뗍니다.
도구 선택: bcc, bpftrace, libbpf
eBPF를 직접 다루는 방법은 크게 세 갈래이고 선택이 배포 방식을 좌우합니다.
- bpftrace: awk를 닮은 고수준 스크립팅 언어. 일회성 조사·애드혹 디버깅에 최적으로 한 줄로 결과를 봅니다.
- bcc: 파이썬으로 유저 공간을, C로 커널 프로그램을 작성. 런타임에 LLVM으로 컴파일하므로 호스트에 헤더·컴파일러가 필요합니다.
- libbpf + CO-RE: 한 번 컴파일하면 여러 커널에서 재사용(Compile Once, Run Everywhere). BTF로 오프셋을 런타임에 재배치해 커널 버전 차이를 흡수하는 배포용 에이전트의 표준입니다.
빠른 조사는 bpftrace, 정식 에이전트는 libbpf CO-RE가 실무의 기본 흐름입니다. 아래 bpftrace 한 줄은 openat 시스템콜을 프로세스별로 집계합니다.
# 프로세스 이름별로 openat 호출 횟수 집계 (Ctrl-C 시 출력)
sudo bpftrace -e '
tracepoint:syscalls:sys_enter_openat { @opens[comm] = count(); }'
# 특정 파일을 여는 주체 찾기: 파일명으로 필터
sudo bpftrace -e 'tracepoint:syscalls:sys_enter_openat
/str(args->filename) == "/etc/passwd"/ { printf("%s pid=%dn", comm, pid); }'
시스템콜 지연시간 추적
“어떤 시스템콜이 느린가”는 조사의 단골 질문입니다. tracepoint의 진입과 반환 사이 시간차를 재면 시스템콜별 지연 분포를 얻고, bpftrace의 내장 히스토그램은 로그 스케일 버킷으로 꼬리 지연을 드러냅니다.
# read() 시스템콜 지연을 마이크로초 히스토그램으로
sudo bpftrace -e '
tracepoint:syscalls:sys_enter_read {
@start[tid] = nsecs; # 스레드 ID별 진입 시각 기록
}
tracepoint:syscalls:sys_exit_read
/@start[tid]/ {
@us = hist((nsecs - @start[tid]) / 1000); # ns→μs, 로그2 히스토그램
delete(@start[tid]);
}'
# 출력: @us 히스토그램. [4,8) 다수, [1K,2K) 소수 → 꼬리 지연 확인
진입/반환을 짝지을 때 키를 PID가 아니라 TID(스레드 ID)로 잡는 것이 핵심입니다. 같은 PID의 여러 스레드가 동시에 같은 시스템콜을 호출하면 PID 키로는 값이 덮어써지기 때문입니다. 프로덕션에서는 임계치를 넘는 느린 호출만 스택 트레이스와 함께 승격시킵니다.
네트워크 연결 추적: TCP 라이프사이클
네트워크 문제는 대개 “누가 어디에 연결을 맺었나”로 귀결됩니다. 소켓 kprobe를 걸면 애플리케이션 코드를 건드리지 않고 모든 연결을 관측합니다. 아래는 새 TCP 연결 수립을 목적지 주소·포트와 함께 추적하는 예입니다.
// tcp_connect.bpf.c — libbpf CO-RE 스타일 커널 프로그램
#include <vmlinux.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_core_read.h>
struct conn_event { __u32 pid; __u32 daddr; __u16 dport; char comm[16]; };
// 링버퍼: 커널→유저 공간 이벤트 스트림
struct {
__uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, 256 * 1024);
} events SEC(".maps");
SEC("kprobe/tcp_v4_connect")
int BPF_KPROBE(trace_connect, struct sock *sk) {
struct conn_event *e = bpf_ringbuf_reserve(&events, sizeof(*e), 0);
if (!e) return 0; // 링버퍼 가득 차면 드롭
e->pid = bpf_get_current_pid_tgid() >> 32;
bpf_get_current_comm(&e->comm, sizeof(e->comm));
// CO-RE: BTF 기반으로 커널 구조체 필드를 안전하게 읽기
e->daddr = BPF_CORE_READ(sk, __sk_common.skc_daddr);
e->dport = BPF_CORE_READ(sk, __sk_common.skc_dport); // 네트워크 바이트 오더
bpf_ringbuf_submit(e, 0);
return 0;
}
char LICENSE[] SEC("license") = "GPL"; // BPF 헬퍼 사용 시 필수
여기서 BPF_CORE_READ가 CO-RE의 핵심입니다. 커널 버전마다 struct sock 필드 오프셋이 다를 수 있는데, 이 매크로는 BTF를 참조해 오프셋을 런타임에 재배치하므로 하나의 바이너리가 다양한 커널에서 동작합니다. dport는 네트워크 바이트 오더라 유저 공간에서 ntohs() 변환이 필요하며, 링버퍼는 예약-제출 방식이라 부분 기록이 노출되지 않습니다.
XDP로 드라이버 레벨 패킷 필터링
XDP(eXpress Data Path)는 드라이버가 패킷을 받은 직후, 커널 스택에 진입하기 전에 eBPF 프로그램을 실행하는 훅입니다. sk_buff를 할당하기도 전 단계라 DDoS 완화나 로드밸런싱에서 극단적으로 높은 패킷 처리율을 냅니다. 프로그램은 패킷마다 XDP_PASS·XDP_DROP·XDP_TX·XDP_REDIRECT 중 하나를 반환합니다.
// xdp_drop.bpf.c — 차단 맵의 소스 IP 패킷을 드라이버 단에서 폐기
SEC("xdp")
int xdp_filter(struct xdp_md *ctx) {
void *data = (void *)(long)ctx->data;
void *data_end = (void *)(long)ctx->data_end;
struct ethhdr *eth = data;
// 검증기 요구사항: 접근 전 반드시 경계 검사
if ((void *)(eth + 1) > data_end) return XDP_PASS;
if (eth->h_proto != bpf_htons(ETH_P_IP)) return XDP_PASS;
struct iphdr *ip = (void *)(eth + 1);
if ((void *)(ip + 1) > data_end) return XDP_PASS;
__u64 *blocked = bpf_map_lookup_elem(&blocklist, &ip->saddr);
if (blocked) {
__sync_fetch_and_add(blocked, 1); // 드롭 카운터 증가
return XDP_DROP; // 스택 진입 전 폐기 → CPU 절약
}
return XDP_PASS;
}
XDP에서 가장 자주 걸리는 지점은 경계 검사(bounds check)입니다. 검증기는 data_end를 넘어서는 접근을 허용하지 않으므로, 역참조 전에 항상 (void *)(ptr + 1) > data_end로 검사해야 하며 빠뜨리면 “invalid access to packet” 오류로 거부됩니다. 차단 목록은 bpf_map_update_elem으로 재로드 없이 갱신됩니다. iptables가 패킷당 규칙 체인을 순회하는 반면 XDP+해시맵은 O(1) 조회로 드라이버 단에서 끝내므로 대규모 필터링에서 유리합니다.
맵 타입 선택과 데이터 전송
eBPF 성능은 맵 선택에서 갈립니다. 프로세스별·연결별 통계에는 HASH(갱신이 잦으면 per-CPU 변형)를, 락 없는 초고빈도 카운터에는 PERCPU_ARRAY를, 순서를 보존하는 개별 이벤트 스트림에는 RINGBUF를, 엔트리가 무한정 늘 수 있는 연결 추적에는 오래된 항목을 자동 제거하는 LRU_HASH를 고릅니다.
핵심 원칙은 “커널에서 집계, 유저 공간에서 상세”입니다. 모든 이벤트를 유저 공간으로 보내면 CPU가 폭증하므로, 정상 트래픽은 per-CPU 카운터로만 누적하고 이상 이벤트만 링버퍼로 승격시켜 관측 도구가 병목이 되는 상황을 피합니다.
프로덕션 관측성 스택 연동
날것의 eBPF 프로그램을 직접 운영하는 대신 검증된 상위 도구를 쓰는 편이 대부분의 팀에 현실적입니다. 이들은 CO-RE로 커널 호환성을 이미 해결했고 Prometheus·OpenTelemetry 파이프라인에 곧바로 연결됩니다. 대표 용례는 파드 간 흐름을 사이드카 없이 수집하는 네트워크 관측, 스택을 상시 샘플링하는 연속 프로파일링, 이상 실행을 탐지하는 런타임 보안입니다.
아래는 커널에서 집계한 지연 히스토그램을 유저 공간 에이전트가 Prometheus 지표로 노출하는 개념 예로, 커널 맵의 버킷을 그대로 히스토그램 메트릭에 매핑합니다.
# eBPF 히스토그램 맵 → Prometheus 히스토그램으로 변환
from prometheus_client import Histogram, start_http_server
BUCKETS = [4, 8, 16, 32, 64, 128, 256, 512, 1024, float("inf")] # μs, 로그2
lat = Histogram("syscall_read_latency_us", "read() 지연", buckets=BUCKETS)
for slot, count in read_percpu_hist(hist_map): # per-CPU 버킷 합산
for _ in range(count):
lat.observe(2 ** slot) # 슬롯 인덱스 → 상한값 관측
start_http_server(9464) # /metrics 엔드포인트 노출
이렇게 하면 커널 레벨의 정밀한 지연 데이터를 기존 Grafana 대시보드·알람에 그대로 얹습니다. 애플리케이션 계측 없이 관측하는 것이 eBPF의 결정적 장점입니다.
한계와 주의점
eBPF가 만능은 아니어서, 도입 전에 제약을 인지해야 합니다.
- 커널 버전 의존성: XDP·링버퍼·BTF는 비교적 최신 커널을 요구하므로 대상 커널을 먼저 확인해야 합니다.
- 검증기 제약: 명령어 수·스택 크기(512바이트)·루프에 상한이 있어, 복잡한 로직은 분할하거나 유저 공간으로 넘깁니다.
- 권한: 대부분
CAP_BPF·CAP_PERFMON또는 root가 필요해 컨테이너에서는 권한과 보안 정책의 균형이 필요합니다.
마무리
eBPF는 “애플리케이션을 수정하지 않고 커널의 시선으로 시스템을 관측한다”는 새로운 층위를 엽니다. 시스템콜 지연, TCP 연결 수명, 드라이버 단 패킷 흐름을 낮은 오버헤드로 상시 추적하며, 검증기와 CO-RE 덕분에 안정성과 커널 이식성까지 확보됩니다. bpftrace 한 줄로 시작해 libbpf CO-RE 에이전트로, 나아가 검증된 상위 도구로 옮겨가는 경로가 가장 견고합니다.
기억할 원칙은 하나입니다. 커널에서 집계하고 유저 공간에서 상세를 다루며, 이상치만 승격시킨다. 이 규율을 지키면 관측 도구가 스스로 병목이 되는 함정을 피하면서 로그로는 보이지 않던 커널의 진실에 닿습니다.
자주 묻는 질문
Q. eBPF 관측을 프로덕션에 상시 켜두면 성능에 얼마나 영향을 주나요?
A. 설계에 크게 좌우됩니다. per-CPU 맵으로 카운터만 갱신하는 tracepoint 프로그램은 이벤트당 수십 나노초라 사실상 무시할 만합니다. 부담은 모든 이벤트를 링버퍼로 보낼 때 생기므로, 이상치만 승격시키면 초당 수백만 이벤트에서도 한 자릿수 퍼센트 이내로 유지됩니다.
Q. XDP와 iptables/nftables는 어떻게 나눠 써야 하나요?
A. 처리량과 세밀함의 트레이드오프입니다. XDP는 sk_buff 할당 전에 패킷을 처리해 초당 수천만 패킷 규모의 DDoS 드롭·로드밸런싱에 압도적이지만, 연결 추적·NAT·복잡한 상태 규칙은 여전히 nftables가 성숙합니다. 실무에서는 XDP로 과다 트래픽을 앞단에서 걷어내고 정교한 정책은 뒤의 nftables에 맡기는 계층 구성이 흔합니다.