TL;DR
- eBPF는 리눅스 커널 안에서 샌드박스 바이트코드를 돌리는 VM이다. 커널 모듈을 빌드하지 않고도 커널 동작을 관측/수정할 수 있다.
- 11개 레지스터 기반 64-bit RISC-like ISA.
clang -target bpf로 C → 바이트코드 컴파일 → JIT로 네이티브화. - Verifier가 안전을 보장한다. 로드 전에 symbolic execution으로 모든 경로를 검증: 무한 루프 금지, 포인터 경계 체크, 초기화되지 않은 레지스터 읽기 금지.
- Maps는 커널-유저 공간 양쪽에서 읽고 쓰는 공유 자료구조다. Hash, Array, LRU, LPM Trie, Ring Buffer, Per-CPU 변형 등 수십 종.
- 프로그램 타입이 훅 포인트를 결정한다.
kprobe(함수 진입),tracepoint(안정적 이벤트),XDP(드라이버 단 패킷),TC(qdisc),LSM(보안),cgroup_skb등. - CO-RE + BTF가 커널 구조체 변화 문제를 해결한다. 한 번 컴파일하면 커널 버전 상관없이 동작.
- XDP는 NIC 드라이버 바로 뒤에서 패킷을 처리해 초당 수천만 패킷을 CPU 한 코어로 포워딩/드롭할 수 있다. DPDK 유사 성능 + 커널 통합.
- 실전 제품: Cilium(K8s 네트워킹/보안), Pixie(관측성), Falco(런타임 보안), bpftrace(ad-hoc 추적), Katran(L4 로드밸런서).
1. 왜 eBPF인가 — 문제의 역사
1.1 커널 확장의 오래된 딜레마
리눅스 커널에 기능을 추가하려면 세 가지 선택이 있었다:
- 커널 패치: 메인라인에 올리기. 몇 년이 걸리고, 작은 기능은 기각된다.
- 커널 모듈 (LKM): 컴파일해서
insmod. 크래시하면 커널 패닉. 커널 버전마다 다시 빌드. - 유저 공간 구현: syscall/ioctl로 커널에 질문. 느리고, 컨텍스트 스위칭이 병목.
네트워크 스택을 예로 들어보자. 당신이 L4 로드밸런서를 만들고 싶다면?
- 유저 공간(HAProxy, nginx): 매 패킷마다 NIC → 커널 → 유저 → 커널 → NIC. 수 μs 레이턴시, 수십만 pps가 한계.
- 커널 모듈(IPVS): 빠르지만 확장성 제한, 버그 시 커널 패닉.
- DPDK: 커널 우회, 유저 공간에서 NIC 직접 제어. 수천만 pps 가능하지만 CPU 한 코어 전부를 소비하고, 커널 네트워크 스택과 공존 불가.
eBPF는 네 번째 길을 열었다. "커널 안에서 샌드박스로 내 코드를 돌려라." 안전성은 verifier가 보장한다.
1.2 BPF의 뿌리 — 1992년
Van Jacobson과 Steven McCanne가 1992년 Berkeley Packet Filter를 만들었다. 목적: tcpdump가 커널에서 패킷을 필터링할 때 유저 공간으로 전부 복사하지 말자.
필터식: "tcp and port 80"
↓
BPF 바이트코드 (32-bit ISA, 2개 레지스터)
↓
커널 안에서 실행 → 매치되는 패킷만 유저로
이 오리지널 BPF를 cBPF (classic BPF)라 부른다. 10-20배 빠른 tcpdump의 비밀.
1.3 Alexei Starovoitov와 2014년의 재작성
2014년, Alexei Starovoitov(현 커널 BPF 메인테이너)가 eBPF(extended BPF)를 만들었다. 주요 변경:
- 10개 → 11개 레지스터, 32-bit → 64-bit.
- Maps 추가: 커널-유저 간 상태 공유.
- Helper 함수: 커널 API를 호출할 수 있는 안전한 인터페이스.
- JIT: 바이트코드 → 네이티브 머신 코드 (x86_64, ARM64, …).
- Verifier 강화: 복잡한 프로그램도 안전 검증 가능.
- 네트워크 외 훅: kprobe, tracepoint, cgroup 등 모든 커널 서브시스템으로 확장.
결과: "커널 모듈의 파워 + 유저 공간 코드의 안전성." 이것이 eBPF의 본질이다.
1.4 2025년 생태계
오늘날 eBPF는:
- Meta: 모든 트래픽이 Katran(eBPF L4 LB)을 거친다. 페이스북 전체 인프라.
- Netflix: FlowLogs, BPF 기반 관측성.
- Google: GKE 네트워킹에서 eBPF 사용.
- Cloudflare: DDoS mitigation에 XDP 활용.
- Cilium: K8s CNI의 사실상 표준. kube-proxy를 완전히 대체.
- Red Hat: RHEL 9부터 eBPF 기반 도구(bpftrace, bcc, libbpf) 공식 지원.
eBPF Foundation(Linux Foundation 산하)까지 생겼다. 말 그대로 "Linux 커널의 두 번째 대폭풍"이다.
2. eBPF 가상머신
2.1 명령어 구조
eBPF 명령어는 고정 64-bit(8 바이트):
struct bpf_insn {
__u8 code; // 8 bits: opcode
__u8 dst_reg:4; // 4 bits: destination register
__u8 src_reg:4; // 4 bits: source register
__s16 off; // 16 bits: signed offset
__s32 imm; // 32 bits: signed immediate
};
opcode는 RISC 스타일 — ALU, 로드/스토어, 분기, 함수 호출.
2.2 레지스터
R0 : 리턴값 (함수 호출 결과)
R1-R5: 함수 인자 (C 스타일 ABI)
R6-R9: 호출 보존 레지스터
R10 : 프레임 포인터 (read-only, 스택 접근용)
11번째 레지스터 R10은 스택 접근 전용 읽기 전용. 이걸 통해서만 스택 메모리를 쓸 수 있다.
2.3 명령어 예시
// C 코드
int add(int a, int b) { return a + b; }
컴파일 (clang -target bpf -O2):
; 0000000000000000 <add>:
; 0: bf 10 00 00 00 00 00 00 r0 = r1 ; R0 = a
; 1: 0f 20 00 00 00 00 00 00 r0 += r2 ; R0 += b
; 2: 95 00 00 00 00 00 00 00 exit ; return R0
세 명령. 컴팩트하다.
2.4 JIT — 네이티브 코드로
인터프리트만 하면 느리다. 리눅스 커널은 바이트코드를 로드할 때 JIT 컴파일한다.
eBPF Bytecode JIT (x86_64)
---------------- -----------------
r0 = r1 mov rax, rdi
r0 += r2 add rax, rsi
exit ret
x86_64, ARM64, RISC-V, MIPS, PowerPC, s390x, SPARC 모두 JIT 지원. 현대 커널(5.x+)에서는 기본 활성화되어 있다:
sysctl net.core.bpf_jit_enable # 1 = enabled
JIT 덕분에 eBPF는 "인터프리트 VM"이지만 성능은 네이티브 C 코드와 거의 동일하다.
2.5 10,000 명령어 제한과 Bounded Loops
- Linux 5.2 이전: 함수 크기 4,096 명령어 제한.
- Linux 5.2+: 함수 1M 명령어 제한, 단 verifier가 추적해야 하는 복잡도 예산은 존재.
- Linux 5.3+: bounded loop 허용 (verifier가 상한을 증명할 수 있으면).
// Linux 5.3+: OK
for (int i = 0; i < 64; i++) {
// ...
}
// 항상 OK: unroll pragma
#pragma unroll
for (int i = 0; i < 4; i++) { ... }
// NG: verifier가 상한을 알 수 없음
for (int i = 0; i < x; i++) { ... } // x가 unknown
3. Verifier — eBPF의 심장
Verifier가 없으면 eBPF는 그냥 위험한 커널 모듈이다. 이 섹션이 eBPF의 본질이다.
3.1 검증 목적
Verifier는 로드 시점에 다음을 증명한다:
- 종료성 (Termination): 무한 루프 없음. 모든 실행 경로가
exit에 도달. - 메모리 안전성: 모든 포인터 접근이 할당된 메모리 범위 안.
- 초기화 보장: 읽기 전에 쓴다. 미초기화 스택 슬롯 읽기 금지.
- 타입 안전성: 레지스터 타입이 일관됨. 정수를 포인터로 역참조 금지.
- 복잡도 한계: verifier 자신이 합리적 시간 안에 끝나도록.
검증에 실패하면 bpf() syscall이 -EINVAL을 반환하고 자세한 에러 로그를 준다.
3.2 Symbolic Execution
Verifier는 추상 해석(abstract interpretation) 기반이다. 각 레지스터와 스택 슬롯에 대해 "가능한 값의 집합"을 추적한다.
int x = get_pid(); // x: [0, 4194304] (pid 범위)
if (x > 100) {
// 이 분기에서 x: [101, 4194304]
use(x);
}
Verifier는 매 분기마다 경로를 fork한다. 각 경로를 끝까지 실행하며:
- 레지스터의 최소/최대값 (
s32_min,s32_max,u32_min,u32_max,s64_min, …) 추적. - 포인터는 "어떤 종류의 포인터인가"와 "오프셋이 얼마인가"를 추적.
3.3 포인터 타입 시스템
Verifier는 포인터를 여러 종류로 분류한다:
PTR_TO_CTX : 프로그램 컨텍스트 (struct __sk_buff, struct xdp_md 등)
PTR_TO_PACKET : 네트워크 패킷 (data)
PTR_TO_PACKET_END : 패킷 끝
PTR_TO_MAP_KEY : 맵 키
PTR_TO_MAP_VALUE : 맵 값
PTR_TO_STACK : 스택 메모리
PTR_TO_SOCKET : 소켓 구조체
SCALAR_VALUE : 포인터가 아닌 스칼라
각 타입에는 엄격한 규칙이 있다:
PTR_TO_MAP_VALUE는 NULL 체크 후에만 역참조:void *val = bpf_map_lookup_elem(&my_map, &key); if (!val) return 0; // 필수: NULL 체크 *(int *)val = 42; // 이제 OK- 포인터 산술은 제한적:
PTR_TO_STACK에 정수를 더할 수 있지만 결과는 여전히 해당 스택 영역 안이어야 함. - 정수와 포인터 혼용 금지.
3.4 Packet Bounds Check
XDP/TC 프로그램에서 가장 흔한 verifier 이슈:
SEC("xdp")
int drop_port_80(struct xdp_md *ctx) {
void *data = (void *)(long)ctx->data;
void *data_end = (void *)(long)ctx->data_end;
struct ethhdr *eth = data;
// NG: verifier는 (eth + sizeof(*eth)) <= data_end 를 모름
if (eth->h_proto == bpf_htons(ETH_P_IP)) { ... }
return XDP_PASS;
}
수정:
if (data + sizeof(*eth) > data_end) // 필수 bounds check
return XDP_PASS;
if (eth->h_proto == bpf_htons(ETH_P_IP)) { ... }
Verifier는 이 if문을 보고 "이 분기 안에서 data + 14 <= data_end가 성립함"을 기록. 이후 eth 필드 접근을 허용.
3.5 Verifier 에러 읽기
실패 로그는 악명 높게 길지만 규칙적이다:
0: R1=ctx(off=0,imm=0) R10=fp0
0: (b7) r0 = 0 ; R0 = 0
1: R0=inv0 R1=ctx(off=0,imm=0) R10=fp0
1: (79) r2 = *(u64 *)(r1 +0) ; r2 = ctx->data
2: R0=inv0 R1=ctx(off=0,imm=0) R2=pkt(off=0,r=0) R10=fp0
...
17: invalid access to packet, off=14 size=2, R1(id=0,off=14,r=0)
R1 offset is outside of the packet
R1 offset is outside of the packet이 핵심. "R1 포인터가 현재 증명된 패킷 범위(r=0)를 넘어선다"는 뜻. 대응: 앞서 본 data + N > data_end 체크 추가.
3.6 복잡도 예산
Verifier가 발견한 경로가 너무 많으면 "BPF program is too complex" 에러. 상한:
- Linux 5.2 이전: 상태 조합 131,072개.
- Linux 5.2+: 1,000,000개 (
BPF_COMPLEXITY_LIMIT_STATES).
복잡도 줄이는 팁:
- loop unroll 쓰지 말기:
bounded loop가 verifier에 더 친절. __always_inline으로 helper 함수 인라인: call 경계를 줄인다.- state pruning: 같은 상태에 두 번 도달하면 한 번만 탐색. Linux 4.19+에서 자동.
4. Maps — 커널-유저 양쪽 자료구조
4.1 왜 필요한가
eBPF 프로그램은:
- 스택 512 바이트 제한.
- 프로그램 간 상태 공유 불가 (각 프로그램은 독립 실행).
- 유저 공간과 통신 필요.
→ Maps가 해결한다. 커널이 관리하는 타입 있는 KV 스토어이며 양쪽에서 접근 가능하다.
4.2 주요 맵 타입
| 타입 | 설명 | 용도 |
|---|---|---|
BPF_MAP_TYPE_HASH | 해시 테이블 | PID별 통계 |
BPF_MAP_TYPE_ARRAY | 고정 크기 배열 (키=인덱스) | 카운터, 설정 |
BPF_MAP_TYPE_PERCPU_HASH | CPU별 해시 | 락 없는 카운터 |
BPF_MAP_TYPE_LRU_HASH | LRU 해시 (자동 축출) | 연결 추적 |
BPF_MAP_TYPE_LPM_TRIE | Longest Prefix Match | 라우팅 테이블 |
BPF_MAP_TYPE_STACK_TRACE | 스택 트레이스 | 프로파일링 |
BPF_MAP_TYPE_PERF_EVENT_ARRAY | perf_event ring | 유저로 이벤트 보내기 (legacy) |
BPF_MAP_TYPE_RINGBUF | 링 버퍼 | 유저로 이벤트 보내기 (신형, 5.8+) |
BPF_MAP_TYPE_PROG_ARRAY | 프로그램 포인터 배열 | tail call |
BPF_MAP_TYPE_DEVMAP | net_device 배열 | XDP redirect |
BPF_MAP_TYPE_CPUMAP | CPU 인덱스 배열 | XDP CPU redirect |
BPF_MAP_TYPE_SK_STORAGE | 소켓별 저장소 | 연결별 컨텍스트 |
BPF_MAP_TYPE_TASK_STORAGE | task_struct별 저장소 | 프로세스별 상태 |
4.3 맵 선언 (libbpf 스타일)
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__type(key, u32);
__type(value, u64);
__uint(max_entries, 10240);
} pid_counts SEC(".maps");
BPF_MAP_TYPE_HASH에 (u32 PID) → (u64 count) 맵 10,240 엔트리.
4.4 Helper 함수로 접근
커널 내 eBPF 프로그램은 helper 함수를 통해서만 맵을 건드릴 수 있다:
u32 pid = bpf_get_current_pid_tgid() >> 32;
u64 *count = bpf_map_lookup_elem(&pid_counts, &pid);
if (!count) {
u64 one = 1;
bpf_map_update_elem(&pid_counts, &pid, &one, BPF_ANY);
} else {
__sync_fetch_and_add(count, 1); // atomic
}
유저 공간에서는 bpf() syscall 또는 libbpf의 bpf_map_lookup_elem:
u32 pid = 1234;
u64 count;
bpf_map_lookup_elem(map_fd, &pid, &count);
printf("PID %u: %llu\n", pid, count);
4.5 Per-CPU 맵 — 락 없는 카운터
글로벌 해시에 __sync_fetch_and_add를 매번 호출하면 캐시 라인 경합 발생. Per-CPU 맵은 CPU마다 자기 슬롯을 가져 경합이 없다.
struct {
__uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
__type(key, u32);
__type(value, u64);
__uint(max_entries, 1);
} counter SEC(".maps");
SEC("kprobe/sys_openat")
int count_openat(struct pt_regs *ctx) {
u32 key = 0;
u64 *val = bpf_map_lookup_elem(&counter, &key);
if (val) (*val)++; // 락 없음, 자기 CPU 슬롯
return 0;
}
유저 공간에서 읽을 때는 CPU별로 합산:
int ncpus = libbpf_num_possible_cpus();
u64 vals[ncpus];
bpf_map_lookup_elem(fd, &key, vals);
u64 total = 0;
for (int i = 0; i < ncpus; i++) total += vals[i];
4.6 Ring Buffer — 이벤트 스트리밍
커널에서 일어난 이벤트(프로세스 exec, 파일 open 등)를 유저로 스트리밍할 때는 ring buffer를 쓴다. Linux 5.8+.
struct {
__uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, 256 * 1024); // 256KB
} events SEC(".maps");
struct event {
u32 pid;
char comm[16];
};
SEC("tp/sched/sched_process_exec")
int handle_exec(void *ctx) {
struct 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));
bpf_ringbuf_submit(e, 0);
return 0;
}
유저 공간에서:
struct ring_buffer *rb = ring_buffer__new(map_fd, handle_event, NULL, NULL);
while (!exiting) {
ring_buffer__poll(rb, 100); // 100ms timeout
}
Ring buffer는 구형 perf_event_array보다 효율적이다:
- MPSC(다중 producer, 단일 consumer) 지원 — CPU별 버퍼 없이 공유.
- back-pressure 탐지:
bpf_ringbuf_reserve가 NULL을 반환하면 꽉 참. - event loss 카운터: 유저가 얼마나 놓쳤는지 알 수 있음.
5. 프로그램 타입과 훅 포인트
eBPF 프로그램은 커널 어디에 "붙는지"에 따라 타입이 정해진다. 각 타입은 고유한 context 구조체와 허용 helper가 다르다.
5.1 Kprobe / Kretprobe
커널 함수 진입/종료에 훅.
SEC("kprobe/do_sys_openat2")
int kprobe_openat(struct pt_regs *ctx) {
const char *filename = (const char *)PT_REGS_PARM2(ctx);
char buf[256];
bpf_probe_read_user_str(buf, sizeof(buf), filename);
bpf_printk("open: %s\n", buf);
return 0;
}
- 장점: 거의 모든 커널 함수에 훅 가능. 동적.
- 단점: 함수 이름/시그니처가 커널 버전마다 다름 → CO-RE 또는 버전별 처리 필요.
5.2 Tracepoint
커널 개발자가 명시적으로 정의한 안정적 ABI 이벤트.
SEC("tracepoint/syscalls/sys_enter_openat")
int tp_openat(struct trace_event_raw_sys_enter *ctx) {
bpf_printk("openat called\n");
return 0;
}
- 장점: ABI 안정. 커널 버전 호환성 높음.
- 단점: 커널 개발자가 정의한 위치에만 붙음.
ls /sys/kernel/debug/tracing/events로 목록 확인.
5.3 Fentry / Fexit (BPF Trampoline, Linux 5.5+)
현대적인 함수 훅. Kprobe보다 빠르다(native 호출 규약 재사용).
SEC("fentry/tcp_v4_connect")
int BPF_PROG(connect_entry, struct sock *sk) {
bpf_printk("connect: sk=%p\n", sk);
return 0;
}
SEC("fexit/tcp_v4_connect")
int BPF_PROG(connect_exit, struct sock *sk, int ret) {
bpf_printk("connect ret=%d\n", ret);
return 0;
}
Kprobe가 int3 인터럽트로 훅한다면, fentry는 커널 함수의 5-byte NOP을 call 명령으로 덮어쓴다. 오버헤드 거의 0.
5.4 XDP (eXpress Data Path)
NIC 드라이버에서 패킷을 받자마자 실행. 스키판타지 성능.
SEC("xdp")
int xdp_drop_tcp_80(struct xdp_md *ctx) {
void *data = (void *)(long)ctx->data;
void *data_end = (void *)(long)ctx->data_end;
struct ethhdr *eth = data;
if (data + sizeof(*eth) > data_end) return XDP_PASS;
if (eth->h_proto != bpf_htons(ETH_P_IP)) return XDP_PASS;
struct iphdr *ip = data + sizeof(*eth);
if ((void *)(ip + 1) > data_end) return XDP_PASS;
if (ip->protocol != IPPROTO_TCP) return XDP_PASS;
struct tcphdr *tcp = (void *)ip + ip->ihl * 4;
if ((void *)(tcp + 1) > data_end) return XDP_PASS;
if (tcp->dest == bpf_htons(80))
return XDP_DROP; // 80 포트로 오는 TCP 드롭
return XDP_PASS;
}
리턴값:
XDP_PASS: 정상 커널 네트워크 스택으로.XDP_DROP: 즉시 드롭. 수천만 pps까지 가능.XDP_TX: 같은 NIC로 재전송 (DDoS 반사).XDP_REDIRECT: 다른 NIC/CPU로 포워딩.XDP_ABORTED: 에러 (tracepoint 트리거).
5.5 TC (Traffic Control) BPF
XDP보다 한 단계 뒤, qdisc 레벨에서 실행. XDP가 받지 못한 것(이미 커널 스택 진입한 것)도 여기선 볼 수 있다.
SEC("classifier")
int tc_prog(struct __sk_buff *skb) {
// skb->data / skb->data_end 접근 가능
return TC_ACT_OK;
}
용도:
- egress 정책 (XDP는 ingress 전용).
- L7 라우팅 준비 (패킷 마킹).
- Cilium의 identity 기반 필터링.
5.6 cgroup_skb / cgroup_sock
cgroup 단위로 네트워크 제어. K8s 네임스페이스 격리에 사용.
SEC("cgroup_skb/egress")
int cgroup_egress(struct __sk_buff *skb) {
// 이 cgroup에 속한 프로세스의 egress 트래픽 필터
if (skb->remote_port == bpf_htons(22)) return 0; // SSH 차단
return 1;
}
5.7 LSM (Linux Security Module) BPF
Linux 5.7+, SELinux/AppArmor와 같은 보안 모듈을 eBPF로 구현할 수 있다.
SEC("lsm/file_open")
int BPF_PROG(block_secret_file, struct file *file) {
// 특정 파일 접근 차단
char name[256];
bpf_probe_read_kernel_str(name, sizeof(name),
file->f_path.dentry->d_iname);
if (strcmp(name, "secret") == 0)
return -EACCES;
return 0;
}
장점: 동적 정책. SELinux처럼 재컴파일 없이 배포 가능.
5.8 Sockops / Socket Filter
소켓 생명주기에 훅. Cilium이 socket-level load balancing을 구현하는 방법.
SEC("sockops")
int bpf_sockmap(struct bpf_sock_ops *ops) {
if (ops->op == BPF_SOCK_OPS_ACTIVE_ESTABLISHED_CB) {
// 연결 수립됨, sockmap에 등록
// ...
}
return 0;
}
이를 통해 connect(svc_ip:80) 시점에 동일 노드 Pod로 localhost 통신으로 바꿔버릴 수 있다 → kube-proxy의 iptables 룰이 전부 불필요.
6. CO-RE — Compile Once, Run Everywhere
6.1 문제
eBPF 프로그램이 커널 구조체(예: struct task_struct)에 접근하면 필드 오프셋이 하드코딩된다. 그런데 커널은 버전마다 구조체를 바꾼다.
// Linux 5.10
struct task_struct {
...
pid_t pid; // offset 1232
...
};
// Linux 6.5
struct task_struct {
...
// 중간에 새 필드 추가됨
pid_t pid; // offset 1264
...
};
→ 한 번 컴파일한 .o 파일이 다른 커널에서 엉뚱한 메모리를 읽는다.
초기 BCC의 해결책: 런타임 컴파일. 각 타겟에서 LLVM을 실행해 그 커널의 헤더로 다시 컴파일. 문제: LLVM이 수백 MB, 빌드에 수 초, 컨테이너에 맞지 않음.
6.2 해결: BTF + CO-RE
BTF (BPF Type Format): 커널 구조체의 디버그 정보를 압축 형식으로 저장. /sys/kernel/btf/vmlinux에서 읽을 수 있다.
CO-RE (Compile Once, Run Everywhere): 컴파일 시 "이 접근은 나중에 relocation될 것"이라고 마킹해둠 → libbpf가 로드할 때 현재 커널의 BTF를 보고 오프셋을 리라이트.
#include <vmlinux.h> // 커널 타입 정의 (BTF에서 생성)
#include <bpf/bpf_core_read.h>
SEC("kprobe/tcp_sendmsg")
int trace_tcp_sendmsg(struct pt_regs *ctx) {
struct sock *sk = (struct sock *)PT_REGS_PARM1(ctx);
u16 sport;
BPF_CORE_READ_INTO(&sport, sk, __sk_common.skc_num);
bpf_printk("tcp_sendmsg sport=%d\n", sport);
return 0;
}
BPF_CORE_READ_INTO는:
- 컴파일 시점에
__sk_common.skc_num접근을 BTF relocation으로 기록. - 로드 시점에 libbpf가 현재 커널의
struct sock의 실제 오프셋을 찾아 리라이트. - 실행 시점에 정확한 주소를 읽음.
6.3 vmlinux.h 생성
bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
이 파일은 커널 전체의 구조체/enum/define을 C 헤더로 변환한 것. 수백만 줄. 당신의 BPF 프로그램은 이걸 include하기만 하면 된다. 커널 헤더 설치 불필요.
6.4 Feature Detection
필드가 아예 없는 경우 (커널 버전 차이):
if (bpf_core_field_exists(task->comm_size)) {
// 새 커널: comm_size 필드 사용
} else {
// 구 커널: 하드코딩된 16 사용
}
bpf_core_field_exists()는 런타임에 BTF를 보고 "이 필드가 존재하는가"를 반환. 조건부 컴파일 대신 런타임 디스패치.
6.5 CO-RE가 바꾼 것
CO-RE 이전 (BCC 시대):
[컨테이너 이미지]
├─ kernel-headers-*
├─ gcc / clang
├─ LLVM IR
├─ bcc runtime
├─ 실제 프로그램
→ 수백 MB
CO-RE 이후 (libbpf):
[컨테이너 이미지]
├─ libbpf (shared library)
├─ foo.bpf.o (CO-RE 가능한 바이트코드)
├─ foo (유저 프로그램)
→ 수 MB
Cilium, Falco, Pixie, Inspektor Gadget 등이 모두 CO-RE로 단일 바이너리를 배포한다.
7. XDP — 초고속 패킷 처리
7.1 XDP의 위치
NIC → XDP (드라이버 RX 직후) → sk_buff 할당 → TC ingress → 라우팅 → TC egress → 드라이버 TX → NIC
↑ ↑
패킷이 sk_buff로 변환되기 전 커널 네트워크 스택 전부
XDP는 sk_buff 할당 이전에 실행된다. sk_buff는 커널의 범용 패킷 표현인데, 생성 자체가 수백 ns 걸린다. XDP는 이 오버헤드를 없앤다.
7.2 XDP 모드
- Native XDP: NIC 드라이버가 XDP 지원. 최고 성능. 수천만 pps.
- Generic XDP: 커널이 소프트웨어로 에뮬레이션. 드라이버 지원 불필요하지만 느림.
- Offloaded XDP: SmartNIC(Netronome 등)에서 하드웨어 실행. 커널 CPU 0% 소비.
7.3 성능 숫자
| 방식 | pps (single core) | 레이턴시 |
|---|---|---|
| 커널 네트워크 스택 (iptables) | ~1 Mpps | ~10 μs |
| XDP Native | ~24 Mpps | ~1 μs |
| DPDK | ~30 Mpps | ~0.5 μs |
XDP는 DPDK에 근접한 성능을 내면서도 커널 네트워크 스택과 공존한다. 이게 엄청난 차이다.
7.4 XDP 응용 — DDoS 필터링
Cloudflare의 접근:
SEC("xdp")
int ddos_filter(struct xdp_md *ctx) {
// 1. IP 블랙리스트 확인 (LPM trie)
void *data = (void *)(long)ctx->data;
void *data_end = (void *)(long)ctx->data_end;
struct ethhdr *eth = data;
if (data + sizeof(*eth) > data_end) return XDP_DROP;
if (eth->h_proto != bpf_htons(ETH_P_IP)) return XDP_PASS;
struct iphdr *ip = data + sizeof(*eth);
if ((void *)(ip + 1) > data_end) return XDP_DROP;
struct lpm_key key = { .prefixlen = 32, .addr = ip->saddr };
if (bpf_map_lookup_elem(&blacklist, &key))
return XDP_DROP; // 블랙리스트 히트
// 2. SYN flood rate limit
if (ip->protocol == IPPROTO_TCP) {
struct tcphdr *tcp = (void *)ip + ip->ihl * 4;
if ((void *)(tcp + 1) > data_end) return XDP_DROP;
if (tcp->syn && !tcp->ack) {
u64 *count = bpf_map_lookup_elem(&syn_counts, &ip->saddr);
if (count && *count > THRESHOLD)
return XDP_DROP;
// ... rate 증가 로직
}
}
return XDP_PASS;
}
Cloudflare의 실제 운영 경험: 5-10 Tbps DDoS를 XDP로 NIC단에서 드롭. 그 이상은 스크러빙 센터로.
7.5 XDP 응용 — Katran (Facebook L4 LB)
Meta의 모든 외부 트래픽이 Katran을 거친다.
클라이언트 → [Katran 서버]
↓ XDP: consistent hash로 backend 선택
↓ IPIP 또는 GUE 인캡슐레이션
[Backend 서버]
Katran의 특징:
- DSR (Direct Server Return): backend가 클라이언트에게 직접 응답. LB는 요청만 처리.
- Maglev 해싱: 균등 분산 + 최소 재할당.
- 수백만 pps를 단일 머신에서 처리.
이전 설계(IPVS 기반)는 CPU의 20-30%를 네트워크 스택에 썼다. Katran은 같은 CPU 소비로 10배 더 많은 트래픽을 처리한다.
8. 실제 프로덕트: Cilium
8.1 K8s 네트워킹의 문제
기본 K8s는 kube-proxy + iptables 또는 IPVS로 서비스를 구현한다:
- iptables 모드: 서비스 수가 늘면 룰이 **O(n)**로 증가 → 패킷마다 수천 룰 선형 스캔.
- IPVS 모드: O(1) 해시지만 여전히 sk_buff 처리 + netfilter 훅 오버헤드.
- Network Policy: iptables 룰로 표현. 복잡도 폭발.
8.2 Cilium의 해결
Cilium은 iptables를 완전히 우회한다.
Pod → Cilium eBPF 프로그램 (veth 페어 TC hook)
├─ Service Resolution (kube-proxy 대체)
├─ Network Policy (Identity 기반)
├─ Load Balancing (Maglev hash)
└─ Encryption (WireGuard/IPsec)
→ Egress → 원격 노드
각 단계가 eBPF 프로그램 한 개 이하다.
8.3 Identity-Based Policy
전통적인 네트워크 정책은 IP 기반이었다. Pod가 죽고 재생성되면 IP가 바뀌어 정책이 무의미해진다.
Cilium은 label 기반 Identity를 사용한다:
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: allow-frontend-to-backend
spec:
endpointSelector:
matchLabels:
app: backend
ingress:
- fromEndpoints:
- matchLabels:
app: frontend
toPorts:
- ports:
- port: "8080"
protocol: TCP
내부적으로:
- 각 라벨 조합에 16-bit identity ID를 부여.
- 패킷 전송 시 Cilium이 identity를 패킷 메타데이터에 붙임.
- 수신 측에서
(src_identity, dst_identity, port)튜플로 BPF 맵을 조회해 허용/거부.
O(1) 정책 조회. 수천 개 정책이 있어도 성능 저하 없음.
8.4 kube-proxy 대체
전통적 흐름:
Pod → connect(svc_ip) → iptables DNAT (수천 룰 스캔) → 실제 backend pod_ip
Cilium 흐름 (sockops):
Pod → connect(svc_ip)
↓
sockops BPF 프로그램 실행
↓
"이 서비스 → 이 backend"로 DNAT (맵 조회 O(1))
↓
connect는 처음부터 backend_ip로 진행 (iptables 건드리지 않음)
같은 노드 Pod라면 더 나아가:
connect(svc_ip) → sockmap redirect → 직접 localhost 통신
커널 네트워크 스택을 완전히 우회한다. 같은 호스트 통신은 수백 ns에서 수십 ns로.
9. Observability: bpftrace, bcc, Pixie
9.1 bpftrace — Ad-hoc 추적
DTrace 유사한 스크립트 언어. 한 줄 커맨드로 즉시 추적.
# syscall 호출 횟수 (1초마다)
bpftrace -e 'tracepoint:syscalls:sys_enter_* { @[probe] = count(); }'
# TCP 연결 레이턴시 히스토그램
bpftrace -e '
kprobe:tcp_v4_connect { @start[tid] = nsecs; }
kretprobe:tcp_v4_connect /@start[tid]/ {
@latency = hist((nsecs - @start[tid]) / 1000);
delete(@start[tid]);
}'
# 어떤 프로세스가 파일 열고 있나
bpftrace -e '
tracepoint:syscalls:sys_enter_openat {
printf("%s %s\n", comm, str(args->filename));
}'
프로덕션에서 "왜 느리지?"를 확인할 때 독보적이다. 설치만 필요하고 컴파일러 필요 없음.
9.2 bcc — Python + BPF
BCC는 Python으로 BPF 프로그램을 작성/로드. libbpf 이전 표준.
from bcc import BPF
program = """
int count(void *ctx) {
bpf_trace_printk("hello\\n");
return 0;
}
"""
b = BPF(text=program)
b.attach_kprobe(event="do_sys_openat2", fn_name="count")
b.trace_print()
단점: 런타임 LLVM 필요. CO-RE 등장 이후 libbpf 기반으로 이전 중.
9.3 libbpf + skeleton — 프로덕션 표준
// trace.bpf.c
#include <vmlinux.h>
#include <bpf/bpf_helpers.h>
struct {
__uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, 256 * 1024);
} events SEC(".maps");
SEC("tp/syscalls/sys_enter_openat")
int handle_openat(void *ctx) {
u32 *e = bpf_ringbuf_reserve(&events, sizeof(u32), 0);
if (!e) return 0;
*e = bpf_get_current_pid_tgid() >> 32;
bpf_ringbuf_submit(e, 0);
return 0;
}
char LICENSE[] SEC("license") = "GPL";
clang -O2 -target bpf -g -c trace.bpf.c -o trace.bpf.o
bpftool gen skeleton trace.bpf.o > trace.skel.h
// trace.c
#include "trace.skel.h"
int main() {
struct trace_bpf *skel = trace_bpf__open_and_load();
trace_bpf__attach(skel);
struct ring_buffer *rb = ring_buffer__new(
bpf_map__fd(skel->maps.events), handle_event, NULL, NULL);
while (!exiting) ring_buffer__poll(rb, 100);
trace_bpf__destroy(skel);
}
- 단일 바이너리, 의존성 libbpf만.
- CO-RE 덕분에 커널 버전 상관없음.
- 프로덕션 배포에 적합.
9.4 Pixie — 자동 계측 없는 K8s 관측성
Pixie는 "코드 수정 없이 K8s 클러스터 관측"을 내세운다. 방법:
- 각 노드에 DaemonSet으로
pem(Pixie Edge Module) 배포. pem이 uprobes로 libssl, libc, libgrpc 등에 훅 → HTTP, gRPC, MySQL, PostgreSQL 프로토콜 해독.- 프로토콜 메타데이터(URL, 상태 코드, 레이턴시)를 ring buffer로 수집.
- PxL이라는 쿼리 언어로 조회.
**"curl-like latency for in-cluster traffic"**을 Prometheus 없이 구현. 특정 요청의 실제 body까지 볼 수 있다(개발/디버깅 모드).
10. Falco — 런타임 보안
10.1 문제
컨테이너 런타임 중 이상 행위를 탐지하려면? 예:
- 컨테이너가
/etc/shadow를 읽음. - Nginx 프로세스가
bash -i를 spawn. - 루트가 아닌 프로세스가
setuid.
iptables는 네트워크만, AppArmor는 정적 정책만. 필요한 건 "행위 기반 탐지".
10.2 Falco의 접근
Falco는 eBPF로 시스템콜 전부를 추적하고, 룰 엔진으로 매치.
- rule: Read sensitive file untrusted
desc: Detect reading sensitive files
condition: >
sensitive_files and open_read
and proc_name_exists
and not proc.name in (trusted_procs)
output: >
Sensitive file opened (user=%user.name
command=%proc.cmdline file=%fd.name)
priority: WARNING
런타임에 syscall이 매치되면 알림 발송.
10.3 eBPF가 필요한 이유
초기 Falco는 커널 모듈을 사용했다 → 배포가 어렵고, 충돌 위험. eBPF로 이전한 후:
- 커널 모듈 불필요 → 어떤 K8s 노드에도 즉시 배포.
- 커널 버전 상관없음 (CO-RE).
- 성능 개선: syscall이 초당 수백만 일어나는 상황에서도 오버헤드 < 1%.
11. 한계와 주의사항
11.1 Verifier의 벽
eBPF는 너무 복잡한 프로그램을 받지 않는다. 실제 사례:
- Cilium의 초기 버전은 프로그램 분할을 해야 했다 (한 함수가 너무 커서).
- 지금도
tail call로 프로그램을 체인시키거나bpf_loophelper(Linux 5.17+)로 런타임 루프를 흉내낸다.
11.2 GPL 제약
bpf_printk, bpf_probe_read_kernel 등 대부분의 유용한 helper는 GPL이다. 당신의 BPF 프로그램도 GPL이어야 사용 가능.
char LICENSE[] SEC("license") = "GPL";
없으면 로드 시 verifier가 거부.
11.3 성능 오버헤드
eBPF는 JIT 컴파일되지만 helper 호출은 일반 함수 호출이다. 매 패킷마다 여러 map lookup을 하면 누적된다. XDP 벤치마크:
- 빈 프로그램: ~24 Mpps
- map lookup 1회: ~18 Mpps
- map lookup 3회 + 간단 로직: ~10 Mpps
"수백만 pps"는 최소한의 프로그램에서만 나온다.
11.4 디버깅의 어려움
bpf_printk는/sys/kernel/debug/tracing/trace_pipe로 나간다 (느리고 전역).- 스택 트레이스 없음.
- GDB 불가.
대신:
bpftool prog dump jited로 JIT 결과 확인.- Verifier 로그를 처음부터 끝까지 읽는 것이 가장 효과적.
- Tests:
libbpf+BPF_PROG_TEST_RUN으로 입력/출력 비교.
11.5 관찰자 효과 (Observer Effect)
Uprobe/kprobe는 매 호출에 5-30 ns 추가. 고주파 함수에 붙이면 눈에 띄게 느려진다. 프로덕션에서 가벼운 필터 먼저 적용 후 나머지는 링 버퍼로 보내는 패턴 권장.
12. 보안 관점
12.1 eBPF는 안전한가
Verifier는 메모리 안전성을 증명하지만 의도 안전성은 아니다.
- Side channels: eBPF로 CPU 캐시 타이밍 측정 → Spectre-like 공격 가능성. 2019년 이후 커널은 kernel lockdown과 함께 여러 완화책 도입.
- DoS: 누군가 복잡한 eBPF 프로그램을 반복 로드하면 verifier 자체가 CPU를 먹을 수 있다.
CAP_BPF권한 필요(루트 제한). - 데이터 유출: 특권 사용자가 eBPF로 커널 메모리를 읽어 비밀 정보 탈취. 그래서 비특권 eBPF는 점점 축소 중.
12.2 권한 체계
CAP_SYS_ADMIN: 전통적 권한. 대부분의 BPF 기능.CAP_BPF: Linux 5.8+, 더 세분화된 권한.CAP_PERFMON: 추적 용.CAP_NET_ADMIN: 네트워크 BPF.
K8s에서 Cilium 같은 제품은 privileged 또는 CAP_BPF + CAP_NET_ADMIN이 필요하다.
12.3 Unprivileged BPF의 쇠퇴
한때 unprivileged BPF (소켓 필터용)가 있었지만, Spectre 문제로 기본 비활성화 (kernel.unprivileged_bpf_disabled=1). 2025년 현재 사실상 금지된 상태.
13. 벤치마킹과 튜닝
13.1 XDP 벤치마크
# 패킷 생성 (다른 머신)
pktgen ... -d target_ip -p 10000 -pps 10000000
# 수신 머신에서 eBPF 로드
ip link set dev eth0 xdp obj drop.o sec xdp
# 트래픽 관찰
ip -s link show eth0
- 커널 stats:
bpftool prog show+bpftool prog profile. - Hardware counters:
perf stat -e cycles,instructions.
13.2 Verifier 프로파일
bpftool prog load prog.o /sys/fs/bpf/prog \
log_level 2 log_size 4194304 2>&1 | tee verifier.log
로그를 보면 "verifier가 어디서 많이 돌았는지" 알 수 있다. 복잡도가 큰 분기를 단순화.
13.3 XDP 성능 최적화 팁
- Bounds check 최소화: 연속된 필드는 한 번의 체크로.
bpf_xdp_adjust_head피하기: 헤더 삽입/삭제는 비싸다.- PCIe NIC의 RX ring 크기 늘리기: 패킷 드롭 방지.
- CPU 친화도 설정:
RPS/RFS로 특정 CPU에 interrupts 고정. - 대용량 페이지 사용: TLB 미스 감소.
14. 심화: BPF Tail Calls & Function Calls
14.1 Tail Call
verifier의 복잡도 한계 때문에 한 프로그램은 ~1M 명령어까지만 가능하다. 더 큰 로직은 tail call로 다른 프로그램으로 점프.
struct {
__uint(type, BPF_MAP_TYPE_PROG_ARRAY);
__uint(max_entries, 4);
__type(key, u32);
__type(value, u32);
} prog_array SEC(".maps");
SEC("xdp")
int entry(struct xdp_md *ctx) {
// 첫 번째 단계 처리
bpf_tail_call(ctx, &prog_array, NEXT_PROG);
return XDP_PASS; // tail call 실패 시
}
- 단점: 스택 유실 (호출자의 로컬 변수 사라짐).
- 이유: 다음 프로그램으로 "goto"하는 방식이라 콜 스택 관리 없음.
- 사용: Cilium, XDP 기반 LB에서 패킷 처리 단계 분할.
14.2 BPF-to-BPF Function Calls (Linux 4.16+)
일반적인 함수 호출. bpf_tail_call보다 자연스럽지만 여전히 verifier 제약.
static __noinline int helper(int x) {
return x * 2;
}
SEC("xdp")
int prog(struct xdp_md *ctx) {
return helper(42);
}
스택이 보존되고 리턴도 된다. 인라인 없이 함수 재사용 가능. 단, 재귀 금지(verifier가 종료 증명 불가).
15. 미래와 트렌드
15.1 eBPF for Windows
Microsoft는 eBPF for Windows를 개발 중. 같은 BPF ELF를 Linux/Windows에서 돌리는 것이 목표. 2025년 현재 베타.
15.2 sched_ext — 커널 스케줄러를 eBPF로
Linux 6.12에 병합된 sched_ext는 프로세스 스케줄러 자체를 eBPF로 작성한다. Meta가 주도:
SEC("struct_ops/scx_example_enqueue")
void BPF_STRUCT_OPS(enqueue, struct task_struct *p, u64 enq_flags) {
scx_bpf_dispatch(p, SCX_DSQ_GLOBAL, SCX_SLICE_DFL, enq_flags);
}
기존 CFS/EEVDF 대신 내 커스텀 정책을 로드. 연구/프로덕션 튜닝 목적.
15.3 eBPF + Rust
Rust로 eBPF를 작성하는 aya 프레임워크가 성숙. C보다 메모리 안전, CO-RE 지원, LLM 생성 친화적.
use aya_bpf::{macros::xdp, programs::XdpContext};
#[xdp]
pub fn my_xdp(ctx: XdpContext) -> u32 {
match unsafe { try_my_xdp(ctx) } {
Ok(ret) => ret,
Err(_) => xdp_action::XDP_PASS,
}
}
15.4 Cloud-Native Service Mesh 대체
Istio의 sidecar(envoy per pod)는 오버헤드가 크다. Cilium Service Mesh는 eBPF + per-node envoy 모델로 대체:
- Pod당 envoy 없음.
- L7 파싱은 여전히 envoy에서(L7은 verifier로 검증하기에 너무 복잡).
- L4 라우팅, mTLS, identity는 eBPF에서.
오버헤드 70% 감소.
16. 학습 로드맵
1단계: 개념
- https://ebpf.io — eBPF의 공식 관문.
- "What is eBPF" 공식 문서.
2단계: 실습
bpftrace설치해서 원라이너 돌려보기.- BCC 레포의
tools/디렉토리 — 100개 이상의 실전 예제(tcp_latency, biosnoop, execsnoop 등).
3단계: libbpf로 직접 작성
- libbpf-bootstrap 레포의 예제들.
vmlinux.h생성 방법 익히기.- skeleton 패턴으로 프로덕션급 프로그램 작성.
4단계: 실전 프로덕트 탐구
- Cilium 소스:
bpf/디렉토리. 대규모 XDP/TC 프로그램의 모범. - Katran 소스: Meta의 L4 LB.
- Tetragon: Cilium의 런타임 보안(Falco 경쟁).
서적:
- "Learning eBPF" — Liz Rice (O'Reilly, 2023).
- "Linux Observability with BPF" — David Calavera & Lorenzo Fontana.
컨퍼런스:
- eBPF Summit (연 1회).
- LPC (Linux Plumbers Conference) — BPF 트랙.
- KubeCon — Cilium/Tetragon 트랙.
17. 요약 — 한 장 정리
┌────────────────────────────────────────────────────┐
│ eBPF Cheat Sheet │
├────────────────────────────────────────────────────┤
│ VM: │
│ - 11 regs (R0=ret, R1-5=args, R10=stack) │
│ - 64-bit RISC-like, JIT to native │
│ - 1M insn limit, bounded loops │
│ │
│ Verifier: │
│ - Symbolic execution │
│ - Proves termination + memory safety │
│ - Pointer types: CTX, PKT, MAP_VALUE, STACK, ... │
│ - Must NULL-check map lookups │
│ - Must bounds-check packet access │
│ │
│ Maps: │
│ - Hash, Array, LRU, LPM trie, Ring buffer │
│ - Per-CPU variants for lock-free counters │
│ - Helpers: lookup/update/delete │
│ │
│ Program types (hooks): │
│ - kprobe/kretprobe : 동적, 함수 진입 │
│ - tracepoint : 안정 ABI 이벤트 │
│ - fentry/fexit : trampoline 기반, 빠름 │
│ - XDP : 드라이버 바로 뒤, 초고속 │
│ - TC (classifier) : qdisc 레벨 │
│ - cgroup_skb : 네임스페이스별 │
│ - LSM : 보안 훅 │
│ - sockops : 소켓 수립 │
│ │
│ CO-RE: │
│ - BTF가 커널 타입 정보 저장 │
│ - libbpf가 로드 시 오프셋 리라이트 │
│ - 한 번 컴파일, 모든 커널 │
│ - vmlinux.h 자동 생성 │
│ │
│ 프로덕션: │
│ - Cilium: K8s CNI, kube-proxy 대체 │
│ - Katran: Meta L4 LB │
│ - Pixie: 관측성, uprobe 기반 │
│ - Falco: 런타임 보안 │
│ - bpftrace: ad-hoc 추적 │
│ │
│ 도구: │
│ - bpftool: prog/map 관리 │
│ - libbpf: 프로덕션 라이브러리 │
│ - bcc: Python, LLVM 런타임 │
│ - aya: Rust 프레임워크 │
└────────────────────────────────────────────────────┘
18. 퀴즈
Q1. eBPF Verifier가 프로그램 로드를 거부하는 가장 흔한 이유는?
A. 포인터 접근 전에 bounds check를 하지 않거나, map_lookup 결과를 NULL 체크 없이 역참조하는 경우. XDP 프로그램이라면 data + N > data_end 체크가 빠진 헤더 접근이 대표적. 로그에 invalid access to packet, off=N size=M 또는 R1 pointer arithmetic on PTR_TO_MAP_VALUE_OR_NULL 같은 메시지가 나온다.
Q2. CO-RE가 해결한 문제는 무엇인가?
A. 커널 구조체 레이아웃이 버전마다 달라서 한 번 컴파일한 eBPF 프로그램이 다른 커널에서 엉뚱한 메모리를 읽는 문제. BTF(BPF Type Format)가 각 커널의 타입 정보를 저장하고, libbpf가 프로그램 로드 시 필드 오프셋을 해당 커널에 맞게 리라이트해서 한 바이너리가 여러 커널 버전에서 동작하게 한다.
Q3. XDP는 왜 전통적인 iptables보다 훨씬 빠른가?
A. iptables는 sk_buff(커널의 범용 패킷 표현)가 할당된 후 netfilter 체인을 스캔한다. XDP는 NIC 드라이버 직후, sk_buff 할당 전에 실행된다. 따라서 수백 ns의 sk_buff 설정 비용과 전체 네트워크 스택을 우회한다. 또한 JIT 컴파일된 네이티브 코드로 실행되므로 룰 해석 오버헤드도 없다. 결과: 단일 코어로 수천만 pps.
Q4. Per-CPU 맵이 일반 해시맵보다 빠른 이유는?
A. 캐시 라인 경합이 없기 때문. 여러 CPU가 같은 카운터를 동시에 증가시키면 MESI 프로토콜로 인해 캐시 라인이 코어 사이를 왔다갔다하며 성능이 붕괴된다. Per-CPU 맵은 각 CPU가 자기 슬롯만 쓰므로 경합 없음. 다만 유저 공간에서 읽을 때 모든 CPU의 값을 합산해야 한다.
Q5. Cilium이 kube-proxy를 대체할 때 어떻게 iptables를 우회하는가?
A. sockops BPF 프로그램이 connect() 시스템콜 훅에서 서비스 IP를 감지하고, BPF 맵을 O(1)로 조회해 실제 backend IP로 DNAT을 먼저 수행한다. 커널 네트워크 스택은 처음부터 backend IP로 연결을 시작하므로 iptables 룰이 전혀 개입하지 않는다. 같은 노드의 Pod라면 sockmap redirect로 커널 스택마저 우회해 직접 localhost 통신으로 바꾼다.
Q6. eBPF 프로그램이 매우 복잡할 때 사용하는 tail call의 단점은?
A. 스택이 유실된다. tail call은 "다음 프로그램으로 goto"하는 방식이라 호출자의 로컬 변수가 사라진다. 데이터를 맵에 저장하거나 프로그램 간 통신을 위해 per-CPU 맵을 활용해야 한다. 또한 최대 호출 깊이 제한(33)이 있어 무한 tail call도 불가.
Q7. 유저 공간 프로그램의 함수에 훅을 걸려면 어떤 프로그램 타입을 써야 하는가?
A. uprobe 또는 uretprobe. 커널 함수용 kprobe의 유저 공간 버전. 예를 들어 /usr/bin/python3의 특정 함수에 훅해서 파라미터를 볼 수 있다. Pixie는 이 방식으로 libssl의 SSL_read/write에 훅해 HTTPS 트래픽을 TLS 종료 후 해독된 상태로 본다.
이 글이 도움이 됐다면 다음 포스트도 확인해 보세요:
- "Linux 네트워크 스택 Deep Dive" — XDP가 우회하는 바로 그 경로.
- "Cilium Architecture 완전 정복" — eBPF를 활용한 K8s 네트워킹.
- "io_uring Deep Dive" — 또 다른 커널 API 혁명.
- "DPDK vs XDP 비교" — 두 고성능 패킷 처리 모델.
현재 단락 (1/753)
- **eBPF**는 리눅스 커널 안에서 샌드박스 바이트코드를 돌리는 VM이다. 커널 모듈을 빌드하지 않고도 커널 동작을 관측/수정할 수 있다.