20200322

Kubernetes prod 장애 회고 — OOM

지난주 목요일 오전 9시 30분, prod 클러스터 중 하나에서 결제 관련 파드들이 줄줄이 죽기 시작했다. OOMKilled. 30분간 결제 실패율이 치솟았고, 우리는 그 30분 동안 장님이었다.

회고 정리.

무슨 일이 있었나

한 서비스가 점점 메모리를 먹다가 limit(1Gi) 찍고 죽음. 재시작. 다시 먹음. 죽음. 이게 10분마다 반복. 처음엔 로드 스파이크 때문인 줄 알았는데 트래픽은 오히려 평소보다 낮았다.

원인

어제 배포한 PR에 있던 캐시 로직. 요청별 temp 데이터를 모듈 레벨 dict에 쌓는데, 만료 로직이 잘못돼서 삭제 안 되고 계속 쌓임. 배포 직후엔 쌓이는 속도가 느려서 테스트 환경에서 안 걸림. 12시간 쯤 지나니 1Gi 넘음.

코드 대략 이런 모양이었다.

_cache = {}

def get_or_set(key, fn, ttl=60):
    now = time.time()
    if key in _cache:
        val, exp = _cache[key]
        if exp > now:
            return val
    val = fn()
    _cache[key] = (val, now + ttl)  # 만료된 key는 영원히 안 지워짐
    return val

TTL 체크만 있고 eviction이 없다. key가 매 요청마다 유니크(request id가 섞여있었음)했기 때문에 증가만 함. 아 이건 진짜 허무한 버그.

왜 바로 못 잡았나

1) 메모리 알람 임계치가 85%인데 pod은 limit에 닿으면 그냥 kill. 알람이 울릴 시간이 없음.

2) prometheus 수집 주기 30초라 짧은 라이프사이클 pod의 메모리 그래프가 "어? 막 생겼다 사라지네" 수준으로만 보임.

3) 로그에 OOMKilled 로그 이벤트 집계 대시보드가 없었음. kubectl describe pod으로 일일이 봐야 알 수 있었다.

고친 것

  • 해당 캐시 로직을 cachetools TTLCache로 교체 (내부에 eviction 있음)
  • kube_pod_container_status_last_terminated_reason="OOMKilled" 메트릭을 alertmanager에 등록
  • 메모리 limit의 80% 닿으면 pre-alert (scrape 주기도 15초로)
  • 모든 서비스에 GOMEMLIMIT 혹은 resource 기반 인메모리 캐시 상한 명시 규칙 추가

개인적 교훈

"모듈 레벨 dict에 뭐 쌓는 순간 이게 무한 성장 가능한지 자문해라." 작년 이맘때도 비슷한 거 한 번 냈었다. 같은 실수 반복. 그래서 이번엔 린트 룰로라도 박을까 고민 중. 아니면 최소한 코드리뷰 체크리스트에 넣든가.

OOM은 보이지 않는 버그라 무섭다. 부하 테스트만으로는 못 잡고 시간이 지나야 드러난다.