[도커 30강] 21강. 리소스 제한(CPU/Memory)과 안정성

[도커 30강] 21강. 리소스 제한(CPU/Memory)과 안정성

컨테이너를 운영하다 보면 “코드는 멀쩡한데 서버가 갑자기 버벅인다”, “한 서비스가 폭주해서 다른 서비스까지 느려진다” 같은 상황을 자주 만납니다. 이때 핵심은 애플리케이션 코드를 손보기 전에 **리소스 경계(CPU/Memory limit)**를 먼저 명확히 두는 것입니다. 도커는 격리 기술 위에서 동작하지만, 기본 설정으로는 컨테이너가 생각보다 많은 자원을 사용할 수 있습니다. 그래서 실무에서는 "잘 돌아간다"보다 "폭주해도 전체 시스템이 버틴다"를 목표로 설정해야 합니다.

이번 강의에서는 CPU/메모리 제한을 단순 옵션 암기 수준이 아니라, 안정성 관점에서 이해합니다. 특히 다음을 분명히 잡고 가겠습니다.

  • CPU 제한은 “속도 제한(스로틀링)”의 성격이 강하다.
  • 메모리 제한은 “한도 초과 시 OOM 종료”로 바로 이어질 수 있다.
  • 제한값은 감으로 정하지 말고 관측치(usage, peak, latency) 기반으로 조정해야 한다.

핵심 개념

  • CPU limit: 컨테이너가 사용할 수 있는 CPU 시간을 제한합니다. 과도한 CPU 점유를 막아 인접 서비스 보호에 효과적입니다. 다만 제한이 너무 낮으면 응답 지연이 증가합니다.
  • Memory limit: 컨테이너가 사용할 메모리 상한을 둡니다. 상한을 넘으면 OOM(Out Of Memory)로 프로세스가 종료될 수 있어, 안정성 설계에서 가장 민감한 항목입니다.
  • Reservation(요청) vs Limit(상한): Compose/오케스트레이션 환경에서는 최소 확보 자원(요청)과 최대 사용 자원(상한)을 분리해 관리합니다. 단일 호스트 도커에서도 이 사고방식이 필요합니다.
  • 관측 후 튜닝: 제한을 먼저 걸고 docker stats, 앱 메트릭, 응답시간을 보면서 단계적으로 조정해야 합니다. 처음부터 완벽한 수치는 없습니다.
  • 장애 격리: 리소스 제한의 목적은 “한 컨테이너가 죽지 않게”보다 “한 컨테이너가 문제를 일으켜도 전체를 죽이지 않게”에 더 가깝습니다.

기본 사용

예제 1) CPU/메모리 제한을 걸고 컨테이너 실행

# CPU 1코어, 메모리 512MB로 제한
docker run -d --name web-limited \
  --cpus="1.0" \
  --memory="512m" \
  --memory-swap="512m" \
  nginx:alpine

# 제한값 확인
docker inspect web-limited --format '{{json .HostConfig}}' | jq '{NanoCpus,Memory,MemorySwap}'

설명:

  • --cpus="1.0"은 대략 1코어 수준의 CPU 시간으로 제한합니다.
  • --memory="512m"은 물리 메모리 상한입니다.
  • --memory-swap="512m"를 동일하게 두면 사실상 스왑 여유 없이 메모리를 강하게 제한합니다.
  • 운영 초반에는 메모리를 너무 타이트하게 두지 말고, 피크 사용량 대비 완충 구간(예: 20~40%)을 두는 것이 안전합니다.

예제 2) 실행 중 컨테이너의 자원 사용량 관측

# 실시간 사용량 확인
docker stats web-limited

# 여러 컨테이너를 함께 비교
docker stats --no-stream --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.PIDs}}"

설명:

  • 제한 설정만 하고 관측하지 않으면, 실제 병목이 CPU인지 메모리인지 판단하기 어렵습니다.
  • PIDs가 비정상적으로 늘어나는 경우 스레드/프로세스 누수 가능성도 함께 점검해야 합니다.
  • 단순 사용률뿐 아니라 “요청 피크 시간대의 응답 지연”을 함께 봐야 안정성 판단이 가능합니다.

