eBPF 관측 Hook 선택: tracepoint·kprobe·fentry·uprobe 차이

반응형

eBPF 프로그램은 혼자 실행되지 않고 커널이나 사용자 공간에서 발생하는 특정 event에 연결된다. 이 연결 지점을 흔히 hook이라고 부른다. 같은 함수 호출을 관찰하더라도 tracepoint, kprobe, fentry, uprobe는 대상과 안정성, 인자 해석 방식이 다르다.

“커널이면 kprobe, 애플리케이션이면 uprobe”만으로 고르면 배포 뒤 symbol이 사라지거나 context 구조가 달라지는 문제를 놓치기 쉽다. 먼저 관찰하려는 event의 의미와 유지해야 할 kernel·binary 범위를 정한다.

Hook을 고르는 기준

Hook 관찰 대상 연결 기준 변경에 민감한 지점
tracepoint 커널이 미리 정의한 event category와 event name event 제공 여부와 context field
raw tracepoint 커널 tracepoint raw argument typed tracepoint보다 인자 해석 책임이 큼
kprobe·kretprobe 커널 instruction·함수 진입/반환 symbol과 offset 함수명, inline, prototype, kernel 내부 구현
fentry·fexit BTF가 있는 커널 함수 진입/반환 BTF function type BTF·trampoline 지원, target function 변경
uprobe·uretprobe 사용자 executable·shared library binary path, symbol·offset build별 symbol·offset, stripped binary, library version
USDT 애플리케이션이 정의한 static probe provider와 probe name 애플리케이션의 probe 계약과 배포 여부

일반적인 출발점은 의미가 맞는 tracepoint 또는 USDT가 있는지 먼저 찾고, 더 깊은 내부 상태가 필요할 때 fentry·kprobe 또는 uprobe로 내려가는 것이다. 다만 tracepoint라는 이유만으로 영구적인 ABI가 보장되는 것은 아니며, 배포 대상 kernel에서 context를 검증해야 한다.

tracepoint: 커널이 이름 붙인 event

Linux tracepoint는 커널 소스의 전략적인 위치에 정의된 static probe point다. process scheduling이나 system call event처럼 관찰 의미가 이미 정해져 있다.

배포 중인 host에서 실제 event를 확인한다.

find /sys/kernel/tracing/events -mindepth 2 -maxdepth 2 -type d

특정 event의 field 형식은 format 파일에서 확인할 수 있다.

cat /sys/kernel/tracing/events/sched/sched_switch/format

libbpf ELF section은 다음 형식을 사용한다.

SEC("tracepoint/sched/sched_switch")

tracepoint는 내부 kernel 함수 이름보다 event 의미가 분명하고 context가 노출돼 있어 관측 도구의 첫 후보로 좋다. 반면 필요한 위치에 event가 없거나 원하는 내부 변수가 context에 포함되지 않을 수 있다.

raw tracepoint는 같은 event에 더 낮은 수준으로 연결하며 raw argument를 직접 해석한다. typed context의 편의보다 낮은 수준의 접근이 꼭 필요한지 먼저 판단한다.

kprobe와 kretprobe: 커널 구현에 동적으로 붙는다

kprobe는 대부분의 kernel instruction 주소에 동적으로 probe를 설치할 수 있고, kretprobe는 함수 반환 시점을 관찰한다. libbpf에서는 다음과 같은 section name을 쓴다.

SEC("kprobe/tcp_sendmsg")
SEC("kretprobe/tcp_sendmsg")

자유도가 높은 대신 target이 kernel 내부 구현이라는 점이 가장 큰 비용이다.

  • 함수 이름이 kernel version이나 configuration에 따라 달라질 수 있다.
  • compiler가 함수를 inline하면 기대한 모든 호출을 잡지 못할 수 있다.
  • 함수 prototype이 바뀌면 register에서 인자를 읽는 코드가 틀릴 수 있다.
  • kprobe blacklist와 architecture별 제약으로 연결할 수 없는 위치가 있다.

