사전 디스크 입출력
개념

디스크 입출력

gabury1

디스크 입출력은 프로그램이 디스크에 데이터를 읽고 쓰는 일입니다. 로그 파일을 남기는 일도 데이터베이스가 행을 찾아오는 일도 여기에 듭니다. 메모리 안에서 끝나는 일보다 한참 오래 걸립니다. 그래서 서버가 느려지면 가장 먼저 의심받습니다.

쉽고 빠른 이해

디스크 입출력은 메모리와 디스크 사이에서 데이터를 옮기는 일입니다. 데이터베이스가 표의 행을 찾으러 디스크를 읽는 것이 한 예입니다.

이 일이 따로 있는 까닭은 메모리가 전원이 꺼지면 비기 때문입니다. 남겨야 할 데이터는 디스크에 둡니다. 그 데이터를 다시 쓰려면 메모리로 올려야 합니다.

도는 순서는 이렇습니다.

  1. 프로그램이 운영체제에 읽기를 부탁합니다
  2. 운영체제는 최근에 읽어 둔 사본이 메모리에 있는지 먼저 봅니다
  3. 없으면 디스크에 요청을 보냅니다. 그동안 프로그램은 멈춰 기다립니다

대가는 기다림입니다. 디스크까지 가는 요청은 메모리 안의 일보다 한참 느립니다. 여러 프로그램이 한 디스크를 나눠 쓰면 요청이 줄을 섭니다. 뒤에 선 요청은 그만큼 늦어집니다.

상세

도서관 열람실을 떠올려 봅니다. 책상 위에 펼친 책은 바로 읽힙니다. 책상에 없는 책은 사서에게 부탁해 서고에서 꺼내 와야 합니다. 책이 올 때까지 읽던 일은 멈춥니다.

비유에서 책상은 메모리입니다. 서고는 디스크입니다. 사서는 운영체제입니다. 서고와 책상 사이를 오가는 심부름이 디스크 입출력입니다.

디스크 입출력은 디스크와 메모리 사이에서 데이터를 옮기는 일입니다. 디스크에서 메모리로 가져오는 것이 읽기입니다. 메모리에서 디스크로 내보내는 것이 쓰기입니다. 실무에서는 흔히 디스크 I/O(Input/Output)라고 부릅니다.

메모리는 빠르지만 전원이 꺼지면 내용이 사라집니다. 디스크는 읽고 쓰는 데 시간이 더 듭니다. 대신 전원이 꺼져도 내용이 남습니다. 그래서 남겨야 할 데이터는 디스크에 둡니다. 그 데이터를 다룰 때마다 메모리로 올립니다.

입출력은 디스크만의 일이 아닙니다. 네트워크로 주고받는 것도 입출력입니다. 이 항목은 그중 디스크를 상대로 하는 입출력을 다룹니다. 네트워크 쪽은 네트워크 입출력이 받습니다.

읽기 요청 하나가 지나는 길

이 소절은 프로그램이 파일 한 조각을 읽을 때 요청이 어디를 거치는지 따라갑니다.

프로그램은 디스크를 직접 건드리지 않습니다. 운영체제에 읽어 달라고 부탁합니다. 이 부탁을 보내는 공식 창구를 시스템 콜이라고 합니다. 여러 프로그램이 한 디스크를 나눠 씁니다. 그래서 순서와 권한을 운영체제 한 곳이 쥡니다.

운영체제는 먼저 페이지 캐시를 봅니다. 페이지 캐시는 최근에 디스크에서 읽은 내용을 메모리에 남겨 둔 사본입니다. 같은 데이터를 또 읽으면 디스크까지 갈 필요가 없어집니다. 찾는 내용이 여기 있으면 요청은 디스크에 닿지 않고 끝납니다.

사본이 없으면 파일 시스템이 나섭니다. 파일 시스템은 파일의 내용이 디스크 어디에 놓였는지 적어 두는 소프트웨어입니다. 이 기록 덕분에 프로그램은 파일 이름과 파일 안에서 몇 번째 바이트인지만 대면 됩니다.

디스크는 한 바이트씩 읽지 않습니다. 정해진 크기의 덩어리를 한 번에 읽고 씁니다. 이 덩어리를 블록이라고 합니다. 한 글자만 필요해도 그 글자가 든 블록 하나를 전부 읽어 옵니다.

읽을 블록이 정해지면 요청은 디바이스 드라이버를 거쳐 디스크로 갑니다. 디바이스 드라이버는 운영체제의 요청을 그 장치가 알아듣는 명령으로 바꿔 주는 소프트웨어입니다. 디스크가 블록을 돌려주면 운영체제는 그 내용을 페이지 캐시에 남긴 뒤 프로그램에 건넵니다.

