컨테이너 자원 관리
네임스페이스가 프로세스에게 자신만의 독립된 공간을 제공한다면, Cgroups(Control Groups)는 그 공간에서 사용할 수 있는 물리적 자원의 양을 엄격하게 제어하는 역할을 합니다.
예를 들어, 하나의 호스트에서 여러 컨테이너가 실행되고 있는 상황에서 특정 컨테이너가 무한 루프에 빠져 CPU를 100% 점유하거나, 메모리 누수(Memory Leak)를 발생시킨다면 어떻게 될까요?
만약 Cgroups라는 자원 통제 메커니즘이 존재하지 않는다면, 단 하나의 결함 있는 컨테이너가 호스트 전체의 CPU와 메모리를 잠식하여 시스템 전체 장애를 유발하는 문제가 발생할 수 있습니다.
따라서 Cgroups는 각 컨테이너가 사용할 수 있는 CPU, 메모리, I/O 등의 자원 한계를 설정함으로써, 특정 컨테이너의 과도한 자원 사용이 다른 컨테이너나 호스트 전체의 안정성에 영향을 주지 않도록 보호하는 핵심 메커니즘입니다.
CPU 제한의 원리
컨테이너의 CPU 자원을 제한할 때 리눅스 커널의 CFS(Completely Fair Scheduler)가 작동합니다. 가장 핵심적인 파라미터는 다음 두 가지입니다.
cpu.cfs_period_us
CPU 시간을 할당하는 기본 주기 (일반적으로 100ms)
cpu.cfs_quota_us
한 주기 동안 컨테이너가 CPU를 차지할 수 있는 시간
실제 컨테이너가 사용할 수 있는 CPU 코어 수는 다음 수식으로 결정됩니다.
Allocated CPU Cores = cfs_quota_us / cfs_period_us
예를 들어, 주기가 100,000us(100ms)일 때 쿼터가 50,000us라면 해당 컨테이너는 0.5 코어만큼의 CPU만 사용할 수 있도록 스로틀링(Throttling)이 걸립니다.
리눅스 시스템에서 Cgroups는 하나의 가상 파일 시스템으로 존재합니다. 일반적으로 /sys/fs/cgroup/ 경로에 마운트되어 있습니다. 컨테이너를 하나 띄우고 실제로 자원 제한이 어떻게 파일로 기록되는지 확인해 볼 수 있습니다.
Throttling
컨테이너 환경에서 애플리케이션을 운영하다 보면, 자원 모니터링 대시보드에서 CPU 사용률이 100%에 도달하지 않았음에도 불구하고 애플리케이션 응답 속도가 간헐적으로 느려지는 현상을 경험하기도 합니다. 이러한 상황에서 가장 먼저 의심해볼 수 있는 원인이 바로 CPU 쓰로틀링(CPU Throttling)입니다.
메모리는 자원이 부족해지면 OOM Killer에 의해 프로세스가 강제로 종료(Kill)되는 반면 CPU는 압축 가능한 자원이기 때문에, 할당된 한계를 초과하는 경우 프로세스를 즉시 종료하지 않고 운영체제가 일정 시간 동안 실행을 제한하거나 일시적으로 멈추는 방식으로 제어합니다.
이처럼 CPU 사용을 제한하는 메커니즘이 CPU 쓰로틀링이며, 이로 인해 실제 CPU 사용률이 100%에 도달하지 않았더라도 애플리케이션의 응답 지연이나 성능 저하가 발생할 수 있습니다. 이제 이러한 메커니즘이 어떻게 동작하는지, 그리고 왜 성능 문제의 원인이 될 수 있는지 자세히 살펴보겠습니다.
CPU 쓰로틀링은 설정된 할당량(Quota)을 주기(Period)가 끝나기 전에 모두 소진했을 때 발생합니다.
예를 들어, 컨테이너가 100ms의 주기 안에서 처음 50ms 동안 CPU를 집중적으로 사용해 할당량을 모두 소진했다면, 남은 50ms 동안은 CPU를 전혀 사용할 수 없으며 대기 상태로 머물러야 합니다. 즉, 다음 주기가 시작되어 새로운 할당량이 부여될 때까지 해당 프로세스는 더 이상 실행되지 못하고 사실상 작동 불능 상태가 됩니다.
이러한 메커니즘은 웹 서버나 API 서버처럼 짧은 시간 내에 요청을 빠르게 처리해야 하는 워크로드에서 특히 치명적인 영향을 줄 수 있습니다. 이러한 현상을 "마이크로 버스트(Micro-burst)" 문제라고 합니다.
예를 들어, 멀티스레드 기반 애플리케이션이 순간적으로 트래픽을 받아 4개의 스레드가 동시에 CPU를 사용한다고 가정해 보겠습니다. 이때 컨테이너의 CPU 할당량이 50ms라면, 4개의 스레드가 동시에 실행되는 순간 약 12.5ms 만에 모든 할당량을 소진하게 됩니다.
이 경우 컨테이너는 남은 약 87.5ms 동안 CPU를 전혀 사용하지 못한 채 대기 상태에 들어가게 됩니다. 결과적으로 1초 단위 평균으로 집계하는 모니터링 시스템에서는 CPU 사용률이 낮게(예: 30~40%) 보일 수 있습니다. 그러나 실제로는 100ms 단위의 미시적인 시간 구간에서 지속적으로 쓰로틀링이 발생하고, 그로 인해 클라이언트가 체감하는 응답 지연 시간(Latency)이 증가하거나 불규칙하게 튀는 현상이 나타나게 됩니다.

