Docker 컨테이너 최적화: 성능 병목 현상 문제 해결

Docker 컨테이너가 느리게 실행되나요? 이 필수 가이드에서는 컨테이너화된 애플리케이션에서 일반적인 성능 병목 현상을 식별하고 해결하는 방법을 자세히 설명합니다. `docker stats`와 같은 Docker 모니터링 도구를 효과적으로 사용하고, 높은 CPU/메모리 사용량을 진단하며, 스토리지 드라이버 인식을 통해 I/O 성능을 최적화하고, 멀티 스테이지 빌드와 같은 모범 사례를 적용하여 더 빠르고 효율적인 운영을 수행하는 방법을 알아보세요.

Docker 컨테이너 최적화: 성능 병목 현상 문제 해결

Docker 컨테이너가 느릴 때, 컨테이너 자체가 전체 원인인 경우는 드뭅니다. 문제는 대개 한 단계 아래에 있습니다: CPU 제한, 메모리 압박, 느린 디스크 쓰기, DNS 지연, 호스트의 시끄러운 이웃, 또는 컨테이너화되기 전부터 이미 비효율적이었던 애플리케이션입니다.

가장 빠르게 시간을 낭비하는 방법은 어떤 리소스가 제한되어 있는지 알기도 전에 Docker 플래그를 변경하기 시작하는 것입니다. 증거부터 시작하고, 하나의 병목 현상을 격리하고, 한 가지를 변경하고, 다시 측정하세요.

튜닝이 아닌 분류부터 시작

docker stats는 빠른 실시간 보기를 제공합니다:

docker stats
docker stats --no-stream

기본 질문에 답하는 데 사용하세요:

  • CPU가 높고 지속되나요?
  • 메모리가 설정된 제한에 가깝나요?
  • 느린 요청 중에 블록 I/O가 증가하나요?
  • 프로세스 수가 예상치 못하게 높나요?
  • 네트워크 I/O가 워크로드와 일치하나요?

그런 다음 컨테이너 상태와 로그를 확인하세요:

docker logs --tail 100 <container_name_or_id>
docker inspect <container_name_or_id> --format 'OOM={{.State.OOMKilled}} Exit={{.State.ExitCode}} Restarting={{.State.Restarting}}'

또한 호스트를 살펴보세요. 호스트가 스와핑 중이거나 디스크가 포화 상태일 때 컨테이너는 무고해 보일 수 있습니다:

top
free -m
vmstat 1
iostat -xz 1

iostatsysstat 패키지가 필요할 수 있습니다. Docker Desktop을 사용 중이라면 호스트 OS와 Linux 컨테이너 사이에 VM이 있어 파일 및 네트워크 동작이 변경된다는 점을 기억하세요.

CPU 병목 현상

높은 CPU는 애플리케이션이 바쁘거나, 과소 프로비저닝되었거나, 제한되었음을 의미할 수 있습니다. 이들은 서로 다른 문제입니다.

구성된 CPU 설정을 확인하세요:

docker inspect <container> --format '{{json .HostConfig.NanoCpus}} {{json .HostConfig.CpuQuota}} {{json .HostConfig.CpuPeriod}}'

컨테이너가 너무 빡빡하게 제한되면 호스트에 여유 CPU가 있음에도 요청이 대기할 수 있습니다. 제어된 증가를 시도해 보세요:

docker run -d --name api --cpus="2" my-api:latest

제한을 높인 후에도 CPU가 계속 높다면 애플리케이션을 프로파일링하세요. 예를 들어, Node 서비스는 JSON 직렬화에 갇혀 있을 수 있고, Python 워커는 GIL 아래에서 CPU 바운드일 수 있으며, Java 서비스는 가비지 컬렉션에 시간을 소비할 수 있습니다. Docker가 이것을 자체적으로 해결할 수는 없습니다.

메모리 압박 및 OOM 종료

메모리 문제는 종종 재시작, 지연 시간 스파이크, 또는 부하 하에서 프로세스가 사라지는 것으로 나타납니다.

Docker가 OOM 종료를 감지했는지 확인하세요:

docker inspect <container> --format '{{.State.OOMKilled}}'

메모리가 천천히 상승하고 절대 떨어지지 않으면 누수 또는 무제한 캐시를 찾아보세요. 특정 요청 중에 메모리가 급증하면 해당 경로를 부하 하에서 재현하세요. 언어 런타임에 자체 힙 제한이 있는 경우 컨테이너와 정렬하세요. 실제 컨테이너 예산을 이해하지 못하는 JVM은 잘못 동작할 수 있습니다. 최신 JVM은 컨테이너를 인식하지만 힙 설정은 여전히 검토할 가치가 있습니다.

메모리 제한은 여유 공간을 남겨두어야 합니다. 정상 트래픽 중에 1GB 제한 중 950MB를 사용하는 컨테이너는 건강하지 않습니다. 가비지 컬렉션, 임시 버퍼, TLS, 압축 및 요청 버스트 모두 공간이 필요합니다.