따라서 “등록이 성공했다”만 확인하지 말고 예상 event rate와 field 값이 합리적인지 검증한다. 여러 kernel version을 지원한다면 각 build의 symbol과 prototype을 CI 또는 별도 호환성 검사에 포함한다.

fentry와 fexit: BTF type을 이용한 함수 관찰

fentry·fexit는 BPF_PROG_TYPE_TRACING과 BPF trampoline을 사용해 함수 진입·반환에 연결한다. BTF function type을 이용하므로 kprobe에서 architecture별 register를 직접 해석하는 방식보다 typed argument를 다루기 쉽다.

SEC("fentry/tcp_sendmsg")
SEC("fexit/tcp_sendmsg")

먼저 target host에 kernel BTF가 있는지 확인한다.

test -r /sys/kernel/btf/vmlinux && echo "kernel BTF available"

fentry가 항상 kprobe의 완전한 대체는 아니다. kernel과 loader가 필요한 BPF attach type·BTF를 지원해야 하고, target function이 BTF에 노출되어야 한다. 함수 자체가 내부 구현인 이상 version 변경으로 사라질 수도 있다. 지원 조건이 맞으면 함수 entry·exit 관찰의 우선 후보로 검토하는 편이 좋다.

uprobe와 uretprobe: 사용자 binary 내부를 본다

uprobe는 executable이나 shared library의 instruction에 연결되고 uretprobe는 함수 반환을 관찰한다.

SEC("uprobe//usr/lib/libexample.so:parse_request")
SEC("uretprobe//usr/lib/libexample.so:parse_request")

실제 자동 연결 API와 section 문법은 loader version에 맞춰 확인한다. 운영에서 더 중요한 것은 어느 binary를 관찰하는지 식별하는 일이다.

  • container image와 host의 binary path가 다를 수 있다.
  • 같은 library name이어도 build ID와 symbol offset이 다를 수 있다.
  • stripped binary에는 기대한 symbol이 없을 수 있다.
  • JIT runtime은 코드 주소가 동적으로 바뀌어 별도 runtime 지원이 필요할 수 있다.
  • process 전체가 아니라 특정 PID·cgroup만 볼지 범위를 정해야 한다.

source code를 고칠 수 있고 장기적인 관측 계약이 필요하다면 애플리케이션이 이름과 인자를 정의하는 USDT probe가 더 나은 선택일 수 있다. uprobe는 제공되지 않은 내부 지점을 관찰하는 강력한 fallback이지만 binary 구현과 함께 versioning해야 한다.

선택 순서

  1. 관찰하려는 질문을 event와 필요한 field로 적는다.
  2. target host의 tracepoint·USDT에 그 의미가 이미 있는지 찾는다.
  3. 커널 함수 내부가 필요하고 BTF 조건이 맞으면 fentry·fexit를 검토한다.
  4. BTF attach가 불가능하거나 instruction offset이 필요하면 kprobe·kretprobe를 검토한다.
  5. 사용자 binary 내부는 USDT를 먼저 찾고, 없으면 uprobe·uretprobe를 사용한다.
  6. 연결 성공, event 누락, field 해석, probe overhead를 target kernel·binary 조합별로 검증한다.

운영에 붙이기 전 확인할 것

  • kernel version, configuration, architecture와 BTF 존재 여부
  • symbol·tracepoint·USDT provider가 실제 host에 존재하는지
  • 고빈도 event에서 sampling이나 aggregation이 필요한지
  • BPF map 크기와 ring buffer가 밀릴 때 drop을 관측하는지
  • PID, cgroup, namespace filter가 데이터 범위를 제대로 제한하는지
  • command line, path, payload에 개인정보·secret이 섞이지 않는지
  • detach와 program cleanup이 실패했을 때 남은 link를 어떻게 찾는지

system call 경계를 먼저 좁히는 방법은 Linux 시스템 콜과 strace 추적, 네트워크 성능 event를 계층별로 나누는 방법은 Linux 네트워크 성능 분석에서 이어서 볼 수 있다.

참고 자료

반응형
KEEP READING
카테고리 전체 보기 →

댓글