flowchart TD
    A["프로그램이 시스템 콜로 읽기를 부탁한다"] --> B{"페이지 캐시에 사본이 있나"}
    B -->|있다| C["사본을 건네고 끝난다"]
    B -->|없다| D["파일 시스템이 읽을 블록을 찾는다"]
    D --> E["디바이스 드라이버가 디스크에 요청한다"]
    E --> F["블록을 페이지 캐시에 남기고 건넨다"]

그림의 갈림에서 「있다」로 빠지면 디스크는 움직이지 않습니다. 시간이 드는 것은 「없다」로 내려가는 길입니다. 자주 읽는 데이터일수록 사본이 남아 있을 가능성이 큽니다.

쓰기가 끝났다고 알리는 시점

쓰기는 읽기와 끝나는 방식이 다릅니다. 이 소절은 프로그램이 완료 답을 받는 때와 데이터가 디스크에 닿는 때가 어떻게 갈리는지 봅니다.

프로그램이 쓰기를 부탁하면 운영체제는 내용을 페이지 캐시에 먼저 담습니다. 담자마자 프로그램에 완료를 알립니다. 디스크로 내려보내는 일은 나중에 모아서 합니다. 작은 쓰기를 모아 한 번에 보내면 디스크에 가는 요청 수가 줄기 때문입니다.

아직 디스크에 안 내려간 캐시 내용을 더티 페이지라고 부릅니다. 페이지는 페이지 캐시가 메모리를 나눠 담는 단위입니다. 메모리 쪽 내용이 디스크와 달라져 「더러워졌다」는 뜻으로 붙은 이름입니다. 이 사이에 전원이 끊기면 더티 페이지에 담긴 내용은 사라집니다.

그래서 반드시 남아야 하는 데이터는 따로 확인을 받습니다. 프로그램이 fsync 같은 호출을 보내면 운영체제는 그 파일의 더티 페이지를 디스크로 내려보냅니다. 디스크가 받았다고 답할 때까지 프로그램은 기다립니다.

sequenceDiagram
    participant 프로그램
    participant 캐시 as 페이지 캐시
    participant 디스크
    프로그램->>캐시: 쓰기를 부탁한다
    캐시-->>프로그램: 바로 완료를 알린다
    Note over 캐시: 더티 페이지로 남는다
    프로그램->>캐시: fsync 를 보낸다
    캐시->>디스크: 더티 페이지를 내려보낸다
    디스크-->>캐시: 받았다고 답한다
    캐시-->>프로그램: 이제 완료를 알린다

그림에서 완료 알림이 두 번 나옵니다. 첫 알림은 메모리에 담았다는 뜻입니다. 둘째 알림이 와야 디스크에 남았다는 뜻입니다.

이 차이 때문에 쓰기는 대개 프로그램을 오래 붙잡지 않습니다. 읽기는 데이터가 손에 들어오기 전에는 다음 일을 할 수 없습니다. 디스크 입출력으로 느려지는 때는 주로 둘입니다. 페이지 캐시에 없는 데이터를 읽을 때와 fsync 로 확인을 기다릴 때입니다.

기다리는 동안의 프로그램

이 소절은 디스크 답이 돌아오기까지 프로그램과 CPU(Central Processing Unit, 중앙처리장치)가 무엇을 하는지 봅니다. 기다림을 피하는 방식 하나도 함께 짚습니다.

디스크에 요청을 보낸 프로그램은 답이 올 때까지 멈춥니다. 이렇게 결과를 받을 때까지 멈춰 서는 방식을 블로킹이라고 합니다. 멈춘 동안 CPU 는 놀지 않습니다. 운영체제가 다른 프로그램을 골라 돌립니다.

그런데 돌릴 다른 프로그램이 없으면 CPU 는 디스크 답만 기다리며 쉽니다. 많은 운영체제가 CPU 사용률을 보여 줄 때 이렇게 쉰 시간을 따로 셉니다. 이 값을 입출력 대기라고 부릅니다. 이 값이 높으면 프로그램들이 CPU 가 아니라 디스크를 기다리고 있다는 뜻입니다.

기다리지 않는 방법도 있습니다. 요청만 걸어 두고 프로그램은 다른 일을 합니다. 디스크 일이 끝나면 운영체제가 알려 줍니다. 이 방식을 비동기 입출력이라고 합니다.

순차 접근과 임의 접근

같은 양을 읽어도 어떤 순서로 읽느냐에 따라 걸리는 시간이 크게 갈립니다. 이 소절은 두 가지 읽는 순서를 견줍니다. 저장 장치 종류에 따라 그 차이가 어떻게 달라지는지도 봅니다.

