마이크로서비스 커널 자원 제약 (Pod / Container 자원 오버커밋 킬링 정책)

핵심 인사이트 (3줄 요약)

  1. 본질: 쿠버네티스(K8s)와 같은 컨테이너 오케스트레이션 환경에서 노드의 물리적 한계를 초과하여 컨테이너들을 배치(오버커밋)하기 위해, 리눅스 커널의 **cgroups (Control Groups)**를 조작하여 CPU와 메모리를 정밀하게 통제한다.
  2. 메커니즘: 자원 제약은 크게 최소 보장량인 **Requests (요청)**와 최대 허용치인 **Limits (제한)**로 나뉜다. CPU 한도를 초과하면 커널 스케줄러가 스로틀링(Throttling)을 걸어 속도만 늦추지만, 메모리 한도를 초과하면 커널의 OOM(Out-Of-Memory) Killer가 발동해 컨테이너를 즉시 살해(Kill)한다.
  3. 가치: 이 메커니즘을 통해 클라우드 서버의 자원 활용률(Utilization)을 극한으로 끌어올리면서도(비용 절감), 특정 마이크로서비스의 메모리 누수(Leak)가 전체 노드를 붕괴시키는 것을 차단하는 완벽한 샌드박스 안정성을 확보한다.

Ⅰ. 개요 및 필요성 (Context & Necessity)

  • 개념: 컨테이너 자원 제약은 리눅스 커널의 cgroups를 기반으로 특정 프로세스 그룹(컨테이너/Pod)이 쓸 수 있는 자원의 상한선을 강제하는 정책이다. '오버커밋(Overcommit)'은 물리 서버가 가진 자원보다 더 많은 양의 자원(Limit의 합)을 컨테이너들에게 허락해 주는 클라우드의 경제적 꼼수다.

  • 필요성 (Noisy Neighbor 문제):

    • 100개의 컨테이너가 하나의 워커 노드(리눅스 서버)에서 돌아간다. 그중 하나의 컨테이너(개발자가 while(1)에 메모리 할당을 실수로 넣은 버그 서버)가 미쳐 날뛰어 램(RAM)을 100% 집어삼켰다.
    • 자원 통제가 없다면 서버의 가용 램이 0이 되어, 멀쩡히 돌아가던 나머지 99개의 중요 서비스 컨테이너까지 덩달아 멈추거나 커널 패닉이 온다. 이를 시끄러운 이웃(Noisy Neighbor) 현상이라고 한다.
    • 해결책: 커널(OS)이 각 컨테이너마다 "너는 CPU 2개, 램 4GB까지만 써"라고 명확한 선을 긋고, 선을 넘는 순간 경찰(OOM Killer)을 보내 즉결 처형해야 한다.
  • 💡 비유:

    • 오버커밋(Overcommit): 비행기가 100석인데 항공사는 손님들이 다 안 탈 것을 예상하고 표(Limit)를 120장 판다.
    • Requests와 Limits: 기내식 배급. Requests(요청)는 "이 손님은 무조건 밥 1개를 보장해 줘"이고, Limits(제한)는 "남는 밥이 아무리 많아도 이 손님은 절대 2개 이상은 주지 마"이다.
    • Throttling (CPU): 승객이 제한보다 말을 많이 하려 하면, 승무원이 입을 틀어막아 말하는 속도를 늦춘다(살려두긴 함).
    • OOM Kill (Memory): 승객이 제한보다 밥을 많이 먹으려 하면, 승무원이 그 승객을 비행기 밖으로 던져버린다(즉사).
  • 발전 과정:

    1. Ulimit / Quota (초기): 사용자(UID) 계정 기반의 단순한 자원 제한.
    2. cgroups v1 (2008): 구글이 리눅스에 기증. CPU, Memory 서브시스템을 트리 형태로 분리하여 제어. 도커(Docker)의 핵심 기반.
    3. cgroups v2 (현재): 계층 구조를 통합하고, 메모리와 I/O의 압박(Pressure Stall Information)을 훨씬 더 정교하게 연동하여 통제.
  • 📢 섹션 요약 비유: 오버커밋은 은행이 예금액보다 더 많은 돈을 대출해 주는 '부분지급준비제도'와 같습니다. 평소엔 이득을 극대화하지만, 뱅크런(자원 경합)이 발생했을 때 누구의 돈부터 뺏을지(OOM Kill) 정해놓은 냉혹한 룰이 바로 자원 제약 정책입니다.


Ⅱ. 아키텍처 및 핵심 원리 (Deep Dive)

