Back

노드 메모리 93%, 가설 9개를 기각하고 남은 진짜 원인

K8s 알림 대응을 자동화해둔 환경에서 worker 노드 메모리 93% 알림이 왔다. 스킬의 기본 진단이 끝난 뒤에도 원인이 안 보였고, 거기서부터 20시간이 시작됐다. 가설을 세우면 검증에서 기각되고, 다시 세우면 또 기각되기를 아홉 번. 그동안 결론이 세 번 뒤집혔다. “메모리가 왜 올랐나”를 붙잡고 있는 동안엔 답이 안 나왔는데, 어느 순간 질문을 거꾸로 뒤집고 나서야 메트릭 그래프 한 줄에서 진짜 원인이 보였다.

알림과 첫 점검

worker 노드 메모리 93%, 15분 이상 90% 초과. 해당 노드에 수십 개 Pod이 올라가 있었고, 메모리 상위는 Airflow worker 2대, 각각 5GB 이상.

전날 정전이 있었다. 모든 노드가 다운됐다가 복구된 상태. worker Pod에 들어가서 airflow jobs check를 쳤더니 No alive jobs found. 태스크 0개인데 5GB를 점유하고 있었다.

가설들을 차례로 세우고 기각

매시간 실패하던 DAG의 세션 정리 누락이 첫 번째 후보였다. 해당 DAG를 중단하고 worker를 롤링 재시작했더니 노드 메모리가 93%에서 68%로 떨어졌다. 10분 후 다시 92%. 일시적인 측정 지연이었을 뿐 실제로는 줄지 않았다. 기각.

두 번째 후보는 “정전 복구 직후 밀린 작업이 한꺼번에 돌면서 worker 프로세스 메모리가 분산됐을 것”이라는 방향이었다. 검증하려고 worker Pod을 삭제하고 새로 뜬 fresh worker의 메모리를 측정했더니 90초 만에 5GB에 도달했다. 일상적인 작업만으로도 같은 메모리 패턴이 나온다는 뜻. 기각.

이 외에도 이미지/라이브러리 변경, DAG 파일 변경 등 원인 후보가 줄줄이 세워지고 줄줄이 기각됐다.

질문을 바꿨다: “왜 올랐나” → “왜 그동안 낮았나”

여기서 접근을 바꿨다. 같은 concurrency 64인데 정전 전 3GB, 정전 후 5GB. 뭐가 다른가.

Prometheus의 container_processes 메트릭 7일 추이가 답을 줬다. 정전 전 37~40 processes, 정전 후 69. 부모/init 프로세스를 빼면 정전 전 child가 32개. 같은 64가 아니었다. 정전 전에는 32로 돌고 있었다.

3주 전에 특정 검증용으로 Secret에 WORKER_CONCURRENCY=64가 patch됐지만, worker Pod을 재시작하지 않아서 적용이 안 됐었다. envFrom: secretRef는 Pod 시작 시 한 번만 주입된다. 정전이 우연한 적용 트리거가 된 것이다.

조치와 결과: 관성을 숫자로 밀어내기

원인은 찾았다. 문제는 조치 방향. concurrency 64는 외부 환경을 참고해 복사한 값이다. 처음엔 “의도적으로 설정한 값이니 건드리면 안 된다”는 입장이 자연스러웠다.

하지만 이 환경에는 그만한 실제 부하가 걸리지 않는다. 14일치 태스크를 분석해보니 623건 중 98%가 5초 이내에 끝나고, 일부 큐는 태스크 0건이었다. 4대 worker가 22GB를 먹으면서 아무 일도 안 하고 있었다. 동시 64개 슬롯이 필요한 부하가 아니다. “참조용 설정이니 유지한다”는 관성을 실제 부하 데이터로 밀어낸 결정이었다. Secret의 환경변수 오버라이드를 제거하고 airflow.cfg 기본값(16)으로 돌렸다. GitOps 매니페스트를 변경하고 sync, 전체 rolling restart.

항목변경 전변경 후
노드 메모리95%73%
6대 worker 합계~33 GB~9.3 GB

73%는 정전 전 베이스라인(78%)보다 낮다. 기본값 16이 이 환경에는 적정이었다.

정전이 아니었으면 아무 문제가 없었다

이 인시던트의 아이러니는 거기 있다. 3주 전 누군가 Secret을 patch하고 worker 재시작을 잊었고, 그 “잊음” 덕분에 옛 설정(concurrency 32)이 계속 살아서 시스템은 멀쩡했다. 정전이 모든 Pod을 강제로 재시작시키면서 숨어 있던 64가 비로소 적용됐다. 적용을 빠뜨린 설정 변경은 사라지지 않고 어딘가에서 트리거를 기다린다. 이번엔 정전이 그 트리거였다. patch만으로는 적용 여부를 알 수 없고, restart 확인까지가 하나의 작업 단위라는 걸 비싸게 배웠다.

20시간을 끌었던 건 가설이 부족해서가 아니라 오히려 그럴듯한 가설이 너무 많아서였다. 세션 누수도, catch-up도, COW 측정 artifact도 전부 말이 됐고, 그래서 결론이 세 번 뒤집혔다. 직감이 맞아도 데이터가 없으면 확신할 수 없지만, 데이터가 있어도 해석이 틀리면 결론은 또 뒤집힌다. 끝을 낸 건 더 많은 가설이 아니라 질문을 거꾸로 돌린 것이었다. “왜 올랐나”를 “왜 그동안 낮았나”로 바꾸자, 그제야 container_processes 그래프의 32와 64라는 숫자가 눈에 들어왔다.