infra
Platform

모듈 맵

[Docker] CPU/메모리 리밋 설정으로 서버 먹통 방지하는 cgroups

0 / 27 완료

펼치기
0 / 27 완료0%

Docker · 13 / 27

[Docker] CPU/메모리 리밋 설정으로 서버 먹통 방지하는 cgroups

cgroups 기반의 CPU/Memory 제한으로 Noisy Neighbor 문제를 시스템적으로 방지합니다

🚨INCIDENT ALERT
HIGH

같은 서버에 올려둔 배치 컨테이너 하나가 메모리를 계속 먹더니 API 컨테이너까지 느려졌습니다. Docker에는 컨테이너가 떠 있었지만, 자원 제한이 없어서 한 컨테이너가 호스트 전체를 사실상 독점했습니다.

컨테이너 격리는 파일시스템과 네트워크만의 문제가 아닙니다. CPU와 메모리 한도를 정하지 않으면 “시끄러운 이웃” 하나가 같은 서버의 모든 서비스를 흔듭니다. 이 모듈에서는 cgroups 기반 제한을 실험으로 확인합니다.

컨테이너 자원 격리와 과점유 방지

컨테이너는 기본적으로 호스트의 CPU와 메모리를 무제한으로 사용할 수 있습니다. 자원 제한 없이 여러 컨테이너를 실행하면 메모리 누수가 있는 컨테이너 하나가 서버 전체를 다운시킬 수 있습니다. 리눅스 cgroups 원리를 이해하고 자원 제한을 설정해 안정적인 멀티 컨테이너 환경을 구성합니다.


이번 챕터에서 배울 것

컨테이너 하나가 호스트 전체 CPU와 메모리를 독점하는 상황을 방지하는 방법을 직접 실험하며 이해합니다.

  • 1cgroups(Control Groups)가 CPU와 메모리를 격리하는 원리
  • 2--cpus / --cpu-shares로 CPU 할당량 제한하는 방법
  • 3--memory / --memory-swap로 메모리 상한 설정과 OOM Killer 동작
  • 4docker stats로 실시간 자원 사용량 모니터링
  • 5자원 과점유 시나리오 재현과 --oom-kill-disable 옵션
실습 환경 준비

stress 도구는 컨테이너 이미지에서도 설치 가능합니다. 호스트에 설치하기 어려운 환경이라면 실습 단계에서 컨테이너 내부에 설치합니다.

Docker 실행 중인지 확인
docker ps
stress 도구 설치 (CPU/메모리 부하 발생용)
sudo apt-get install -y stress
호스트 CPU 코어 수 확인
nproc
호스트 메모리 확인
free -h
💡개념

리눅스 cgroups와 Docker 자원 격리의 원리

docker run으로 컨테이너를 실행하면 기본적으로 호스트 CPU와 메모리를 무제한으로 사용합니다. 같은 서버에 여러 컨테이너가 있고 하나에 메모리 누수가 있다면, 그 컨테이너가 점점 메모리를 잠식하다 결국 다른 서비스까지 OOM Kill됩니다. Docker가 컨테이너마다 자원 제한을 강제할 수 있는 이유는 Linux 커널의 cgroups 기능을 사용하기 때문입니다. --cpus 0.5-m 256m을 입력하면 Docker는 해당 컨테이너 ID로 cgroup을 만들고 커널에 한도값을 기록합니다. 커널이 직접 이 한도를 강제하기 때문에 컨테이너 안의 프로세스는 제한을 우회할 수 없습니다. 이 ConceptBlock에서는 cgroups가 CPU와 메모리를 격리하는 원리와 Docker 자원 제한 옵션의 동작 방식을 다룹니다.

리눅스 cgroups와 Docker 자원 격리의 원리

cgroups(Control Groups)란?

cgroups는 2008년 리눅스 커널 2.6.24에 통합된 기능으로, 프로세스 그룹의 시스템 자원 사용량을 제한, 측정, 격리하는 역할을 합니다. Docker는 컨테이너마다 cgroup을 생성해 자원을 격리합니다.

리눅스 커널
└── cgroups
    ├── /sys/fs/cgroup/cpu/
    │   └── docker/[container_id]/
    │       └── cpu.shares, cpu.cfs_quota_us
    ├── /sys/fs/cgroup/memory/
    │   └── docker/[container_id]/
    │       └── memory.limit_in_bytes
    └── /sys/fs/cgroup/blkio/     # 디스크 I/O

