Files
joungmin 2d41f22b83 fix(infra): #316 backend resource request 재산정 + RollingUpdate 25%/25% 복귀
노드 다운사이징(2×1OCPU/6GB) 이후 backend CPU request 500m이 노드 한도
의 절반을 차지해 rollingUpdate 데드락 발생. 임시 패치(maxSurge=0/
maxUnavailable=1) 상태를 합리화하여 25%/25% 기본 정책으로 복귀.

변경:
- cpu 500m/1 → 300m/800m
- mem 768Mi/1536Mi → 512Mi/1024Mi
- strategy 25%/25% 명시 (기본값 복귀)

근거: 실측 idle CPU 0.7%, RSS ~305 MB. peak 30-40% 추정 안에서 안전.
검증: 적용 후 노드 잔여 330m → 다음 배포 시 두 Pod 공존 가능 (무중단).
다운타임: 이번 1회 ~25초 (구 500m Pod 점유 해제), 다음 배포부터 0초.

설계서: docs/design/316-backend-resource-rightsize/README.md (Approved).

Refs: #316
2026-06-15 12:07:47 +09:00

128 lines
7.1 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 설계서: backend resource request 재산정 (#316)
> **상태**: Approved <!-- Draft | Approved | Superseded -->
> **작성**: [AI] Architect · **최종수정**: 2026-06-15
> **추적성** — Redmine: #316 · 관련 ADR: 없음 · 부모 이슈: #267 (현행화/배포 컨텍스트)
> · 구현 파일: `k8s/backend-deployment.yaml`
> · 테스트: kubectl rollout 무중단 (수동 검증) · 자동 테스트 없음
## 1. 목적 (Why)
노드 다운사이징(2 × 2 OCPU/8 GB → 2 × 1 OCPU/6 GB) 이후 `backend` Deployment의 CPU request 500m이 노드 가용 자원의 절반을 차지하여, RollingUpdate 시 신/구 Pod 공존이 불가능. 임시로 `maxSurge=0, maxUnavailable=1` 패치(매 배포 ~30초 다운타임) 상태를 합리화하여 25%/25% 정책으로 복귀하고 무중단 배포를 회복한다.
## 2. 범위 (Scope)
- **포함**
- `k8s/backend-deployment.yaml``resources.requests`/`limits` 재산정.
- 같은 파일의 `spec.strategy` 또는 라이브 deploy의 strategy를 25%/25%로 복귀(이미 패치되어 있다면 디폴트로 환원).
- **제외 (out of scope)**
- frontend/redis 등 다른 Deployment.
- JVM heap/-XX 옵션(별도 튜닝 이슈).
- HPA, VPA 도입.
- 노드 수/형상 추가 변경.
## 3. 인수조건 (Acceptance Criteria)
- [ ] backend CPU request ≤ 300m, memory request ≤ 512Mi (실측 ~305 MB 사용 기준 여유 포함).
- [ ] limit은 노드 한도(1 OCPU / 6 GB) 안에서 cpu ≤ 800m, mem ≤ 1Gi.
- [ ] Deployment strategy: `maxSurge: 25%`, `maxUnavailable: 25%`(또는 default).
- [ ] `kubectl apply` 직후 새 Pod이 Pending 없이 Running 진입.
- [ ] 적용 동안 `https://www.tasteby.net/api/health` 가 끊김 없이 200 응답.
- [ ] 변경 사항을 git 커밋·push.
## 4. 컨텍스트 & 제약
- 의존성: OKE 1.34.2, ARM64 노드 1 OCPU/6 GB × 2.
- 노드 가용 CPU(allocatable, 시스템 데몬 차감 후): 약 940-960m per node.
- 노드 가용 메모리(allocatable): 약 5.0-5.3 GiB per node.
- 같은 노드에 frontend(200m/256Mi), kube-system DaemonSet들(약 200m/300Mi 합계), 가끔 redis/cert-manager Pod.
- 두 backend Pod이 한 노드에 공존하지 않아도 됨(replicas=1이지만 RollingUpdate 동안 일시적으로 2개).
- 제약: 비용 X(코드 변경 없음), 운영 영향(작지만 rollout 한 번 발생).
- 가정: 운영 실측 idle CPU 0.7%, peak 추정 30-40% (영상 추출·벡터 검색 시).
## 5. 아키텍처 개요
```
git: k8s/backend-deployment.yaml
▼ (수정)
spec.replicas: 1 (변경 없음)
spec.strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 25% (=0 → 25%, 임시 패치 복귀)
maxUnavailable: 25% (=1 → 25%, 임시 패치 복귀)
spec.template.spec.containers[0].resources:
requests: { cpu: 300m, memory: 512Mi } (500m/768Mi → 다운)
limits: { cpu: 800m, memory: 1024Mi } (1/1536 → 다운)
▼ (kubectl apply)
OKE rolling update → 새 Pod 1개 surge → Ready → 구 Pod 종료
www.tasteby.net 트래픽 무중단 (Ingress → Service → Pod, 100ms 응답 유지)
```
I/O 경계: 매니페스트(선언)와 클러스터 상태(런타임)는 `kubectl apply`로 단방향 동기화. 검증은 `rollout status` + 외부 curl.
## 6. 데이터 모델
| 필드 | 변경 전 | 변경 후 | 근거 |
|------|---------|---------|------|
| `resources.requests.cpu` | `500m` | `300m` | idle 0.7%, peak 추정 30%, 1 OCPU 노드에서 frontend(200m) + 잔여 시스템(~200m) 후 여유 |
| `resources.requests.memory` | `768Mi` | `512Mi` | 실측 ~305 MB, JVM heap·코드캐시·메타스페이스 여유 |
| `resources.limits.cpu` | `1` | `800m` | 1 OCPU 노드에서 throttle 방지 + 다른 Pod 여유 |
| `resources.limits.memory` | `1536Mi` | `1024Mi` | OOM 위험 줄이고 노드당 5 GiB allocatable에서 안정 |
| `strategy.rollingUpdate.maxSurge` | `0` (임시) | `25%` | 무중단 RollingUpdate 복귀 |
| `strategy.rollingUpdate.maxUnavailable` | `1` (임시) | `25%` | 동일 |
## 7. 함수 명세
매니페스트 변경이라 코드 함수 없음. **변경 단위 표**:
| 변경 단위 | 위치 | 책임 | 검증 |
|-----------|------|------|------|
| `requests.cpu` 다운 | `k8s/backend-deployment.yaml:37` | 새 Pod 스케줄링 가능 | `kubectl describe pod` Events에 FailedScheduling 없음 |
| `requests.memory` 다운 | `k8s/backend-deployment.yaml:38` | 같음 + OOM 안전 | Pod RSS / requests = 70% 이하 |
| `limits.cpu` 다운 | `k8s/backend-deployment.yaml:40` | throttle 제어 | `cpu.stat` throttled_usec 안 늘어남 |
| `limits.memory` 다운 | `k8s/backend-deployment.yaml:41` | OOM 보호 | OOMKilled 없음 |
| `strategy` 추가 | `k8s/backend-deployment.yaml` spec | 25%/25% 명시 | live patch와 GitOps 일치 |
> 모두 단순 선언적 변경. 복잡 함수 별도 fn-*.md 불필요.
## 8. 흐름 / 알고리즘
1. `k8s/backend-deployment.yaml` 편집(resources + strategy).
2. `kubectl apply -f k8s/backend-deployment.yaml`.
3. 신 Pod 1개 surge → Ready 대기 (~30-60초, JVM startup).
4. Ready 되면 구 Pod 종료(graceful, terminationGracePeriodSeconds 기본 30초).
5. `kubectl rollout status deploy/backend -n tasteby` PASS.
6. 외부 `curl https://www.tasteby.net/api/health` 연속 200 확인.
## 9. 엣지케이스 & 에러 처리
- **JVM startup이 readinessProbe initialDelaySeconds(30s)보다 길면**: 새 Pod이 Ready 못 받음 → 구 Pod 유지 → rollout 진행 안 됨. 현재 backend는 보통 20-25초에 Ready.
- **노드 가용 메모리 부족**: 두 backend Pod이 한 노드에 갈 경우 ~1 GiB 차지. frontend + DaemonSet 합치면 압박 가능. scheduler가 적절히 분산 기대 (replicas=1 surge 시).
- **OCIR 풀 실패**: imagePullBackOff 시 rollout 중단. 이미지 이미 풀돼 있으므로 영향 적음.
- **rollback**: 문제 시 `kubectl rollout undo deploy/backend` 또는 git revert + apply.
## 10. 테스트 계획
- 수동: 적용 직후 새 Pod Running 확인 + 외부 health 200 연속(약 2분간 5초 간격 polling).
- 부하 측정 후속: 운영 부하 24시간 관찰 → CPU throttle/OOM 없음 확인 (별도 follow-up).
- 자동 테스트: 해당 없음 (인프라 매니페스트).
## 11. 리스크 & 대안 검토
- **선택**: cpu 300m / mem 512Mi.
- **대안 A**: cpu 250m / mem 384Mi — 더 여유롭지만 peak 시 throttle 가능성.
- **대안 B**: cpu 400m / mem 640Mi — 안전 마진 크지만 25%/25% 복귀해도 두 Pod 공존 불가 가능(노드 잔여 CPU 부족).
- **대안 C**: replicas=2 + topologySpreadConstraints — 가용성↑이지만 비용·리소스↑, 현재 노드 한도에서 부적합.
- **트레이드오프**: 선택안은 peak에서 약간 빠듯하나 limits 800m로 burst 허용. 운영 24시간 관찰 후 재조정.
## 12. 미해결 질문
- peak 시(영상 추출 동시 다수) CPU 실제 사용량 — 현재 metrics-server 미설치라 정확 측정 불가. 추후 설치 후 재산정.
- HPA 도입 여부 — 노드 1 OCPU에선 의미 적음. 노드 추가 후 검토.
- replicas=2 가용성 강화 — 새 노드 형상에서 메모리 압박 우려, 별도 결정 필요.