느린 Docker 컨테이너 문제 해결: 단계별 성능 가이드

CPU, 메모리, 디스크 I/O, 네트워킹, 제한, 마운트 및 로깅을 확인하여 Docker 컨테이너가 느린 이유를 찾으세요.

느린 Docker 컨테이너 문제 해결: 단계별 성능 가이드

Docker 컨테이너가 느리다고 느껴질 때, 이미지를 다시 빌드하거나 임의의 런타임 플래그를 변경하는 것부터 시작하지 마세요. 먼저 "느리다"는 것이 무엇을 의미하는지 결정하세요. API 응답 시간이 높은가요? 작업자가 뒤처지고 있나요? 시작이 느린가요? 빌드가 느린가요? 호스트가 과부하 상태인가요? 각각은 다른 수정 사항을 가리킵니다.

컨테이너는 물리 법칙으로부터의 마법적인 격리가 아닙니다. 여전히 호스트 CPU, 호스트 메모리, 호스트 스토리지, 호스트 네트워킹 및 사용자가 제공한 애플리케이션 코드를 사용합니다. Docker는 이러한 리소스 주변에 제어 및 네임스페이스를 추가하지만, 느린 쿼리를 빠르게 만들거나 포화된 디스크를 유휴 상태로 만들지는 않습니다.

빠른 실시간 보기로 시작하세요:

docker stats

속도 저하를 재현하는 동안 컨테이너를 관찰하세요. 단일 스냅샷은 부하 상태에서 무엇이 변하는지 보는 것보다 덜 유용합니다. CPU가 급등하고 높게 유지되면 CPU 문제가 있습니다. 메모리가 컨테이너가 죽을 때까지 계속 올라가면 메모리 경로를 따르세요. BLOCK I/O가 요청이 지연되는 동안 많이 움직이면 스토리지에 주목하세요. 컨테이너가 평온해 보이지만 사용자가 여전히 지연 시간을 경험하면 앱, 네트워크 호출, 데이터베이스 또는 업스트림 서비스를 살펴보세요.

먼저, 컨테이너와 호스트 상태 비교

느린 컨테이너는 단순히 느린 호스트에서 실행 중일 수 있습니다. 두 수준을 모두 확인하세요.

docker stats <container>
top
free -h
df -h

Linux에서는 iostat -xz 1을 사용할 수 있으면 유용합니다. 높은 디스크 사용률이나 긴 대기 시간은 느린 데이터베이스, 패키지 설치 및 로그가 많은 서비스를 설명할 수 있습니다. Docker Desktop에서는 Docker VM에 할당된 CPU 및 메모리도 확인하세요. 충분한 메모리가 있는 Mac도 Docker Desktop이 너무 낮게 제한되면 컨테이너가 부족할 수 있습니다.

모든 컨테이너가 느리면 호스트가 의심됩니다. 하나의 컨테이너가 느리고 이웃이 정상이면 해당 워크로드, 제한, 마운트 및 종속성에 집중하세요.

CPU 병목 현상

docker stats에서 CPU는 코어 전체 사용량을 보고하기 때문에 100%를 초과할 수 있습니다. 200%를 사용하는 컨테이너는 대략 두 개의 코어를 사용하는 것입니다. 중요한 질문은 이것이 워크로드에 예상되는 것인지 여부입니다.

런타임 제한 확인:

docker inspect <container> --format 'NanoCPUs={{.HostConfig.NanoCpus}} CpuQuota={{.HostConfig.CpuQuota}} CpuPeriod={{.HostConfig.CpuPeriod}} Cpuset={{.HostConfig.CpusetCpus}}'

서비스가 --cpus=0.5로 시작된 경우 정상 트래픽에서 제한될 수 있습니다. Kubernetes 또는 Compose에서 동일한 문제가 CPU 제한에 숨겨질 수 있습니다. 노트북에서 빠르게 작업을 처리한 작업자는 CI에서 절반의 CPU만 얻기 때문에 느려질 수 있습니다.

애플리케이션 수준 CPU의 경우 추측 대신 프로세스를 프로파일링하세요. Node의 경우 내장 CPU 프로파일링 또는 clinic 스타일 도구를 사용하세요. Python의 경우 허용되는 곳에서 py-spy로 샘플링하세요. Java의 경우 JFR 또는 async-profiler를 사용하세요. 프로덕션 이미지 내부에 도구를 설치할 수 없는 경우 스테이징 환경에서 동일한 이미지를 실행하거나 디버그 컨테이너 패턴을 사용하세요.

일반적인 CPU 원인으로는 빡빡한 폴링 루프, 비싼 JSON 직렬화, 정규식 역추적, 이미지 처리, 압축 및 너무 적은 코어를 놓고 경쟁하는 너무 많은 작업자 스레드가 있습니다. CPU를 늘리는 것은 앱이 사용할 수 있고 호스트에 용량이 있는 경우에만 도움이 됩니다.

메모리 압력 및 OOM 종료

메모리 문제는 메모리 사용량 증가, 빈번한 가비지 수집, 호스트의 스왑 활동 또는 갑작스러운 종료로 나타납니다. OOM 상태 확인:

docker inspect <container> --format 'exit={{.State.ExitCode}} oom={{.State.OOMKilled}} memory={{.HostConfig.Memory}}'

OOMKilled=true이면 컨테이너가 메모리 상황을 초과한 것입니다. 이는 명시적인 --memory 제한, Docker Desktop VM 제한 또는 호스트 전체 압력일 수 있습니다.

실제 트래픽을 보내면서 docker stats를 사용하세요. 메모리가 평평해지지 않고 계속 증가하면 누수, 무제한 캐시, 대기열 축적 또는 한 번에 너무 많은 데이터를 로드하는 워크로드를 의심하세요. 시작 중에 메모리가 급등했다가 안정되면 제한이 런타임에 비해 단순히 너무 낮을 수 있습니다.

언어 기본값이 중요합니다. Java, Node 및 일부 애플리케이션 서버는 버전 및 구성에 따라 컨테이너 내부에서 메모리를 다르게 예약하거나 사용할 수 있습니다. 예측 가능한 동작이 필요할 때 명시적인 힙 또는 메모리 옵션을 설정하세요. 예를 들어, Java 서비스는 컨테이너 인식 힙 백분율이 필요할 수 있습니다. Node 서비스는 --max-old-space-size가 필요할 수 있습니다. 데이터베이스는 프로세스와 파일 시스템을 위한 공간을 남겨두는 캐시 설정이 필요합니다.

앱이 모든 시간을 가비지 수집에 소비하도록 메모리 제한을 너무 빡빡하게 설정하지 마세요. 충돌하지 않지만 지속적으로 일시 중지되는 컨테이너는 여전히 고장난 것입니다.

디스크 I/O 및 느린 바인드 마운트

CPU 및 메모리 그래프가 정상으로 보이기 때문에 스토리지 문제는 놓치기 쉽습니다. Docker에서 디스크 속도 저하는 종종 네 가지 중 하나에서 발생합니다: 무거운 애플리케이션 I/O, 과도한 로그, 스토리지 드라이버 또는 Docker Desktop의 바인드 마운트.

Docker의 보기 확인:

docker stats <container>
docker logs --tail 20 <container>

로그가 매우 시끄러우면 로깅 드라이버가 할 일이 있는 것입니다. JSON-file 로그는 회전이 구성되지 않으면 빠르게 커질 수 있습니다. 바쁜 서비스에서 모든 요청 본문이나 디버그 라인을 로깅하는 것은 실제 성능 문제가 될 수 있습니다.

로깅 설정 검사:

docker inspect <container> --format '{{json .HostConfig.LogConfig}}'

로컬 및 소규모 서버 설정의 경우 데몬 구성 또는 Compose 파일에서 로그 회전을 고려하세요. 프로덕션 플랫폼의 경우 플랫폼의 로깅 시스템으로 로그를 전송하고 애플리케이션 로그 볼륨을 의도적으로 유지하세요.

바인드 마운트는 macOS 및 Windows에서 특별한 주의가 필요합니다. 호스트에서 Linux 컨테이너로 마운트된 소스 트리는 가상화 계층을 교차합니다. 이는 개발에 편리하지만 종속성 폴더, 데이터베이스 또는 쓰기 작업이 많은 디렉토리의 경우 명명된 볼륨보다 훨씬 느릴 수 있습니다.

예를 들어, Node 개발 컨테이너는 node_modules가 바인드 마운트에 있는 경우 느릴 수 있습니다. 더 나은 패턴은 소스 코드를 바인드 마운트하지만 종속성을 명명된 볼륨에 유지하는 것입니다:

services:
  app:
    volumes:
      - .:/app
      - node_modules:/app/node_modules
volumes:
  node_modules:

데이터베이스의 경우 호스트 경로가 필요한 특정 백업 또는 검사 워크플로가 없는 한 바인드 마운트보다 명명된 볼륨을 선호하세요.

네트워크 지연 시간 및 종속성 속도 저하

컨테이너는 다른 서비스를 기다리고 있기 때문에 "느릴" 수 있습니다. 로컬 프로세스는 정상이지만 DNS, 데이터베이스, Redis, API 또는 프록시가 느릴 수 있습니다.

컨테이너 내부에서 테스트:

docker exec -it <container> sh
curl -w '
lookup:%{time_namelookup} connect:%{time_connect} start:%{time_starttransfer} total:%{time_total}
' -o /dev/null -s http://service:8080/health

curl -w 출력은 DNS 조회, TCP 연결, 첫 번째 바이트 및 총 시간을 분리합니다. DNS 조회가 느리면 /etc/resolv.conf 및 Docker 데몬 DNS 설정을 검사하세요. 연결이 느리거나 실패하면 네트워크, 방화벽 및 서비스 바인딩을 확인하세요. 첫 번째 바이트까지의 시간이 느리면 업스트림 서비스가 연결을 수락했지만 응답하는 데 시간이 걸린 것입니다.