컨테이너를 실행하면 Docker는 해당 컨테이너 ID를 이름으로 하는 cgroup을 생성하고, 거기에 자원 제한값을 기록합니다. 커널이 직접 이 값을 읽어서 제한을 강제합니다.

cgroups v1 vs v2

특징cgroups v1cgroups v2
구조자원 유형별 별도 계층단일 통합 계층
지원 시작커널 2.6.24 (2008)커널 4.5 (2016)
주요 배포판CentOS 7, Ubuntu 18.04Ubuntu 22.04, RHEL 9

Docker는 두 버전 모두 지원하며, 현대 리눅스 배포판은 대부분 v2를 사용합니다.

Docker 자원 격리의 세 가지 핵심 축

1. CPU 제한 (--cpus, --cpu-shares)

  • --cpus: 사용 가능한 CPU 코어 수 (실수 지정 가능)
  • --cpu-shares: 상대적 CPU 가중치 (기본값 1024)

2. 메모리 제한 (-m, --memory-swap)

  • -m 또는 --memory: 최대 메모리 사용량
  • --memory-swap: 스왑 포함 최대 메모리 (기본값: memory의 2배)

3. 블록 I/O 제한 (--blkio-weight)

  • 디스크 읽기/쓰기 속도 제어
로컬 터미널
# 실습 디렉토리 준비
mkdir -p /tmp/docker/part4/exam_13 && cd /tmp/docker/part4/exam_13

# 종합 자원 제한 예시
docker run \
  --cpus 0.5 \          # CPU: 0.5코어 분량
  -m 256m \             # 메모리: 최대 256MB
  --memory-swap 512m \  # 스왑 포함 최대 512MB
  --blkio-weight 300 \  # I/O 가중치 (기본 500)
  nginx

💡개념

Noisy Neighbor 문제와 자원 제한의 필요성

같은 서버에 API 서버, 배치 작업, 모니터링 에이전트가 모두 올라가 있습니다. 어느 날 배치 작업이 대용량 파일 처리를 시작하면서 CPU를 거의 다 점유합니다. API 응답 시간이 갑자기 치솟고 알람이 옵니다. 서버가 죽은 게 아닌데 API가 느려진 이유를 처음엔 찾기 어렵습니다. 자원 제한 없이 컨테이너를 운영하면 이런 Noisy Neighbor 상황이 언제든 발생할 수 있습니다. 각 서비스에 CPU와 메모리 상한을 설정하면 한 컨테이너가 폭주해도 다른 서비스는 보호됩니다. 이 ConceptBlock에서는 자원 제한이 없을 때 실제로 어떤 장애 시나리오가 펼쳐지는지, 그리고 제한값을 얼마로 설정해야 하는지 다룹니다.

Noisy Neighbor 문제와 자원 제한의 필요성

Noisy Neighbor: 조용한 재앙

Noisy Neighbor(시끄러운 이웃) 문제는 클라우드와 컨테이너 환경에서 가장 흔한 장애 원인 중 하나입니다. 자원 제한이 없는 환경에서 한 컨테이너가 자원을 독점하면 같은 호스트의 모든 서비스가 영향을 받습니다.

[자원 제한 없는 서버 — 재앙 시나리오]

서버 스펙: 4 CPU, 8GB RAM
├── web-service   (정상, 예상 사용: 0.2 CPU, 200MB)
├── api-service   (정상, 예상 사용: 0.3 CPU, 300MB)
└── batch-job     (메모리 누수 버그 존재)
    → 처음: 100MB
    → 1시간 후: 2GB
    → 3시간 후: 7GB ← 서버 전체 RAM 잠식!
    → web-service 응답 불가, api-service OOM Kill
    → 서비스 전체 다운!

메모리 누수 컨테이너의 실제 영향

로컬 터미널
# 자원 제한 없는 컨테이너가 메모리를 계속 점유하는 상황
docker stats --no-stream
# CONTAINER   CPU %   MEM USAGE / LIMIT   MEM %
# batch-job   5.2%    6.8GiB / 7.5GiB     90%  ← 위험!
# web-srv     45.3%   OOM Kill             --   ← 피해자
# api-srv     0.0%    EXIT                 --   ← 피해자

자원 제한으로 격리 보장

Docker
# 자원 제한 적용 후
docker run -m 512m --cpus 0.5 batch-job

# 결과: batch-job이 512MB를 초과하면 OOMKilled로 자기 자신만 종료
# web-service와 api-service는 영향 없음

자원 제한 설계 원칙