cgroups의 자원 통제 아키텍처 (CPU vs Memory)

CPU는 '압축 가능한(Compressible)' 자원이고, 메모리는 '압축 불가능한(Incompressible)' 자원이다. 이 차이가 처벌 방식을 완전히 가른다.

자원 종류커널 제어 파라미터 (cgroups)K8s 용어 (Limit)한도 초과 시 커널의 조치비유
CPUcpu.cfs_quota_us / cpu.cfs_period_uslimits.cpuThrottling (스로틀링): 스케줄러가 남은 시간 동안 CPU를 뺏고 다음 주기가 될 때까지 멈춰 세움말 너무 빨리 하면 강제로 숨 고르게 하기 (느려짐)
Memorymemory.limit_in_byteslimits.memoryOOM Kill (강제 종료): 초과 즉시 프로세스에 SIGKILL을 날려 죽이고 메모리 회수밥 너무 많이 먹으면 식당에서 쫓아내기 (죽음)

쿠버네티스(K8s)의 Requests와 Limits 동작 원리

K8s는 사용자가 YAML 파일에 적은 Requests와 Limits를 Kubelet을 통해 리눅스 커널의 cgroups 설정으로 번역한다.

  ┌───────────────────────────────────────────────────────────────────┐
  │                 K8s 자원 제약 (Requests / Limits) 커널 매핑            │
  ├───────────────────────────────────────────────────────────────────┤
  │                                                                   │
  │  [K8s Pod YAML 설정]                                               │
  │  resources:                                                       │
  │    requests:                                                      │
  │      cpu: "1"        (1 코어 보장)                                  │
  │      memory: "1Gi"   (1 GB 보장)                                   │
  │    limits:                                                        │
  │      cpu: "2"        (최대 2 코어 제한)                               │
  │      memory: "2Gi"   (최대 2 GB 제한)                               │
  │            │                                                      │
  │  ==========▼ (Kubelet이 Linux cgroups 파일에 기록) ====================│
  │                                                                   │
  │  [Linux cgroups v1 파일 시스템]                                       │
  │  1. CPU Requests ──▶ cpu.shares = 1024 (상대적 가중치 비율)            │
  │                      * 경쟁이 심할 때 1코어만큼의 시간을 보장해줌.         │
  │                                                                   │
  │  2. CPU Limits   ──▶ cpu.cfs_quota_us = 200000, period = 100000    │
  │                      * 100ms(주기) 동안 200ms(2코어 분량)만 쓰도록 강제. │
  │                        초과하면 CPU 뺏김 (Throttled)                 │
  │                                                                   │
  │  3. Mem Requests ──▶ 메모리는 '가중치' 개념이 없음. 오직 스케줄러가 노드를 │
  │                      선택할 때(빈 공간이 1GB 남았나?)만 사용됨.           │
  │                                                                   │
  │  4. Mem Limits   ──▶ memory.limit_in_bytes = 2147483648 (2GB)      │
  │                      * 프로세스가 2GB + 1바이트를 할당 요청하는 순간,      │
  │                        커널의 OOM Killer가 발동하여 프로세스 사살!         │
  └───────────────────────────────────────────────────────────────────┘

[다이어그램 해설] CPU Request는 시스템에 여유가 있을 때는 아무 의미가 없다. 컨테이너는 Limit까지 CPU를 펑펑 쓴다. 그러다 모든 컨테이너가 CPU를 달라고 싸우기 시작하면(경합), 커널 스케줄러인 CFS(Completely Fair Scheduler)는 cpu.shares 비율에 맞춰 정확히 Request 양만큼만 공평하게 CPU 시간을 쪼개어 준다. 반면 메모리는 저장 공간이므로 비율로 나눌 수 없다. Memory Request는 단순히 "이 노드에 나를 올리려면 최소 1GB의 빈 공간이 있어야 해"라고 K8s 마스터에게 스케줄링을 부탁하는 용도로만 쓰이고, 커널 차원에서는 오직 Memory Limit만이 방어선 역할을 한다.


OOM (Out-Of-Memory) Killer의 살해 대상 선정 (QoS 클래스)

