K8s 중급 #4 resources.requests / limits — Pod의 자원 요청과 상한
K8s 중급 시리즈의 네 번째 글입니다. #3까지 시점은 클러스터 바깥에 있었습니다 — Service, Ingress, Ingress Controller로 외부 트래픽이 어떻게 클러스터 안의 워크로드까지 도달하는가의 모델이었습니다. 이번 글의 시점은 다시 Pod 안으로 돌아옵니다. 들어온 트래픽을 받아 일하는 컨테이너가 CPU와 메모리를 어떻게 요청하고 어떻게 상한을 부여받는가의 모델, 즉 resources.requests와 resources.limits의 이야기입니다. 이 두 필드의 분리가 K8s의 스케줄링과 안정성을 동시에 떠받치는 토대입니다.
이번 시리즈는 K8s 중급 7편입니다.
- #1 StatefulSet / DaemonSet / Job / CronJob — Deployment가 아닌 다른 컨트롤러들
- #2 PV / PVC / StorageClass — 영속 데이터 모델
- #3 Ingress와 Ingress Controller — 외부 진입점
- #4 resources.requests / limits — Pod의 자원 요청과 상한 ← 이번 글
- #5 Health check — liveness / readiness / startup probe
- #6 오토스케일링 — HPA / VPA / Cluster Autoscaler
- #7 RBAC / NetworkPolicy / ResourceQuota — 보안과 자원 정책
requests와 limits — 두 값의 역할이 다르다 #
K8s 매니페스트에서 자원 모델을 표현하는 필드는 두 개입니다. 컨테이너 한 개의 resources.requests와 resources.limits입니다. 둘이 비슷해 보이지만 보는 주체와 보는 시점이 완전히 다릅니다.
| 필드 | 보는 주체 | 보는 시점 | 의미 |
|---|---|---|---|
resources.requests | 스케줄러 (kube-scheduler) | Pod를 어느 노드에 둘지 결정할 때 | 이 컨테이너가 떠 있으려면 보장돼야 하는 최소 자원 |
resources.limits | kubelet (cgroup) | 컨테이너가 실제로 돌고 있을 때 | 이 컨테이너가 절대로 넘을 수 없는 상한 |
스케줄러는 새 Pod를 어디에 둘지 결정할 때 후보 노드들의 allocatable 자원(노드 전체 자원에서 시스템 데몬,kubelet 몫을 뺀 값)에서 이미 떠 있는 Pod들의 requests 합을 차감합니다. 새 Pod의 requests가 그 남은 양 안에 들어가는 노드만 후보가 됩니다. limits는 이 결정에 들어가지 않습니다. 한 노드의 limits 합이 allocatable을 초과해도 K8s는 Pod를 그 위에 띄웁니다 — 이를 오버커밋(overcommit) 이라 부르고, 노드의 자원을 통계적으로 효율적으로 쓰기 위한 기본 동작입니다.
limits는 그 다음 층에서 일합니다. Pod가 노드에 배정되고 컨테이너가 떠오르면, kubelet이 그 컨테이너의 cgroup에 limits 값을 설정합니다. 컨테이너가 그 한도를 넘기려고 하면 리눅스 커널이 강제로 막습니다. 이때 자원 종류에 따라 동작이 갈립니다 — CPU는 throttling(연산을 잠시 못 받게 함), 메모리는 OOMKilled(컨테이너 강제 종료)입니다. 이 차이는 뒤에서 따로 다루겠습니다.
머릿속 한 줄로 줄이면 — requests는 “보장돼야 하는 양"이라 스케줄링이 보고, limits는 “절대 넘으면 안 되는 양"이라 런타임이 강제합니다. 이 두 값을 다르게 쓸 수 있다는 점이 K8s 자원 모델의 핵심입니다.
CPU와 메모리의 단위 #
매니페스트에서 자주 헷갈리는 부분이 단위 표기입니다. CPU와 메모리가 각각 다른 표기법을 씁니다.
CPU — 코어와 밀리코어 #
CPU는 코어 단위입니다. 1은 1 코어, 2는 2 코어를 뜻합니다. 한 코어보다 작게 쪼개려면 밀리코어(millicore) 표기를 씁니다.
| 표기 | 의미 |
|---|---|
1 | 1 코어 (1000 millicore) |
500m | 0.5 코어 |
250m | 0.25 코어 |
100m | 0.1 코어 |
0.5 | 500m와 같음 |
운영 매니페스트에서는 100m, 250m처럼 밀리코어 정수형으로 적는 경우가 많습니다. 0.1 같은 소수 표기는 YAML 파싱 단계에서 헷갈릴 여지가 있어 피하는 패턴입니다. CPU 단위는 컨테이너 cgroup의 CPU quota로 매핑됩니다 — 100m이면 매 100ms 사이클당 10ms의 CPU 시간을 받는 식입니다.
메모리 — 바이너리 vs 십진수 #
메모리는 단위 접미사가 두 계열이 있어 운영 사고의 단골 원인이 됩니다.
| 표기 | 값 | 비고 |
|---|---|---|
1Ki | 1024 바이트 | 바이너리 |
1Mi | 1024 KiB = 1,048,576 바이트 | 바이너리 |
1Gi | 1024 MiB = 1,073,741,824 바이트 | 바이너리 |
1K | 1000 바이트 | 십진수 |
1M | 1,000,000 바이트 | 십진수 |
1G | 1,000,000,000 바이트 | 십진수 |
1Gi와 1G는 약 7% 차이가 납니다(1GiB가 더 큼). 운영 매니페스트의 표준은 바이너리 접미사(Mi, Gi) 입니다. 컨테이너 런타임과 OS가 메모리를 다루는 단위가 바이너리이고, kubectl top 같은 도구가 표시하는 값도 바이너리이기 때문입니다. 1G로 적었는데 사용량 표시는 0.93Gi로 나오는 사고는 단위 불일치에서 옵니다.
매니페스트 한 장 #
위 두 단위를 그대로 매니페스트에 적용해 보겠습니다. Deployment의 Pod 템플릿 안 컨테이너 정의에 resources 키를 넣는 모양이 표준입니다.
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
replicas: 3
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
containers:
- name: app
image: myapp/web:1.4.0
ports:
- containerPort: 8080
resources:
requests:
cpu: "250m"
memory: "512Mi"
limits:
cpu: "1"
memory: "1Gi"이 컨테이너는 0.25 코어와 512 MiB 메모리가 보장돼야 떠 있을 수 있습니다. 스케줄러는 새 Pod를 받을 때 그만큼의 여유가 있는 노드에만 둡니다. 떠 있는 동안에는 최대 1 코어와 1 GiB 메모리까지 쓸 수 있고, 그 한도를 넘기려는 시도는 cgroup이 강제로 막습니다.
resources 필드는 컨테이너 단위입니다 — 한 Pod 안에 여러 컨테이너가 있다면 컨테이너마다 따로 적습니다. Pod 전체의 requests / limits는 컨테이너들의 합으로 계산됩니다. 사이드카 컨테이너(예: 로그 수집기)가 있다면 그 컨테이너에도 작은 requests / limits를 잊지 말고 적어야 합니다.
kubectl top pod
kubectl top pod -n <namespace> --containersNAME CPU(cores) MEMORY(bytes)
web-7d4f8b9c5-abc12 180m 420Mi
web-7d4f8b9c5-def34 220m 480Mikubectl top은 metrics-server가 클러스터에 설치돼 있어야 동작합니다. 표시되는 값은 컨테이너 cgroup의 실제 사용량이므로 단위는 바이너리입니다.
requests / limits 조합 네 가지 #
매니페스트에 둘을 어떻게 적느냐에 따라 동작이 크게 갈립니다. 네 조합을 한 표로 정리하겠습니다.
| 조합 | 동작 | QoS | 운영 적합성 |
|---|---|---|---|
| 둘 다 적음 | 가장 안전. 스케줄링 보장 + 런타임 상한이 모두 명확 | requests = limits면 Guaranteed, 그 외 Burstable | 권장 |
requests만 적음 | 스케줄링 보장은 있지만 런타임 상한 없음. 컨테이너가 노드 전체 자원을 잠재적으로 점유 | Burstable | 사정상 limits를 빼는 경우만 |
limits만 적음 | K8s가 requests = limits로 간주함. 결과적으로 가장 보수적인 모양 | Guaranteed | 무난하지만 명시 적기 권장 |
| 둘 다 안 적음 | 스케줄링 시 차감 없음. 런타임 상한 없음 | BestEffort | 비권장 |
가장 자주 부딪히는 함정이 둘 다 안 적은 경우입니다. 이 컨테이너는 BestEffort QoS가 되고, 노드가 자원 압박을 받았을 때 가장 먼저 eviction(축출) 대상이 됩니다. 스케줄러도 이 Pod의 자원을 0으로 보고 노드를 고르므로, 한 노드에 BestEffort Pod가 잔뜩 몰리는 모양도 생깁니다. 운영 매니페스트에서는 작더라도 requests / limits를 항상 적는 편이 안전합니다.
requests만 적고 limits를 빼는 패턴은 의도가 분명한 경우에만 씁니다 — CPU limits의 throttling 동작이 응답 지연을 키우기 때문에 의도적으로 CPU만 limits를 빼는 운영자가 있습니다(뒤에서 자세히). 그러나 메모리는 limits를 빼면 잘못된 코드가 노드의 메모리를 다 잡아먹을 수 있으므로 거의 항상 적어 둡니다.
QoS 클래스 — Guaranteed / Burstable / BestEffort #
K8s는 Pod의 requests / limits 모양을 보고 세 등급의 QoS 클래스로 분류합니다. 이 분류가 노드 자원 압박 시 누가 먼저 쫓겨나는지를 결정합니다.
| QoS | 조건 | eviction 우선순위 |
|---|---|---|
Guaranteed | 모든 컨테이너의 모든 자원에 대해 requests == limits이고 둘 다 명시 | 마지막 (가장 안전) |
Burstable | requests만 있거나, requests / limits가 다르거나, 일부 컨테이너에만 적힌 경우 | 중간 |
BestEffort | 모든 컨테이너에서 requests / limits 모두 없음 | 첫 번째 (가장 위험) |
eviction은 노드의 메모리,디스크 압박 같은 신호가 임계치를 넘었을 때 kubelet이 Pod를 강제로 종료해 자원을 회수하는 동작입니다. BestEffort → Burstable → Guaranteed 순서로 후보가 됩니다. 같은 등급 안에서는 자원을 더 많이 쓰는 Pod가 먼저 후보가 됩니다.
kubectl get pod web-7d4f8b9c5-abc12 -o jsonpath='{.status.qosClass}'Burstable운영의 표준 패턴은 다음과 같습니다.
- DB,메시지 큐 같은 stateful 핵심 워크로드 — Guaranteed로 두기. requests = limits로 적어 eviction 가능성을 최소화.
- 일반 stateless 웹 / API 서버 — Burstable. 평소에 쓰는 양을 requests로, 버스트 가능 상한을 limits로.
- 배치 / 임시 워크로드 — Burstable 또는 BestEffort. 클러스터 자원이 부족할 때 먼저 양보해도 되는 워크로드.
BestEffort를 운영에서 쓸 일은 거의 없지만, 단기 디버깅용으로 띄운 임시 Pod 정도가 그 위치에 있습니다.
CPU limit의 함정 — throttling #
여기서부터가 운영 사고의 단골 부분입니다. CPU와 메모리가 limits를 초과했을 때의 동작이 완전히 다릅니다.
CPU limit은 throttling으로 강제됩니다. 컨테이너 cgroup의 CPU quota가 매 사이클(보통 100ms)마다 limits 만큼만 할당되고, 컨테이너가 그 양을 다 쓰면 다음 사이클이 올 때까지 연산을 못 받습니다. 컨테이너가 죽지는 않습니다 — 그저 잠시 멈춰 있다가 다음 사이클에 다시 깨어납니다.
예를 들어 cpu: limits: 100m인 컨테이너가 있다고 가정하겠습니다. 이 컨테이너는 매 100ms 사이클에서 10ms의 CPU 시간만 받을 수 있습니다. 그런데 요청 한 건이 50ms의 CPU를 필요로 한다면 — 그 요청은 첫 10ms를 쓰고 90ms를 기다리고, 다시 10ms를 쓰고 90ms를 기다리는 식으로 처리됩니다. 본래 50ms면 끝났을 작업이 약 410ms가 걸립니다.
이 동작이 운영에서 가장 흔히 만나는 사고가 응답 지연 폭증입니다. 평균 CPU 사용률은 limits보다 한참 낮은데, p99 응답 시간이 갑자기 튀는 패턴이 그것입니다. 단기 버스트가 limits를 친 순간 throttling이 걸린 것입니다. kubectl describe node나 cAdvisor 메트릭(container_cpu_cfs_throttled_seconds_total)에서 throttling 누적 시간을 확인할 수 있습니다.
이 부담 때문에 CPU limits를 의도적으로 빼는 운영 패턴도 존재합니다. requests로 보장량만 잡아 두고, 노드에 여유가 있을 때는 그 위로 자유롭게 버스트하게 두는 식입니다. 이 패턴은 다음 두 조건이 받쳐줄 때 쓰입니다.
- 노드의 자원이 충분히 여유 있고, 워크로드끼리 서로 적당한 수준에서 버스트해도 노드가 흔들리지 않음
- requests가 합리적으로 잡혀 있어 한 워크로드의 폭주가 다른 워크로드의 보장량을 침범하지 않음
반대로 메모리는 limits를 빼는 패턴이 거의 없습니다 — 메모리 폭주는 노드 전체를 위태롭게 합니다. 이어서 그 동작을 봅니다.
메모리 limit의 함정 — OOMKilled #
메모리 limit은 throttling이 아닌 hard cap입니다. 컨테이너가 limit 이상의 메모리를 할당하려고 하면 리눅스 커널의 OOM Killer가 그 컨테이너의 프로세스를 즉시 강제 종료합니다. K8s는 이 종료를 감지하고 컨테이너의 종료 사유를 OOMKilled로 기록합니다.
kubectl describe pod web-7d4f8b9c5-abc12Containers:
app:
Last State: Terminated
Reason: OOMKilled
Exit Code: 137
Started: Mon, 18 May 2026 14:22:10 +0900
Finished: Mon, 18 May 2026 14:35:42 +0900
Restart Count: 3Reason: OOMKilled와 Exit Code: 137(SIGKILL)이 짝으로 보이는 모양이 전형적입니다. Restart Count가 빠르게 올라가면서 같은 사유가 반복된다면 메모리 limit이 워크로드의 실제 사용량보다 작게 잡혀 있다는 신호입니다.
메모리 limits를 빼면 어떻게 될까요. 컨테이너 cgroup에 메모리 한도가 없는 상태가 되어, 잘못된 코드(메모리 누수, 큰 파일을 통째로 메모리에 올림 등)가 노드의 전체 메모리를 잡아먹을 수 있습니다. 그러면 노드 차원의 메모리 압박이 발생하고, 그 노드 위의 다른 Pod들이 BestEffort → Burstable → Guaranteed 순서로 eviction 대상이 됩니다. 한 워크로드의 사고가 같은 노드의 다른 워크로드까지 흔드는 모양입니다. 메모리 limits를 항상 적는 이유가 여기 있습니다.
요약하면 — CPU limits 초과는 throttling(즉시 종료 아님), 메모리 limits 초과는 OOMKilled(즉시 종료) 입니다.
JVM과 Go 런타임의 cgroup 인식 #
자원 limits를 적는 것만으로 충분하지 않은 런타임이 있습니다. JVM과 Go가 그 대표 사례입니다.
JVM #
옛 JVM은 호스트의 /proc/cpuinfo와 /proc/meminfo를 그대로 읽어 워커 스레드 수, 힙 크기, GC 스레드 수 등을 결정했습니다. 컨테이너의 cgroup limits는 보지 못했습니다 — cpu: limits: 500m인 컨테이너 안의 JVM이 호스트의 32 코어를 보고 GC 스레드를 32개 만들어 throttling에 걸리는 사고가 흔했습니다.
Java 8u131+ / Java 10+ 부터 -XX:+UseContainerSupport가 도입됐고, Java 10+ 부터는 이 옵션이 기본 활성화입니다. 이 옵션이 켜져 있으면 JVM이 cgroup의 CPU,메모리 limits를 인식해 스레드 수와 힙 크기를 결정합니다. 운영 환경의 컨테이너 이미지가 옛 JDK라면 이 옵션을 명시적으로 켜는 것이 안전합니다.
Go #
Go 런타임의 GOMAXPROCS(병렬 실행 가능한 OS 스레드 수)는 기본값으로 runtime.NumCPU()를 따릅니다. 그런데 이 값은 호스트의 코어 수를 반환합니다 — Go 런타임은 cgroup CPU limits를 자동으로 인식하지 않습니다. cpu: limits: 500m인 컨테이너의 Go 프로세스가 32 코어 호스트 위에서 GOMAXPROCS=32로 떠올라 throttling에 걸리는 패턴이 나옵니다.
해결은 두 가지가 표준입니다.
automaxprocs라이브러리 —go.uber.org/automaxprocs패키지를 import하면 프로세스 시작 시 cgroup CPU limits를 읽어GOMAXPROCS를 자동으로 맞춰 줍니다. 운영 표준에 가까운 패턴입니다.- 환경변수 수동 지정 — Pod 매니페스트의
env에GOMAXPROCS를 직접 설정.
env:
- name: GOMAXPROCS
value: "1"다른 언어 런타임도 비슷한 함정이 있을 수 있습니다. Node.js의 libuv 스레드풀 크기, Python의 multiprocessing.cpu_count() 같은 부분이 호스트 기준으로 잡히는지 cgroup 기준으로 잡히는지 한 번씩 확인해 두는 편이 안전합니다.
메모리 사용량 vs 메모리 limits 측정의 미묘함 #
메모리 사용량을 어떻게 측정하느냐에 따라 OOMKilled 시점에 대한 직관이 다르게 옵니다. cgroup이 보는 메모리는 RSS(Resident Set Size) + 페이지 캐시 같은 값이고, 컨테이너가 다루는 파일 I/O가 페이지 캐시를 채우면 그것도 limits에 포함됩니다. kubectl top이 표시하는 값은 보통 working set(RSS와 비슷하지만 회수 가능한 캐시 일부 제외)이라 OOM 직전까지의 사용량을 그대로 보여 주지는 않을 수 있습니다.
운영에서 OOMKilled가 반복된다면 다음 순서로 살펴보는 것이 안전합니다.
kubectl describe pod의Last State로 OOMKilled 사실과 횟수 확인.kubectl top pod --containers로 평소 사용량 확인.- cAdvisor 메트릭 또는 Prometheus의
container_memory_working_set_bytes,container_memory_rss로 시계열 확인. - 애플리케이션 차원의 메모리 누수 가능성과 limits 상향 조정 양쪽을 같이 검토.
LimitRange — 네임스페이스 단위 기본값 #
매니페스트마다 requests / limits를 일일이 적는 것은 사람이 잊어버리기 쉽습니다. K8s는 이 망각을 막는 객체로 LimitRange 를 제공합니다. 네임스페이스 단위로 기본값과 허용 범위를 걸어 두는 객체입니다.
apiVersion: v1
kind: LimitRange
metadata:
name: default-resource-limits
namespace: dev
spec:
limits:
- type: Container
default:
cpu: "500m"
memory: "512Mi"
defaultRequest:
cpu: "100m"
memory: "128Mi"
max:
cpu: "2"
memory: "2Gi"
min:
cpu: "50m"
memory: "64Mi"각 필드의 의미는 다음과 같습니다.
| 필드 | 의미 |
|---|---|
default | 컨테이너에 limits가 없으면 자동으로 부여될 기본값 |
defaultRequest | 컨테이너에 requests가 없으면 자동으로 부여될 기본값 |
max | 컨테이너 한 개의 자원 상한. 이 값을 넘는 매니페스트는 거부됨 |
min | 컨테이너 한 개의 자원 하한. 이 값보다 작은 매니페스트는 거부됨 |
이 LimitRange가 dev 네임스페이스에 적용된 상태에서 누군가 requests / limits를 빼먹은 매니페스트를 apply하면, K8s가 자동으로 default / defaultRequest 값을 채워 넣습니다. BestEffort QoS Pod를 실수로 만드는 사고가 차단됩니다. 반대로 한 컨테이너가 max 이상을 요구하면 매니페스트 적용 자체가 거부되어, 한 사람의 실수로 노드 전체 자원을 점유하는 사고도 막을 수 있습니다.
운영 패턴은 보통 이렇습니다.
- dev 네임스페이스 — 작은 default와 작은 max로 잡아 두기. 개발자가 가볍게 띄우게.
- stage,prod 네임스페이스 — 워크로드 특성에 맞춰 default를 넉넉하게 잡되, max는 한 컨테이너가 노드 전체를 점유하지 못하게 제한.
ResourceQuota — 네임스페이스 단위 합계 상한 #
LimitRange가 컨테이너 한 개 단위의 정책이라면, ResourceQuota 는 네임스페이스 전체의 합계 정책입니다. 한 네임스페이스 안의 모든 Pod의 requests / limits 합이 이 값을 넘지 못하게 막는 객체입니다.
apiVersion: v1
kind: ResourceQuota
metadata:
name: dev-quota
namespace: dev
spec:
hard:
requests.cpu: "10"
requests.memory: "20Gi"
limits.cpu: "20"
limits.memory: "40Gi"
pods: "50"이 ResourceQuota가 적용된 dev 네임스페이스에서는 모든 Pod의 requests.cpu 합이 10 코어를 넘지 못하고, Pod 개수가 50개를 넘지 못합니다. 새 Pod의 매니페스트가 이 한도를 깨면 apply가 거부됩니다.
ResourceQuota는 LimitRange와 짝으로 거는 패턴이 많습니다 — LimitRange가 매니페스트의 누락된 requests / limits를 채워 주지 않으면 ResourceQuota가 합계를 계산할 수 없기 때문입니다. requests가 비어 있는 BestEffort Pod는 ResourceQuota가 0으로 보지만, 운영에서는 LimitRange로 그 0을 미리 막아 두는 모양이 안전합니다.
ResourceQuota의 본격적인 활용은 #7 RBAC / NetworkPolicy / ResourceQuota 에서 다루겠습니다 — 보안과 자원 정책의 한 축으로 묶어 보는 편이 자연스럽습니다.
운영 패턴 — 어떻게 시작하고 어떻게 조정하나 #
새 워크로드의 requests / limits를 처음 정할 때 정확한 값을 알 길은 없습니다. 운영에서 정착된 패턴은 다음과 같은 순서를 따릅니다.
- 보수적인 requests + 여유 있는 limits로 시작 — 첫 단계는 추정입니다. 비슷한 워크로드의 과거 데이터를 참고하거나, 로컬에서의 부하 테스트로 대략의 값을 잡습니다. requests는 평소 사용량의 70~80%, limits는 그 두세 배 정도가 흔한 출발점입니다.
- 운영 데이터 수집 — Prometheus + metrics-server, 혹은 Datadog / New Relic 같은 APM이 노출하는 컨테이너별 CPU,메모리 시계열을 며칠 모읍니다. 트래픽이 가장 많은 시간대의 p95 / p99 사용량을 살핍니다.
- VPA recommender 활용 — Vertical Pod Autoscaler를
updateMode: Off로 띄워 두면 실제 자원 변경 없이 추천값만 받아볼 수 있습니다. K8s가 워크로드 특성을 학습해 적절한 requests / limits를 제안해 줍니다. VPA의 동작은 #6에서 깊게 다루겠습니다. - 조정과 재배포 — 추천값과 모니터링 데이터를 합쳐 매니페스트의 requests / limits를 갱신, 다음 배포에 반영. requests를 키우면 새 Pod가 떠야 적용되므로 보통 롤링 업데이트로 자연스럽게 흘러갑니다.
이 사이클을 워크로드 단위로 한 번씩만 돌려도 클러스터 전체의 자원 사용 효율과 안정성이 크게 달라집니다. 처음부터 정답을 맞추려고 시간을 쓰기보다, 빠르게 적당한 값으로 띄우고 데이터로 조정하는 편이 빠릅니다.
다음 글의 주제인 liveness / readiness probe가 자원 압박과도 직접 엮입니다 — Pod가 throttling으로 응답이 느려지거나 OOM 직전에 메모리 GC로 멈춰 있을 때, probe가 그 상태를 어떻게 감지하느냐에 따라 워크로드의 회복 행동이 달라집니다.
정리 #
이번 글에서 잡은 흐름을 정리하겠습니다.
requests와limits의 역할이 다릅니다 — requests는 스케줄러가 노드를 고를 때 보는 보장량이고, limits는 kubelet이 cgroup으로 강제하는 런타임 상한입니다. 둘이 다른 층의 정책입니다.- 단위 — CPU는 코어와 밀리코어(
1,500m,100m). 메모리는 바이너리(Mi,Gi)가 운영 표준.1Gi와1G는 7% 차이. - 조합 네 가지 — 둘 다 적기가 표준. limits만 적으면 requests = limits로 간주되어 Guaranteed. 둘 다 빼면 BestEffort, eviction 1순위.
- QoS 클래스 — Guaranteed(requests = limits) / Burstable(그 사이) / BestEffort(둘 다 없음). 자원 압박 시 BestEffort → Burstable → Guaranteed 순으로 eviction.
- CPU limits 초과는 throttling — 즉시 종료 아님, 응답 지연 폭증의 흔한 원인. 의도적으로 CPU limits만 빼는 운영 패턴도 존재.
- 메모리 limits 초과는 OOMKilled — 즉시 강제 종료.
kubectl describe pod의Last State: Terminated+Reason: OOMKilled+Exit Code: 137이 시그널. - JVM은
-XX:+UseContainerSupport(Java 10+ 기본 활성)로 cgroup 인식. Go의GOMAXPROCS는 cgroup을 인식하지 않으므로automaxprocs라이브러리 또는 env 수동 설정 필요. LimitRange— 네임스페이스 단위 기본값(default/defaultRequest)과 허용 범위(min/max). requests / limits 누락 매니페스트에 자동 부여.ResourceQuota— 네임스페이스 전체 합계 상한. LimitRange와 짝으로 거는 패턴. 자세한 활용은 #7.- 운영 사이클 — 보수적인 requests + 여유 있는 limits로 시작, 모니터링과 VPA recommender로 조정, 재배포로 반영.
이 모델까지 손에 들어오면 매니페스트의 resources 블록을 만났을 때 그 한 컨테이너가 어떤 QoS이고 노드 자원 압박 시 어떻게 행동할지를 한 줄로 읽을 수 있습니다.
다음 — Health check (liveness / readiness / startup probe) #
이번 글까지 다룬 것은 컨테이너가 받는 자원의 양의 모델이었습니다. 다음 편의 주제는 시점을 자원에서 컨테이너의 살아 있음으로 옮깁니다 — K8s가 컨테이너가 정상 동작 중인지를 어떻게 알아내고, 비정상 상태를 어떻게 감지해 회복 동작을 시작하는가의 모델입니다.
#5 Health check — liveness / readiness / startup probe에서는 세 종류의 probe를 한 사이클로 정리하겠습니다. liveness probe가 컨테이너 재시작을 트리거하는 신호이고, readiness probe가 Service의 엔드포인트에서 빼고 더하는 신호이고, startup probe가 시작이 느린 컨테이너에 그레이스 기간을 주는 신호입니다. 세 probe의 책임이 어떻게 다른지, HTTP / TCP / exec 세 검사 방식 중 무엇을 언제 쓰는지, initialDelaySeconds / periodSeconds / failureThreshold 같은 튜닝 파라미터의 의미, 그리고 이번 글의 자원 모델과 어떻게 엮이는지를 매니페스트 한 장의 모양으로 따라가겠습니다.