예제 3) 컨테이너 업데이트로 제한값 단계 조정

# 실행 중인 컨테이너 제한값 조정
docker update \
  --cpus="1.5" \
  --memory="768m" \
  --memory-swap="768m" \
  web-limited

# 반영 확인
docker inspect web-limited --format 'CPU={{.HostConfig.NanoCpus}} MEM={{.HostConfig.Memory}} SWAP={{.HostConfig.MemorySwap}}'

설명:

  • 실제 운영에서는 처음 값이 틀릴 확률이 높습니다. 그래서 update 기반 점진 튜닝이 중요합니다.
  • CPU만 높였는데도 지연이 줄지 않으면 메모리 압박/GC/IO 병목일 수 있으니 지표를 교차 확인하세요.
  • 반대로 메모리만 늘리고 CPU를 방치하면, 트래픽 급증 시 스로틀링으로 tail latency가 악화될 수 있습니다.

예제 4) Compose에 자원 정책 명시하기

cat > compose.resource.yml <<'YAML'
services:
  api:
    image: myorg/myapi:1.0.0
    ports:
      - "8080:8080"
    deploy:
      resources:
        limits:
          cpus: '1.00'
          memory: 768M
        reservations:
          cpus: '0.25'
          memory: 256M
YAML

# 파일 검증
docker compose -f compose.resource.yml config

설명:

  • 팀 프로젝트에서는 로컬 실행 옵션보다 Compose 파일에 자원 정책을 명시해 공유하는 쪽이 훨씬 일관적입니다.
  • reservations는 “최소 기대치”, limits는 “최대 허용치”라는 감각으로 운영 문서에 함께 기록하세요.
  • 개발/운영 프로파일을 분리해 운영 환경에서만 더 엄격한 제한을 적용하는 전략도 자주 씁니다.

자주 하는 실수

실수 1) 메모리 제한을 너무 낮게 잡아 OOM 재시작 루프 발생

  • 원인: “낮을수록 안전하다”는 오해로 피크 메모리 고려 없이 일괄 제한.
  • 문제: 컨테이너가 반복 종료되어 오히려 장애가 확대됩니다.
  • 해결: 피크 사용량 + 완충 구간을 기준으로 제한을 잡고, 초기에는 알람 임계치를 먼저 설정한 뒤 단계적으로 줄이세요.

실수 2) CPU 제한 없이 멀티테넌트 서비스 운영

  • 원인: 초기 트래픽이 낮아 문제가 없어서 설정을 미룸.
  • 문제: 특정 배치/크론/버그가 CPU를 독점하면 전체 노드 응답성이 급격히 하락합니다.
  • 해결: 핵심 서비스부터 CPU 상한을 먼저 지정하고, 우선순위 낮은 작업은 더 작은 값으로 분리하세요.

실수 3) docker stats만 보고 안정성 판단

  • 원인: 도커 기본 지표만으로 충분하다고 판단.
  • 문제: 실제 사용자 지연(latency), 에러율, 큐 적체가 놓쳐집니다.
  • 해결: 앱 메트릭(APM/Prometheus)과 함께 보고, “리소스 사용률 ↔ 응답시간” 상관관계를 확인하세요.

실수 4) 스왑 정책을 이해하지 못한 채 기본값 사용

  • 원인: --memory만 지정하고 --memory-swap 의미를 모르고 넘어감.
  • 문제: 메모리 압박 시 예상과 다른 성능 저하(과도한 스왑, 긴 지연)가 발생할 수 있습니다.
  • 해결: 서비스 특성(실시간 API vs 배치)에 맞춰 스왑 허용 범위를 명확히 정하고 문서화하세요.

실무 패턴