노드 전체의 물리 메모리가 부족해지면 커널은 누구를 먼저 죽일지 점수(oom_score_adj)를 매긴다. K8s는 자원 설정에 따라 3가지 QoS (Quality of Service) 클래스로 분류하여 이 점수를 커널에 주입한다.

  1. Guaranteed (가장 늦게 죽음): Requests == Limits (메모리와 CPU 모두 동일하게 설정). 가장 중요한 서비스. (oom_score_adj = -997)
  2. Burstable (중간에 죽음): Requests < Limits. 평소엔 적게 쓰다 가끔 많이 쓰는 서비스. (oom_score_adj = 사용량에 따라 동적 계산)
  3. BestEffort (가장 먼저 죽음): Requests와 Limits를 아예 안 적은 서비스. 공용 자원을 갉아먹는 1순위 제거 대상. (oom_score_adj = 1000)
  • 📢 섹션 요약 비유: 타이타닉호가 추락할 때(OOM), 구명조끼를 안 입은 밀항자(BestEffort)를 가장 먼저 바다로 던지고, 그 다음 3등석 승객(Burstable)을 던집니다. 돈을 많이 내고 지정석을 산 1등석 승객(Guaranteed)은 배가 완전히 가라앉기 전까지 끝까지 보호받습니다.

Ⅲ. 융합 비교 및 다각도 분석

자원 오버커밋(Overcommit) 철학 비교

구분No Overcommit (전통적 VM)K8s Burstable (클라우드 오버커밋)
설계 철학보장된 자원만 1:1로 쪼개어 판매남는 자원을 돌려쓰며 효율 극대화
설정 방식Requests == LimitsRequests < Limits
자원 활용률낮음 (20~30%, 남는 자원 버려짐)매우 높음 (70~80% 이상)
위험도낮음 (성능 일정함)높음 (OOM Kill, Throttling 발생 가능)

과목 융합 관점

  • 컴퓨터구조 (CA): 자바(JVM)나 Node.js V8 엔진은 과거에 자기가 도는 환경이 물리 서버인 줄 알고, 물리 서버의 전체 RAM 크기를 읽어와 힙(Heap) 메모리를 세팅했다. cgroups 제약을 무시하고 메모리를 쓰다 보니 컨테이너만 띄우면 1초 만에 OOM Killed를 당했다. (현재는 +UseContainerSupport 옵션으로 JVM이 cgroups limits를 읽도록 아키텍처가 패치되었다.)

  • 소프트웨어공학 (SE): MSA 아키텍처에서는 한 컨테이너가 죽어도(OOM Kill) K8s의 ReplicaSet이 1초 만에 다시 살려내므로 치명적이지 않다. 즉, 소프트웨어 아키텍처 자체가 **"언제든 죽을 수 있음(Design for Failure)"**을 전제로 짜여 있기 때문에 OS 커널이 무자비한 킬링 정책(OOM)을 사용할 수 있는 것이다.

  • 📢 섹션 요약 비유: 과거에는 서버가 안 죽게 하려고 넉넉한 텐트를 쳐주었다면(전통적 VM), 지금은 컨테이너가 숨 막혀 죽으면 바로 치워버리고 새 복제인간을 찍어내는 매트릭스 공장(MSA)의 효율을 택한 것입니다.


Ⅳ. 실무 적용 및 기술사적 판단

실무 시나리오

  1. 시나리오 — CPU Throttling으로 인한 무한 랙(Hang) 발생 현상: Java Spring Boot 웹 서버 컨테이너의 CPU Limits를 "1" 코어로 설정했다. 트래픽이 몰리지도 않았는데 응답 시간이 평소 10ms에서 갑자기 500ms로 튀는 지터(Jitter)가 주기적으로 발생.

    • 원인 분석: 자바는 멀티스레드 기반이다. 요청 하나를 처리할 때 4개의 스레드가 동시에 돌면, 0.25초 만에 Limit("1" 코어) 분량의 할당량을 전부 소진해 버린다. 커널의 CFS 스케줄러는 이 컨테이너를 악성으로 간주하고 남은 0.75초 동안 CPU를 아예 주지 않고 멈춰버린다(Throttled). 트래픽은 적은데 순간 버스트(Burst) 때문에 스로틀링 덫에 걸린 것이다.
    • 대응 (기술사적 가이드):
      1. 가장 확실한 방법: **CPU Limits를 아예 삭제(Remove)**한다. Limits가 없어도 Requests 비율에 따라 스케줄러가 알아서 남는 CPU를 공평하게 나눠주므로, 넷플릭스나 구글 등 최상위 테크 기업들은 "CPU Limits를 쓰지 마라"고 강력히 권고한다.
      2. 유지해야 한다면: Limits 값을 스레드 개수 이상(예: "4")으로 넉넉히 열어두어 짧은 순간의 버스트를 허용하게 한다.
  2. 시나리오 — Node 노드 통째로 다운 (OOM 커널 패닉): K8s 워커 노드 하나가 뻗어서 알람이 울렸다. 들어가 보니 노드 자체에 여유 메모리가 없어서 커널이 SSH 데몬이나 Kubelet 데몬까지 OOM 킬러로 다 죽여버렸다. 컨테이너가 아니라 호스트가 붕괴한 것이다.

    • 원인 분석: K8s 마스터가 노드에 컨테이너를 밀어 넣을 때, 호스트 OS가 쓸 메모리(System reserve)를 뺴지 않고 100% 꽉 채워 스케줄링했기 때문이다.
    • 대응: Kubelet 설정(kubelet config)에서 --system-reserved=memory=1Gi--kube-reserved=memory=1Gi를 반드시 명시하여, 커널과 K8s 에이전트가 숨 쉴 공간(하드 리밋 방어선)을 노드 차원에서 미리 빼두어야(Allocatable 축소) 한다.