1. 측정 먼저: docker stats로 정상 부하 시 사용량 파악
2. 여유 있게: 평균 사용량의 1.5~2배로 제한 설정
3. 단계적 적용: 개발 → 스테이징 → 운영 순서로 적용
4. 알림 연동: 자원 사용률 80% 도달 시 경보 발송

docker stats로 실시간 자원 사용률 모니터링

실행 중인 컨테이너들의 자원 사용률을 실시간으로 확인하는 방법을 익힙니다.

1단계: 테스트용 컨테이너 실행

실습 전 디렉토리와 예제 파일을 먼저 준비합니다.

로컬 터미널
# 실습 디렉토리 준비
mkdir -p /tmp/docker/part2/exam_3 && cd /tmp/docker/part2/exam_3

# 부하 테스트용 Dockerfile 생성
cat > Dockerfile << 'EOF'
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y stress-ng
EOF

이제 실습을 진행합니다.

Docker
# CPU 부하를 일으키는 컨테이너
docker run -d --name cpu-load alpine sh -c "while true; do :; done"

# 메모리를 점진적으로 사용하는 컨테이너
docker run -d --name mem-load nginx

# 정상 컨테이너
docker run -d --name normal redis:alpine

2단계: 실시간 자원 사용률 모니터링

로컬 터미널
# 모든 컨테이너 실시간 모니터링 (1초 간격 갱신)
docker stats

# 출력 예시:
# CONTAINER   CPU %  MEM USAGE/LIMIT   MEM %  NET I/O    BLOCK I/O
# cpu-load    99.8%  1.2MiB/7.5GiB     0.0%   ...        ...
# mem-load     0.1%  23.5MiB/7.5GiB    0.3%   ...        ...
# normal       0.0%  5.8MiB/7.5GiB     0.1%   ...        ...

3단계: 스냅샷 모드 (스크립트에 유용)

로컬 터미널
# 갱신 없이 현재 상태 한 번만 출력
docker stats --no-stream

# 특정 컨테이너만
docker stats --no-stream cpu-load

# JSON 형식으로 출력 (파싱에 편리)
docker stats --no-stream --format "{{json .}}" cpu-load

4단계: 커스텀 출력 형식

로컬 터미널
docker stats --no-stream \
  --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}"
# NAME       CPU %    MEM USAGE / LIMIT    MEM %
# cpu-load   99.8%    1.2MiB / 7.5GiB     0.0%
# mem-load   0.1%     23.5MiB / 7.5GiB    0.3%

5단계: 정리

위험 명령어실행 중인 실습 컨테이너를 즉시 종료하고 삭제합니다.

자원 사용률 실습 컨테이너 강제 삭제

안전한 실행 조건: cpu-load, mem-load, normal이 이 실습에서 만든 임시 컨테이너인 경우에만 실행합니다.

실행 전 반드시 확인

  • docker ps -a에서 세 컨테이너 이름을 확인했다
  • 컨테이너 내부에 보존할 데이터가 없음을 확인했다
  • 자원 사용률 관찰을 마친 뒤 정리하는 단계임을 확인했다
docker rm -f cpu-load mem-load normal

위 항목을 모두 확인한 후 복사할 수 있습니다

🔍실행 후 확인할 것
  • docker stats 출력에서 cpu-load의 CPU 사용률이 높게 표시되는가?
  • mem-load와 normal 컨테이너의 메모리 사용량을 같은 표에서 비교했는가?
  • docker stats --no-stream으로 한 번만 출력하는 스냅샷을 확인했는가?
  • 정리 후 docker ps -a에서 실습 컨테이너가 남아있지 않은가?

CPU와 메모리 제한 설정 및 효과 확인

실제 자원 제한을 적용하고 제한이 동작하는 것을 확인합니다.

1단계: 제한 없이 CPU 부하 테스트

Docker
# 제한 없는 CPU 집중 컨테이너
docker run -d --name no-limit alpine sh -c "while true; do :; done"
docker stats --no-stream no-limit
# CPU: 거의 100% 사용

2단계: CPU 제한 적용

Docker
# CPU 0.3코어로 제한
docker run -d --name limited-cpu --cpus 0.3 \
  alpine sh -c "while true; do :; done"

docker stats --no-stream limited-cpu
# CPU %: 약 30% 수준으로 제한됨 ← 효과 확인!

3단계: 메모리 제한 및 OOMKill 확인

Docker
# 메모리 100MB 제한으로 컨테이너 실행
docker run -d --name limited-mem -m 100m \
  alpine sh -c "
    # dd로 150MB 메모리 할당 시도
    dd if=/dev/zero of=/tmp/bigfile bs=1M count=150
    sleep 3600
  "

