ContainerMemoryNearLimit 대응 절차
컨테이너가 자기 limits.memory 의 90% 를 30분 넘게 쓰면 발화한다.
다음 트래픽 증가에서 OOMKill 이 날 가능성이 높다는 뜻이다.
:::info 왜 노드 알람으로는 부족한가
기존 메모리 알람은 전부 노드·쿼터 수준이라 노드가 한가하면 침묵한다.
2026-08-08 MLflow 사고 때 컨테이너가 자기 상한의 98.5% 에 17시간 붙어
있었는데, 노드는 70% 예약이라 아무 알람도 울리지 않았다. 그러다 트래픽이
오자 OOMKilled 5연발로 서비스가 죽었다(AVAILABLE 0).
이 알람은 컨테이너 대 자기 limit 을 본다. 같은 상황에서 17시간 전에 울린다(역사 재생으로 확인). :::
1. 원인 확인 — 계단인가 우상향인가
가장 먼저 이것부터 가른다. 상한만 올리는 게 맞는지가 여기서 갈린다.
kubectl --context aks-genos-prod -n kube-prometheus-stack \
port-forward svc/kube-prometheus-stack-prometheus 19090:9090 &
curl -s -G http://127.0.0.1:19090/api/v1/query_range \
--data-urlencode 'query=max(container_memory_working_set_bytes{namespace="gend",container="<컨테이너>"})/1024/1024' \
--data-urlencode "start=$(python3 -c 'import time;print(int(time.time())-604800)')" \
--data-urlencode "end=$(date +%s)" --data-urlencode 'step=21600'
7일 추이를 보고 판정한다:
| 모양 | 원인 | 조치 |
|---|---|---|
| 계단식 점프 후 평평 | 버전 업그레이드·설정 변경이 베이스라인을 올렸다 | 상한 상향 (§2) |
| 완만한 우상향 | 메모리 누수 | 상한 상향은 크래시를 늦출 뿐 — 코드를 고친다 |
| 주기적 톱니 | 배치·캐시 워밍 | 피크 기준으로 상한 재산정 |
:::danger 누수에 상한만 올리지 말 것 우상향인데 상한을 올리면 며칠 뒤 같은 자리에서 다시 죽는다. 그동안 알람은 "조치했다" 는 이유로 무시되기 쉽다. :::
배포 시점과 점프 시점이 겹치는지도 함께 본다:
kubectl --context aks-genos-prod -n gend rollout history deploy/<이름>
git log --oneline -5 -- infra/<서비스>/
2. 상한 조정 (계단식일 때)
리포 매니페스트를 고치고 PR 로 올린다 — kubectl patch 만 하면
다음 배포에서 되돌아간다.
resources:
requests:
memory: 1Gi # 실사용의 1/2 이상
limits:
memory: 4Gi # 실사용의 1.5배 이상
기준:
limits는 관측 실사용의 1.5배 이상. 2배면 넉넉하다.requests는 실사용의 1/3 이상. 너무 낮으면 스케줄러가 여유 없는 노드에 얹는다 — MLflow 사고 때 실제로 그랬다(실사용 2Gi 인데 예약 512Mi).
노드 여유를 먼저 확인한다:
kubectl --context aks-genos-prod describe node <노드> | \
sed -n '/Allocated resources/,/Events/p'
긴급 복구가 필요하면 patch 로 먼저 살리고, 같은 세션에 PR 을 올려 리포와 prod 의 드리프트를 남기지 않는다.
kubectl --context aks-genos-prod -n gend patch deploy <이름> --type=strategic \
-p '{"spec":{"template":{"spec":{"containers":[{"name":"<컨테이너>","resources":{"limits":{"memory":"4Gi"}}}]}}}}'
:::warning 컨테이너는 이름으로 지정할 것
/spec/template/spec/containers/0/... 같은 인덱스 지정은 위험하다.
Linkerd 사이드카는 파드에는 있지만 deployment 스펙에는 없어서, 파드에서
본 인덱스와 deployment 인덱스가 다르다.
:::
3. 워크로드 축소 (상한을 못 올릴 때)
노드에 여유가 없으면 소비 쪽을 줄인다.
- 워커 수: MLflow 3.x 는 uvicorn 워커를 여러 개 띄우고 각각이 앱을
통째로 적재한다.
--workers N으로 줄이면 거의 비례해 준다(동시성도 같이 준다). - 캐시·배치 크기: 서비스별 설정 확인.
- replica 분산: 단일 파드에 몰린 부하를 replica 로 나눈다.
4. 조치 후 확인
파드가 Running 인 것은 기능 증거가 아니다. 엔드포인트와 실제 응답을
본다.
kubectl --context aks-genos-prod -n gend get endpoints <서비스> # 비어 있으면 안 됨
kubectl --context aks-genos-prod -n gend get pod | grep <이름> # restarts 증가 멈춤
# 사용률이 실제로 내려갔나
curl -s -G http://127.0.0.1:19090/api/v1/query --data-urlencode \
'query=container_memory_working_set_bytes{namespace="gend",container="<컨테이너>"}
/ on(namespace,pod,container) kube_pod_container_resource_limits{resource="memory"}'
룰 반영 방식 (v1.2+, #3253)
infra/monitoring/alerts/** 가 main 에 머지되면 alert-rules-apply 워크플로가
자동으로 kubectl apply 하고 엔진 로드까지 검증한다(/api/v1/rules 로
선언 전건 존재 + health != err). 실패하면 잡이 빨갛게 죽는다 — 수동
kubectl apply 는 이제 비상시에만 쓴다.
★ 이 자동화가 생긴 이유: 이 문서의 알람 자체가 머지됐는데 클러스터에 없었다. CR 존재 ≠ 엔진 로드이므로, 검증은 항상 rules API 기준이다.
임계값 근거
0.9 / for: 30m 은 실측으로 골랐다 (2026-08-08 prod).
| 임계 | 발화 건수 | 판정 |
|---|---|---|
> 0.95 | 0건 | 여유가 너무 적어 조치 시간이 없다 |
> 0.9 | 0건 | 채택 — 평시 조용 + 사고는 17시간 전 포착 |
> 0.8 | 3건 (정상 가동 중) | 상시 발화해 알람이 무시된다 |
for: 30m 은 배치 작업의 일시적 피크를 걸러낸다.
:::note 임계를 바꾼다면
회귀 가드 scripts/test_container_memory_limit_alert.py 가 임계가 이
사고를 실제로 잡는지 검사한다. 알람의 존재만 보면 판별력이 없어
(0.99 로 써 놔도 통과) 사고 당시·수정 후 실측값을 둘 다 넣어 가르는지 본다.
임계를 바꾸면 이 가드도 함께 갱신하고, 재실측 근거를 남길 것.
:::
관련
- Grafana 대시보드
- 알람 정의:
infra/monitoring/alerts/gend-alerts.yaml - 회귀 가드:
scripts/test_container_memory_limit_alert.py