순차 접근은 디스크에서 이어진 구간을 차례로 읽는 것입니다. 로그 파일을 처음부터 끝까지 훑는 것이 그렇습니다. 임의 접근은 떨어진 위치를 골라 읽는 것입니다. 인덱스를 따라 행 몇 개를 여기저기서 꺼내 오는 것이 그렇습니다.

원반을 돌리는 디스크인 HDD(Hard Disk Drive, 하드 디스크 드라이브)에서 차이가 가장 큽니다. 읽을 위치까지 헤드라는 팔을 옮겨야 합니다. 원반이 그 위치까지 돌아오기도 기다려야 합니다. 임의 접근은 요청마다 이 시간을 새로 치릅니다.

칩에 데이터를 담는 SSD(Solid State Drive, 솔리드 스테이트 드라이브)는 옮겨 다닐 부품이 없습니다. 그래서 두 방식의 차이가 훨씬 작습니다. 그래도 요청 하나마다 드는 처리 비용은 남습니다. 작은 요청을 여러 번 보내는 것보다 크게 묶어 한 번 보내는 편이 빠릅니다.

데이터를 끝에 덧붙이기만 하는 저장 방식이 많은 까닭이 이것입니다. 새 내용을 끝에 이어 쓰면 쓰기가 순차 접근이 됩니다. 데이터베이스가 변경 기록 로그의 끝에 새 기록을 붙이는 것이 그 예입니다. 이 변경 기록 로그는 뒤의 「백엔드 서버가 디스크를 기다리는 때」에서 다시 나옵니다.

덧붙이기를 저장 구조 전체로 넓힌 것도 있습니다. LSM 트리(Log-Structured Merge tree, 로그 구조 병합 트리)는 새로 들어온 데이터를 메모리에 모았다가 디스크에 한꺼번에 이어 씁니다. 디스크 여기저기에 있는 기존 데이터를 고쳐 쓰지 않으니 쓰기가 순차 접근으로 바뀝니다.

디스크 입출력을 재는 지표

디스크가 얼마나 바쁜지는 한 숫자로 안 잡힙니다. 요청의 수와 옮긴 양, 요청 하나가 기다린 시간이 따로 움직이기 때문입니다. 요청 수를 세는 지표는 IOPS(Input/Output Operations Per Second, 초당 입출력 횟수)라고 부릅니다. 아래 표는 흔히 보는 지표 넷을 모은 것입니다.

지표 무엇을 세나 크게 나오는 때
IOPS 초당 끝낸 읽기·쓰기 요청 수 작은 요청이 흩어져 들어올 때
처리량 초당 옮긴 데이터의 양 큰 파일을 죽 읽거나 쓸 때
지연 요청 하나가 끝나기까지 걸린 시간 요청이 줄을 서서 오래 기다릴 때
큐 깊이 아직 안 끝나고 쌓여 있는 요청 수 디스크가 받는 속도보다 요청이 빨리 올 때

표의 넷은 서로 묶여 움직입니다. 디스크가 처리하는 속도보다 요청이 빨리 들어오면 큐 깊이가 늘어납니다. 줄이 길어진 만큼 지연도 늘어납니다. IOPS 나 처리량이 더 오르지 않은 채 지연만 오르면 디스크가 포화됐다고 봅니다.

CPU 가 한가한 채 응답만 느려지면 디스크를 병목으로 의심합니다. 입출력 대기가 높고 지연이 오른 것이 함께 보이면 그 의심이 굳어집니다.

한 디스크를 여럿이 나눠 쓸 때

서버 한 대 위에서 여러 프로그램이 같은 디스크를 씁니다. 이 소절은 그 요청들이 어떻게 줄을 서는지, 한쪽이 다른 쪽을 밀어낼 때 무엇으로 막는지 봅니다.

디스크 하나가 동시에 처리하는 요청 수에는 한계가 있습니다. 한계를 넘는 요청은 큐에 줄을 섭니다. 운영체제의 입출력 스케줄러가 이 줄에서 무엇을 먼저 보낼지 정합니다. 가까운 위치의 요청끼리 묶거나 읽기를 앞세우는 식입니다.

문제는 한 프로그램이 줄을 독차지할 때입니다. 백업 작업이 큰 파일을 쉬지 않고 읽으면 줄이 그 요청으로 찹니다. 옆 서비스가 보낸 작은 읽기 하나가 그 뒤에서 오래 기다립니다. 이렇게 옆 프로그램 때문에 느려지는 상황을 시끄러운 이웃 문제라고 부릅니다.

