최신 커널은 블록 장치마다 기본으로 mq-deadline을 쓰지만, /sys/block/<dev>/queue/scheduler를 열어보면 bfq, kyber 같은 대안이 함께 나열돼 있다. 이름만 봐서는 언제 바꿔야 하는지 감이 안 오는데, 셋은 애초에 최적화하는 목표 자체가 다르다. 이 글에서는 각 스케줄러의 설계 목표를 정리하고, 실제로 전환해가며 fio 혼합 워크로드로 IOPS·지연시간 차이를 확인한다.
스케줄러별 설계 목표
| 스케줄러 | 목표 | 적합한 상황 |
|---|---|---|
none | 재정렬 없이 그대로 디스패치 | NVMe처럼 디바이스 자체가 큐잉/재정렬을 잘 하는 경우 |
mq-deadline | 각 요청에 만료 시각(deadline)을 부여해 순서를 보장하면서 굶주림 방지 | 범용 기본값. 예측 가능한 지연이 필요한 서버 워크로드 |
bfq | 프로세스별로 공정하게 대역폭 분배(CFQ의 후속) | 데스크톱, 여러 프로세스가 동시에 I/O를 두고 경쟁하는 환경 |
kyber | 읽기/쓰기 목표 지연시간을 두고 동적으로 큐 깊이 조절 | 빠른 SSD/NVMe에서 낮은 오버헤드로 지연시간 튜닝 |
스케줄러 전환
$ cat /sys/block/sda/queue/scheduler
none [mq-deadline] bfq kyber
$ echo bfq | sudo tee /sys/block/sda/queue/scheduler
$ cat /sys/block/sda/queue/scheduler
none mq-deadline [bfq] kyber
대괄호로 감싼 항목이 현재 활성 스케줄러다. 재부팅 없이 런타임에 바로 전환되며, 디바이스가 여러 개면 각 디바이스마다 별도로 설정해야 한다.
fio 혼합 워크로드 비교
6코어 VM(가상 디스크, VirtualBox)에서 랜덤 read/write 70:30 혼합, 4KB 블록, iodepth 8로 각 스케줄러당 6초씩 fio를 돌렸다.
$ fio --name=test --size=256M --rw=randrw --rwmixread=70 \
--bs=4k --iodepth=8 --ioengine=libaio --direct=1 \
--numjobs=1 --runtime=6 --time_based --group_reporting
| 스케줄러 | read IOPS | write IOPS | read 평균 지연 |
|---|---|---|---|
| mq-deadline | 113 | 51 | 69.2ms |
| bfq | 100 | 46 | 66.4ms |
| kyber | 97 | 44 | 76.0ms |
| none | 96 | 44 | 62.2ms |
혼합 워크로드에서는 이렇게 스케줄러 간 차이가 수 % 수준으로 묻힌다. 같은 조건에서 1MB 블록 순차 쓰기(--rw=write --bs=1M)로 바꿔 다시 돌리자 결과가 크게 갈렸다.
$ fio --name=seqtest --size=256M --rw=write --bs=1M --iodepth=4 \
--ioengine=libaio --direct=1 --numjobs=1 --runtime=6 --time_based
mq-deadline: 55.5 MiB/s
bfq: 92.4 MiB/s
kyber: 84.2 MiB/s
none: 94.3 MiB/s
순차 쓰기에서는 mq-deadline이 가장 느리고(55.5MiB/s) none이 가장 빠르다(94.3MiB/s) — 거의 70% 차이다. 이 VM처럼 가상 디스크라도 순차 쓰기처럼 재정렬 오버헤드가 상대적으로 부담되는 패턴에서는 스케줄러 선택이 체감될 만큼 영향을 준다는 뜻이다. 워크로드 종류에 따라 결론이 달라지므로, 실제 서비스 트래픽과 비슷한 패턴으로 직접 비교해보는 게 중요하다.
여러 프로세스가 동시에 경쟁할 때: bfq의 공정성 확인
앞의 두 벤치마크는 fio 프로세스 하나가 디스크를 독점하는 상황이었다. bfq가 내세우는 “프로세스별 공정한 대역폭 분배”를 검증하려면 애초에 여러 프로세스가 동시에 경쟁해야 한다. 대용량 순차 쓰기를 돌리는 백그라운드 프로세스와, 지연시간에 민감한 4KB 랜덤 읽기를 돌리는 foreground 프로세스를 하나의 fio 잡파일로 동시에 실행해 스케줄러별로 foreground 읽기 지연이 얼마나 보호되는지 비교했다.
[global]
ioengine=libaio
direct=1
runtime=6
time_based
[bg-writer]
filename=/tmp/sched_test/bgwrite.dat
rw=write
bs=1M
iodepth=16
size=2G
[fg-reader]
filename=/tmp/sched_test/readtarget.dat
rw=randread
bs=4k
iodepth=1스케줄러마다 캐시를 비우고(drop_caches) 3회씩 반복 실행해 평균을 냈다.
| 스케줄러 | fg-reader 평균 지연(3회 평균) | bg-writer 처리량(3회 평균) |
|---|---|---|
| mq-deadline | 234.6ms | 64.1 MiB/s |
| bfq | 59.8ms | 51.7 MiB/s |
| kyber | 227.0ms | 66.0 MiB/s |
bfq에서만 foreground 읽기 지연이 mq-deadline·kyber 대비 4분의 1 수준(59.8ms)으로 뚝 떨어진다. 대신 배경 쓰기 처리량은 20% 정도 낮아진다(51.7 vs 64~66 MiB/s) — 대용량 쓰기 프로세스가 대역폭을 독점하지 못하도록 bfq가 실제로 읽기 프로세스에 지분을 나눠준 결과다. 반면 mq-deadline과 kyber는 둘 다 만료 시각이나 목표 지연시간으로 개별 요청을 관리할 뿐 프로세스 단위 공정성은 고려하지 않아, 순차 쓰기가 큐를 채우는 동안 랜덤 읽기가 그대로 밀린다.
주의사항
| 항목 | 내용 |
|---|---|
| 워크로드 패턴에 따라 결론이 반대로 갈린다 | 이 VM에서도 랜덤 혼합은 차이가 묻혔지만 순차 쓰기는 70% 가까이 갈렸다. 한 워크로드로 테스트한 결과를 다른 패턴에 그대로 적용하지 말 것 |
| 재부팅 시 초기화 | 런타임 전환이라 재부팅하면 커널 기본값(대개 mq-deadline)으로 돌아간다. 영구 적용은 udev 규칙이나 커널 부팅 파라미터(elevator=)로 |
| 블록 디바이스별 개별 설정 | 디스크가 여러 개면 /sys/block/<dev>/queue/scheduler를 디바이스마다 각각 설정해야 한다 |
| bfq는 CPU 오버헤드가 더 크다 | 공정성 계산 로직이 복잡해 매우 빠른 NVMe에서는 스케줄러 자체가 병목이 될 수 있다. 이런 경우 none이나 kyber가 더 적합 |
| 단발성 측정은 결과가 크게 흔들린다 | 동시 경쟁 워크로드는 캐시 상태·직전 실행 잔여 I/O에 따라 같은 스케줄러에서도 결과가 수 배씩 차이 났다. 한 번 실행한 숫자로 단정하지 말고 캐시를 비운 뒤 여러 번 반복해 평균을 볼 것 |
마무리
스케줄러 선택은 절대적인 승자가 있는 게 아니라 워크로드 특성(공정성 vs 지연시간 vs 오버헤드 최소화)과 디바이스 종류(HDD/SATA SSD/NVMe)에 달려 있다. 단일 프로세스 벤치마크에서는 그 차이가 워크로드에 따라 묻히기도, 크게 벌어지기도 했지만, 여러 프로세스가 동시에 경쟁하는 상황에서는 bfq의 공정성 설계가 foreground 지연시간을 실측으로도 확인될 만큼 눈에 띄게 지켜줬다. 물리 서버에서 I/O 병목이 의심된다면 기본값을 바꾸기 전에 이 글의 fio 명령으로 실제 워크로드와 비슷한 조건에서 먼저 비교해볼 것을 권한다.