하드웨어 고급 #2 eBPF 관측 — 평균이 숨기는 꼬리를 보다
1편의 perf는 샘플링 도구였습니다. 초당 수천 번 CPU의 현재 위치를 찍어서 “시간이 어디에 쓰이는가"의 통계적 그림을 그렸습니다. 이번 글의 eBPF는 접근이 다릅니다. 커널 안의 이벤트가 일어나는 길목에 작은 프로그램을 걸어 두고, 이벤트가 일어날 때마다 직접 기록합니다. perf가 특정 시점마다 CPU 위치를 찍는 통계적 방식이라면, eBPF는 이벤트가 발생할 때마다 직접 기록하는 방식입니다. 디스크 I/O 한 건 한 건의 지연, 프로세스가 CPU를 받기까지 기다린 시간 한 건 한 건이 전부 잡힙니다.
이 차이가 중요한 이유는 운영에서 정말 아픈 문제가 평균이 아니라 꼬리에 있기 때문입니다.
평균이 숨기는 것 — 분포와 꼬리 지연 #
중급 1편에서 다룬 %wa와 로드 애버리지는 좋은 출발점이지만, 둘 다 평균입니다. 평균은 분포를 뭉개 버립니다.
디스크 I/O 1,000건 중 990건이 0.2ms, 10건이 200ms 걸렸다고 하겠습니다. 평균은 약 2.2ms로 멀쩡해 보입니다. 하지만 사용자 요청 하나가 I/O를 수십 번 일으킨다면, 상당수의 요청이 저 느린 10건 중 하나를 밟습니다. 사용자가 체감하는 “가끔 한 번씩 멈칫한다"의 정체가 바로 이 꼬리 지연(tail latency, 분포의 끝쪽에 있는 소수의 느린 이벤트)입니다. 평균과 사용률 지표로는 이 꼬리가 보이지 않습니다. 분포 전체, 즉 히스토그램이 필요합니다.
eBPF가 이 일에 맞는 도구인 이유는 모든 이벤트를 커널 안에서 집계할 수 있어서입니다. 이벤트마다 사용자 공간으로 데이터를 퍼 나르면 비용이 크지만, 커널 안에서 히스토그램으로 누적해 두고 끝날 때 요약만 가져오면 비용이 작습니다.
eBPF — 커널 안에서 도는 작은 프로그램 #
eBPF(extended Berkeley Packet Filter)는 이름처럼 패킷 필터에서 출발했지만, 지금은 커널 안에서 안전하게 실행되는 범용 미니 프로그램의 실행 환경입니다. 시스템 콜 진입, 디스크 I/O 완료, 스케줄러의 작업 전환 같은 커널 이벤트에 프로그램을 붙여 두면, 그 이벤트가 일어날 때마다 프로그램이 돌면서 시각을 재고 카운터를 올립니다.
“커널 안에서 임의 코드를 돌린다"는 말이 위험하게 들린다면 정상입니다. 같은 일을 커널 모듈로 하면 버그 하나가 커널 패닉으로 이어집니다. eBPF가 다른 점은 검증기(verifier)입니다. 프로그램을 커널에 올리기 전에 검증기가 모든 실행 경로를 검사해서, 끝나지 않을 수 있는 루프, 허용되지 않은 메모리 접근, 한도를 넘는 크기를 가진 프로그램을 전부 거부합니다. 검증을 통과한 프로그램만 실행되므로, eBPF 추적 도구가 커널을 죽이는 일은 구조적으로 막혀 있습니다.
eBPF를 직접 작성할 일은 드뭅니다. 운영자는 보통 두 층의 도구를 씁니다. 즉석 질문에는 bpftrace, 정형화된 분석에는 BCC 도구 모음입니다.
bpftrace — 한 줄로 던지는 질문 #
bpftrace는 eBPF 프로그램을 한 줄짜리 스크립트로 쓰게 해 주는 도구입니다. “이벤트 /조건/ { 동작 }” 형태입니다.
# 지금 이 서버에서 어떤 프로세스가 어떤 파일을 여는지 실시간 출력
$ sudo bpftrace -e 'tracepoint:syscalls:sys_enter_openat { printf("%s %s\n", comm, str(args->filename)); }'
nginx /var/log/nginx/access.log
postgres /var/lib/pgsql/data/base/16384/2619tracepoint:syscalls:sys_enter_openat이 걸어 둘 이벤트이고, 중괄호 안이 이벤트마다 실행할 동작입니다. 같은 문법으로 히스토그램도 한 줄입니다.
# read()가 돌려준 바이트 수의 분포를 커널 안에서 집계
$ sudo bpftrace -e 'tracepoint:syscalls:sys_exit_read /args->ret > 0/ { @bytes = hist(args->ret); }'Ctrl-C로 끝내면 그동안 누적된 히스토그램이 출력됩니다. 이 “끝날 때 요약만 받는” 모양이 eBPF 관측의 기본형입니다.
biolatency — 블록 I/O 지연의 분포 읽기 #
BCC의 biolatency는 블록 디바이스 I/O 한 건 한 건의 지연을 재서 히스토그램으로 보여 줍니다. 10초 동안 한 번 집계한 예입니다.
$ sudo biolatency 10 1
Tracing block device I/O... Hit Ctrl-C to end.
usecs : count distribution
64 -> 127 : 9 |* |
128 -> 255 : 156 |********************** |
256 -> 511 : 274 |****************************************|
512 -> 1023 : 92 |************* |
1024 -> 2047 : 18 |** |
32768 -> 65535 : 5 | |
65536 -> 131071 : 4 | |구간은 2배씩 커지는 로그 스케일입니다. 읽는 순서는 두 가지입니다.
- 본체가 어디 있는가 — 대부분의 I/O가 128〜511us에 모여 있습니다. SSD라면 정상 범위입니다.
- 꼬리가 있는가 — 65〜131ms 구간에 4건이 있습니다. 본체보다 수백 배 느린 I/O입니다.
iostat의 평균 지연(await)에서는 본체에 묻혀 보이지 않던 것이, 히스토그램에서는 따로 떨어진 봉우리로 드러납니다. 이 꼬리의 원인 후보가 SSD 내부의 가비지 컬렉션, 쓰기 캐시 플러시, 다른 작업과의 큐 경합 같은 것들이고, 그때부터가 진짜 분석의 시작입니다.
평균 한 숫자였다면 “디스크는 괜찮다"로 닫았을 사건이, 분포에서는 “990건은 괜찮고 4건이 이상하다"는 추적 가능한 단서로 바뀝니다.
runqlat — CPU를 받기까지 기다린 시간 #
CPU 사용률은 50%인데 애플리케이션이 느린 서버가 있습니다. 이때 봐야 하는 것이 스케줄러 대기입니다. 실행할 준비가 끝난 작업이 실행 큐(run queue)에서 CPU를 배정받기까지 기다린 시간으로, BCC의 runqlat이 이 분포를 보여 줍니다.
usecs : count distribution
0 -> 1 : 1306 |********************** |
2 -> 3 : 2342 |****************************************|
4 -> 7 : 1217 |********************* |
8192 -> 16383 : 41 | |
16384 -> 32767 : 23 | |대부분은 몇 us 안에 CPU를 받지만, 꼬리에서 8〜32ms를 기다린 작업이 60건 넘게 보입니다. 사용률이 낮은데도 이런 꼬리가 생기는 전형적인 원인은 컨테이너의 CPU 제한(cgroup 쿼터를 소진하면 다음 주기까지 강제 대기), 특정 코어로의 작업 쏠림, 짧은 버스트의 동시 도착입니다. 중급 1편의 로드 애버리지가 “줄이 있다"까지 알려 줬다면, runqlat은 그 줄이 각 작업을 실제로 몇 ms 세워 뒀는지를 알려 줍니다.
BCC 도구 지도 #
BCC에는 이런 식의 단일 목적 도구가 수십 개 있습니다. 자주 쓰는 것만 추리면 다음과 같습니다.
| 도구 | 보여 주는 것 |
|---|---|
execsnoop | 새로 실행되는 프로세스 전부. 짧게 살다 죽는 프로세스 폭주를 잡습니다 |
opensnoop | 파일 열기 전부. 설정 파일을 어디서 읽는지 추적할 때도 유용합니다 |
biolatency / biosnoop | 블록 I/O 지연의 히스토그램 / 건별 상세 |
runqlat | 스케줄러 대기 시간 분포 |
ext4slower | 임계값보다 느린 파일시스템 연산만 골라 출력(XFS 등 변형도 있음) |
cachestat | 페이지 캐시 적중률 |
tcplife | TCP 연결의 수명과 주고받은 양. 연결 단위 요약입니다 |
tcpretrans | TCP 재전송 발생 순간과 대상 |
도구 이름의 규칙이 보입니다. -snoop은 이벤트를 건별로 흘려 보여 주고, -lat은 지연 히스토그램을 집계하고, -slower는 느린 것만 거릅니다. 새 도구를 만나도 이름에서 동작을 짐작할 수 있습니다.
오버헤드와 운영 주의 #
eBPF는 가볍지만 공짜는 아닙니다. 비용은 이벤트 빈도에 비례합니다.
- 이벤트 빈도를 먼저 생각합니다.
execsnoop이 거는 프로세스 실행은 초당 수십 건 수준이라 부담이 없지만,runqlat이 거는 스케줄러 전환은 초당 수십만 건일 수 있습니다. 바쁜 서버에서 몇 % 수준의 CPU를 더 쓸 수 있다는 뜻이므로, 부하가 심각한 순간에는 측정 자체가 부하가 된다는 점을 기억해 둡니다. - 집계형 도구가 건별 출력보다 쌉니다. 히스토그램은 커널 안에 머물고 요약만 넘어옵니다. 같은 이벤트라도
biosnoop(건별)보다biolatency(집계)가 부담이 작습니다. - 진단 도구와 상시 모니터링을 구분합니다. 이 글의 도구들은 문제가 보일 때 켜고 답을 얻으면 끄는 용도입니다. 상시 수집이 필요하면 eBPF 기반 모니터링 에이전트를 따로 검토하는 쪽이 맞습니다.
- 커널 버전과 권한의 전제가 있습니다. 비교적 최신 커널(RHEL 8, Ubuntu 18.04 이후 수준)과 root 권한이 필요합니다. 검증기 덕에 커널이 죽지는 않지만, 오래된 커널에서는 도구가 아예 올라가지 않을 수 있습니다.
정리 #
이번 글에서 잡은 그림입니다.
- %wa와 로드 애버리지 같은 평균 지표는 분포를 뭉갭니다. 체감 품질을 깎는 꼬리 지연은 히스토그램으로만 보입니다.
- eBPF는 커널 이벤트에 걸어 두는 작은 프로그램이고, 검증기가 안전을 보장하며, 커널 안 집계 덕에 비용이 작습니다.
biolatency로 블록 I/O의 본체와 꼬리를 가르고,runqlat으로 사용률이 낮은데도 느린 서버의 스케줄러 대기를 확인합니다.- 즉석 질문은
bpftrace원라이너로, 정형 분석은 BCC 도구로 실행하되, 이벤트 빈도와 집계 여부로 오버헤드를 가늠합니다.
다음 — 메모리 심화 #
다음 글인 “하드웨어 고급 #3 메모리 심화"에서는 두 번째 자원으로 들어가겠습니다. 가상 메모리와 페이지 테이블, TLB 미스의 비용, 페이지 회수와 캐시의 줄다리기까지, 사용량 그래프 뒤에서 메모리가 실제로 어떻게 움직이는지를 다루겠습니다.