디스크 스케줄링과 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가 자원을 보호하고 시스템의 보안을 유지하는 방법을 다루겠습니다.