디스크 스케줄링과 I/O 최적화
하드 디스크는 기계 장치입니다. 헤드가 물리적으로 이동해야 데이터를 읽을 수 있습니다. 여러 프로세스가 동시에 디스크 I/O를 요청하면, 요청을 어떤 순서로 처리하느냐에 따라 성능이 크게 달라집니다.
디스크의 물리적 구조
하드 디스크(HDD)는 회전하는 플래터(Platter) 위에 헤드(Head)가 데이터를 읽고 씁니다.
- 트랙(Track): 플래터 위의 동심원. 바깥쪽 트랙이 0번입니다.
- 섹터(Sector): 트랙을 나눈 최소 단위 (전통 512바이트, 현대 4KB/Advanced Format)
- 실린더(Cylinder): 같은 반지름의 트랙들을 수직으로 모은 것. 헤드 이동 없이 접근 가능합니다.
디스크 접근 시간은 세 요소로 구성됩니다.
| 구성 요소 | 의미 | 일반적인 시간 |
|---|---|---|
| 탐색 시간(Seek Time) | 헤드를 원하는 트랙으로 이동 | 3~15ms |
| 회전 지연(Rotational Latency) | 원하는 섹터가 헤드 아래로 회전 | 2~6ms (7200RPM 약 4.2ms) |
| 전송 시간(Transfer Time) | 데이터를 실제로 읽기 | 0.01~0.1ms |
탐색 시간이 가장 크므로, 디스크 스케줄링의 목표는 헤드 이동 거리를 최소화하는 것입니다.
디스크 스케줄링 알고리즘
요청 큐: 트랙 [98, 183, 37, 122, 14, 124, 65, 67], 현재 헤드 위치: 53.
FCFS (First-Come, First-Served)
요청이 도착한 순서대로 처리합니다. 53→98→183→37→122→14→124→65→67.
헤드 이동 거리: 45+85+146+85+108+110+59+2 = 640 트랙. 헤드가 디스크 전체를 왔다 갔다 합니다.
SSTF (Shortest Seek Time First)
현재 헤드 위치에서 가장 가까운 요청을 먼저 처리합니다. 53→65→67→37→14→98→122→124→183.
헤드 이동: 12+2+30+23+84+24+2+59 = 236 트랙. FCFS보다 크게 개선됩니다.
단점: 멀리 있는 요청이 계속 뒤로 밀려 기아(Starvation)가 발생할 수 있습니다. SJF 스케줄링과 같은 문제입니다.
SCAN (엘리베이터 알고리즘)
헤드가 한쪽 끝에서 다른 쪽 끝으로 이동하면서 경로 상의 모든 요청을 처리하고, 끝에 도달하면 방향을 바꿉니다.
53으로 시작, 왼쪽으로 진행: 53→37→14→0(끝)→65→67→98→122→124→183.
기아가 발생하지 않습니다. 하지만 끝에서 방향을 바꾸면 바로 지나온 영역은 요청이 비어 있으므로 비효율적일 수 있습니다.
C-SCAN (Circular SCAN)
한 방향으로만 요청을 처리하고, 끝에 도달하면 반대쪽 끝으로 즉시 점프합니다. 모든 트랙에 균등한 대기 시간을 제공합니다.
53 오른쪽으로: 53→65→67→98→122→124→183→(끝)→0(점프)→14→37.
LOOK / C-LOOK
SCAN/C-SCAN의 실용적 변형입니다. 디스크의 물리적 끝까지 가지 않고, 마지막 요청이 있는 위치에서 방향을 전환합니다. 불필요한 이동을 줄입니다.
def sstf(requests, head):
"""SSTF 스케줄링 시뮬레이션"""
pending = list(requests)
order = []
total_movement = 0
current = head
while pending:
# 가장 가까운 요청 찾기
closest = min(pending, key=lambda x: abs(x - current))
total_movement += abs(closest - current)
current = closest
order.append(closest)
pending.remove(closest)
return order, total_movement
def c_look(requests, head):
"""C-LOOK 스케줄링 시뮬레이션"""
left = sorted([r for r in requests if r < head])
right = sorted([r for r in requests if r >= head])
order = right + left # 오른쪽 끝까지 → 왼쪽 처음으로 점프
total = 0
current = head
for r in order:
total += abs(r - current)
current = r
return order, total
requests = [98, 183, 37, 122, 14, 124, 65, 67]
head = 53
for name, func in [("SSTF", sstf), ("C-LOOK", c_look)]:
order, movement = func(requests, head)
print(f"{name}: 이동={movement}, 순서={order}")알고리즘 비교
| 알고리즘 | 이동 거리 | 기아 | 공평성 | 실무 사용 |
|---|---|---|---|---|
| FCFS | 최대 | 없음 | 공평 | 거의 안 씀 |
| SSTF | 최소에 가까움 | 있음 | 불공평 | 드물게 |
| SCAN | 중간 | 없음 | 양 끝 불리 | 기반 알고리즘 |
| C-SCAN | 중간 | 없음 | 균등 | 변형 사용 |
| C-LOOK | 좋음 | 없음 | 균등 | 널리 사용 |
SSD의 등장
SSD(Solid State Drive)는 반도체(NAND 플래시) 기반의 저장 장치로, 기계적 움직임이 없습니다. 탐색 시간과 회전 지연이 0입니다.
| 특성 | HDD | SATA SSD | NVMe SSD |
|---|---|---|---|
| 순차 읽기 | ~200 MB/s | ~550 MB/s | ~3,500 MB/s |
| 순차 쓰기 | ~200 MB/s | ~520 MB/s | ~3,000 MB/s |
| 랜덤 읽기 (IOPS) | ~100 | ~90,000 | ~500,000 |
| 랜덤 쓰기 (IOPS) | ~100 | ~80,000 | ~400,000 |
| 지연 시간 | 3~15ms | ~0.1ms | ~0.02ms |
SSD에서는 전통적인 디스크 스케줄링의 의미가 크게 줄어듭니다. 어떤 위치를 읽든 접근 시간이 거의 동일하기 때문입니다.
NVMe SSD는 PCIe 버스에 직접 연결되어 SATA 인터페이스의 병목을 제거합니다. 큐 깊이도 SATA의 32에서 NVMe의 65,535로 늘어나 다중 I/O 병렬 처리가 가능합니다.
Linux I/O 스케줄러
Linux에서 I/O 요청은 커널의 I/O 스케줄러를 거칩니다.
noop (none): 스케줄링 없이 순서대로 처리합니다. SSD에 적합합니다. 스케줄링 오버헤드가 필요 없기 때문입니다.
deadline (mq-deadline): 각 요청에 만료 시간(읽기 500ms, 쓰기 5초)을 부여합니다. 기아를 방지하면서도 탐색 시간을 최적화합니다. 범용적으로 우수합니다.
CFQ (Completely Fair Queuing): 프로세스별로 공평하게 I/O 대역폭을 분배합니다. 데스크탑 환경에서 반응성을 유지합니다. 커널 5.0에서 제거되었습니다.
BFQ (Budget Fair Queueing): CFQ의 후계자. 프로세스별 I/O 예산을 관리합니다. 인터랙티브 작업(텍스트 편집기)이 백그라운드 작업(대용량 복사)에 의해 느려지지 않게 합니다.
kyber: 지연 시간 기반 스케줄러. 읽기/쓰기의 목표 지연 시간을 설정하고, 큐 깊이를 자동 조절합니다. 고성능 NVMe에 적합합니다.
# 현재 스케줄러 확인
cat /sys/block/sda/queue/scheduler
# [mq-deadline] kyber bfq none
# NVMe SSD에 none 설정
echo "none" > /sys/block/nvme0n1/queue/scheduler
# HDD에 mq-deadline 설정
echo "mq-deadline" > /sys/block/sda/queue/scheduler
# I/O 성능 모니터링
iostat -xz 1 5
# %util: 장치 사용률, await: 평균 응답 시간
# await이 높으면 I/O 병목I/O 성능 최적화 실무
Zero-Copy
전통적인 파일 전송(파일 → 네트워크)은 4번의 데이터 복사가 발생합니다. 디스크→커널 버퍼→사용자 버퍼→커널 소켓 버퍼→NIC. Zero-Copy(sendfile() 시스템 콜)는 사용자 공간 복사를 제거하여 2번으로 줄입니다. Nginx, Kafka 등 고성능 서버가 사용합니다.
I/O 배치와 병합
I/O 스케줄러는 인접한 블록에 대한 개별 요청을 병합(Merge)합니다. 4KB 읽기 4번을 16KB 읽기 1번으로 합칩니다. 애플리케이션 레벨에서도 작은 쓰기를 모아서 큰 쓰기로 배치하면 성능이 크게 향상됩니다.
ionice
프로세스의 I/O 우선순위를 설정합니다. 백업 작업은 낮은 우선순위로, 데이터베이스는 높은 우선순위로 설정하면 서로 간섭이 줄어듭니다.
# Idle 클래스로 백업 실행 (다른 I/O가 없을 때만 실행)
ionice -c 3 rsync -a /data /backup
# Best-effort 클래스의 높은 우선순위로 DB 관련 I/O
ionice -c 2 -n 0 mysqld다음 장에서는 OS가 자원을 보호하고 시스템의 보안을 유지하는 방법을 다루겠습니다.
아래 다이어그램은 디스크 요청이 섹터 위치와 헤드 이동 비용에 따라 처리되는 흐름을 핵심 질문과 판단 순서로 정리한 것입니다.
디스크 요청 큐, 스케줄러 선택, SSD 예외, I/O 튜닝 지표를 함께 점검합니다.