# 잠시 후 상태 확인
docker ps -a --filter name=limited-mem
# STATUS: Exited (137) ← OOM Kill로 종료됨!

4단계: OOMKilled 상태 상세 확인

로컬 터미널
docker inspect limited-mem --format '
  컨테이너 상태: {{.State.Status}}
  OOMKilled: {{.State.OOMKilled}}
  종료코드: {{.State.ExitCode}}
'
# 컨테이너 상태: exited
# OOMKilled: true   ← 메모리 초과로 종료 확인!
# 종료코드: 137

5단계: 제한 내에서 정상 동작 확인

Docker
# 50MB만 사용 — 100MB 제한 내이므로 정상 동작
docker run -d --name ok-mem -m 100m \
  alpine sh -c "dd if=/dev/zero of=/tmp/file bs=1M count=50; sleep 3600"

docker stats --no-stream ok-mem
# MEM USAGE: ~50MiB — 정상 동작

위험 명령어실행 중이거나 종료된 실습 컨테이너를 즉시 삭제합니다.

메모리 제한 실습 컨테이너 강제 삭제

안전한 실행 조건: 메모리 제한 실습 결과 확인이 끝났고 삭제 대상이 실습 컨테이너뿐인 경우에만 실행합니다.

실행 전 반드시 확인

  • docker ps -a --filter name=mem으로 대상을 확인했다
  • OOMKilled 상태와 종료 코드를 기록했다
  • 삭제 후 다시 실습을 재현할 수 있다
docker rm -f ok-mem limited-mem

위 항목을 모두 확인한 후 복사할 수 있습니다


docker inspect로 자원 설정과 OOMKilled 상태 확인

컨테이너의 자원 설정값과 종료 원인을 docker inspect로 분석하는 방법을 익힙니다.

1단계: 자원 제한이 설정된 컨테이너 실행

Docker
docker run -d --name inspect-demo \
  --cpus 1.5 \
  -m 256m \
  --memory-swap 512m \
  nginx:alpine

2단계: 자원 설정값 확인

로컬 터미널
docker inspect inspect-demo --format '
메모리 제한: {{.HostConfig.Memory}} bytes
스왑 제한: {{.HostConfig.MemorySwap}} bytes
CPU 제한: {{.HostConfig.NanoCpus}} nanocpus
'
# 메모리 제한: 268435456 bytes (= 256MB)
# 스왑 제한: 536870912 bytes (= 512MB)
# CPU 제한: 1500000000 nanocpus (= 1.5 CPU)

3단계: OOMKill 이력 있는 컨테이너 분석

Docker
# OOMKill 유발
docker run --name oom-demo -m 50m \
  alpine sh -c "dd if=/dev/zero of=/tmp/big bs=1M count=100"

# 종합 상태 분석
docker inspect oom-demo --format '
=== 컨테이너 상태 분석 ===
상태: {{.State.Status}}
시작시각: {{.State.StartedAt}}
종료시각: {{.State.FinishedAt}}
종료코드: {{.State.ExitCode}}
OOMKilled: {{.State.OOMKilled}}
재시작횟수: {{.RestartCount}}
메모리제한: {{.HostConfig.Memory}}
'

4단계: 이벤트 로그에서 OOM 이벤트 확인

로컬 터미널
# Docker 이벤트 스트림에서 OOM 이벤트 확인
docker events --filter type=container --filter event=oom --since 10m
# 2024-01-01T12:00:05 container oom oom-demo (...)

5단계: 정리

위험 명령어OOM 진단에 필요한 컨테이너 상태 정보가 삭제됩니다.

inspect/OOM 실습 컨테이너 강제 삭제

안전한 실행 조건: docker inspect와 docker events 확인을 마친 뒤 실습 컨테이너를 정리할 때만 실행합니다.

실행 전 반드시 확인

  • OOMKilled와 ExitCode 확인을 완료했다
  • docker events 결과를 필요한 만큼 확인했다
  • 삭제 대상이 inspect-demo와 oom-demo뿐임을 확인했다
docker rm -f inspect-demo oom-demo

위 항목을 모두 확인한 후 복사할 수 있습니다


증상

로컬 터미널
docker ps -a
# NAME        STATUS
# my-app      Exited (137) 5 minutes ago ← 갑자기 종료!

docker logs my-app
# (마지막 정상 로그 이후 갑자기 끊김 — 종료 메시지 없음)

원인 분석

