안동민 개발노트

본문 시작

디스크 스케줄링과 I/O 최적화

FCFS·SSTF·SCAN 계열의 헤드 이동을 비교하고 SSD와 Linux I/O 스케줄러에서 달라지는 최적화 기준을 이해합니다.

하드 디스크는 기계 장치입니다.

헤드가 물리적으로 이동해야 데이터를 읽을 수 있습니다.

여러 프로세스가 동시에 디스크 I/O를 요청하면, 요청을 어떤 순서로 처리하느냐에 따라 성능이 크게 달라집니다.


디스크의 물리적 구조

하드 디스크(HDD)는 회전하는 플래터(Platter) 위에 헤드(Head)가 데이터를 읽고 씁니다.

  • 트랙(Track): 플래터 위의 동심원. 단순한 CHS 도식에서는 바깥쪽을 0번으로 둡니다. 현대 장치의 LBA 주소와 실제 물리 배치는 컨트롤러가 추상화합니다.
  • 섹터(Sector): 트랙을 나눈 최소 단위 (전통 512바이트, 현대 4KB/Advanced Format)
  • 실린더(Cylinder): 같은 반지름의 트랙들을 수직으로 모은 것. 헤드 이동 없이 접근 가능합니다.

디스크 접근 시간은 세 요소로 구성됩니다.

구성 요소의미일반적인 시간
탐색 시간(Seek Time)헤드를 원하는 트랙으로 이동3~15ms
회전 지연(Rotational Latency)원하는 섹터가 헤드 아래로 회전2~6ms (7200RPM 약 4.2ms)
전송 시간(Transfer Time)데이터를 실제로 읽기0.01~0.1ms

표의 시간은 대략적인 규모 예시이며 요청 크기·장치에 따라 달라집니다. HDD 스케줄링은 헤드 이동 비용뿐 아니라 대기 시간과 공정성도 고려합니다.


디스크 스케줄링 알고리즘

