사전 입출력 대기
개념

입출력 대기

gabury1고친 사람 github-actions[bot]

입출력 대기는 컴퓨터가 쉬는 시간 가운데 디스크를 기다리며 쉰 몫을 따로 떼어 세는 값입니다. 컴퓨터가 얼마나 바빴는지 보여 주는 모니터링 도구 대부분이 이 값을 따로 한 칸으로 보여 줍니다. 값이 높다는 것은 프로그램들이 계산보다 디스크를 기다리고 있다는 뜻입니다. 디스크 응답을 기다리며 멈춘 프로그램의 상태를 같은 이름으로 부르기도 합니다.

쉽고 빠른 이해

입출력 대기는 CPU(Central Processing Unit, 중앙 처리 장치)가 할 일 없이 쉰 시간 중에서 디스크 일이 끝나기를 기다리며 쉰 시간입니다. 데이터베이스가 디스크에서 큰 테이블을 읽는 동안 서버에 다른 할 일이 없으면 그 시간이 여기로 잡힙니다.

이 값을 따로 세는 까닭은 원인을 가르기 위해서입니다. 서버 응답이 늦어졌는데 CPU 가 한가하다면 범인은 계산이 아니라 디스크 쪽일 수 있습니다. 쉰 시간을 한 덩어리로만 보면 이 차이가 안 보입니다.

이렇게 셉니다:

  1. 프로그램이 디스크에 읽기나 쓰기를 부탁한 뒤 멈춰 기다립니다
  2. 그동안 돌릴 다른 프로그램이 없으면 CPU 가 쉽니다
  3. 운영체제가 이 쉰 시간을 입출력 대기로 따로 적습니다

대가는 이 값이 쉰 시간의 일부라서 쉽게 흔들린다는 것입니다. 다른 계산이 CPU 를 채우면 디스크가 똑같이 밀려 있어도 값이 낮게 나옵니다. 그래서 이 값 하나만 보고 디스크가 괜찮다고 판단하지 못합니다.

상세

식당 주방장이 손을 놓은 시간을 둘로 나눠 적는다고 합시다. 들어온 주문이 없어서 쉰 시간과, 주문한 재료가 아직 안 와서 쉰 시간입니다. 뒤쪽이 길게 쌓이면 손을 더 빨리 놀릴 일이 아니라 재료 배달을 들여다볼 일입니다.

입출력 대기는 CPU가 쉰 시간 가운데 디스크 일이 끝나기를 기다리던 시간입니다. 영어로는 iowait 라고 씁니다. 도구 화면에는 흔히 wa 로 줄여 찍힙니다.

데이터베이스가 디스크에서 큰 테이블을 읽는 장면을 떠올려 봅시다. 그동안 서버에 다른 할 일이 없다면 CPU 는 디스크 응답만 기다리며 쉽니다. 그 시간이 입출력 대기로 잡힙니다.

CPU 시간을 나누는 칸

운영체제는 짧은 간격마다 CPU 가 무엇을 하고 있었는지 셉니다. 이렇게 모은 시간을 비율로 보인 것이 CPU 사용률입니다. 모니터링 화면에서 「CPU 를 몇 % 썼다」고 보이는 숫자가 이것입니다.

센 시간은 먼저 일한 시간과 쉰 시간으로 나뉩니다. 일한 시간은 다시 둘입니다. 프로그램이 자기 코드를 돌린 시간과, 커널이 프로그램 대신 일한 시간입니다.

커널은 운영체제의 핵심 부분입니다. 디스크 읽기처럼 하드웨어를 만지는 일은 프로그램이 커널에 맡깁니다.

쉰 시간도 둘로 나뉩니다. 그냥 할 일이 없어 쉰 시간과, 할 일은 없지만 끝나지 않은 디스크 요청이 남아 있던 시간입니다. 뒤쪽이 입출력 대기입니다.

flowchart TD
    T["CPU 시간"] --> W["일한 시간"]
    T --> I["쉰 시간"]
    W --> U["프로그램 코드"]
    W --> S["커널 코드"]
    I --> IO["입출력 대기"]
    I --> ID["그냥 쉼"]

그림에서 볼 것은 입출력 대기가 쉰 시간 쪽 가지에 달려 있다는 점입니다. 입출력 대기 동안 CPU 는 바쁜 것이 아닙니다. 돌릴 일이 생기면 바로 그 일을 돌릴 수 있는 상태입니다.

입출력 대기로 세는 조건

운영체제는 실행 중인 프로그램 하나를 프로세스라고 부릅니다. 어떤 순간이 입출력 대기로 적히려면 두 조건이 함께 맞아야 합니다.

첫째, 프로세스가 디스크 같은 저장 장치에 요청을 걸어 두고 멈춰 있어야 합니다. 디스크에서 읽기를 부탁한 프로세스는 데이터가 올 때까지 다음 줄로 못 넘어갑니다. 이렇게 결과를 받을 때까지 멈춰 서는 방식을 블로킹이라고 합니다.