ExitCode 137 = 128 + 9 (SIGKILL 시그널 번호) = OOM Killer가 프로세스를 강제 종료한 것입니다.

로컬 터미널
# OOMKilled 여부 직접 확인
docker inspect my-app --format '{{.State.OOMKilled}}'
# true ← 메모리 초과로 OOM Kill 발생

# 호스트 시스템 로그에서도 확인 가능 (리눅스)
sudo dmesg | grep -i "oom\|kill"
# [12345.678] Out of memory: Kill process 9876 (node) ...
# [12345.679] Killed process 9876 (node) total-vm:524288kB ...

해결 방법

1. 현재 메모리 사용량 파악 (제한 설정 전)

Docker
# 메모리 제한 없이 실행하고 정상 부하 시 사용량 측정
docker run -d --name my-app-test my_app_image
docker stats my-app-test --no-stream
# MEM USAGE: 180MiB ← 정상 부하 시 사용량

2. 여유 있는 제한값 설정

Docker
# 정상 사용량(180MB)의 약 1.5배인 300MB로 설정
docker run -d --name my-app -m 300m --restart unless-stopped my_app_image

3. 메모리 누수 근본 원인 해결

로컬 터미널
# 메모리 사용량 추이 모니터링 (메모리 누수 탐지)
docker stats my-app --format "{{.MemUsage}}"
# 지속적으로 증가한다면 → 애플리케이션 메모리 누수 버그 수정 필요

4. 알람 설정 (사전 탐지)

로컬 터미널
# 간단한 모니터링 스크립트 (실제로는 Prometheus/Grafana 활용)
#!/bin/bash
MEM_PERCENT=$(docker stats --no-stream --format "{{.MemPerc}}" my-app | tr -d '%')
if (( $(echo "$MEM_PERCENT > 80" | bc -l) )); then
  echo "경고: my-app 메모리 사용률 ${MEM_PERCENT}% — OOM Kill 위험!"
fi

증상

--cpus 0.1로 제한을 설정했더니 정상 요청도 타임아웃이 발생합니다.

Docker
docker run --cpus 0.1 -p 8080:80 my_api
curl http://localhost:8080/api/data
# curl: (28) Operation timed out after 5000 milliseconds

원인

CPU 제한값이 실제 처리에 필요한 CPU 량보다 너무 낮게 설정되어 있습니다. CPU 스로틀링(throttling)이 발생해 요청 처리 시간이 극도로 길어집니다.

로컬 터미널
# CPU 스로틀링 확인
docker inspect my_api_container \
  --format '{{.HostConfig.NanoCpus}}'
# 100000000 nanocpus = 0.1 CPU ← 너무 낮음

# cgroup에서 직접 확인 (리눅스)
cat /sys/fs/cgroup/cpu/docker/[CONTAINER_ID]/cpu.stat
# throttled_time 12345678  ← 스로틀링 발생 시간(나노초)

해결 방법

1. 실제 CPU 사용량 측정

Docker
# 제한 없이 실행하고 부하 시 CPU 사용량 파악
docker run -d --name api-test my_api
# 실제 요청을 보내며 모니터링
docker stats api-test --no-stream
# CPU %: 35% (4코어 서버 기준 = 약 1.4 CPU 사용)

2. 적정 제한값으로 조정

로컬 터미널
docker rm api-test
# 측정값(1.4 CPU)의 1.5배인 2.0으로 설정
docker run --cpus 2.0 -p 8080:80 my_api

3. cpu-shares로 상대적 우선순위 방식 사용

절대적 제한 대신 상대적 우선순위로 자원을 배분하는 방법도 있습니다.

Docker
# 중요도 높은 서비스: 높은 가중치
docker run --cpu-shares 1024 -d my_api      # 기본값
docker run --cpu-shares 512  -d batch_job   # 절반 우선순위

# 유휴 시간에는 batch_job도 CPU를 최대한 사용하지만,
# 경합이 생기면 my_api가 2배 우선권을 가짐

💼
실무 맥락
현업 패턴

실무에서 자원 제한은 단순한 보호막이 아니라 서비스 안정성 SLA를 보장하는 핵심 설계입니다. 클라우드 인스턴스 비용 최적화와도 직결됩니다.

환경별 자원 제한 설계 접근법

Docker
# 1단계: 기준선(Baseline) 측정
# 프로덕션과 동일한 부하를 줘서 정상 사용량 파악
docker run -d my_service
# 부하 테스트 후 stats 확인
docker stats --no-stream my_service
# CPU: 0.3 CPU, MEM: 180MB

