대학원 수업을 듣던 중 흥미로웠던 과제가 있어서 슬쩍 짚고 넘어가보려고 합니다.
extended Berkely Packet Filter (eBPF) 라는 다이나믹 캡처 툴을 활용해서 fio 응용의 계층별 context를 단일 흐름으로 파악하고 cache hit / miss 에 대한 관찰 및 병목 지점에 대한 정량적 측정을 해보았습니다.
결과적으로 아래의 영상과 같이 풍성한 정보를 캡처 할 수 있었고 보다 더 시스템 프로그래밍에 친숙해줄 수 있는 기회가 된 것 같습니다.
목표는 응용(fio)의
read()가 Page Cache miss일 때 지연시간과 NVMe request 크기를 계층별(SYSCALL→VFS→FS→BLOCK→DEVICE)로 측정하고, I/O Flow를 한 눈에 보기 쉽게 표현하는 것입니다.
uftrace로 fio가 밟는 커널 경로를 먼저 훑어 진입 시드 함수를 골랐습니다.
__x64_sys_read에 바인딩되어 read path를 타는 것을 확인했고ftrace/eBPF 훅의 시발점으로 채택했습니다.ftrace로 계층별 타임라인을 얻었습니다.
vfs_read(_ret)ext4_file_read_iter(_ret) / generic_file_read_iter(_ret)block_bio_queue, block_rq_insert/issue/completenvme_sq, nvme_complete_rqMiss 시 FS→BLOCK→DEVICE까지 내려가므로, 위 이벤트 조합으로 Cache Hit/Miss, Queue/Service/Device Latency까지 복원 가능하다고 판단했습니다.
Syscall
└─ sys_enter_read / sys_exit_read
│
▼
VFS
└─ vfs_read / vfs_read_ret
│
▼
File System (ext4)
└─ ext4_file_read_iter / _ret
│
▼
Block layer (blk-mq)
└─ block_bio_queue → block_rq_insert → block_rq_issue → block_rq_complete
│
▼
Device (NVMe)
└─ nvme_sq → nvme_complete_rq
인덱스 체인: thread → syscall_idx → vfs_idx → fs_idx
각 레벨 enter/ret에 타임스탬프(ns) 기록하여 해당 호출의 Flow_*_Lat 계산했습니다.
누적 바이트를 계산할때는 인접한 계층의 callback을 활용해서 Adjacent-only 방식의 전파를 차용했습니다.
BLOCK → FS → VFS → SYSCALL 방향으로만 상향 누적VFS_ret에서 FS 누적이 0이고 사용자 ret>0이면 Page Cache HIT 보정으로 VFS bytes에 더했습니다.(dev, sector, nr_sectors)를 키로 blkkey2flow에 상위 flow를 바인딩함.request* 포인터를 키로 req2flow/req2submit_ns 매핑 → nvme_complete_rq에서 Device Latency 계산했습니다.vfs_idx, fs_idx를 각각 증가시켜 동일 함수 다회 호출도 분리 추적했습니다sys_enter_*에서 comm=="fio"인 TGID 화이트리스트allowed_tgid 확인 + 후처리에서 Flow_SYSCALL>0 조건으로 flow source 보존했습니다.Flow 인덱스: key_thread2syscall_idx, key_syscall2vfs_idx, key_vfs2fs_idx
타임스탬프: key_syscall2enter_ns, key_vfs2enter_ns, key_fs2enter_ns
누적 바이트(상향 전파): key_fs2bytes → key_vfs2bytes(+HIT 보정) → key_syscall2bytes
상태: key_*2cache_state(UNK/HIT/MISS/DIRECT), key_*2isdirect
Block/NVMe 키:
blkkey2flow, blkkey2insert_ns, blkkey2issue_nsreq2flow, req2submit_nsTime, Hier, Stage, TGID/TID/Comm/CPUFlow_SYSCALL, Flow_VFS, Flow_FSBytes, LBA/NR, SectorSz, ReqPtr, DevDiskFlow_SYSCALL_Lat, Flow_VFS_Lat, Flow_FS_Lat, Queue_Lat, Service_Lat, Device_LatCache_State(HIT/MISS/DIRECT/UNK), IsDirect--direct=0 --ioengine=sync --iodepth=1 --rw=randread
ASCII 타임라인:
read()
│
├─ sys_enter_read (0 μs)
├─ vfs_read (~4.8 μs)
├─ ext4_file_read_iter (~10.4 μs)
│ ├─ block_rq_insert (~42 μs)
│ ├─ nvme_queue_rq / block_rq_issue (~60 μs)
│ ├─ nvme_complete_rq (~438 μs) ← Device
│ └─ block_rq_complete (~445 μs) ← Q≈18 μs, S≈415 μs
├─ ext4_file_read_iter_ret (~465 μs)
├─ vfs_read_ret (~471 μs)
└─ sys_exit_read (~506 μs)
관찰 포인트
F_SY가 F_FS보다 조금 더 길어지는 것입니다.블록 크기 변화에 따른 병목 이동
Cache 판정은 sys_exit 기준으로 보는 게 덜 왜곡됩니다.
Block 이벤트는 한 번의 read()에 여러 번 찍히므로 exit 시점의 사용자 반환 단위가 더 현실적인 척도입니다.
UFTRACE_DIR=./uftrace_run
UFTRACE_DATA="$UFTRACE_DIR/uf.data"
RESULT_FILE=result_uftrace_nodirect.txt
rm -rf "$UFTRACE_DIR" "$RESULT_FILE"; mkdir -p "$UFTRACE_DIR"
uftrace record -k -d "$UFTRACE_DATA" -P 'fio*' -- \
fio --filename=/nvmeDir/testfile --rw=randread --size=256k \
--bs=4k --direct=0 --ioengine=sync --thinktime=10 \
--iodepth=1 --name=test --numjobs=1
uftrace graph -k -d "$UFTRACE_DATA" > "$RESULT_FILE"
TR=/sys/kernel/tracing
echo 0 > $TR/tracing_on
echo nop > $TR/current_tracer
: > $TR/trace; : > $TR/set_ftrace_pid; : > $TR/set_graph_function
: > $TR/set_ftrace_notrace
echo '__x64_sys_read' > $TR/set_graph_function
echo function_graph > $TR/current_tracer
# block/nvme tracepoints
for e in block_bio_queue block_rq_insert block_rq_issue block_rq_complete; do
echo 1 > $TR/events/block/$e/enable
done
for e in nvme_sq nvme_complete_rq nvme_async_event; do
echo 1 > $TR/events/nvme/$e/enable
done
fio --filename="/nvmeDir/testfile" --rw=randread --size=256k --bs=4k \
--direct=0 --ioengine=sync --thinktime=10 --iodepth=1 \
--name=test --numjobs=1 &
echo $! > $TR/set_ftrace_pid
echo 1 > $TR/tracing_on
wait $!
echo 0 > $TR/tracing_on
cat $TR/trace > ~/HW/hw2/result_ftrace_nodirect.txt
echo "trace saved."
# reset
for e in block_bio_queue block_rq_insert block_rq_issue block_rq_complete; do
echo 0 > $TR/events/block/$e/enable
done
for e in nvme_sq nvme_complete_rq nvme_async_event; do
echo 0 > $TR/events/nvme/$e/enable
done
echo nop > $TR/current_tracer
: > $TR/set_ftrace_pid; : > $TR/set_graph_function
# 4 KiB
fio --filename="/nvmeDir/testfile" --rw=randread --size=1024m --bs=4k \
--direct=0 --ioengine=sync --thinktime=10 --iodepth=1 \
--name=test --numjobs=1
# 128 MiB
fio --filename="/nvmeDir/testfile" --rw=randread --size=1024m --bs=128m \
--direct=0 --ioengine=sync --thinktime=10 --iodepth=1 \
--name=test --numjobs=1