[시스템 엔지니어링] I/O Bound 문제
본문 바로가기

Backend

[시스템 엔지니어링] I/O Bound 문제

I/O Bound 란?

시스템의 전체 성능이 CPU 연산 속도가 아니라 I/O(입출력) 작업 속도에 의해 결정되는 상태를 말한다.

여기서 I/O는 디스크 접근, 네트워크 통신, 데이터베이스 쿼리, 파일 읽기/쓰기, 시스템 콜 대기 등 CPU 외부장치와 데이터를 주고받는 모든 작업을 포함한다. 

 

즉, CPU가 데이터를 더 처리할 여유가 있음에도 불구하고, I/O 완료를 기다리느라 놀고 있는 시간(iowait)이 많아지는 상황이 I/O Bound 상태이다.

 

대표 증상

  • 평균 CPU 사용률은 낮은데(또는 코어가 남는데), iowait 시간이 높다
  • RPS/Throughput이 낮고 레이턴시가 길며, burst에서 급격히 악화(큐가 쌓임)
  • 동시성(concurrency)을 늘리면 어느 정도까지는 개선되지만, 임계점을 넘으면 오히려 타임아웃, 재시도 폭증
  • 디스크/네트워크/DB 모니터링에서 대기열 길이(queue depth) 증가, 디스크 서비스 시간(svc_t) 상승

전형적 원인 유형

  1. 디스크 I/O 지연: 작은 랜덤 I/O 다발, sync flush, 로그/스냅샷 경쟁, 파일 시스템 저널링 부담
  2. 네트워크 I/O 지연: RTT 높음, Nagle/Delayed ACK 영향, 재전송·혼잡, 외부 API 지연
  3. DB/스토리지 병목: 인덱스 부재, 풀 스캔, 락 경합, Connection Pool 부족, 캐시 미스
  4. 서비스 체인: Downstream 서비스가 느리거나 재시도 폭탄으로 전파적 지연
  5. 시스템 콜 대기: read()/write()/accept()/connect() 블로킹, 동기 DNS 조회

개선 전략

( 요약 : 비동기 처리, 캐싱, 배치, I/O 장치 성능 개선, 데이터 접근 최적화 )

  1. 디스크/파일 I/O
    • 캐싱·버퍼링: 핫 데이터 메모리 캐시, 페이지 캐시 확인.
    • 배치/병합: 작은 I/O를 묶어 쓰기 합치기(write coalescing).
    • 비동기 I/O: aio/io_uring, 파이프라인 처리.
    • 데이터 구조 최적화: 순차 접근 유도(LSM-tree, Append-only 로그), 압축/분할.
    • 스토리지 계층: SSD/NVMe, RAID 레이아웃, 파일시스템 마운트 옵션 점검.
  2. 네트워크/외부 API
    • 커넥션 재사용(HTTP keep-alive), 풀링 크기 조정.
    • 타임아웃/재시도/백오프 표준화, 서킷 브레이커·버크헤드로 격리.
    • 배치·압축(gRPC, HTTP/2), 헤더 최소화, Nagle 비활성화(TCP_NODELAY) 상황별 적용.
    • 근접 배치: 리전/존 근접, CDN/에지 캐시.
  3. 데이터베이스
    • 적절한 인덱스쿼리 플랜 점검(Explain).
    • N+1 제거(조인/프리패치/집계), 읽기·쓰기 경로 분리(read replica).
    • Connection Pool: 최소/최대, 대기 시간 모니터링.
    • 캐시 계층: Redis/멤캐시, TTL/무효화 전략 설계.
  4. 애플리케이션 아키텍처
    • 비동기/논블로킹로 I/O 대기 중 CPU를 해방:
      • Python: asyncio, aiohttp, uvloop
      • Node.js: 기본 이벤트 루프 활용, Promise/Stream 제대로 사용
      • JVM: Netty, Loom(가상 스레드), reactive stack
    • 파이프라이닝/프로듀서-컨슈머: 큐 기반 버퍼링, 백프레셔 적용.
    • 사전 계산/프리페치: 오프라인 ETL, warm-up, 결과 캐시.
    • 배치 전송Scatter-Gather 패턴으로 왕복 수(RTT)를 줄이기.
  5. 운영 레버
    • 동시성 조절: 워커 수/세마포어/풀 크기를 점진적으로 늘려 한계점 관찰(포화 이전 지점이 최적).
    • 스로틀링큐 제한으로 폭주 방지.
    • 리소스 격리: 핫패스/백그라운드 작업 분리, 다른 noisy neighbor와의 경쟁 최소화.

동기 vs 비동기 I/O 예시

CPU 바운드라면 비동기화로 큰 이득이 없지만, I/O 대기 시간이 지배적인 경우 동시성 확대만으로도 체감 성능이 크게 개선된다

# 동기식: 파일/네트워크 읽기 동안 스레드가 놀게 된다.
for url in urls:
    resp = requests.get(url, timeout=2)
    process(resp.text)

# 비동기식: I/O 대기 중에도 다른 코루틴이 실행되어 CPU 유휴를 줄인다.
import asyncio, aiohttp
async def fetch(session, url):
    async with session.get(url, timeout=2) as r:
        return await r.text()

async def main(urls):
    async with aiohttp.ClientSession() as s:
        texts = await asyncio.gather(*(fetch(s, u) for u in urls))
        for t in texts: process(t)

asyncio.run(main(urls))