Docker 컨테이너 성능 최적화: CPU 및 메모리 제한 설정
CPU 및 메모리 제한을 설정하여 Docker 컨테이너 성능을 최적화하는 방법을 알아보세요. 이 가이드는 CPU 공유, 할당량, 메모리 제한, 스왑과 같은 필수 구성 옵션을 다룹니다. `docker stats`를 사용하여 컨테이너 리소스 사용량을 모니터링하고 리소스 부족을 방지하며 애플리케이션 안정성을 개선하고 전체 시스템 효율성을 향상시키는 모범 사례를 구현하는 방법을 알아보세요.
Docker 컨테이너 성능 최적화: CPU 및 메모리 제한 설정
Docker 컨테이너는 고정된 리소스를 가진 작은 가상 머신처럼 자동으로 동작하지 않습니다. Docker에 달리 지시하지 않는 한, 컨테이너는 다른 프로세스처럼 호스트 CPU와 메모리를 놓고 경쟁할 수 있습니다. 이는 노트북에서는 편리하지만 공유 서버에서는 위험합니다.
CPU 및 메모리 제한은 마법 같은 성능 향상 도구가 아닙니다. 너무 낮은 제한은 애플리케이션을 느리게 하거나 불안정하게 만듭니다. 너무 높은 제한은 호스트를 보호하지 못합니다. 목표는 각 컨테이너가 정상 및 피크 작업을 수행할 수 있는 충분한 공간을 제공하면서 하나의 잘못된 프로세스가 머신의 다른 모든 것을 중단시키는 것을 방지하는 것입니다.
제한이 중요한 이유
일반적인 실패 모드는 익숙합니다. 하나의 컨테이너가 폭주 작업을 시작하고, 메모리가 증가하며, 호스트가 스와핑을 시작하고, 관련 없는 서비스가 느려집니다. 또는 CPU 집약적인 배치 작업이 모든 코어를 사용하고 옆에 있는 API 컨테이너가 지연 시간 목표를 놓치기 시작합니다.
제한은 세 가지 실용적인 문제를 해결하는 데 도움이 됩니다:
- 단일 컨테이너가 모든 메모리를 사용하는 것으로부터 호스트를 보호합니다.
- 컨테이너가 프로덕션과 유사한 제약 조건에서 실행되므로 성능 테스트가 더 정직해집니다.
- 애플리케이션이 이웃에서 리소스를 조용히 빌리는 대신 확장이나 튜닝이 필요할 때 이를 인지하도록 강제합니다.
임의의 숫자로 시작하지 마십시오. 부하가 있는 상태에서 서비스를 실행하고, 실제 사용량을 관찰하고, 여유 공간을 두고 제한을 설정하십시오. 작은 백그라운드 작업자와 JVM 서비스는 매우 다른 처리가 필요합니다.
CPU 제어
Docker는 몇 가지 CPU 제어 기능을 제공합니다. 일상적으로 가장 유용한 것은 --cpus입니다.
컨테이너를 약 1.5개의 CPU로 제한합니다:
docker run -d --name api --cpus="1.5" nginx
이는 CFS 할당량과 기간을 수동으로 설정하는 것보다 읽기 쉽습니다. 내부적으로 Docker는 Linux CPU 스케줄링 제어를 사용합니다.
--cpu-shares는 다릅니다. 이는 하드 캡이 아닌 CPU 경합 중 상대적 가중치입니다:
docker run -d --name important-api --cpu-shares 2048 nginx
docker run -d --name batch-worker --cpu-shares 512 worker-image
호스트에 유휴 CPU가 있으면 두 컨테이너 모두 더 많이 사용할 수 있습니다. 경합할 때 첫 번째 컨테이너가 더 많은 스케줄링 가중치를 갖습니다. 이는 우선순위 지정에 유용하지만, 컨테이너가 여유 CPU를 사용하는 것을 막지는 않습니다.
정확한 CFS 설정이 필요한 경우 --cpu-period 및 --cpu-quota를 계속 사용할 수 있습니다:
docker run -d --name limited-api \
--cpu-period 100000 \
--cpu-quota 50000 \
nginx
이는 각 100,000마이크로초 기간 동안 컨테이너에 50,000마이크로초의 CPU 시간을 제공하며, 대략 CPU 0.5개에 해당합니다. 대부분의 팀에게 --cpus="0.5"가 동일한 의도를 더 명확하게 전달합니다.
메모리 제어
메모리 제한은 CPU 제한보다 더 위험합니다. 초과하면 프로세스가 종료될 수 있기 때문입니다. 신중하게 설정하고 피크 동작을 테스트하십시오.
기본 옵션은 --memory입니다:
docker run -d --name web --memory 512m nginx
컨테이너가 제한을 초과하면 커널이 컨테이너 내 프로세스를 종료할 수 있습니다. Docker 출력에서 OOM 종료로 자주 표시됩니다:
docker inspect web --format '{{.State.OOMKilled}}'
스왑 동작은 이해하기 쉽지 않습니다. --memory가 설정되면 --memory-swap은 스왑 허용량 자체가 아니라 총 메모리와 스왑 허용량입니다.
다음은 256MB의 RAM과 최대 256MB의 스왑을 허용하여 총 512MB를 제공합니다:
docker run -d --name worker --memory 256m --memory-swap 512m alpine
--memory-swap을 --memory와 동일하게 설정하면 스왑 계정이 활성화된 시스템에서 해당 컨테이너에 대한 추가 스왑을 비활성화합니다:
docker run -d --name no-extra-swap --memory 256m --memory-swap 256m alpine
--memory-swap -1을 설정하면 호스트의 사용 가능한 스왑까지 무제한 스왑을 허용합니다. 이는 프로세스를 계속 실행할 수 있지만 지연 시간이 매우 나빠질 수 있습니다.
전후 모니터링
서비스가 실제 부하 상태일 때 docker stats를 사용하십시오:
docker stats
docker stats web worker
CPU %, MEM USAGE / LIMIT, BLOCK I/O, PIDS를 관찰하십시오. 메모리 제한의 100%에 있는 서비스는 "효율적"이 아닙니다. 트래픽 급증 하나만으로 종료될 위험이 있습니다. 지속적으로 제한되는 CPU 바운드 서비스는 사용자가 느린 요청을 경험하는 동안 평균 CPU가 허용 가능하게 나타날 수 있습니다.
빠른 일회성 보기를 위해:
docker stats --no-stream
OOM 및 재시작 단서를 위해:
docker ps -a
docker inspect <container> --format 'OOM={{.State.OOMKilled}} Exit={{.State.ExitCode}}'
docker logs --tail 100 <container>
지속적인 모니터링을 위해 컨테이너 메트릭을 Prometheus, Grafana, 클라우드 모니터링 서비스 또는 이미 사용 중인 플랫폼으로 내보내십시오. docker stats는 장기 경고 시스템이 아닌 트라이지입니다.
합리적인 튜닝 워크플로우
테스트 환경에서 엄격한 제한 없이 시작하고 유휴, 정상 및 피크 사용량을 캡처하십시오. 그런 다음 가비지 수집, 캐시 증가, TLS 핸드셰이크 및 짧은 버스트를 위한 충분한 여유 공간을 두고 관찰된 피크 위로 메모리를 설정하십시오. CPU의 경우 하드 캡이 필요한지 아니면 상대적 우선순위만 필요한지 결정하십시오.
소규모 API의 예:
docker run -d --name api \
--cpus="2" \
--memory 1g \
--memory-swap 1g \
-p 8080:80 \
my-api:latest
이는 API가 최대 2개의 CPU, 1GB의 RAM 및 추가 스왑 허용량 없음을 의미합니다. 보편적으로 올바른 것은 아닙니다. 해당 범위 근처에서 테스트된 서비스에 대한 명확한 시작점입니다.
Docker Compose의 경우 로컬 Docker 엔진은 다음과 같은 리소스 옵션을 지원합니다:
services:
api:
image: my-api:latest
mem_limit: 1g
cpus: 2.0
Compose와 Swarm/Kubernetes 리소스 설정은 동일하지 않으므로 필드가 모든 곳에서 동일하게 동작한다고 가정하기 전에 배포 대상을 확인하십시오.
일반적인 실수
메모리를 너무 낮게 설정하는 것이 가장 일반적인 실수입니다. 애플리케이션이 시작되고, 스모크 테스트를 통과한 다음, 테스트가 다루지 않은 것보다 더 많은 메모리를 할당하는 요청 패턴에서 죽습니다.
비효율적인 코드를 숨기기 위해 CPU 제한을 사용하는 것도 또 다른 실수입니다. 서비스가 요청당 비용이 많이 드는 작업을 수행하기 때문에 느린 경우, 더 낮은 CPU 제한은 증상을 더 명확하게 만듭니다. 코드 경로를 수정하지는 않습니다.
언어 런타임을 무시하는 것도 해롭습니다. JVM, Node.js, Go, Python 및 Ruby 앱은 메모리 압력에 다르게 반응합니다. 일부 런타임은 내부 메모리 가정이 컨테이너 제한과 일치하도록 명시적인 힙 설정이 필요합니다.
마지막으로, 하나의 컨테이너만 튜닝하지 마십시오. 호스트는 여전히 커널, Docker, 로깅, 모니터링 에이전트 및 모든 사이드카 프로세스를 위한 CPU와 메모리가 필요합니다. 여유 공간을 남겨 두십시오.
좋은 제한은 실패를 더 작게 만들고 성능을 더 예측 가능하게 만듭니다. 추측이 아닌 측정에서 비롯되어야 합니다.