컨테이너 간 트래픽의 경우 사용자 정의 브리지 네트워크를 사용하여 컨테이너가 이름으로 서로를 확인할 수 있도록 하세요:

docker network create appnet
docker run -d --name api --network appnet my-api
docker run --rm --network appnet curlimages/curl http://api:8080/health

실제 트래픽이 컨테이너 간일 때 게시된 호스트 포트를 통해 벤치마킹하지 마세요. 프로덕션에서 사용하는 경로를 테스트하세요.

시작 성능은 별도의 문제

느린 시작은 종종 이미지 풀 시간, 컨테이너 시작 시 종속성 설치, 데이터베이스 마이그레이션 또는 애플리케이션 워밍업에서 발생합니다.

컨테이너는 매번 시작할 때 패키지를 설치해서는 안 됩니다. 엔트리포인트가 매번 부팅 시 npm install, pip install, apt-get을 실행하거나 바이너리를 다운로드하는 경우, 강력한 이유가 없는 한 해당 작업을 이미지 빌드로 이동하세요.

앱에서 제공하는 경우 타임스탬프가 있는 시작 로그를 확인하세요. 그렇지 않은 경우 디버깅하는 동안 엔트리포인트 단계 주변에 간단한 타임스탬프를 추가하세요:

date; echo 'starting migrations'
# migration command
date; echo 'starting server'
# server command

네트워크를 통해 풀링된 이미지의 경우 이미지 크기가 중요합니다. 다단계 빌드, .dockerignore 및 더 작은 런타임 베이스는 콜드 스타트 및 배포 속도를 향상시킵니다. 그러나 이미지가 이미 존재하고 컨테이너가 실행 중이면 이미지 크기는 일반적으로 CPU, 메모리, I/O 및 애플리케이션 동작보다 덜 중요합니다.

빌드 성능은 런타임 성능이 아닙니다

느린 Docker 빌드는 실망스럽지만 다른 종류의 문제입니다. 코드 변경이 매 빌드마다 종속성 설치를 강제하는 경우 레이어 순서를 수정하세요:

COPY package.json package-lock.json ./
RUN npm ci
COPY . .

모든 소스 변경이 종속성 레이어를 무효화하지 않으려면 종속성을 설치하기 전에 전체 저장소를 복사하지 마세요.

또한 빌드 컨텍스트를 작게 유지하세요:

.git
node_modules
coverage
dist
*.log

BuildKit 캐시 마운트는 반복되는 종속성 다운로드에 도움이 될 수 있지만, 먼저 Dockerfile이 올바르게 정렬되었는지 확인하세요. 캐시 마운트는 너무 일찍 캐시를 무효화하는 Dockerfile을 완전히 저장할 수 없습니다.

리소스 제한은 호스트를 보호하고 앱을 해칠 수 있습니다

CPU 및 메모리 제한은 하나의 컨테이너가 호스트를 다운시키지 않도록 하기 때문에 유용합니다. 또한 워크로드를 측정하지 않고 예제에서 복사하면 인위적인 속도 저하를 만들 수 있습니다.

제한 검사:

docker inspect <container> --format '{{json .HostConfig}}' | jq '{Memory, NanoCpus, CpuQuota, CpuPeriod, BlkioWeight}'

jq를 사용할 수 없는 경우 컨테이너를 정상적으로 검사하고 HostConfig를 검색하세요.

Compose의 경우 실제 렌더링된 구성을 확인하세요:

docker compose config

이는 재정의 파일 또는 환경 변수에서 상속된 제한을 포착합니다. 일반적인 놀라움은 낮은 제한을 설정하고 실수로 테스트 환경에서 사용되는 개발 재정의 파일입니다.

실용적인 진단 흐름

불만이 단순히 "컨테이너가 느리다"일 때 이 흐름을 사용하세요:

  1. 느린 동작을 재현하고 재현 중에 docker stats를 실행하세요.
  2. 호스트 CPU, 메모리, 디스크 및 Docker Desktop VM 제한을 확인하세요.
  3. 컨테이너 CPU 및 메모리 제한을 검사하세요.
  4. 재시도, 연결 시간 초과, 마이그레이션, 디버그 로깅 또는 OOM 힌트에 대한 로그를 읽으세요.
  5. curl, dig 또는 목적에 맞게 제작된 디버그 이미지를 사용하여 컨테이너 내부에서 종속성을 테스트하세요.
  6. 마운트 확인: 쓰기 작업이 많은 경로를 적절한 경우 명명된 볼륨으로 이동하세요.
  7. 리소스 그래프가 코드를 가리키는 경우 애플리케이션을 프로파일링하세요.

가장 좋은 수정 사항은 구체적인 경향이 있습니다: 너무 낮은 메모리 제한을 높이기, 큰 페이로드 로깅 중단, 데이터베이스 데이터를 바인드 마운트에서 이동, 느린 DNS 경로 수정, Dockerfile 레이어 재정렬 또는 애플리케이션 런타임 조정. 일반적인 "Docker 최적화" 조언은 실제로 어떤 리소스가 느린지 증명하는 것보다 덜 유용합니다.