요청 큐: 트랙 [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.

요청 유입과 서비스 시간을 제한한 일반적인 SCAN 모형에서는 한 방향의 스캔으로 요청을 계속 방문하여 SSTF의 기아 위험을 줄입니다.

하지만 끝에서 방향을 바꾸면 바로 지나온 영역은 요청이 비어 있으므로 비효율적일 수 있습니다.

C-SCAN (Circular SCAN)

한 방향으로만 요청을 처리하고, 끝에 도달하면 반대쪽 끝으로 돌아오는 동안 요청을 처리하지 않습니다. 물리적인 복귀 이동에도 시간과 거리가 듭니다.

반복 스캔에서 위치에 따른 대기 편차를 줄이려는 방식이며, 모든 요청의 대기 시간이 정확히 같다는 보장은 아닙니다.

53 오른쪽으로: 53→65→67→98→122→124→183→(끝)→0(점프)→14→37.

LOOK / C-LOOK

SCAN/C-SCAN의 실용적 변형입니다.

LOOK은 진행 방향의 마지막 요청에서 반전하고, C-LOOK은 반대편 첫 요청으로 복귀한 뒤 같은 방향으로 서비스합니다. 본문 시뮬레이션은 복귀 거리도 합산합니다.

불필요한 이동을 줄입니다.

disk_scheduling.py
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이 요청열은 640 트랙. 도착 순서를 지키지만 항상 최장 이동은 아님
SSTF이 요청열은 236 트랙. 매 순간 가까운 요청을 고르며 전체 최적해를 보장하지 않음
SCAN시작 방향·끝 트랙·종료 기준을 정해야 거리를 비교할 수 있음
C-SCAN복귀 중에는 서비스하지 않음. 복귀 이동 비용도 포함
LOOK / C-LOOK물리 끝 대신 남은 요청의 끝을 방문. 새 요청 유입 정책도 공정성에 영향

코드의 출력은 실행 기록으로 제시하지 않습니다. 현재 요청열과 거리 합산 규칙을 고정한 시뮬레이션입니다.


SSD의 등장

SSD(Solid State Drive)는 반도체(NAND 플래시) 기반의 저장 장치로, 기계적 움직임이 없습니다.

탐색 시간과 회전 지연이 0입니다.

다음 수치는 특정 장치의 보장 성능이나 최신 제품 비교가 아니라 규모를 설명하는 예시입니다.

특성HDDSATA SSDNVMe 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 인터페이스의 병목을 제거합니다.

NVMe는 여러 제출·완료 큐를 지원합니다. 큐 개수와 큐당 엔트리 수, 실제 장치 한도를 구분해야 하며 숫자 하나를 SATA와 NVMe의 공통 큐 깊이처럼 비교하지 않습니다.


Linux I/O 스케줄러

Linux에서 I/O 요청은 커널의 I/O 스케줄러를 거칩니다.

none: blk-mq에서 추가 I/O 스케줄러 없이 하드웨어 큐로 요청을 전달하는 선택입니다. 구형 noop과 같은 이름의 구현이 아니며 전체 요청 완료 순서가 FIFO라는 보장도 아닙니다. 장치 특성과 부하에 따라 낮은 오버헤드가 도움이 됩니다.

deadline (mq-deadline): 각 요청에 만료 시간(읽기 500ms, 쓰기 5초)을 부여합니다.

오래 기다린 요청을 우선하는 정책과 정렬·병합을 함께 사용합니다. 만료 설정은 장치의 완료 시간 상한을 보장하는 deadline이 아닙니다.

실제 사용 가능 스케줄러와 설정은 blk-mq 및 해당 장치의 sysfs에서 확인합니다.

CFQ (Completely Fair Queuing): 프로세스별로 공평하게 I/O 대역폭을 분배합니다.

데스크탑 환경에서 반응성을 유지합니다.

커널 5.0에서 제거되었습니다.

BFQ (Budget Fair Queueing): CFQ의 후계자.

프로세스별 I/O 예산을 관리합니다.

대화형 작업의 지연과 프로세스 간 공정성을 개선하려는 정책이며 모든 부하에서 지연을 없애는 보장은 아닙니다.

kyber: 지연 시간 기반 스케줄러.

읽기/쓰기의 목표 지연 시간을 설정하고, 큐 깊이를 자동 조절합니다.

목표 지연과 처리량을 실제 부하에서 비교해 선택합니다.

io_scheduler_tuning.sh
# 현재 스케줄러 확인
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 성능 최적화 실무

Zero-Copy

단순한 파일 읽기 후 소켓 쓰기 모형은 다음 이동 경로로 설명할 수 있습니다. 실제 복사 횟수는 캐시·장치 지원에 따라 다릅니다.

디스크→커널 버퍼→사용자 버퍼→커널 소켓 버퍼→NIC.

sendfile은 파일 데이터를 사용자 공간으로 가져왔다가 다시 넘기는 복사를 피할 수 있습니다. DMA를 포함한 모든 이동이 사라지거나 항상 정확히 두 번만 남는다는 뜻은 아닙니다.

Nginx, Kafka 등 고성능 서버가 사용합니다.

I/O 배치와 병합

I/O 스케줄러는 인접한 블록에 대한 개별 요청을 병합(Merge)합니다.

인접하고 병합 조건을 충족하는 4KiB 요청 네 개는 16KiB 요청으로 묶을 수 있습니다.

애플리케이션 레벨에서도 작은 쓰기를 모아서 큰 쓰기로 배치하면 성능이 크게 향상됩니다.

ionice

프로세스의 I/O 우선순위를 설정합니다. 효과는 사용 중인 스케줄러와 I/O 경로의 지원에 따라 달라집니다.

백업 작업은 낮은 우선순위로, 데이터베이스는 높은 우선순위로 설정하면 서로 간섭이 줄어듭니다.

ionice_example.sh
# 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 튜닝 지표를 함께 점검합니다.