따라서 데이터베이스의 경우 AWR(Automatic Workload Repository) 기능을 통해 특정 시점의 성능 상태나 메트릭을 기반으로 분석을 수행할 수 있는 반면 Kubernetes나 컨테이너 환경에서는 특성상 세밀한 시스템 모니터링 체계를 구축하는 것이 필수적입니다.
특히 운영 환경에서는 CPU 쓰로틀링 발생 여부와 발생 횟수를 지속적으로 관찰해야 하며, 실제로 쓰로틀링이 발생했다면 그로 인해 어느 정도의 Latency가 발생했는지까지 면밀하게 분석할 필요가 있습니다.
CPU 쓰로틀링으로 인한 성능 저하를 해결하기 위해 다음과 같은 전략을 고려할 수 있습니다.
CPU Limit 상향 조정
애플리케이션의 피크(Peak) 사용량을 분석하여 여유 있는 Limit을 부여합니다.
CPU Limit 제거
메모리와 달리 CPU는 Limit을 넘는다고 컨테이너가 죽지 않습니다. 성능에 민감하고 레이턴시가 중요한 서비스라면 CPU Request(최소 보장량)만 설정하고 Limit은 아예 해제하여 남는 자원을 자유롭게 끌어다 쓰도록 구성하는 것도 흔히 쓰이는 패턴입니다.
OOM(Out of Memory) Killer
앞서 CPU 쓰로틀링(Throttling)을 설명하며, CPU는 자원이 부족하더라도 프로세스의 실행을 잠시 멈추는 방식으로 대응할 수 있는 압축 가능한 자원이라고 말씀드렸습니다.
하지만 메모리는 성격이 다릅니다. 메모리가 고갈되면 운영체제는 더 이상 새로운 프로세스를 생성하거나 데이터를 할당할 수 없게 되며, 최악의 경우 시스템 전체가 멈추는 커널 패닉 상황으로 이어질 수 있습니다.
이러한 최악의 상황을 방지하기 위해 리눅스 커널이 사용하는 마지막 방어 수단이 바로 OOM(Out of Memory) Killer입니다.
시스템 전체 또는 특정 Cgroup에 할당된 메모리 한계치에 도달했을 때, 커널은 메모리를 확보하기 위해 어떤 프로세스를 종료할지 판단하고 강제로 종료(Kill)합니다.
이제 커널이 어떤 기준으로 희생 프로세스를 선택하는지, 그리고 실제 운영 환경에서 OOM이 발생했을 때 어떤 방식으로 원인을 분석하고 대응할 수 있는지 그 알고리즘과 트러블슈팅 방법을 자세히 살펴보겠습니다.
두 가지 종류의 OOM: Host OOM vs Cgroup OOM
컨테이너 환경에서 OOM은 크게 두 가지 상황에서 발생합니다.
Host OOM (System OOM)
물리적인 서버(또는 VM) 자체의 메모리가 완전히 고갈된 상태입니다. 이때 커널은 호스트 시스템을 살리기 위해 호스트 내에서 가장 적합한 프로세스를 찾아 강제 종료합니다.
Cgroup OOM (Container OOM)
호스트의 메모리는 넉넉하지만, 특정 컨테이너가 자신에게 설정된 메모리 제한(memory.limit_in_bytes)을 초과하려고 할 때 발생합니다. 이때 OOM Killer는 해당 Cgroup 내부에 있는 프로세스들 중 하나를 타겟으로 삼아 종료시킵니다. 쿠버네티스에서 흔히 보는 OOMKilled 상태가 바로 이것입니다.
OOM Killer는 무작위로 프로세스를 죽이지 않습니다. 철저한 점수제를 통해 "죽였을 때 가장 메모리 확보에 도움이 되면서도, 시스템에 덜 중요한 프로세스"를 선정합니다.
모든 프로세스는 /proc/<PID>/oom_score라는 파일을 가지고 있으며, 이 점수는 0부터 1000점까지 매겨집니다. 점수가 높을수록 1순위 타겟이 됩니다. 점수를 계산하는 주요 기준은 다음과 같습니다.
메모리 사용량: 메모리를 많이 차지하고 있을수록 점수가 대폭 상승합니다.
프로세스의 권한: root 권한으로 실행된 프로세스는 약간의 점수 차감을 받아 상대적으로 보호받습니다.
실행 시간: 오래 실행된 프로세스일수록 점수가 낮아지는 경향이 있습니다.
사실상 컨테이너 환경을 사용하는 이유도 Application을 돌리기 위해 올리는거기 때문에 무조건 1순위가 Java 프로세스일 수 밖에 없습니다.
쿠버네티스는 내부적으로 이 oom_score_adj 메커니즘을 아주 영리하게 활용하여 파드(Pod)의 QoS(Quality of Service) 클래스를 구현합니다. 노드의 메모리가 부족해져 Host OOM이 발생할 위기에 처하면, 쿠버네티스는 다음 순서대로 파드를 희생시킵니다.