# 2단계: 제한값 설정 (기준선의 1.5~2배)
docker run --cpus 0.5 -m 300m my_service

# 3단계: 스테이징에서 부하 테스트
# 4단계: 프로덕션 적용 + 알림 설정

Docker Compose에서 자원 제한 설정

YAML
version: "3.9"

services:
  api:
    image: mycompany/api:latest
    deploy:
      resources:
        limits:
          cpus: "0.50"
          memory: 256M
        reservations:     # 최소 보장 자원 (스케줄링 힌트)
          cpus: "0.25"
          memory: 128M

  worker:
    image: mycompany/worker:latest
    deploy:
      resources:
        limits:
          cpus: "1.00"
          memory: 512M
        reservations:
          cpus: "0.50"
          memory: 256M

자원 제한과 Kubernetes 연계

Docker의 --cpus-m 개념은 Kubernetes의 resources.limits/resources.requests로 그대로 이어집니다.

YAML
# Kubernetes Pod 자원 설정 (Docker 개념과 1:1 대응)
resources:
  limits:
    cpu: "500m"      # = --cpus 0.5
    memory: "256Mi"  # = -m 256m
  requests:
    cpu: "250m"      # = --cpu-shares 기반 보장치
    memory: "128Mi"

Docker에서 자원 제한에 익숙해지면 Kubernetes 자원 관리도 자연스럽게 이해됩니다. 이 개념은 DevOps/SRE 포지션 면접에서도 자주 출제됩니다.


💡개념

메모리 swap과 OOMKilled — 진단과 대응

Node.js 서비스가 매일 새벽에 갑자기 내려갑니다. 로그는 정상 요청을 처리하다가 중간에 끊깁니다. 에러 메시지도 없습니다. docker ps -a를 보면 Exited (137)입니다. 137은 SIGKILL 시그널로 종료됐다는 의미입니다. docker inspectOOMKilled: true를 확인하면 원인이 명확해집니다. 메모리 제한을 초과하자 Linux 커널의 OOM Killer가 프로세스를 강제 종료한 것입니다. 메모리 누수 버그일 수도 있고, 제한값이 너무 낮게 설정된 것일 수도 있습니다. 이 ConceptBlock에서는 OOMKilled 상태를 진단하는 방법과 메모리 swap 옵션 활용 패턴을 다룹니다.

메모리 swap과 OOMKilled — 진단과 대응

OOMKilled란?

컨테이너가 메모리 제한을 초과하면 Linux 커널의 OOM Killer가 컨테이너 프로세스를 강제 종료합니다. 종료 코드는 137(SIGKILL)입니다.

로컬 터미널
# OOMKilled 발생 확인
docker inspect my-container --format '{{.State.OOMKilled}}'
# true ← 메모리 부족으로 강제 종료됨

docker inspect my-container --format '{{.State.ExitCode}}'
# 137 ← SIGKILL

# 실시간 메모리 사용량 모니터링
docker stats my-container --no-stream
# CONTAINER      MEM USAGE / LIMIT     MEM %
# my-container   498MiB / 512MiB       97.3%  ← 한계치 근접!

# 모든 컨테이너 실시간 통계
docker stats

메모리 swap 옵션

Docker
# -m / --memory: 메모리 제한
docker run -m 512m myapp

# --memory-swap: 메모리 + swap 합산 제한
# 주의: --memory-swap은 메모리+swap 합산값이지, swap 크기가 아님!

docker run -m 512m --memory-swap 512m myapp  # swap 비활성화 (메모리만 512MB)
docker run -m 512m --memory-swap 1g myapp    # 메모리 512MB + swap 512MB
docker run -m 512m --memory-swap -1 myapp    # swap 무제한
# --memory-swap 미지정                        # swap = 메모리 × 2 (기본)
--memory-swap 값 계산:
  메모리 512m + swap 512m = --memory-swap 1g
  (swap 크기 = --memory-swap - --memory)

OOMKilled 대응 방법

로컬 터미널
# 1. 메모리 제한 증가 (실행 중에도 적용 가능)
docker update --memory 1g my-container

# 2. 메모리 + swap 모두 증가
docker update --memory 1g --memory-swap 2g my-container

# 3. JVM 기반 앱: JVM 힙 크기 제한
docker run -m 1g \
  -e JAVA_OPTS="-Xmx800m -Xms400m" \
  java-app

# 4. Node.js: max-old-space-size 설정
docker run -m 512m \
  -e NODE_OPTIONS="--max-old-space-size=400" \
  node-app