안정성 중심 팀에서는 리소스 제한을 “한 번 넣고 끝”이 아니라, 배포 파이프라인과 운영 루틴에 녹여둡니다. 아래 패턴이 실전에서 효과가 좋습니다.

  • 패턴 1: 기본 가드레일 선적용
    신규 서비스는 시작할 때부터 CPU/메모리 상한을 갖고 출발합니다. “나중에 넣자”는 거의 100% 미뤄집니다.

  • 패턴 2: 클래스별 프리셋 운영
    API, 워커, 스케줄러처럼 서비스 타입별 기본 제한 템플릿을 둡니다. 예: API(1CPU/768MB), 워커(2CPU/1.5GB), 크론(0.5CPU/512MB). 팀 온보딩 속도가 빨라집니다.

  • 패턴 3: 배포 전 부하 리허설
    큰 릴리스 전에는 간단한 부하 테스트로 피크 CPU/메모리를 기록하고 제한값을 재조정합니다. 특히 메모리 피크는 트래픽 급증 구간에서 별도 측정이 필요합니다.

  • 패턴 4: OOM 사건 후 표준 대응 문서화
    OOM이 한 번이라도 났다면 “재시작 로그, 직전 메트릭, 제한값, 코드 변경점”을 템플릿으로 남깁니다. 다음 장애에서 복구 시간이 크게 줄어듭니다.

  • 패턴 5: 임계치 경보를 제한값보다 먼저 준비
    제한 초과로 죽기 전에 경고를 받아야 합니다. 예를 들어 메모리 80%/90% 알람, CPU 지속 85% 이상 알람을 두면 사전 대응이 가능합니다.

핵심은 단순합니다. 리소스 제한은 성능을 깎는 설정이 아니라 장애 전파를 막는 안전장치입니다. 값이 너무 타이트하면 서비스가 힘들고, 너무 느슨하면 노드 전체가 위험합니다. 그래서 “측정 → 조정 → 재측정” 사이클이 가장 현실적인 정답입니다.

오늘의 결론

한 줄 요약: CPU/Memory 제한은 컨테이너 성능 옵션이 아니라 시스템 안정성을 위한 가드레일이며, 관측 기반으로 반복 튜닝해야 실무에서 효과를 낸다.

연습문제

  1. 현재 실행 중인 컨테이너 하나를 골라 CPU/메모리 제한을 적용하고, 적용 전후 docker stats와 응답시간 변화를 비교해보세요.
  2. API 컨테이너와 배치 컨테이너를 분리해 서로 다른 제한값을 부여하고, 한쪽 부하가 다른 쪽 지연에 미치는 영향을 실험해보세요.
  3. Compose 파일에 reservations/limits를 명시한 뒤 팀 문서에 “값 선정 근거(피크, 완충, 알람 임계치)”를 함께 적어보세요.

이전 강의 정답

20강 연습문제 해설:

  • logs → inspect → 비대화형 exec 순서의 핵심은 불필요한 운영 개입 최소화입니다. 먼저 읽기 정보로 가설을 세우고, 그다음 최소한의 exec를 수행해야 사고 범위를 줄일 수 있습니다.
  • docker cp 증거 수집 자동화에서는 파일 해시와 수집 시각을 같이 남겨야 분석 신뢰도가 올라갑니다. 단순 복사만 하면 나중에 “언제 기준 데이터인지”가 흐려집니다.
  • 임시 변경 후 영구 반영 절차는 “운영 핫픽스 → 코드/설정 반영 PR → 새 이미지 빌드/배포”가 한 세트입니다. 이 체인이 끊기면 재배포 때 같은 문제가 재발합니다.

실습 환경/재현 정보

  • OS: macOS 15+ 또는 Ubuntu 22.04+
  • Docker 버전: Docker Engine/Desktop 25.x 이상
  • 실습 이미지: nginx:alpine 또는 사내 API 이미지
  • 실행 순서:
    1. 제한 없이 컨테이너 실행 후 기본 사용량 관측
    2. CPU/메모리 제한 적용
    3. docker stats와 애플리케이션 응답시간 비교
    4. docker update로 제한 단계 조정
    5. Compose에 정책 반영 후 설정 검증
  • 재현 체크:
    • 제한 적용 전후 CPU/메모리 변화가 확인되는가?
    • 과도한 제한에서 지연 또는 OOM 증상이 재현되는가?
    • 완충 구간을 포함한 제한값으로 조정 시 안정성이 개선되는가?
    • 팀 문서에 제한 근거와 알람 임계치가 함께 기록되었는가?