실제 필자도 시스템 운영 시 유의깊게 보는 부분이기도 합니다.
BestEffort (가장 먼저 죽음)
Requests와 Limits를 아예 설정하지 않은 파드입니다. oom_score_adj가 1000으로 설정되어 OOM Killer의 최우선 타겟이 됩니다.
Burstable
Requests와 Limits를 다르게 설정한 파드입니다. 메모리 사용량에 따라 동적으로 점수가 계산됩니다.
Guaranteed (가장 마지막까지 생존)
Requests와 Limits를 동일하게 설정한 파드입니다. oom_score_adj가 -997로 설정되어, 시스템의 핵심 데몬을 제외하고는 가장 강력한 보호를 받습니다.
OOM Killer는 시스템의 붕괴를 막기 위한 마지막 장치입니다. 이를 방지하기 위해서는 애플리케이션의 메모리 프로파일링을 통해 정확한 베이스라인을 측정하고, 컨테이너에 적절한 Memory Limit을 설정함과 동시에, 쿠버네티스의 QoS 클래스를 전략적으로 활용하여 중요한 워크로드를 보호하는 설계가 필수적입니다.
'쿠버네티스' 카테고리의 다른 글
| [쿠버네티스] "도커 네트워크가 어렵나요?" Docker0와 CNI로 풀어보는 컨테이너 통신 원리 (0) | 2025.11.24 |
|---|---|
| [쿠버네티스] 네임스페이스(Namespace)로 만드는 마법: 컨테이너가 서로를 볼 수 없는 이유 (0) | 2025.11.24 |
| [쿠버네티스] "어디서든 돌아가는 컨테이너의 비밀" 표준 규격 OCI가 중요한 이유 (0) | 2025.10.26 |
| [쿠버네티스] 컨테이너의 대명사 '도커', 아키텍처로 이해하는 빌드와 배포의 메커니즘 (1) | 2025.07.28 |
| [쿠버네티스] 물리 서버에서 클라우드 네이티브까지: 서버 인프라 격변의 중심 '컨테이너' (1) | 2025.07.28 |