Kubernetes 환경에서의 cgroup과 global OOM과 cgroup OOM. 단일 통합 root는 cgroup V2에서부터 도입되었다고(…cgroup v2 has only a single process hierarchy…)
Introduction
전편과 마찬가지로, 아이디어 출처 대부분은 AI이지만 전부 직접 작성했다. 논리 구조 확인, AI 간 cross check 뿐 아니라 의심스럽다 싶은 부분은 죄다 공식 문서 참조 링크를 달았다. 그럼에도 간간히 공식 문서 뒷바침이 없는 부분이 있는데, 그건 지쳐서 그런 것이니…
틀린 부분이 보인다 → 코멘트를 남겨주시면 그저 감사할 뿐. 
Summary
•
global OOM killer는 프로세스의 cgroup 소속에 무관하게 메모리 할당 실패가 발생하면 동작한다. 보통 메모리 할당 실패는 node 메모리가 고갈되는 시점으로, kill 대상에는 k8s 관리 프로세스, 시스템 프로세스도 포함된다(따라서 무섭다).
•
Kubernetes에서 사실 상 모든 프로세스는 cgroup의 통제를 받아 cgroup의 memory controller(memcg)가 cgroup OOM killer이다. 해당 cgroup의 limit 초과 여부가 OOM 발생의 기준이다(limit 설정은 걍 필수라 생각하자).
•
어짜피 완벽한 안정성이란 없다. 최선의 안정성과 효율성 trade-off만 있을 뿐이며 resources, PriorityClass, HPA 설정 뿐 아니라 app 종류 별 우선 순위를 두어 각기 다르게 이들 값을 다르게 가져가는 게 정신 건강에 좋아 보인다. 그리고 cgroup V2는 그냥 얼렁 도입하자.
•
효율성은 Burstable QoS 활용에서 나온다. request ~ limit 사이의 공유 메모리 영역 활용이 관건이고 systemReserved, kubeReserved 조절을 통해 해당 영역 조절이 가능하다.
Linux OOM killers
OOM killer는 cgroup과 global로 나뉜다. 앞선 그림에 보이듯 cgroup OOM은 container, pod에 각각 대응하여 cgroup 단위로 kill을 수행하고, global OOM은 cgroup OOM 발생 조건을 만족하지 않는 경우의 프로세스에 대해 kill을 수행하여, kill 대상에 root cgroup (global)에 위치한 k8s, 시스템 프로세스가 포함된다.
global OOM killer
상대적으로 다룰 내용이 적어 global OOM killer 먼저 다룬다.
Global OOM killer의 kill 대상 단위는 프로세스로서 node 수준에서 동작한다. 메모리 할당 실패 시 프로세스 별로 점수(oom_score )를 매겨 가장 높은 프로세스를 kill한다. 여기서 중요한 점은 이 프로세스 범위 내에 kubelet, containerd 등의 k8s 관리 프로세스와 system 프로세스가 포함되어 node 회복 불가를 야기할 수 있다는 점이다.
oom_score 는 사실 상 메모리 사용률 + QoS이기에 꽤나 직관적이다. 메모리 사용률 계산 시 분모로 (당연스럽게도) 가용 메모리, 즉 노드 전체 RAM과 swap 공간이 분모가 된다. oom_score_adj 란 값으로 표현되는 QoS의 경우 Guaranteed는 -997을, Burstable은 2 ~ 999, BestEffort는 1000을 가져, oom_score가 0 ~ 1000 범위라는 점을 고려했을 때 QoS가 매우 큰 영향을 미침을 알 수 있다.
cgroup OOM (killer)
일단 cgroup이 뭔지를 간단히 알아야 하겠는데, cgroup은 프로세스를 계층적으로 구성하고(cgroup core), 해당 계층 구조에 따라 리소스를 분배(cgroup controller)하는 놈으로 정의된다. version 1(V1)과 version 2(V2)가 있는데, V2는 나온지 꽤 오래되었음에도 여전히 V1은 많이 사용되는 듯하다. 어쨌건 cgroup의 가장 중요한 기능은 격리(isolation)로서, container 및 k8s pod는 이를 활용한 일종의 application이 되겠다(앞선 그림 참조).
cgroup OOM은 memcg(memory controller)라 불리는 cgroup controller에 의해 수행되어, memory limit을 넘어서면 OOM을 일으킨다. memory limit은 k8s의 resources.limits.memory 에 대응하는 값으로, V1에서 memory.limit_in_bytes , V2에서 memory.max 로 지정된다.
유의할 점으로, 일시적으로 사용량이 이 limit을 넘어설 수 있거니와(the usage may go over the limit temporarily), 이에 잡히지 않는 예외 경로가 존재하기에 완전히 잡아내는 것은 아니며, 무엇보다 limit 지정을 안하면 cgroup OOM은 동작하지 않아 결국에는 global OOM으로 넘어간다.
아래 표는 Linux의 메모리를 회수 가능 여부(reclaimable; OOM 없이 회수 가능 영역)와 cgroup version 별로 OOM 관리 수준을 나눈 것으로, 보다시피 cgroup V2가 V1에 비해 많은 부분, 특히 TCP socket buffer를 포함한 kernel memory 대부분을 커버하기에 global OOM 발생을 막는데 상당히 유리하다. 참고로 표 내용 중 RSS(Resident Set Size)란 User space 중 실제 물리 메모리 사용 영역을 뜻한다.
memory 분류 | Space | cgroup v1 OOM 관리 | cgroup v2 OOM 관리 | 설명 |
Anonymous RSS | User | - 대표적 회수 불가능 메모리
- heap, stack, anon shm(공유 메모리)을 포함.
- OOM의 대표적 원인 | ||
File-backed RSS | User | - 파일을 메모리처럼 접근하기 위한 매핑으로 실행 파일
- 실행 파일, shared lib, file mmap 포함
- memory pressure 시 회수 지연으로 OOM 발생 가능 | ||
Reclaimable
(회수 가능) | Kernel | - dentry, inode, page cache 등이 포함
- memory pressure 시 회수 지연으로 OOM 발생 가능 | ||
Unreclaimable
(회수 불가능) | Kernel | - TCP socket buffer, kernel stack 포함
- cgroup V2에서는 대부분 측정(account)
- Spike 시 TCP socket buffer 급증이 대표적 OOM 원인 |
참고: Working Set 이란
보통 실제 메모리 사용량을 보려면 container_memory_working_set_bytes cadvisor metric 을 사용하라 하는데(...usage... 가 아니라), working set이 뭔지 싶어 함께 남긴다. 참고로, working set은 뇌과학에서도 거의 유사한 의미로 사용된다.
•
회수하면 당장 문제가 될 가능성이 높은 사용 중인 메모리
•
RSS – (즉시 회수 가능한 file cache) 또는 anonymous RSS + active file-backed RSS
한편, Kubernetes는 cgroup V2의 메모리 관리 구조와 정렬하려는 움직임이 있으나 2023년 기준 alpha 버전이 2년이 훌쩍 넘어간 현 시점에도 GA가 안되었다. 따라서 global OOM 발생을 막는데 상당히 유효해보이는 V2의 soft limit 기능은 언감생심.
cgroup V2에서 memory.max, memory.min 은 k8s resources.limits, resources.requests 에 매핑되고 의 soft limit인 memory.high 는 QoS에 따라 이들 두 값을 통해 자동 산출된다는 Kubernetes 문서. 문제는 2023년 현재 알파 버전인데 2년을 훌쩍 넘긴 아직도 바뀌지 않았다는 점. 따라서 오직 k8s GA로는 memory.max 밖에 활용 못한다.
안정성과 효율성의 동시 확보를 위한 전략
이제 마지막에 다 왔다(아이구 힘들다). 해결안 제시에 앞서 강조해야 할 점으로, Kubernetes 위에 올라간 workload는 원래 언제든 k8s 및 OS에 의해 죽을 수도 있다는 점이다. 그리고 k8s가 Guaranteed QoS와 Priority 등 안정성을 위한 다양한 장치를 마련함에도 완벽히 보장은 못한다는 점이다. 이를 언제나 전제를 깔고 app 개발을 해야지, k8s 위에 올라가고 나서 app 로직에 문제 없는데 왜 OOMKilled 가 나느냐..라고 해봐야 아무런 소용 없다.
따라서, 아래 내용은 해결안이라기 보다는 안정성과 효율성 동시 확보를 위한 최선안이다.
global OOM 발생 최소화 전략
global OOM을 최소화해야하는 이유는 앞서 설명했다(k8s, system 프로세스 kill 위험 완화). 이 말은 결국 global OOM 대신 cgroup OOM이 발생하도록 최대한 유도하는 것이다.
•
k8s의 resources.limits.memory 사용: limit이 없으면 global OOM으로 빠진다.
•
cgroup V2 지원 OS 사용: V1보다 관리 범위가 넓기에, 특히 spike에 취약한 TCP socket buffer를 다루기에 cgroup V2 지원 OS를 사용하는 것이 좋겠다. 메이저 업그레이드인데 V2를 쓰면 좋을 다른 이유도 많을 듯.
resources , HPA 설정 기본 전략
flowchart LR
subgraph NODE["<b>Node 전체 메모리</b>"]
direction LR
subgraph RESERVED["<b>🛡️ Reserved 영역</b>"]
direction LR
SYS["systemReserved"]
KUBE["kubeReserved"]
end
POOL["<b>Burstable 영역(공유 pool)</b><br/>request ~ limit 사이 영역<br/>━━━━━━━━<br/>• 선착순 사용<br/>• Spike 대응용<br/>• 미사용 시 타 Pod 활용 가능"]
subgraph POD_A["Pod #1"]
A_REQ["<b>pod 보장 영역</b><br/>request: 256Mi"]
A_BURST["limit: 512Mi"]
end
subgraph POD_B["Pod #N"]
B_BURST["limit"]
end
end
B_BURST -.->|"Burst 시 사용"| POOL
A_BURST -.->|"Burst 시 사용"| POOL
RESERVED -.->|"Reserved ↑ =<br/>→ Pod 보장 영역 ↓<br/>→ 스케줄링 가능 Pod 수 ↓<br/>→ Burst 여유 공간 ↑"| POOL
style NODE fill:#f1f5f9,stroke:#334155,stroke-width:1px
style RESERVED fill:#fecaca,stroke:#dc2626,stroke-width:2px
style SYS fill:#fee2e2,stroke:#ef4444,stroke-width:1px
style KUBE fill:#fee2e2,stroke:#ef4444,stroke-width:1px
style POOL fill:#fef9c3,stroke:#ca8a04,stroke-width:1px
style POD_A fill:#dbeafe,stroke:#3b82f6,stroke-width:1px
style POD_B fill:#dbeafe,stroke:#3b82f6,stroke-width:1px
style A_REQ fill:#bfdbfe,stroke:#2563eb
style A_BURST fill:#eff6ff,stroke:#60a5fa,stroke-dasharray: 3 3
style B_BURST fill:#eff6ff,stroke:#60a5fa,stroke-dasharray: 3 3Mermaid
복사
효율적 memory 사용을 위한 Burstable QoS 활용 전략. request ~ limit 영역은 사실 상 공유 pool로 일시적 요구에 대응하고, kubeReserved, systemReserved 조절을 통해 pool 크기를 조절한다.
지금 껏 논의는 죄다 안정성에 관한 것이었는데 이제야 효율성을 고려할 수 있겠다. 안정적으로 운영하면서도 효율적 리소스 사용을 고려한 방법이다.
•
resources.requests : Pod가 상시, 안정적으로 사용하는 대표값(e.g. P50, average)으로 설정한다.
◦
resources.requests 는 kube-scheduler에 의한 pod 스케줄링 기준이자 node-pressure eviction의 eviction 우선순위 판단 및 에 참조되는 값으로, app이 보장받아야 할 안정 구간의 의미로 사용한다.
•
resources.limits: Spike 등의 일시적 요구에 대응 가능한 량으로 설정한다.
◦
resources.limits 는 cgroup OOM이 동작하는 기준 값으로,
◦
AI는 resources.requests 대비 2~3배 수준을 논하는데 더 커져도 무방할 듯하다. 어디까지? app이 정상적이면 넘어설 수 없는 값이 상한이 아닐까 싶다.
•
pod의 requqest ~ limit 사이 공간은 ‘사실 상’ 공유 메모리 풀이므로 일정 량을 사전 지정한다.
◦
타 pod가 이 공간을 차지 중이라면 다른 pod는 사용 못한다. 즉, limit 까지 도달하기 전에 OOMKilled 가 발생할 수 있다는 뜻.
◦
◦
•
HPA(averageUtilization): resource.requests 보다 약간 작은 수준으로 설정한다.
◦
효율성 확보를 위한 주된 장치로, 값이 작을 수록 효율성은 떨어지고 높을 수록 커진다.
◦
AI는 80~90%을 논하는데, 적절히 requests, limits를 설정했다면 더 커져도 무방해 보인다.
서비스 종류 별 QoS, Priority 설정 전략
하기 전략은 노드 메모리 관리를 중요도 순으로 계층화하여, 노드의 관리 프로세스 생존을 최대화하고(안정성), Burstable QoS를 통해 노드 자원의 초과 할당을 허용하여 자원 효율성을 높이는 것을 목표로 한다.
순위 | 대상 워크로드 | 목표 | 설정 | 설정 사유 |
1 | 클러스터 관리 프로세스 (e.g. Kubelet, Containerd, OS 데몬) | 무조건 생존 보장 (노드 자체 회복력) | - kubeReserved, systemReserved 적정량 설정
- oom_score_adj 낮게설정 | - 리소스 보장
- 최대한 후순위로 global OOM 발생하도록 |
2 | Guaranteed App | 최대 생존 보장 | - QoS: Guaranteed
- PriorityClass: medium | - oom_score 이 낮아 OOM, eviction에 대해 유리
- 1순위 및 control plane 서비스보다는 낮지만 타 순위 대비 할당 가능성 높임 |
3 | DaemonSet (OTel Agent 등) | 생존 및 유연성 확보 (다양한 노드 크기 대응) | - QoS: Burstable
- PriorityClass: highest | - 효율적인 리소스 운용. 다만 상대적으로 안정성 낮음(1,2 순위 대비). 다양한 리소스 크기별 노드 타입에 효율적 대응
- 죽었을 경우 최대한 빠르게 노드에 진입하도록 |
4 | Burstable Deployment App (일반 워크로드) | 최대 효율성 (자원 초과 할당 허용) | - QoS: Burstable
- PriorityClass: low/default | - 안정성보다 효율성 중심.
- 리소스 이슈 발생을 pod에 격리하기 위해 limit 사용(따라서 BestEffort 사용하지 않음)
- 되도록이면 request도 사용하여 예측력을 높이는 것이 좋을 듯 |
기타 사항
다음은 본 주제 관련한 기타 AI가 전하는 사항이다. 난 설득되었다.
•
Local rate limiting은 spike에 상당히 도움 된다.
◦
kernel 네트워크 메모리 spike를 직접 차단하지는 못하지만, socket buffer 회전율을 높여 노드가 global OOM 또는 hard lockup에 도달하기까지의 시간을 지연시키며, 이 지연 구간은 대부분의 일시적 traffic spike가 소멸되거나 HPA가 개입할 수 있는 유일한 생존 창이 된다.
◦
Rate Limiting 참조.
•
TCP socket buffer 급증 문제는 HTTP/2(or gRPC)에서 상당부분 완화 가능하다.
◦
client는 server 측 WINDOW를 sync 받고 상황이 안좋으면 안보내는 구조임(flow control 기반 backpressure).
그림 코드 백업