의사결정 및 튜닝 플로우

  ┌───────────────────────────────────────────────────────────────────┐
  │                 컨테이너 자원(Requests/Limits) 설정 의사결정 플로우        │
  ├───────────────────────────────────────────────────────────────────┤
  │                                                                   │
  │   [새로운 마이크로서비스(Pod)를 K8s 클러스터에 배포]                       │
  │                │                                                  │
  │                ▼                                                  │
  │      이 서비스가 죽으면 시스템 전체가 마비되는 핵심(Core) 서비스인가?         │
  │          ├─ 예 ─────▶ [QoS 클래스: Guaranteed 설정 강제]             │
  │          │            - CPU Requests == CPU Limits                │
  │          │            - Mem Requests == Mem Limits                │
  │          │            (단, CPU Throttling 방지를 위해 Limits는 충분히 크게) │
  │          └─ 아니오 (배치 작업, 개발 서버 등)                             │
  │                │                                                  │
  │                ▼                                                  │
  │      자원 효율성(Overcommit)을 극대화하여 비용을 줄여야 하는가?            │
  │          ├─ 예 ─────▶ [QoS 클래스: Burstable 설정]                 │
  │          │            - CPU Limits는 아예 삭제 (또는 높게)             │
  │          │            - Mem Limits는 Requests의 1.5~2배 수준으로 설정 │
  │          │            (단, 메모리 누수로 죽을 수 있음은 감수해야 함)      │
  │          │                                                        │
  │          └─ 아니오 ──▶ 모든 서비스에 Limits 강제 룰(LimitRange) 적용   │
  └───────────────────────────────────────────────────────────────────┘

[다이어그램 해설] "Limits를 빡빡하게 거는 것이 안전하다"는 것은 초보자의 가장 큰 오해다. 메모리 Limits는 컨테이너를 지키는 방패가 아니라 컨테이너의 목을 조르는 밧줄이다. 컨테이너를 살리고 싶다면 Limits를 Requests와 동일하게 넉넉히(Guaranteed) 주어야 하며, 클러스터의 집적도(돈)를 높이고 싶다면 Limits를 풀고 OOM 킬(죽음)을 받아들이는 아키텍처로 가야 한다.

도입 체크리스트

  • JVM/Node.js 튜닝: 컨테이너의 메모리 Limit을 2GB로 줬다면, 자바 애플리케이션의 힙 메모리 옵션(-Xmx)을 절대 2GB로 주면 안 된다. JVM 자체의 메타스페이스, 스레드 스택 메모리를 고려해 힙은 1.5GB(-XX:MaxRAMPercentage=75) 수준으로 여유를 두었는가?

  • LimitRange와 ResourceQuota: 개발자가 실수로 엄청나게 큰 Request를 적어서 클러스터를 통째로 점유하는 것을 막기 위해, K8s 네임스페이스(Namespace) 단위로 최대 할당 가능 자원 총량(Quota)을 제한해 두었는가?

  • 📢 섹션 요약 비유: Limit 설정은 '아이에게 용돈 주기'와 같습니다. 너무 빡빡하게(낮은 Limit) 주면 아이가 답답해서 아무 일도 못하고(스로틀링/OOM), 너무 제한 없이 풀어주면 집안 기둥뿌리가 뽑힙니다(노드 패닉). 앱의 진짜 소비 패턴을 관찰하고 적절한 상한선을 긋는 것이 기술입니다.


Ⅴ. 기대효과 및 결론

정량/정성 기대효과