둘째, 그 CPU 에 당장 돌릴 다른 프로세스가 없어야 합니다. 돌릴 준비가 된 프로세스들은 실행 큐라는 줄에 서서 CPU 차례를 기다립니다. 이 줄이 비어 있으면 CPU 는 쉴 수밖에 없습니다.

두 조건이 맞은 시간만 입출력 대기로 적힙니다. 아래 그림은 디스크 읽기 하나가 지나가는 동안 그 시간이 어디서 생기는지를 보입니다.

sequenceDiagram
    participant 프로세스
    participant 커널
    participant 디스크
    프로세스->>커널: 파일 읽기를 부탁한다
    커널->>디스크: 읽기 요청을 보낸다
    Note over 프로세스,커널: 프로세스는 멈추고 돌릴 다른 프로세스가 없다
    Note over 커널: 이 동안의 CPU 시간을 입출력 대기로 적는다
    디스크-->>커널: 데이터가 도착한다
    커널-->>프로세스: 깨워서 데이터를 넘긴다

디스크가 답하기 전에 다른 프로세스가 실행 큐에 들어오면 사정이 바뀝니다. CPU 는 그 프로세스를 돌립니다. 그 시간은 일한 시간으로 적히므로 입출력 대기는 늘지 않습니다.

네트워크를 기다리는 시간은 대개 여기에 안 잡힙니다. 첫째 조건이 저장 장치 요청만 세기 때문입니다. 데이터베이스 응답을 기다리는 웹 서버는 오래 멈춰 있어도 그냥 쉰 시간으로 잡히는 것이 보통입니다.

도구에서 보이는 이름

같은 값을 도구마다 조금씩 다른 이름으로 보여 줍니다. 리눅스에서 흔히 쓰는 도구의 칸 이름을 모으면 이렇습니다.

도구 칸 이름 어디서 보나
top wa 화면 위쪽의 CPU 줄
vmstat wa 오른쪽 끝의 CPU 칸 묶음
iostat %iowait CPU 평균 줄
sar %iowait 지난 시간대의 CPU 기록
mpstat %iowait 코어마다 한 줄씩

값은 모두 측정 간격 동안의 비율입니다. 그 간격 가운데 CPU 가 디스크를 기다리며 쉰 시간이 얼마만큼이었는지를 보여 줍니다.

코어가 여럿일 때 희석되는 값

요즘 CPU 에는 계산을 하는 장치가 여럿 들어 있습니다. 이 장치 하나하나를 코어라고 부릅니다. 도구가 맨 위에 보여 주는 값은 코어들의 값을 평균 낸 것입니다.

코어가 넷인 서버에서 프로세스 하나만 디스크를 기다린다고 합시다. 다른 코어 셋에는 돌릴 일이 없습니다.

멈춘 프로세스는 어느 코어에도 올라가 있지 않습니다. 그래서 리눅스는 그 프로세스가 마지막에 돌던 코어에 기다림을 매깁니다. 실제로 그 코어가 기다린 것이 아니라 셈하기 편하게 정해 둔 약속입니다.

그 결과 그 코어 하나만 입출력 대기로 적힙니다. 다른 코어 셋은 그냥 쉰 것으로 적힙니다. 그래서 전체 평균에는 코어 넷 가운데 하나의 몫만 잡힙니다.

코어가 많을수록 전체 값은 더 희석됩니다. 그럴 때는 코어마다 따로 보여 주는 mpstat 으로 한 코어에 몰린 기다림이 있는지 봅니다.

값을 읽을 때 헷갈리는 점

입출력 대기는 쉰 시간의 한 갈래입니다. 이 한 가지 사실에서 헷갈리기 쉬운 점 둘이 나옵니다.

첫째, 값이 높다고 CPU 가 모자란 것이 아닙니다. CPU 는 오히려 놀고 있습니다. CPU 를 늘려도 디스크 응답이 빨라지지 않으니 기다림도 줄지 않습니다.

둘째, 값이 낮다고 디스크가 괜찮은 것도 아닙니다. 같은 디스크 상태에서도 CPU 가 다른 일로 얼마나 바쁜지에 따라 값이 달라집니다.

디스크 CPU 의 다른 일 입출력 대기
밀려 있다 없다 높게 나온다
밀려 있다 CPU 를 꽉 채우는 계산이 있다 낮게 나온다
한가하다 없다 0 에 가깝다

둘째 줄이 함정입니다. 디스크는 첫째 줄과 똑같이 밀려 있습니다. 그런데 CPU 가 쉴 틈이 없으니 쉰 시간의 한 갈래인 입출력 대기도 생길 틈이 없습니다.

그래서 디스크가 정말 밀렸는지는 디스크 쪽 숫자로 확인합니다. 하나는 요청 하나가 끝나기까지 걸린 시간인 지연입니다. 다른 하나는 아직 안 끝나고 줄 서 있는 요청 수입니다.

iostat 의 장치 줄에서 지연은 읽기 r_await·쓰기 w_await 칸에 나옵니다. 줄 선 요청 수는 aqu-sz 칸에 나옵니다.