입력/출력(I/O) 성능 문제 해결

느린 디스크 액세스는 데이터베이스, 큐, 검색 엔진, 캐시 워밍업 및 로깅에 영향을 미칩니다. 먼저 쓰기가 컨테이너 쓰기 가능 계층, 명명된 볼륨 또는 바인드 마운트 중 어디로 가는지 알아내세요.

docker inspect <container> --format '{{json .Mounts}}'
docker info --format 'StorageDriver={{.Driver}}'

최신 Linux Docker 설치에서 overlay2는 일반적인 기본 스토리지 드라이버입니다. 일반적으로 좋은 선택이지만, 무거운 가변 데이터를 컨테이너 계층에 쓰는 것은 여전히 좋지 않은 패턴입니다.

영구 애플리케이션 데이터에는 명명된 볼륨을 사용하세요:

docker volume create app-data
docker run -d --name app -v app-data:/var/lib/app my-image

특정 호스트 경로가 필요할 때는 바인드 마운트를 사용하되 테스트하세요. macOS 및 Windows의 Docker Desktop 바인드 마운트는 파일 작업이 가상화 경계를 넘기 때문에 네이티브 Linux 파일 시스템 액세스보다 훨씬 느릴 수 있습니다.

임시 고속 파일의 경우 /dev/shm이 도움이 될 수 있지만 메모리 기반이며 제한적입니다:

docker run --shm-size=512m my-image

이는 브라우저, 테스트 러너 및 공유 메모리가 필요한 애플리케이션에 일반적입니다. 실제 스토리지를 대체하지는 않습니다.

이미지 크기 및 빌드 성능 최적화

이미지 크기는 주로 빌드, 풀, 스캔 및 배포 시간에 영향을 미칩니다. 컨테이너가 실행된 후 요청 처리를 더 빠르게 만들지는 않지만 운영상 여전히 중요합니다.

멀티 스테이지 빌드를 사용하여 컴파일러, 패키지 캐시 및 테스트 도구가 런타임 이미지에 포함되지 않도록 하세요:

FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o app

FROM alpine:3.20
COPY --from=builder /app/app /app
CMD ["/app"]

캐시 재사용을 위해 Dockerfile 지시문을 정렬하세요:

FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

소스 코드를 변경해도 종속성 파일이 변경되지 않는 한 종속성 다운로드가 강제되어서는 안 됩니다.

네트워크 성능 고려 사항

네트워크 속도 저하는 종종 애플리케이션 속도 저하처럼 보입니다. 컨테이너 내부에서 테스트하세요:

docker exec -it <container> sh
time getent hosts api.example.com
time wget -qO- https://api.example.com/health

DNS 확인이 느리거나 불안정하면 컨테이너의 /etc/resolv.conf를 검사하고 호스트와 비교하세요. 런타임에 DNS 서버를 제공할 수 있습니다:

docker run -d --name web --dns 1.1.1.1 my-image

이는 무작위 수정이 아닌 진단 또는 정책 선택으로 수행하세요. 기업 네트워크에서는 내부 DNS가 필요할 수 있습니다.

Docker의 기본 브리지 네트워크는 NAT 및 iptables 처리를 추가합니다. 대부분의 웹 애플리케이션에서 오버헤드는 허용 가능합니다. 호스트 네트워킹은 Linux에서 오버헤드를 줄일 수 있습니다:

docker run --network host my-image

또한 네트워크 네임스페이스 격리를 제거하고 포트 처리를 변경합니다. 실제 필요성을 측정했을 때 사용하세요.

현장 체크리스트

컨테이너가 느릴 때 이 목록을 따라 작업하세요:

  1. 특정 요청, 작업 또는 워크로드로 속도 저하를 재현하세요.
  2. docker stats --no-stream, 로그 및 컨테이너 검사 출력을 캡처하세요.
  3. 호스트 CPU, 메모리, 스왑, 디스크 I/O 및 네트워크를 확인하세요.
  4. 제한을 변경하기 전에 제한된 리소스를 식별하세요.
  5. 영구적이거나 무거운 쓰기를 볼륨으로 이동하세요.
  6. 로컬 개발이 유일하게 느린 곳이라면 Linux와 Docker Desktop의 바인드 마운트 동작을 비교하세요.
  7. 컨테이너 메트릭이 속도 저하를 설명하지 못할 때 애플리케이션을 프로파일링하세요.
  8. 하나의 설정을 변경하고 다시 측정하세요.

유용한 사고방식은 간단합니다: Docker는 격리와 패키징을 제공하지만 일반적인 시스템 작업을 제거하지는 않습니다. CPU, 메모리, 디스크 및 네트워크가 여전히 서비스의 속도를 결정합니다.