OOM 발생 전 경보 설정

로컬 터미널
# 간단한 셸 스크립트로 모니터링
watch -n 5 'docker stats --no-stream --format "table {{.Name}}\t{{.MemPerc}}"'

💡개념

CPU 제한 심화 — cpus vs cpu-shares vs cpuset

같은 서버에 API 서버와 데이터 집계 배치 작업이 올라가 있습니다. 배치가 실행될 때마다 API 응답이 느려집니다. --cpus로 배치의 CPU 한도를 고정하면 배치가 빨리 끝나지 않더라도 API가 느려지지 않습니다. 반면 낮 시간에는 배치 실행 빈도가 낮아 CPU 여유가 있는데도 --cpus로 묶어두면 낭비가 됩니다. 이런 상황에서는 --cpu-shares로 경합 시 우선순위만 설정하고 평소에는 자유롭게 쓰게 하는 것이 더 효율적입니다. --cpuset-cpus는 특정 코어에 컨테이너를 고정해야 하는 저지연 서비스에 씁니다. 세 옵션은 각자 다른 문제를 해결합니다. 이 ConceptBlock에서는 세 가지 CPU 제한 방식의 동작 원리와 선택 기준을 다룹니다.

CPU 제한 심화 — cpus vs cpu-shares vs cpuset

세 가지 CPU 제한 방식

CPU를 제한하는 옵션이 세 가지 있으며 각자 역할이 다릅니다.

--cpus: 절대적 사용량 제한

컨테이너가 사용할 수 있는 CPU 코어의 최대량을 고정합니다.

Docker
# 최대 1.5개 코어 분량 사용
docker run --cpus 1.5 myapp

# 4코어 서버에서:
# --cpus 1.0 → 전체 CPU의 25% 사용 가능
# --cpus 2.0 → 전체 CPU의 50% 사용 가능
# --cpus 4.0 → 제한 없음 (전체 사용)

--cpu-shares: 경합 시 상대적 우선순위

CPU가 여유 있을 때는 제한이 없고, 경합이 생겼을 때 비율대로 분배합니다.

Docker
# 기본값: 1024
# high-priority: 2048 (기본의 2배)
# low-priority: 512 (기본의 절반)

docker run --cpu-shares 2048 --name critical-service myapp   # 고우선순위
docker run --cpu-shares 512  --name batch-worker myapp       # 저우선순위

# 경합 없을 때: 두 컨테이너 모두 필요한 만큼 CPU 사용
# 경합 있을 때: critical-service가 batch-worker의 4배 CPU 받음

--cpuset-cpus: 특정 코어에 고정

컨테이너를 지정한 CPU 코어에만 실행되도록 고정합니다.

Docker
# CPU 0번, 1번만 사용 (나머지 코어는 다른 컨테이너용)
docker run --cpuset-cpus "0,1" nginx

# CPU 2번부터 3번 (범위 표기)
docker run --cpuset-cpus "2-3" worker

# CPU 0번 하나만 (단일 코어 격리)
docker run --cpuset-cpus "0" realtime-app

세 옵션 비교표

옵션제한 방식CPU 여유 시CPU 경합 시주요 사용 사례
--cpus절대 상한선상한선까지만상한선까지만일반적인 격리
--cpu-shares상대 비율무제한비율대로 분배중요도 차등
--cpuset-cpus코어 고정해당 코어만해당 코어만NUMA 최적화, 저지연

실전 조합 예시

Docker Compose 버전에 따른 컨테이너 자원 제한 설정 필드 계보

여러 서비스를 유기적으로 연결하고 자원을 안정적으로 통제하기 위해, SRE는 Docker Compose YAML 파일 상에 리소스 상한선(Limits)과 예약 수준(Reservations)을 명세해야 합니다. 이때 Docker Compose 버전에 따라 사용하는 스펙 명세법이 상이하므로 아래의 계보 정리 표를 참조하여 혼란을 방지해야 합니다.

Docker Compose 자원 제한 설정 계보:

Compose 버전메모리 제한 키CPU 제한 키구동 형태 및 제약
v2 스펙 (Legacy)mem_limit: 512mcpus: 0.5docker-compose up을 통한 로컬 개발 환경에서 즉시 제어 작동
v3 스펙 (Modern)deploy.resources.limits.memory: 512Mdeploy.resources.limits.cpus: '0.5'deploy 객체 아래 선언. 기본적으로 Swarm/K8s 스택 배포용이나 Compose v2 엔진에서도 호환 가능
YAML
version: '3.8'