둘이 함께 늘면 디스크가 받아 내는 속도보다 요청이 빨리 들어오고 있다는 뜻입니다. 이런 상태를 포화라고 합니다.

입출력을 기다리는 프로세스

같은 말이 CPU 시간이 아니라 프로세스의 상태를 가리킬 때도 있습니다. 「이 프로세스가 입출력 대기 중이다」라고 하면 디스크 응답을 기다리며 멈춰 있다는 뜻입니다. 앞 소절들의 값이 CPU 쪽에서 본 기다림이라면, 이쪽은 프로세스 쪽에서 본 기다림입니다.

운영체제는 프로세스에 짧은 알림을 보낼 수 있습니다. 이 알림을 시그널이라고 합니다. 프로세스를 끝내라는 요청도 시그널로 보냅니다.

리눅스에서 디스크를 기다리는 프로세스는 대개 시그널로 깨울 수 없는 잠에 들어갑니다. 이 상태를 uninterruptible sleep 이라고 부릅니다. ps 나 top 의 상태 칸에는 D 로 찍힙니다. 이 상태의 프로세스는 끝내라는 시그널을 받아도 기다림이 끝날 때까지 반응하지 않는 경우가 많습니다.

vmstat 의 b 칸이 이렇게 멈춘 프로세스 수를 셉니다. b 는 CPU 가 바쁜지와 상관없이 기다리는 프로세스를 셉니다. 그래서 앞 표의 둘째 줄처럼 wa 가 묻히는 경우에도 디스크 밀림을 드러냅니다.

로드 애버리지도 이 프로세스들의 영향을 받습니다. 로드 애버리지는 CPU 를 쓰고 있거나 쓰려고 기다리는 프로세스 수를 1분·5분·15분에 걸쳐 평균 낸 값입니다. 리눅스는 여기에 디스크를 기다리며 멈춘 프로세스도 넣어 셉니다. CPU 사용률은 낮은데 로드 애버리지만 높으면 이런 프로세스가 쌓였는지 먼저 봅니다.

응답 지연을 살필 때 이 값을 쓰는 순서

응답 시간이 늘었는데 CPU 사용률이 낮은 서버가 있다고 합시다. 계산이 원인은 아니라는 뜻이니 무엇을 기다리는지 찾아야 합니다. 입출력 대기는 이때 디스크를 의심할지 정하는 첫 단서입니다.

  1. CPU 줄의 wa 를 봅니다. 높으면 디스크를 병목으로 의심합니다
  2. vmstat 의 b 와 로드 애버리지를 봅니다. 멈춘 프로세스가 쌓였는지 확인합니다
  3. iostat 의 장치 줄에서 지연과 줄 선 요청 수를 봅니다. 둘이 늘었으면 의심이 굳어집니다

디스크를 기다리게 만드는 흔한 원인은 셋입니다. 백엔드 서버에서 자주 만나는 순서대로 모으면 이렇습니다.

원인 디스크를 기다리는 까닭
메모리에 없는 데이터 읽기 운영체제는 최근 읽은 파일 내용을 메모리에 남겨 둡니다. 이것을 페이지 캐시라고 합니다. 여기에 없는 데이터는 디스크까지 가서 읽어 옵니다
쓰기 확인 기다리기 데이터베이스는 커밋할 때 데이터가 디스크에 적혔다는 답을 받고서야 넘어갑니다. 이 답을 받아 오는 호출이 fsync 입니다
메모리 부족 모자란 메모리를 디스크로 내보냈다가 다시 읽어 옵니다. 이것을 스와핑이라고 합니다. 디스크가 이 일로 바빠집니다

셋째 줄은 원인이 디스크가 아니라 메모리입니다. 스와핑이 시작되면 wa 와 b 가 함께 오르니 디스크 문제로 잘못 읽기 쉽습니다. 입출력 대기는 디스크를 의심하게 만드는 신호입니다. 원인을 확정하는 값은 아닙니다.

관련 항목

입출력 대기와 나란히 CPU 시간을 나누는 칸

사용률 · 유휴 시간 · 스틸 시간 · 사용자 모드 · 커널 모드

입출력 대기를 보여 주는 도구

top · vmstat · iostat · sar · mpstat · ps

입출력 대기와 함께 읽는 디스크 지표

지연 · 처리량 · IOPS · 큐 깊이 · 대기열 · 포화

입출력 대기를 만드는 운영체제 동작

블로킹 · 프로세스 · 커널 · 실행 큐 · 스케줄러 · 문맥 교환 · 시스템 콜 · 인터럽트

입출력 대기를 끌어올리는 원인

디스크 입출력 · 페이지 캐시 · fsync · 스와핑 · 스래싱 · NFS

입출력 대기와 함께 오르는 프로세스 쪽 신호

로드 애버리지 · uninterruptible sleep · 시그널

디스크 기다림을 피하는 입출력 방식

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

입출력 대기를 다루는 분석 분야

성능 · 병목 · CPU · 코어 · USE 방법

다른 이름: iowait · I/O wait · IO wait · 아이오 대기