Table of Contents
프로덕션 환경에서 Kubernetes 클러스터를 운영하다 보면 예상치 못한 성능 저하, 리소스 고갈, 그리고 원인을 찾기 어려운 장애 상황을 마주하게 됩니다. 이 글에서는 실제 프로덕션 환경에서 발생한 문제들과 그 해결 과정을 통해 효과적인 디버깅과 성능 최적화 전략을 다룹니다.
프로덕션 환경의 현실
2025년 현재, 85%의 기업이 마이크로서비스 아키텍처를 채택하고 있으며, 그 중심에는 Kubernetes가 자리하고 있습니다. 하지만 분산 시스템의 복잡성은 개발자들에게 새로운 도전 과제를 안겨줍니다.
실제로 프로덕션에서 자주 마주치는 문제들:
- Pod가 예기치 않게 종료되거나 재시작되는 현상 (OOMKilled)
- 특정 시간대에 응답 시간이 급격히 증가
- 리소스를 충분히 할당했는데도 성능 저하 발생 (CPU Throttling)
- 마이크로서비스 간 통신에서 발생하는 알 수 없는 지연
- 노드 전체가 응답하지 않는 상황
리소스 관리: 가장 흔한 문제의 근원
문제 시나리오 1: 리소스 설정 없는 Pod
많은 팀이 처음 Kubernetes를 도입할 때 다음과 같은 Deployment를 작성하는 실수를 범합니다.
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-server
spec:
replicas: 3
template:
spec:
containers:
- name: api
image: myapp/api:v1.2.0
# 리소스 요청(requests) 및 제한(limits)이 없음!
ports:
- containerPort: 8080
문제점: 리소스 요청(requests)과 제한(limits)이 없으면 다음과 같은 심각한 문제가 발생합니다.
- 스케줄링 불가: 스케줄러가 Pod를 어느 노드에 배치해야 최적인지 판단할 근거가 없습니다.
- Noisy Neighbor 문제: 여러 Pod가 하나의 노드에 몰려 리소스 경합(Contention)이 발생합니다.
- 리소스 독점: 특정 Pod가 메모리 누수 등으로 노드의 모든 리소스를 독점하여 다른 Pod들을 죽일 수 있습니다.
진단 방법
실제 리소스 사용량을 확인하여 문제를 파악합니다.
# 노드별 리소스 사용량
kubectl top nodes
# Pod별 리소스 사용량
kubectl top pods -n production
# 특정 노드의 상세 리소스 할당 현황 (Allocatable vs Allocated)
kubectl describe node worker-node-1 | grep -A 5 "Allocated resources"
출력 예시에서 문제 발견:
Allocated resources:
CPU Requests: 7800m (97%) # ⚠️ CPU 거의 한계!
Memory Requests: 14Gi (87%)
CPU Limits: 12000m (150%) # ⚠️ Overcommit! (위험할 수 있음)
Memory Limits: 20Gi (125%)
해결: 적절한 리소스 설정
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-server
spec:
replicas: 3
template:
spec:
containers:
- name: api
image: myapp/api:v1.2.0
resources:
requests:
# Pod 실행을 위해 보장받아야 할 최소 리소스
memory: "512Mi"
cpu: "500m"
limits:
# Pod가 사용할 수 있는 최대 리소스
memory: "1Gi"
cpu: "1000m"
ports:
- containerPort: 8080
권장 전략:
- requests: 실제 사용량의 p50 (중앙값) ~ p75 기준으로 설정하여 안정적인 스케줄링을 보장합니다.
- limits: p95~p99 사이 값으로 설정하여 리소스 낭비를 막되, 피크 트래픽을 감당할 수 있게 합니다.
- CPU Throttling 주의: CPU limits를 너무 타이트하게 잡으면, 리소스가 남는데도 불구하고 성능 저하(Throttling)가 발생할 수 있습니다. 최근에는 CPU limits를 생략하고 requests만 설정하는 방식도 고려되고 있습니다.
- Memory limits 필수: 메모리는 압축 불가능한(incompressible) 리소스이므로, 반드시 limits를 설정하여 OOMKilled를 방지하고 노드 안정성을 지켜야 합니다.
OOM Killer와의 전쟁
문제 시나리오 2: 반복적인 Pod 재시작
$ kubectl get pods -n production
NAME READY STATUS RESTARTS AGE
api-server-7d8f5c9b4-9xk2p 1/1 Running 47 3d
api-server-7d8f5c9b4-kl9m3 0/1 OOMKilled 0 2m
RESTARTS 횟수가 47회! 명백히 문제가 있습니다. OOMKilled 상태는 컨테이너가 메모리 제한을 초과하여 커널에 의해 강제 종료되었음을 의미합니다.
원인 진단
# Pod 이벤트 및 종료 원인 상세 확인
kubectl describe pod api-server-7d8f5c9b4-kl9m3 -n production
OOMKilled의 주요 원인:
- 메모리 누수(Memory Leak): 애플리케이션 코드 결함으로 메모리가 계속 증가합니다.
- 트래픽 급증: 동시 접속자 수가 늘어나 힙 메모리 사용량이 급증했습니다.
- 부적절한 메모리 설정: JVM 등의 런타임 메모리 설정(Heap Size)이 컨테이너 limits보다 작게 설정되지 않았거나, 컨테이너 오버헤드를 고려하지 않았습니다.
실시간 디버깅: kubectl debug
Kubernetes 1.18+부터 사용 가능한 kubectl debug는 실행 중인 Pod에 디버깅용 컨테이너(Sidecar)를 붙여 실시간 분석을 가능하게 합니다. 쉘이 없는 distroless 이미지를 사용할 때 특히 유용합니다.
# 실행 중인 Pod에 우분투 컨테이너를 붙여 쉘 접속 (프로세스 네임스페이스 공유)
kubectl debug -it api-server-7d8f5c9b4-9xk2p \
--image=ubuntu \
--target=api \
--share-processes -- bash
# 접속 후 프로세스 및 네트워크 확인
$ ps aux
$ netstat -tulpn
네트워크 레이턴시와 분산 트레이싱
문제 시나리오 3: 응답 시간 급증
API 응답 시간이 갑자기 200ms에서 3초로 증가했습니다. 모놀리식 아키텍처라면 프로파일러 하나로 충분했겠지만, 수십 개의 서비스가 얽힌 마이크로서비스 환경에서는 어디가 범인인지 찾기 어렵습니다.
User Request
→ API Gateway (50ms)
→ Auth Service (100ms)
→ User Service (200ms)
→ Database Query (?ms) ← 여기서 병목?
→ Product Service (?) ← 아니면 여기?
→ Cache Service (?)
**분산 트레이싱(Distributed Tracing)**을 도입하여 전체 요청 흐름을 시각화해야 합니다. OpenTelemetry와 Jaeger 또는 Zipkin을 활용하면 요청이 각 서비스를 통과할 때 걸리는 시간을 워터폴 차트로 확인할 수 있습니다.
결론
Kubernetes 프로덕션 환경에서의 디버깅과 성능 최적화는 단순히 도구를 사용하는 것을 넘어, 시스템을 깊이 이해하고 선제적으로(Proactive) 대응하는 문화를 만드는 것입니다.
핵심 원칙:
- 측정할 수 없으면 개선할 수 없다: Prometheus와 Grafana로 모든 지표를 모니터링하세요.
- 리소스 설정은 필수: 감(Feel)이 아닌 실제 사용량 데이터 기반으로
requests와limits를 설정하세요. - 분산 트레이싱은 선택이 아닌 필수: 복잡한 마이크로서비스 환경에서 병목 지점을 찾는 유일한 지도입니다.
- 예측적 스케일링: HPA(Horizontal Pod Autoscaler)를 통해 트래픽 변화에 자동으로 대응하세요.
- Graceful Degradation: 장애는 언제든 발생합니다. 시스템 전체가 멈추지 않고 핵심 기능은 유지되도록 설계하세요.
이 글에서 다룬 기법들을 하나씩 적용하면서, 여러분의 Kubernetes 클러스터가 더 안정적이고 효율적으로 운영되기를 바랍니다.