services:
  web:
    image: nginx:alpine
    ports:
      - "8080:80"
    deploy:
      resources:
        limits:
          cpus: '0.5'           # 컨테이너가 사용할 수 있는 최대 CPU 코어 수 (0.5개)
          memory: 512M          # 컨테이너가 쓸 수 있는 최대 메모리 임계치 (mem_limit)
        reservations:
          memory: 256M          # 컨테이너가 보장받을 수 있는 최소 물리 메모리
  • 리소스 적용 및 가동: 상위 명세서가 담긴 디렉토리에서 docker-compose up 명령어를 통해 환경을 기동하면, Docker Engine은 cgroups API를 자동으로 호출하여 각 컨테이너 데몬에 물리적 메모리 상한과 CPU 할당 주기를 강제 제어합니다.

Docker Compose v3에서의 컨테이너 자원 제한 설정

여러 서비스를 유기적으로 연결하고 자원을 안정적으로 통제하기 위해, SRE는 Docker Compose YAML 파일 상에 리소스 상한선(Limits)과 예약 수준(Reservations)을 명세해야 합니다.

Docker Compose v3 이상 스펙에서는 deploy.resources.limits 아래에 cpusmemory를 설정하여 컨테이너의 CPU와 메모리를 강력하게 제어합니다.

YAML
version: '3.8'

services:
  web:
    image: nginx:alpine
    ports:
      - "8080:80"
    deploy:
      resources:
        limits:
          cpus: '0.5'           # 컨테이너가 사용할 수 있는 최대 CPU 코어 수 (0.5개)
          memory: 512M          # 컨테이너가 쓸 수 있는 최대 메모리 임계치 (mem_limit)
        reservations:
          memory: 256M          # 컨테이너가 보장받을 수 있는 최소 물리 메모리
  • 리소스 적용 및 가동: 상위 명세서가 담긴 디렉토리에서 docker-compose up 명령어를 통해 환경을 기동하면, Docker Engine은 cgroups API를 자동으로 호출하여 각 컨테이너 데몬에 물리적 메모리 상한과 CPU 할당 주기를 강제 제어합니다.
Docker
# 중요한 API 서버: 최소 2코어 보장 + CPU 0,1에 고정
docker run -d \
  --name api-server \
  --cpus 2.0 \
  --cpuset-cpus "0,1" \
  api:latest

# 배치 작업: CPU 2,3 사용 + 낮은 우선순위
docker run -d \
  --name batch-job \
  --cpus 2.0 \
  --cpuset-cpus "2,3" \
  --cpu-shares 512 \
  batch:latest

정리

컨테이너 자원 제한의 핵심 명령어를 요약합니다.

명령의미
--cpus 0.5CPU 0.5코어 분량으로 제한
-m 256m메모리 최대 256MB
--memory-swap 512m스왑 포함 최대 512MB
docker stats실시간 자원 사용률 모니터링
docker inspect ... OOMKilledOOM 종료 여부 확인

자원 제한 없이 운영하는 것은 안전벨트 없이 운전하는 것과 같습니다. 모든 컨테이너에 적절한 CPU와 메모리 제한을 설정해 Noisy Neighbor 문제를 예방하세요.

다음 모듈에서는 컨테이너 로그를 확인하고, 로그 로테이션으로 디스크 100% 장애를 예방하는 방법을 다룹니다.

지식 확인

퀴즈 — 5문제

Q1

리눅스 cgroups(Control Groups)의 주요 역할은 무엇인가요?

Q2

`docker run --cpus 0.5` 옵션의 의미는 무엇인가요?

Q3

컨테이너가 메모리 제한을 초과했을 때 발생하는 이벤트는 무엇인가요?

Q4

Noisy Neighbor 문제란 무엇인가요?

Q5

Docker Compose에서 컨테이너 자원 제한을 설정하는 올바른 방법은?

0 / 5 답변

🧪 실습으로 확인하기

Docker Compose 멀티 서비스 구성

초급

docker-compose.yml로 nginx + 앱 컨테이너를 함께 정의하고, 서비스 간 통신과 볼륨 마운트를 구성한다.

35📋 4단계💻 직접 환경
실습 시작하기 →

이것도 배워보세요

docker중급 · 60
[Docker] 컨테이너 환경에서 처음 마주하는 흔한 오류 5가지 극복기
Docker 트랙 계속
networking입문 · 45
[Network] OSI 7계층과 TCP/IP 4계층 모델 실무적 관점 분석
Networking 트랙 시작점