리눅스에는 cgroup(control group, 컨트롤 그룹)이 있습니다. cgroup 은 프로그램 여럿을 한 묶음으로 묶어 자원 몫을 나눠 주는 운영체제 기능입니다. 이 묶음마다 디스크를 쓸 몫을 정해 두면 한 묶음이 줄을 독차지하지 못합니다. 초당 읽고 쓸 수 있는 양이나 요청 수에 상한을 거는 식입니다.

상한을 넘긴 묶음의 요청은 거절되지 않습니다. 몫이 다시 채워질 때까지 붙잡혀 있습니다. 이렇게 속도를 늦춰 몫을 지키게 하는 일을 스로틀링이라고 합니다.

컨테이너는 프로그램을 다른 프로그램과 떼어 돌리는 실행 묶음입니다. 컨테이너에 디스크 상한을 걸면 안쪽에서는 이 스로틀링이 일어납니다. 상한에 걸린 컨테이너는 죽지 않고 느려집니다.

flowchart TD
    subgraph GA["백업 묶음"]
        A1["큰 읽기 요청이 쉬지 않고 나온다"]
    end
    subgraph GB["서비스 묶음"]
        B1["작은 읽기 요청 하나"]
    end
    A1 --> T["상한을 넘긴 요청은 붙잡아 둔다"]
    T --> Q["디스크 앞의 큐"]
    B1 --> Q
    Q --> D["디스크"]

그림에서 백업 묶음의 요청만 한 단계를 더 거칩니다. 상한이 없으면 큐가 백업 요청으로 가득 찹니다. 상한이 있으면 서비스 묶음의 작은 읽기가 큐에 끼어들 틈이 생깁니다.

백엔드 서버가 디스크를 기다리는 때

백엔드 개발자가 디스크 입출력을 직접 부르는 일은 드뭅니다. 그래도 데이터베이스와 로그를 거쳐 응답 시간에 드러납니다. 이 소절은 흔히 만나는 세 경우를 봅니다.

데이터베이스는 커밋을 끝냈다고 답하기 전에 바뀐 내용을 변경 기록 로그에 적어 디스크까지 내려보냅니다. 서버가 갑자기 꺼져도 커밋한 내용을 잃지 않으려는 것입니다. 이 방식을 선행 기록 로그라고 합니다. 그래서 커밋 하나의 응답 시간에는 디스크 쓰기를 기다린 시간이 들어 있습니다.

데이터베이스는 자주 읽는 데이터를 메모리에 올려 둡니다. 이렇게 자주 찾는 데이터의 묶음을 작업 집합이라고 합니다. 작업 집합이 메모리보다 커지면 읽을 때마다 디스크로 내려갑니다. 요청 수가 같아도 데이터가 자라는 것만으로 응답이 느려지는 까닭입니다.

애플리케이션이 남기는 로그도 디스크 쓰기입니다. 요청마다 많은 줄을 남기면 그만큼 디스크가 바빠집니다. 쌓인 로그가 공간을 다 채우는 디스크 부족이 오면 새 쓰기가 전부 실패합니다.

관련 항목

디스크 입출력을 받아 주는 저장 장치

디스크 · HDD · SSD · 플래시 메모리 · 블록 장치 · 블록 스토리지 · RAID

디스크 입출력 요청이 거치는 처리 단계

시스템 콜 · 운영체제 · 페이지 캐시 · 파일 시스템 · 블록 · 입출력 스케줄러 · 디바이스 드라이버

쓰기를 디스크까지 내려보내는 수단

fsync · 더티 페이지 · 라이트백 · 직접 입출력 · 선행 기록 로그 · 저널링

디스크 입출력을 기다리는 방식과 기다리지 않는 방식

블로킹 · 논블로킹 입출력 · 비동기 입출력 · 입출력 대기 · 메모리 맵 파일 · 프로세스

디스크 입출력을 재는 지표

IOPS · 처리량 · 지연 · 큐 깊이 · 대역폭 · 꼬리 지연

디스크 입출력을 나누고 제한하는 기능

cgroup · 스로틀링 · 컨테이너 · 시끄러운 이웃 · 쿼터 · 큐

디스크 입출력에서 자주 나는 장애

병목 · 포화 · 디스크 부족 · 쓰기 증폭 · 데이터 손상

디스크 입출력 비용을 줄이는 기법

캐싱 · 버퍼 · 순차 접근 · 임의 접근 · LSM 트리 · B-tree · 작업 집합

디스크 입출력과 함께 성능을 가르는 자원

CPU · 메모리 · 네트워크 입출력 · 성능

디스크 입출력으로 응답이 늦어지는 데이터베이스 동작

데이터베이스 · 커밋 · 트랜잭션 · 버퍼 풀 · 인덱스

다른 이름: disk IO · disk I-O · 디스크 IO · 디스크 I-O