구분자원 제약 없음 (No Limits)올바른 Requests/Limits 설정개선 효과
정성 (안정성)좀비 컨테이너 1개가 노드 전체 다운샌드박스화 되어 해당 컨테이너만 사망시끄러운 이웃(Noisy Neighbor) 완벽 차단
정량 (자원 효율)피크치 기준 할당으로 서버 낭비 극심평균치 기준 Request 할당 (Overcommit)노드당 컨테이너 집적도 2배 이상 증가 (비용 절감)
정성 (스케줄링)K8s 스케줄러가 노드 여유분 판단 불가Request 기반 빈 공간 테트리스 배치클러스터 로드밸런싱 최적화 완료

미래 전망

  • cgroups v2와 PSI (Pressure Stall Information): 현재 cgroups v1은 OOM이 터지기 전까지 시스템이 얼마나 숨 막히는지(Pressure) 알 방법이 없었다. cgroups v2의 PSI 기술을 도입하면, CPU/Memory/IO가 부족해 스레드가 대기(Stall)하는 시간을 커널이 백분율(%)로 리포팅해 준다. K8s는 이를 바탕으로 OOM이 나기 전에 미리 안전하게 컨테이너를 다른 노드로 이주시키는(Eviction) 지능형 스케줄링으로 진화하고 있다.
  • eBPF 기반 동적 자원 제어: Limits를 고정값으로 박아두는 대신, 커널 내부의 eBPF 프로그램이 네트워크 트래픽과 디스크 I/O 양을 실시간으로 분석하여 Limits를 마이크로초 단위로 동적(Dynamic)으로 늘렸다 줄였다 하는 AI 오토 튜닝이 연구되고 있다.

결론

마이크로서비스 커널 자원 제약(cgroups Requests/Limits)은 '유토피아(클라우드)'를 지탱하기 위한 '디스토피아적 통제 시스템'이다. 무한해 보이는 클라우드 자원도 결국 한정된 물리 서버의 조각일 뿐이다. OS 커널은 스로틀링(Throttling)과 OOM Killer라는 무자비한 칼을 휘둘러 이기적인 컨테이너들을 통제하고 다수의 생존을 보장한다. 이 잔인하지만 공평한 커널 킬링 정책의 철학을 이해하고, 내 애플리케이션이 언제 스로틀링 걸리고 죽을지(QoS)를 설계하는 것이 진정한 클라우드 네이티브 엔지니어의 첫걸음이다.

  • 📢 섹션 요약 비유: 한정된 구명보트(노드)에 많은 사람을 태우기 위해(오버커밋), 배가 가라앉을 위기가 오면 주저 없이 무임승차자부터 바다로 던져버리는(OOM Kill) 냉혹하고도 완벽한 생존 시스템입니다.

📌 관련 개념 맵 (Knowledge Graph)

개념 명칭관계 및 시너지 설명
cgroups (Control Groups)K8s의 Requests/Limits를 리눅스 커널 단에서 실제로 강제하고 통제하는 핵심 커널 서브시스템
OOM (Out-Of-Memory) KillerMemory Limits를 1바이트라도 초과하는 순간 커널이 발동시키는 프로세스 사형 집행기
CFS (Completely Fair Scheduler)CPU Requests(가중치)와 Limits(주기/할당량)를 바탕으로 스로틀링을 조율하는 리눅스 기본 CPU 스케줄러
QoS (Quality of Service) ClassRequests와 Limits의 설정 조합에 따라 컨테이너의 생존 우선순위(Guaranteed, Burstable, BestEffort)를 나누는 K8s의 등급 체계
Noisy Neighbor (시끄러운 이웃)자원 제약이 없을 때 한 프로세스가 자원을 독점하여 물리 서버를 공유하는 다른 정상 프로세스들을 굶어 죽이는 클라우드의 적

👶 어린이를 위한 3줄 비유 설명

  1. 거대한 피자(서버 메모리)를 100명의 친구들(컨테이너)이 나눠 먹기로 했어요. 근데 한 욕심쟁이 친구가 피자를 혼자 다 먹으려고 해요.
  2. 그래서 선생님(운영체제 커널)이 미리 규칙을 정했어요. "너는 무조건 2조각은 보장해 줄게(Request). 하지만 4조각 이상 먹으려 하면 식당에서 내쫓을 거야!(Limit)"
  3. 만약 그 친구가 5조각째를 먹으려고 손을 뻗는 순간, 선생님이 호루라기를 불고 즉시 식당 밖으로 던져버린답니다(OOM Kill). 그래서 다른 친구들이 굶지 않고 평화롭게 피자를 먹을 수 있어요.