Kubernetes 노드 메모리가 바닥나면 누가 먼저 죽는가 — QoS 클래스가 아니라 usage − request

seonwooj0810·3일 전

1. 도입

"메모리가 모자라면 BestEffort → Burstable → Guaranteed 순으로 죽는다." 많이 들은 설명인데, Node-pressure Eviction 공식 문서에는 "kubelet does not use the pod's QoS class to determine the eviction order"라고 적혀 있다.

노드에서 파드를 없애는 주체는 kubelet(eviction)과 커널 OOM killer 둘이다. 두 경로를 소스까지 따라가 보니 둘 다 "request보다 얼마나 더 쓰는가"로 움직였고, QoS 순서는 그 결과를 대충 맞히는 라벨이었다.

2. 두 개의 방어선

kubelet은 housekeeping-interval(기본 10s)마다 memory.available을 임계값과 비교해 파드 단위로 퇴출한다. 그런데 그 사이 메모리가 급하게 오르면 kubelet이 MemoryPressure를 보기도 전에 커널 OOM killer가 프로세스를 SIGKILL한다(문서의 Known issues). 그래서 두 경로를 따로 봐야 한다.

3. 내부 동작

kubelet: 3단 비교자

pkg/kubelet/eviction/helpers.go의 메모리 압박 정렬은 한 줄이다.

// request 초과 여부 → PriorityClass → (usage − request) 순으로 비교
orderedBy(exceedMemoryRequests(stats), priority, memory(stats)).Sort(pods)

BestEffort는 request가 0이라 1단계에서 늘 "초과" 그룹이고, Guaranteed는 limit = request라 늘 "미초과" 그룹이다. 흔히 말하는 QoS 순서는 이 1단계의 부산물이고, 같은 그룹 안에서는 Priority가 먼저 비교된다.

커널: kubelet이 심어둔 oom_score_adj

커널은 Priority를 모른다. 대신 kubelet이 컨테이너를 만들 때 pkg/kubelet/qos/policy.go의 공식대로 oom_score_adj를 넣어 둔다.

QoSoom_score_adj
Guaranteed-997
BestEffort1000
Burstable1000 − 1000 × request / 노드 메모리 (3~999로 클램프)

mm/oom_kill.c의 oom_badness()는 rss + swap + 페이지테이블(페이지 단위)에 adj × totalpages / 1000을 더해 점수를 만든다. 노드 전체 페이지를 T, 사용량을 U, request를 R이라 하고 Burstable 공식을 대입하면 이렇게 된다.

points = U + (1000 − 1000·R/T) · T/1000 = T + (U − R)

T는 모든 컨테이너에 똑같이 붙으니 순위는 U − R로만 갈린다. kubelet의 3단계 비교자와 같은 축이다.

두 킬러 모두 "request 대비 초과 사용량"을 보고, QoS는 그 근사치일 뿐이다.

4. 같은 노드, 다른 피해자

공식을 숫자로 대입해 봤다. 노드 메모리 16Gi, 스왑 없음, 컨테이너당 프로세스 하나라고 가정한다.

파드QoSrequestusagePriorityadj커널 점수(Gi 환산)
ABestEffort01Gi010001 + 16 = 17
BBurstable2Gi5Gi10008755 + 14 = 19
CBurstable4Gi3Gi07503 + 12 = 15
DGuaranteed4Gi3.9Gi0-9973.9 − 15.95 ≈ -12

kubelet 순서는 A → B → D → C다. 초과 그룹(A, B) 안에서는 Priority가 낮은 A가 먼저다. 미초과 그룹은 Priority가 같으니 usage − request가 큰 D(-0.1Gi)가 C(-1Gi)보다 앞선다. 즉 Guaranteed가 Burstable보다 먼저 퇴출된다.

커널 OOM이 먼저 터지면 B가 죽는다. 커널은 Priority를 모르고, B의 초과량 3Gi가 A의 1Gi보다 크다. 어느 경로가 먼저 발동했느냐에 따라 피해자가 달라진다.

-997도 면제가 아니다. 커널이 후보에서 빼는 값은 -1000(OOM_SCORE_ADJ_MIN)뿐이라, 음수 점수라도 남은 후보가 그것 하나면 죽는다. 확인은 이렇게 한다.

kubectl exec web -- cat /proc/1/oom_score_adj   # Burstable req 4Gi / 노드 16Gi → 750
kubectl exec web -- cat /proc/1/oom_score       # 0~2000 스케일, 이 순서가 U−R 순서와 맞는지 비교

다만 문서가 경고하듯 oom_badness는 프로세스 단위라, 컨테이너 안에 작은 프로세스가 여럿이면 이 근사가 깨진다.

5. 정리

kubelet은 (request 초과 → Priority → usage − request)로, 커널은 T + (U − R)로 고른다. QoS 클래스는 어느 쪽에서도 직접 입력이 아니다. 컨테이너 limit에 닿는 memcg OOM은 이전 글 cgroup v2 메모리 컨트롤러와 OOM에 정리했고, 다음엔 Memory QoS(memory.min/memory.high)가 OOM 이전에 긋는 보호선을 파 볼 생각이다.

참고 자료

0개의 댓글