[Kubernetes] 실무 리소스 설계 완벽 가이드 - CPU/Memory/Pod 최적화 전략
Kubernetes 환경에서 애플리케이션의 CPU, Memory, Pod 수를 어떻게 설정해야 하는지 실무 관점에서 명확한 기준과 전략을 제시한다.
들어가며
Kubernetes에서 애플리케이션을 운영하다 보면 가장 자주 마주치는 질문들이 있다:
- “CPU를 늘려야 할까, 메모리를 늘려야 할까?”
- “파드 수를 늘리면 GC 문제가 해결될까?”
- “API Gateway는 어떤 리소스 전략을 써야 할까?”
이런 질문들에 대한 답은 단순히 “더 많이 주면 된다”가 아니다. 왜 그런 문제가 발생했는지, 어떤 리소스가 실제 병목인지를 이해해야 올바른 결정을 내릴 수 있다.
이 글에서는 실무에서 자주 겪는 리소스 설계 상황들을 바탕으로, CPU, Memory, Pod 수를 언제, 어떻게 조정해야 하는지 명확한 기준을 제시한다.
1. 메모리 vs 파드 수 결정 기준
CPU 사용률 패턴 분석
리소스 증설 결정의 첫 단계는 CPU 사용률 패턴을 분석하는 것이다.
Case 1: CPU 사용률이 높고 GC 빈도/시간이 증가
증상
CPU 사용률: 70~90% (지속)
GC 빈도: 초당 2~3회 (정상: 0.5회 이하)
GC 시간: 200~500ms (정상: 50ms 이하)
응답 시간: 점진적 증가
원인 분석
힙 메모리가 부족하여 Young GC가 빈번하게 발생하고, Old 영역이 빠르게 채워지면서 CPU를 과도하게 사용하는 상황이다.
메모리 부족
↓
Young Generation 빠른 포화
↓
Young GC 빈번 발생
↓
Old Generation으로 승격 증가
↓
Old 영역 포화
↓
Full GC 발생
↓
CPU 급증
해결 방법
✅ 메모리 증설 우선
# Before
resources:
requests:
memory: 512Mi
limits:
memory: 512Mi
# After
resources:
requests:
memory: 1Gi # 2배 증설
limits:
memory: 1Gi
효과
- Young Generation 크기 증가 → GC 빈도 감소
- Old Generation 여유 확보 → Full GC 지연
- CPU 사용률 정상화 (30~50%)
Case 2: CPU 사용률이 트래픽에 비례해 선형 증가
증상
트래픽 증가: 1000 → 2000 req/s
CPU 사용률: 50% → 100%
GC 빈도: 정상 유지
응답 시간: 증가 (큐잉 발생)
원인 분석
메모리나 GC 문제가 아닌, 순수 처리량 한계에 도달한 상황이다.
트래픽 증가
↓
요청 처리 스레드 포화
↓
CPU 100% 도달
↓
추가 요청 대기 (큐잉)
↓
응답 지연 증가
해결 방법
✅ 파드 수 증설 우선
# HPA 설정
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: my-app
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: my-app
minReplicas: 3
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70 # CPU 70% 초과 시 스케일 아웃
효과
- 부하 분산으로 CPU 사용률 정상화
- 처리량 증가
- 응답 시간 개선
Case 3: 트래픽이 줄어도 CPU가 내려가지 않음
증상
트래픽 감소: 2000 → 500 req/s
CPU 사용률: 80% 유지 (변동 없음)
메모리 사용: 꾸준히 증가
원인 분석
Old GC 또는 백그라운드 작업이 CPU를 지속적으로 점유하는 상황이다.
가능한 원인들
- 메모리 누수 ```java // 잘못된 코드 예시 private static final Map<String, Object> cache = new HashMap<>();
public void cacheData(String key, Object value) { cache.put(key, value); // 계속 누적, 삭제 안 됨 }
2. **Old GC 빈발**
Old Generation 지속 포화 (>90%) ↓ Full GC 반복 발생 (수 초마다) ↓ CPU 지속 사용
3. **백그라운드 작업**
```java
// 문제가 되는 패턴
@Scheduled(fixedDelay = 100) // 100ms마다 실행
public void heavyTask() {
// 무거운 작업
processLargeDataSet();
}
해결 방법
✅ 메모리 증설 또는 코드 개선
1단계: 메모리 프로파일링
# Heap dump 생성
kubectl exec -it <pod-name> -- jmap -dump:format=b,file=/tmp/heap.hprof 1
# 로컬로 복사
kubectl cp <pod-name>:/tmp/heap.hprof ./heap.hprof
# VisualVM, Eclipse MAT 등으로 분석
2단계: 문제 패턴 수정
// 개선된 코드
private final Map<String, Object> cache = new ConcurrentHashMap<>();
private static final int MAX_CACHE_SIZE = 1000;
public void cacheData(String key, Object value) {
if (cache.size() >= MAX_CACHE_SIZE) {
// LRU 정책으로 오래된 항목 제거
cache.remove(cache.keySet().iterator().next());
}
cache.put(key, value);
}
3단계: 백그라운드 작업 최적화
// 개선: 실행 빈도 조정
@Scheduled(fixedDelay = 60000) // 1분마다로 변경
public void heavyTask() {
// 배치 크기 제한
processDataSetInBatches(1000);
}
의사결정 플로우차트
CPU 사용률이 높은가?
├─ YES
│ ├─ GC 빈도/시간이 비정상인가?
│ │ ├─ YES → 메모리 증설
│ │ └─ NO → 트래픽에 비례하는가?
│ │ ├─ YES → 파드 수 증설
│ │ └─ NO → 트래픽과 무관한가?
│ │ └─ YES → 메모리 또는 코드 개선
│ └─ 결정 완료
└─ NO → 현상 유지 또는 다른 병목 확인
2. API Gateway / BFF의 리소스 성향
API Gateway와 BFF(Backend For Frontend)는 일반적인 백엔드 서비스와 다른 리소스 특성을 가진다.
리소스 사용 패턴 분석
CPU 사용 특성
주요 CPU 소비 작업
- 데이터 변환 (Transformation)
// API Gateway의 전형적인 패턴 public ResponseDTO transform(InternalResponse internal) { return ResponseDTO.builder() .userId(internal.getUserId()) .userName(internal.getUserName()) .orders(internal.getOrders().stream() .map(this::convertOrder) // 변환 작업 .collect(Collectors.toList())) .build(); } - 직렬화/역직렬화 (Serialization)
// JSON 변환이 빈번 String json = objectMapper.writeValueAsString(response); // CPU 소비 ResponseDTO dto = objectMapper.readValue(json, ResponseDTO.class); - 라우팅 및 필터링
// Spring Cloud Gateway public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { // 헤더 검증, 인증, 로깅 등 return chain.filter(exchange) .then(Mono.fromRunnable(() -> { // 응답 후처리 })); }
권장 CPU 설정
resources:
requests:
cpu: 500m # 중간 이상 필요
limits:
cpu: 1000m # burst 허용
메모리 사용 특성
메모리 압박 요인
- 다수의 동시 연결
1000 req/s × 평균 응답시간 100ms = 100개의 동시 처리 요청 = 각 요청당 메모리 할당 - 응답 버퍼링
// Reactive 스트림에서의 버퍼 Flux<DataBuffer> body = response.getBody(); // 메모리에 버퍼링될 수 있음 - 빈번한 객체 생성
// 요청마다 새로운 객체 생성 for (Request req : requests) { ResponseDTO dto = new ResponseDTO(); // Young Gen 압박 // ... 처리 }
GC 안정성을 위한 메모리 설정
resources:
requests:
memory: 1Gi # 충분한 Young Generation 확보
limits:
memory: 1Gi
# JVM 옵션
env:
- name: JAVA_OPTS
value: |
-Xms1g -Xmx1g
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:InitiatingHeapOccupancyPercent=45
왜 메모리가 중요한가?
요청 증가
↓
Young Generation 빠른 포화
↓
Young GC 빈번 발생
↓
GC 중 "Stop-The-World" 발생
↓
응답 지연 발생 (tail latency 증가)
↓
사용자 체감 성능 저하
Pod 수 전략
과도한 파드 수가 불필요한 이유
- Gateway는 Stateless
- 세션 저장 불필요
- 단순 요청 전달 및 변환
- 수평 확장보다 수직 확장이 효율적
- 파드 수 증가 = 네트워크 홉 증가
- 파드 간 부하 분산 오버헤드
- 비용 효율
- 적은 수의 파드에 충분한 리소스 할당이 더 경제적
권장 설정
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-gateway
spec:
replicas: 3 # 최소한의 HA 보장
template:
spec:
containers:
- name: gateway
resources:
requests:
cpu: 500m
memory: 1Gi # 메모리 여유 확보
limits:
cpu: 2000m # burst 허용
memory: 1Gi
---
# HPA 설정
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: api-gateway
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: api-gateway
minReplicas: 3
maxReplicas: 10 # 너무 많이 확장하지 않음
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
권장 리소스 전략 요약
| 리소스 | 우선순위 | 이유 |
|---|---|---|
| 메모리 | 최우선 | GC 안정성 확보, tail latency 최소화 |
| CPU | 중간 | 데이터 변환, 직렬화 처리, burst 허용 |
| Pod 수 | 최소화 | HA 보장 수준만 유지, 비용 효율 |
실전 예시
# ❌ 잘못된 설정
spec:
replicas: 20 # 과도한 파드 수
resources:
requests:
memory: 256Mi # 메모리 부족 → GC 빈발
cpu: 100m
# ✅ 올바른 설정
spec:
replicas: 3 # 최소한의 HA
resources:
requests:
memory: 1Gi # 충분한 메모리
cpu: 500m
limits:
cpu: 2000m # burst 허용
memory: 1Gi
3. 메모리와 GC의 관계
Java 애플리케이션에서 메모리 설정은 GC 동작에 직접적인 영향을 미친다.
메모리 크기별 GC 동작 비교
Case 1: 메모리가 작을 때 (512Mi)
힙 구조
Total Heap: 512Mi
├─ Young Generation: ~170Mi (1/3)
│ ├─ Eden: ~136Mi
│ └─ Survivor: ~34Mi
└─ Old Generation: ~342Mi (2/3)
GC 동작 패턴
[00:00] 애플리케이션 시작
├─ Eden 영역 빠르게 채워짐 (수십 MB/초)
└─ 10초 후 Eden 포화
[00:10] Young GC 발생 (1회차)
├─ Stop-The-World: 50ms
├─ 살아남은 객체 → Survivor로 이동
└─ 일부 → Old로 승격
[00:20] Young GC 발생 (2회차)
├─ Stop-The-World: 60ms
├─ Old 영역 계속 증가 (50% → 60%)
└─ CPU 사용률 증가 시작
[00:40] Young GC 발생 (4회차)
├─ Stop-The-World: 80ms
├─ Old 영역 포화 (90%)
└─ CPU 70% 도달
[01:00] Full GC 발생
├─ Stop-The-World: 2000ms (2초!)
├─ 애플리케이션 완전 정지
└─ 사용자 요청 타임아웃 발생
문제점
- Young GC 빈도: 초당 0.1~0.5회 (매우 빈번)
- Full GC 빈도: 수 분마다
- CPU 사용률: 지속 상승 (60~90%)
- 응답 시간: 불안정 (tail latency 높음)
Case 2: 메모리가 클 때 (2Gi)
힙 구조
Total Heap: 2Gi
├─ Young Generation: ~680Mi (1/3)
│ ├─ Eden: ~544Mi
│ └─ Survivor: ~136Mi
└─ Old Generation: ~1368Mi (2/3)
GC 동작 패턴
[00:00] 애플리케이션 시작
├─ Eden 영역 여유 있음
└─ 객체 할당 여유롭게 진행
[01:00] Young GC 발생 (1회차)
├─ Stop-The-World: 100ms
├─ Eden이 커서 GC 시간 증가
└─ Old로 승격 최소화 (충분한 Young 영역)
[05:00] Young GC 발생 (2회차)
├─ Stop-The-World: 120ms
├─ Old 영역 천천히 증가 (20% → 25%)
└─ CPU 안정적 유지 (40%)
[30:00] Young GC 발생 (6회차)
├─ Old 영역 50% 수준
└─ Full GC 아직 발생 안 함
[60:00] 운영 1시간 후
├─ Young GC 총 12회
├─ Full GC 0회
└─ CPU 안정적 (30~50%)
장점
- Young GC 빈도: 수십 초~수 분마다 (안정적)
- Full GC 빈도: 거의 없음 또는 드물게
- CPU 사용률: 안정적 (30~50%)
- 응답 시간: 일정 (tail latency 낮음)
단점
- Young GC 한 번의 시간 증가 (50ms → 100~150ms)
- 메모리 비용 증가
GC 빈도와 비용의 균형
목표: Sweet Spot 찾기
메모리 크기와 GC의 관계
━━━━━━━━━━━━━━━━━━━━━━━━━━━━
메모리 너무 작음 (256Mi~512Mi)
├─ GC 빈도: 매우 높음 (초당 여러 번)
├─ GC 시간: 짧음 (10~50ms)
├─ Full GC: 빈번 (수 분마다)
└─ CPU: 높음 (70~90%)
메모리 적정 (1Gi~2Gi)
├─ GC 빈도: 적정 (수십 초마다)
├─ GC 시간: 중간 (50~150ms)
├─ Full GC: 드물게 (수 시간~하루)
└─ CPU: 안정 (30~50%)
메모리 너무 큼 (4Gi~8Gi)
├─ GC 빈도: 매우 낮음 (수 분마다)
├─ GC 시간: 김 (200~500ms)
├─ Full GC: 거의 없음
└─ CPU: 안정하지만 GC 발생 시 스파이크
실전 튜닝 가이드
1단계: 현재 GC 패턴 분석
# GC 로그 활성화
JAVA_OPTS: |
-Xlog:gc*:file=/var/log/gc.log:time,uptime,level,tags
-XX:+UseG1GC
# GC 로그 분석
kubectl logs <pod-name> | grep "GC"
2단계: 메트릭 확인
# Prometheus 쿼리
# Young GC 빈도
rate(jvm_gc_pause_seconds_count{gc="G1 Young Generation"}[5m])
# Full GC 빈도
rate(jvm_gc_pause_seconds_count{gc="G1 Old Generation"}[5m])
# GC 소요 시간
jvm_gc_pause_seconds_sum / jvm_gc_pause_seconds_count
3단계: 메모리 조정
# GC가 빈번한 경우 (초당 0.1회 이상)
resources:
requests:
memory: 1Gi # 현재의 2배
limits:
memory: 1Gi
# GC 한 번이 너무 긴 경우 (500ms 이상)
# → 메모리를 줄이거나 GC 알고리즘 변경
env:
- name: JAVA_OPTS
value: |
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200 # 목표 GC 시간
핵심 원칙
| 증상 | 원인 | 해결 |
|---|---|---|
| GC 빈도 높음 | Young Gen 작음 | 메모리 증설 |
| Full GC 빈번 | Old Gen 빠른 포화 | 메모리 증설 또는 메모리 누수 확인 |
| GC 시간 김 | 힙 크기 과도 | 메모리 감소 또는 GC 알고리즘 변경 |
| CPU 지속 높음 | GC 반복 | 메모리 증설 |
결론: 메모리는 GC 빈도를 조절하는 가장 중요한 요소다. 충분한 메모리는 GC 빈도를 낮추고 CPU 사용을 안정화한다.
4. CPU 코어 수와 GC의 관계
GC는 CPU를 사용하는 작업이므로, CPU 코어 수는 GC 성능에 직접 영향을 미친다.
CPU 리소스와 GC 경쟁
Case 1: CPU가 부족한 경우 (500m = 0.5 코어)
동작 시나리오
[트래픽 처리 중]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━
CPU 0.5코어
├─ 애플리케이션 스레드: 0.4코어 사용 (80%)
└─ 여유: 0.1코어 (20%)
↓ Young GC 발생
[GC 실행]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━
GC 스레드가 CPU 요구
├─ GC 필요 CPU: 0.3코어
└─ 가용 CPU: 0.5코어
애플리케이션 스레드와 경쟁
├─ 애플리케이션: 일시 중단 또는 느려짐
└─ GC: CPU 부족으로 느리게 실행
결과
├─ GC 시간: 200ms (정상: 50ms)
├─ 응답 지연: 증가
└─ 처리량: 감소
문제점
- GC와 애플리케이션이 CPU를 놓고 경쟁
- GC 시간 증가 (Stop-The-World 길어짐)
- 응답 시간 불안정
Case 2: CPU가 충분한 경우 (2000m = 2 코어)
동작 시나리오
[트래픽 처리 중]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━
CPU 2코어
├─ 애플리케이션 스레드: 1.2코어 사용 (60%)
└─ 여유: 0.8코어 (40%)
↓ Young GC 발생
[GC 실행]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━
GC 스레드가 CPU 사용
├─ GC 사용 CPU: 0.6코어
└─ 가용 CPU: 2코어
병렬 GC 효율적 실행
├─ 애플리케이션: 계속 실행 가능 (Concurrent GC)
├─ GC: 빠르게 완료
└─ 상호 간섭 최소
결과
├─ GC 시간: 50ms (정상)
├─ 응답 지연: 최소
└─ 처리량: 유지
장점
- GC 병렬 처리 효율 증가
- CPU 스파이크 완화
- 안정적인 응답 시간
G1 GC와 CPU 코어
G1 GC는 병렬 GC를 수행하므로 CPU 코어 수의 영향을 크게 받는다.
G1 GC 스레드 수 계산
# G1 GC의 기본 병렬 스레드 수
-XX:ParallelGCThreads=<N>
# 기본값 계산
N = 8 + (CPU코어 - 8) * 5/8 (CPU > 8인 경우)
N = CPU코어 (CPU ≤ 8인 경우)
예시
| CPU 코어 | ParallelGCThreads | 설명 |
|---|---|---|
| 0.5 (500m) | 1 | 단일 스레드 GC (느림) |
| 1 (1000m) | 1 | 단일 스레드 GC |
| 2 (2000m) | 2 | 병렬 GC 가능 |
| 4 (4000m) | 4 | 효율적인 병렬 GC |
CPU 코어별 GC 성능
실험 결과 (동일한 워크로드, 2Gi 메모리)
CPU: 500m (0.5코어)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Young GC 평균 시간: 180ms
Full GC 시간: 5000ms
CPU 사용률: 90% (GC 포함)
응답 시간 p99: 800ms
CPU: 1000m (1코어)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Young GC 평균 시간: 100ms
Full GC 시간: 2500ms
CPU 사용률: 70%
응답 시간 p99: 400ms
CPU: 2000m (2코어)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Young GC 평균 시간: 50ms
Full GC 시간: 1200ms
CPU 사용률: 50%
응답 시간 p99: 200ms
실무 권장 설정
일반 백엔드 서비스
resources:
requests:
cpu: 1000m # 최소 1코어
memory: 1Gi
limits:
cpu: 2000m # burst 허용 (2코어)
memory: 1Gi
env:
- name: JAVA_OPTS
value: |
-Xms1g -Xmx1g
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:ParallelGCThreads=2 # CPU limits에 맞춤
API Gateway / BFF
resources:
requests:
cpu: 500m # 최소 0.5코어
memory: 1Gi
limits:
cpu: 2000m # burst 허용 (2코어, 트래픽 급증 대비)
memory: 1Gi
env:
- name: JAVA_OPTS
value: |
-Xms1g -Xmx1g
-XX:+UseG1GC
-XX:MaxGCPauseMillis=100 # 더 낮은 목표 (latency 중요)
고처리량 서비스
resources:
requests:
cpu: 2000m # 2코어
memory: 2Gi
limits:
cpu: 4000m # burst 허용 (4코어)
memory: 2Gi
env:
- name: JAVA_OPTS
value: |
-Xms2g -Xmx2g
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:ParallelGCThreads=4
-XX:ConcGCThreads=1
핵심 정리
| CPU 설정 | GC 영향 | 권장 사항 |
|---|---|---|
| requests | 보장된 CPU, GC 최소 성능 | 최소 1코어 (1000m) |
| limits | burst 허용, GC 스파이크 대응 | 2~4코어 (트래픽 패턴에 따라) |
| ParallelGCThreads | 병렬 GC 성능 | limits 값에 맞춤 |
결론: CPU는 burst를 허용하는 것이 중요하다. requests는 안정적인 운영을 위한 최소값, limits는 트래픽 급증 시 GC 성능 유지를 위한 여유분이다.
5. 실무 리소스 설계 원칙
역할별 우선순위
메모리는 “품질 안정성”
메모리는 애플리케이션 안정성의 기반이다.
메모리 부족 시 발생하는 문제들
메모리 부족
↓
GC 빈번 발생
↓
CPU 사용률 증가
↓
응답 시간 증가 (tail latency)
↓
최악의 경우: OutOfMemoryError
↓
Pod Crash → Restart
↓
서비스 불안정
메모리 충분 시 효과
충분한 메모리
↓
GC 빈도 감소
↓
CPU 안정
↓
응답 시간 일정
↓
사용자 경험 향상
권장 사항
- 메모리는 여유 있게 설정
- 비용보다 안정성 우선
- GC 메트릭을 지속 모니터링
파드 수는 “처리량 대응”
파드 수는 트래픽 처리량을 확장하는 수단이다.
파드 수 증가가 효과적인 경우
트래픽 증가 → CPU 100% 도달
↓
처리량 한계
↓
파드 수 증가 (HPA)
↓
부하 분산
↓
처리량 증가
파드 수 증가가 비효과적인 경우
GC 문제로 인한 CPU 사용
↓
파드 수 증가
↓
각 파드는 여전히 GC 문제 유지
↓
전체 파드 수만 증가
↓
비용 증가, 문제 미해결
권장 사항
- 파드 수는 필요 시에만 증가
- HPA로 자동 조절
- 최소 replicas는 HA 보장 수준(예: 3)
CPU는 burst 허용이 중요
CPU는 순간적인 부하 증가에 대응해야 한다.
CPU requests vs limits
# ❌ 잘못된 설정
resources:
requests:
cpu: 1000m
limits:
cpu: 1000m # burst 불가능
문제점
정상 트래픽: CPU 70% 사용
↓
트래픽 급증 (1.5배)
↓
CPU 100% 도달 (limits 제한)
↓
요청 큐잉 발생
↓
응답 지연 증가
# ✅ 올바른 설정
resources:
requests:
cpu: 1000m # 보장
limits:
cpu: 2000m # burst 허용
효과
정상 트래픽: CPU 70% 사용
↓
트래픽 급증 (1.5배)
↓
CPU 120% 사용 가능 (limits 여유)
↓
부하 흡수
↓
응답 시간 유지
잘못된 접근 사례
안티패턴 1: 파드 수로 GC 문제 해결 시도
상황
문제: CPU 사용률 80%, GC 빈번
잘못된 접근: "파드를 3개에서 10개로 늘리자"
결과
Before (3 Pods)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━
각 Pod
├─ CPU: 80% (GC 때문)
├─ Memory: 512Mi
└─ GC: 초당 0.3회
After (10 Pods)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━
각 Pod
├─ CPU: 여전히 80% (GC는 해결 안 됨)
├─ Memory: 여전히 512Mi
└─ GC: 여전히 초당 0.3회
총 비용: 3.3배 증가
문제 해결: ✗
올바른 접근
문제 분석: GC가 원인
해결: 메모리 512Mi → 1Gi 증설
결과 (3 Pods 유지)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━
각 Pod
├─ CPU: 40% (GC 감소로 정상화)
├─ Memory: 1Gi
└─ GC: 초당 0.05회
총 비용: 2배 증가 (메모리만)
문제 해결: ✓
안티패턴 2: 메모리 과도한 절약
상황
목표: 비용 절감
잘못된 접근: "메모리를 최소화하자 (256Mi)"
결과
비용 절감 효과
━━━━━━━━━━━━━━━━━━━━━━━━━━━━
메모리 비용: 50% 감소 ✓
성능 저하
━━━━━━━━━━━━━━━━━━━━━━━━━━━━
GC 빈도: 10배 증가
CPU 사용: 2배 증가 (GC 때문)
응답 시간: 3배 증가
장애 발생: OutOfMemoryError 빈발
추가 비용
━━━━━━━━━━━━━━━━━━━━━━━━━━━━
파드 수 증가 필요: 2배
온콜 대응 비용: 증가
사용자 이탈: 비즈니스 손실
총 비용: 오히려 증가
올바른 접근
메모리는 충분히 확보 (1Gi)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━
비용: 약간 증가
성능 향상
━━━━━━━━━━━━━━━━━━━━━━━━━━━━
GC 안정: 초당 0.05회
CPU 안정: 40%
응답 시간: 안정적
장애: 없음
장기 비용: 최적화됨 (파드 수 최소화)
실전 의사결정 가이드
문제 발생 시 체크리스트
1단계: 증상 확인
# CPU 사용률
kubectl top pods
# GC 메트릭
# Prometheus에서 확인
jvm_gc_pause_seconds_count
jvm_gc_pause_seconds_sum
# 메모리 사용
kubectl exec -it <pod> -- jmap -heap 1
2단계: 원인 분석
CPU 높음 + GC 빈번
→ 메모리 부족
→ 해결: 메모리 증설
CPU 높음 + GC 정상
→ 처리량 한계
→ 해결: 파드 수 증가 (HPA)
CPU 높음 + 트래픽 무관
→ 메모리 누수 또는 백그라운드 작업
→ 해결: 코드 개선
3단계: 조치
# 메모리 증설이 필요한 경우
resources:
requests:
memory: 2Gi # 현재의 2배
limits:
memory: 2Gi
# 파드 수 증가가 필요한 경우
# HPA 설정 또는 replicas 증가
spec:
replicas: 5 # 3 → 5
# CPU burst 허용이 필요한 경우
resources:
limits:
cpu: 2000m # requests의 2배
6. 핵심 요약
의사결정 매트릭스
| 증상 | CPU 사용 | GC 상태 | 트래픽 관계 | 해결책 |
|---|---|---|---|---|
| 높은 CPU | 70~90% | 빈번 | - | 메모리 증설 |
| 높은 CPU | 70~90% | 정상 | 비례 | 파드 수 증가 |
| 높은 CPU | 70~90% | 정상 | 무관 | 코드 개선 |
| 응답 지연 | 정상 | 빈번 | - | 메모리 증설 |
| 처리량 부족 | 100% | 정상 | 비례 | 파드 수 증가 |
리소스별 핵심 원칙
메모리 (Memory)
목적: 품질 안정성
전략: 여유 있게 확보
근거: GC 빈도 감소 → CPU 안정 → 응답 시간 안정
권장: 최소 1Gi (일반 서비스)
CPU
목적: 순간 부하 대응
전략: burst 허용 (requests < limits)
근거: 트래픽 급증 시 처리 능력 유지
권장: requests 1코어, limits 2코어
Pod 수
목적: 처리량 확장
전략: 필요 시에만 증가 (HPA)
근거: 비용 효율, HA 보장
권장: 최소 3개, HPA로 자동 조절
Gateway/BFF 특화 전략
# API Gateway / BFF 권장 설정
spec:
replicas: 3 # 최소한의 HA
resources:
requests:
memory: 1Gi # 품질 안정성 최우선
cpu: 500m
limits:
cpu: 2000m # burst 허용
memory: 1Gi
# HPA
hpa:
minReplicas: 3
maxReplicas: 10 # 과도한 확장 방지
targetCPU: 70%
# JVM 옵션
jvm:
heap: 1g
gc: G1GC
maxGCPauseMillis: 200
전략 요약
- 파드 수: 최소화
- 메모리: 여유 확보 (GC 안정성)
- CPU: burst 허용 (트래픽 급증 대비)
안티패턴 회피
❌ 하지 말아야 할 것
- 파드 수로 GC 문제 해결 시도
- GC는 각 파드의 메모리 문제
- 파드 수 증가는 비용만 증가
- 메모리 과도한 절약
- 단기 비용 절감 → 장기 비용 증가
- 성능 저하 → 사용자 경험 악화
- CPU limits를 requests와 동일하게 설정
- burst 불가능
- 순간 부하 대응 불가
- 메트릭 없이 추측으로 결정
- 문제 원인 오판
- 잘못된 해결책 적용
✅ 해야 할 것
- GC 메트릭 모니터링
- Prometheus + Grafana
- GC 빈도, 시간, 원인 파악
- 메모리 우선 확보
- 안정성의 기초
- GC 최적화
- HPA 활용
- 자동 스케일링
- 비용 효율
- 부하 테스트
- 프로덕션 배포 전 검증
- 적절한 리소스 설정 파악
7. 실전 적용 가이드
신규 서비스 초기 설정
1단계: 기본 설정으로 시작
apiVersion: apps/v1
kind: Deployment
metadata:
name: new-service
spec:
replicas: 3 # HA 기본값
template:
spec:
containers:
- name: app
image: new-service:1.0.0
resources:
requests:
memory: 1Gi # 기본값
cpu: 500m
limits:
cpu: 2000m # burst 허용
memory: 1Gi
env:
- name: JAVA_OPTS
value: |
-Xms1g -Xmx1g
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
---
# HPA 설정
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: new-service
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: new-service
minReplicas: 3
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
2단계: 부하 테스트
# k6를 사용한 부하 테스트
k6 run --vus 100 --duration 10m load-test.js
# 또는 hey
hey -z 10m -c 100 -q 10 http://new-service/api/endpoint
관찰할 메트릭
1. CPU 사용률
├─ 평균: 50% 미만 (양호)
├─ 최대: 80% 미만 (양호)
└─ 90% 이상 (조정 필요)
2. 메모리 사용
├─ 안정적 유지 (양호)
└─ 계속 증가 (메모리 누수 의심)
3. GC 메트릭
├─ Young GC: 10초~1분마다 (양호)
├─ Full GC: 거의 없음 (양호)
└─ 빈번한 GC (메모리 증설 필요)
4. 응답 시간
├─ p50: <100ms
├─ p95: <500ms
└─ p99: <1000ms
3단계: 리소스 최적화
CPU 조정
# CPU 사용률이 지속적으로 80% 이상
resources:
requests:
cpu: 1000m # 500m → 1000m
limits:
cpu: 2000m # 유지
메모리 조정
# GC가 빈번한 경우 (초당 0.1회 이상)
resources:
requests:
memory: 2Gi # 1Gi → 2Gi
limits:
memory: 2Gi
파드 수 조정
# 트래픽 패턴에 맞춘 HPA 조정
spec:
minReplicas: 5 # 3 → 5 (평시 트래픽 기준)
maxReplicas: 30 # 20 → 30 (피크 타임 대비)
기존 서비스 최적화
문제 진단 프로세스
1단계: 현재 상태 파악
# Pod 리소스 사용 확인
kubectl top pods -n production
# Pod 수 확인
kubectl get deployment -n production
# HPA 상태 확인
kubectl get hpa -n production
2단계: 메트릭 분석
# Prometheus에서 쿼리
# CPU 사용률 추이 (최근 24시간)
rate(container_cpu_usage_seconds_total{namespace="production"}[5m])
# 메모리 사용 추이
container_memory_usage_bytes{namespace="production"}
# GC 메트릭
rate(jvm_gc_pause_seconds_count[5m])
jvm_gc_pause_seconds_sum / jvm_gc_pause_seconds_count
3단계: 로그 분석
# GC 로그 확인
kubectl logs <pod-name> | grep "GC"
# OOM 확인
kubectl logs <pod-name> | grep -i "OutOfMemory"
# 에러 로그
kubectl logs <pod-name> | grep -i "error"
최적화 시나리오별 가이드
시나리오 1: 높은 CPU, 빈번한 GC
진단
CPU: 80% (지속)
GC: 초당 0.3회
메모리: 512Mi
해결
# 메모리 2배 증설
resources:
requests:
memory: 1Gi # 512Mi → 1Gi
limits:
memory: 1Gi
# JVM 힙 조정
env:
- name: JAVA_OPTS
value: "-Xms1g -Xmx1g -XX:+UseG1GC"
예상 결과
CPU: 40% (감소)
GC: 초당 0.05회 (개선)
응답 시간: 50% 감소
시나리오 2: 트래픽 증가 시 응답 지연
진단
평시: CPU 50%, 응답 200ms
피크: CPU 100%, 응답 2000ms
HPA: minReplicas 3, maxReplicas 10
해결
# HPA 조정
spec:
minReplicas: 5 # 평시 트래픽도 여유 있게
maxReplicas: 30 # 피크 대비
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 60 # 70% → 60% (빠른 스케일 아웃)
# CPU burst 여유 확보
resources:
limits:
cpu: 3000m # 2000m → 3000m
예상 결과
평시: 5 Pods, CPU 30~40%
피크: 15~20 Pods, CPU 60%
응답 시간: 안정적 (p99 < 500ms)
시나리오 3: 메모리 누수 의심
진단
메모리 사용: 지속 증가 (1시간마다 +100Mi)
Full GC: 빈번 (5분마다)
CPU: 지속 상승 (60% → 90%)
해결
# 1단계: Heap dump 생성
kubectl exec -it <pod-name> -- jmap -dump:format=b,file=/tmp/heap.hprof 1
# 2단계: 분석
# Eclipse MAT, VisualVM 등으로 메모리 누수 위치 파악
# 3단계: 코드 수정
# 누수 원인 제거 (캐시 미정리, 리스너 미해제 등)
# 4단계: 임시 조치 (코드 수정 전)
# Pod 자동 재시작 설정
livenessProbe:
exec:
command:
- sh
- -c
- |
# 메모리 사용률 80% 초과 시 재시작
USED=$(cat /sys/fs/cgroup/memory/memory.usage_in_bytes)
LIMIT=$(cat /sys/fs/cgroup/memory/memory.limit_in_bytes)
if [ $((USED * 100 / LIMIT)) -gt 80 ]; then exit 1; fi
periodSeconds: 60
모니터링 설정
Prometheus + Grafana 대시보드
필수 메트릭
# GC 메트릭
- jvm_gc_pause_seconds_count
- jvm_gc_pause_seconds_sum
- jvm_gc_memory_allocated_bytes_total
- jvm_gc_memory_promoted_bytes_total
# 메모리 메트릭
- jvm_memory_used_bytes
- jvm_memory_max_bytes
- container_memory_usage_bytes
# CPU 메트릭
- container_cpu_usage_seconds_total
- container_cpu_cfs_throttled_seconds_total
# 애플리케이션 메트릭
- http_server_requests_seconds_count
- http_server_requests_seconds_sum
알람 설정
# Prometheus Alert Rules
groups:
- name: resource-alerts
rules:
# GC가 빈번한 경우
- alert: HighGCRate
expr: rate(jvm_gc_pause_seconds_count[5m]) > 0.1
for: 10m
annotations:
summary: "GC가 빈번합니다 (초당 회)"
description: "메모리 증설을 고려하세요"
# Full GC 발생
- alert: FullGCDetected
expr: rate(jvm_gc_pause_seconds_count{gc="G1 Old Generation"}[5m]) > 0
for: 5m
annotations:
summary: "Full GC가 발생했습니다"
description: "메모리 부족 또는 누수를 확인하세요"
# CPU 지속 높음
- alert: HighCPU
expr: rate(container_cpu_usage_seconds_total[5m]) > 0.8
for: 15m
annotations:
summary: "CPU 사용률이 높습니다 ()"
description: "GC 또는 처리량 문제를 확인하세요"
# 메모리 부족
- alert: HighMemory
expr: container_memory_usage_bytes / container_spec_memory_limit_bytes > 0.9
for: 10m
annotations:
summary: "메모리 사용률이 높습니다 ()"
description: "메모리 증설 또는 누수 확인이 필요합니다"
결론
Kubernetes 환경에서의 리소스 설계는 단순히 숫자를 늘리는 것이 아니라, 문제의 본질을 이해하고 올바른 해결책을 적용하는 것이다.
핵심 원칙 다시 보기
1. 메모리는 품질 안정성
- GC 빈도 감소
- CPU 안정화
- 응답 시간 개선
- 여유 있게 확보
2. 파드 수는 처리량 대응
- 부하 분산
- HPA로 자동 조절
- 필요 시에만 증가
3. CPU는 burst 허용
- 순간 부하 흡수
- GC 성능 유지
- requests < limits
실전 적용 체크리스트
배포 전
- 메모리 최소 1Gi 확보
- CPU limits가 requests의 2배 이상
- HPA 설정 (minReplicas ≥ 3)
- GC 옵션 설정 (G1GC, MaxGCPauseMillis)
- 부하 테스트 수행
운영 중
- GC 메트릭 모니터링
- CPU/메모리 사용률 추적
- 응답 시간 (p95, p99) 확인
- 알람 설정 및 대응 프로세스
문제 발생 시
- CPU + GC 패턴 분석
- 메모리 vs 파드 수 결정
- 코드 개선 필요 여부 판단
- 점진적 조정 및 검증
마지막 조언
“메모리는 아껴서 좋을 게 없다. GC 문제는 파드 수로 해결되지 않는다. CPU는 여유를 두어야 한다.”
리소스 설계의 목표는 비용 최소화가 아니라 안정성과 비용의 균형이다. 초기에 충분한 리소스를 확보하고, 모니터링을 통해 점진적으로 최적화하는 것이 가장 현명한 접근 방식이다.
댓글남기기