본문으로 건너뛰기

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.950건여유가 너무 적어 조치 시간이 없다
> 0.90건채택 — 평시 조용 + 사고는 17시간 전 포착
> 0.83건 (정상 가동 중)상시 발화해 알람이 무시된다

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