사건이 난 자리에서 센다
한 줄 요약
eBPF 는 커널 안의 정해진 자리에 작은 프로그램을 붙여, 사건이 날 때마다 그 자리에서 세고 더해 맵에 모읍니다. bpftrace 는 그 프로그램을 한 줄짜리 스크립트로 쓰게 해 주는 언어입니다.
왜 필요했나
앞 단원들에서 본 도구에는 각자 한계가 있었습니다. strace 는 사건마다 프로세스를 멈춰 운영에서 쓰기 무겁고, ftrace 는 사건을 글로 쌓아 사람이 걸러야 하고, perf 는 표본이라 드문 사건을 놓칩니다. 운영자가 정말 묻고 싶은 것은 대개 집계입니다. "이 프로세스가 연 파일을 이름별로 몇 번", "읽기 지연의 분포", "누가 이 프로세스를 죽였나". eBPF 는 이 집계를 커널 안에서 끝내고 결과만 넘깁니다. 사건 하나에 드는 비용이 문맥 교환이 아니라 수십 나노초 수준의 짧은 프로그램 실행입니다.
어떻게 동작하나
bpftrace 스크립트는 자리 /조건/ { 동작 } 의 묶음입니다. 자리는 네 종류를 가장 많이 씁니다. tracepoint:syscalls:sys_enter_openat 처럼 약속된 자리, kprobe:vfs_read 와 kretprobe:vfs_read 처럼 커널 함수의 입구와 출구, uprobe:/path/bin:func 처럼 사용자 프로그램 함수의 입구입니다. tracepoint 의 필드는 args->filename 으로, kprobe 와 uprobe 의 인자는 arg0, arg1 로, kretprobe 의 반환값은 retval 로 읽습니다. comm·pid·tid·nsecs 는 지금 실행 중인 태스크의 이름·번호·시각입니다.
결과는 @ 로 시작하는 맵에 모읍니다. @[str(args->filename)] = count() 는 파일 이름별로 세고, hist() 는 2의 거듭제곱 구간 히스토그램을, sum() 은 합을 만듭니다. 맵은 커널 안에 있고, bpftrace 가 끝날 때 남은 맵을 전부 찍습니다. 입구와 출구를 짝지어 지연을 잴 때는 입구에서 @start[tid] = nsecs 로 적고 출구에서 빼는데, 다 쓴 칸을 delete() 로 지우지 않으면 맵이 계속 자랍니다.
붙일 자리의 인자가 무엇인지 모를 때는 bpftrace -lv 'tracepoint:syscalls:sys_enter_openat' 처럼 -v 를 붙이면 필드 이름과 타입이 나옵니다. BTF 가 켜진 커널에서는 kprobe 대신 kfunc:vfs_read(fentry)를 쓸 수 있는데, 인자를 args->count 처럼 이름으로 읽고 진입 비용도 더 작습니다. 다만 kprobe 와 마찬가지로 커널 함수 이름에 기대므로 판이 바뀌면 깨질 수 있다는 점은 같습니다.
bpftrace 는 스크립트를 LLVM 으로 BPF 바이트코드로 컴파일해 커널에 올립니다. 그래서 시작에 몇 초가 걸리고, 올리는 순간 커널의 검증기를 통과해야 합니다. 이 과정은 다음 단원에서 직접 들여다봅니다.
현장에서 만나는 모습
"프로세스가 가끔 SIGTERM 으로 죽는데 누가 보냈는지 모른다" 는 질문은 tracepoint:signal:signal_generate 한 줄로 답이 나옵니다. 이 tracepoint 의 comm·pid 필드는 신호를 받는 쪽이고, 내장 변수 comm·pid 는 지금 실행 중인 보내는 쪽입니다. 대부분은 서비스 관리자나 감시 스크립트가 띄운 kill·pkill 이 범인입니다.
디스크가 느리다는 신고에는 vfs_read 나 블록 계층 tracepoint 의 지연 히스토그램을 그립니다. 평균은 꼬리를 숨기지만 히스토그램은 두 봉우리(캐시 적중과 디스크 읽기)를 바로 보여 줍니다. 로그를 고칠 수 없는 프로그램의 함수 인자를 보고 싶을 때는 uprobe 가 답입니다.
다만 운영에서는 자리를 고를 때 tracepoint 를 먼저 찾고, 아주 자주 일어나는 사건에는 printf 대신 집계를 씁니다. 사건마다 글을 찍으면 결국 strace 와 같은 비용을 냅니다.
English corner
- one-liner: 한 줄 프로그램입니다. bpftrace 문서와 발표에서 가장 자주 나오는 낱말입니다. "Here is a one-liner to count syscalls by process."
- aggregation: 커널 안에서 세고 더하는 집계입니다. "Do the aggregation in kernel, not in userspace."
- Can you share a reproducer?: 버그 보고에 리뷰어가 가장 먼저 돌려주는 말입니다. 재현 스크립트를 달라는 뜻입니다.
- in-kernel / out-of-tree: 커널 소스 안에 있는 것과 밖에서 따로 관리되는 것입니다. bpftrace 는 out-of-tree 도구이고 BPF 자체는 in-kernel 기능입니다.
다음 실습에서 할 것
붙일 자리를 찾고, 가게 프로그램이 연 파일을 이름별로 세고, 읽기 크기와 지연을 히스토그램으로 그립니다. uprobe 로 함수 인자를 합치고, 신호 tracepoint 로 누가 프로세스를 죽였는지 찾고, 다시 쓸 수 있는 스크립트 파일로 남깁니다.