단일 k3s 노드에서 110개 Pod 제한을 넘기기
CEO & Fullstack Engineer
배경
AWS 비용을 줄이기 위해 내부용 워크로드와 QA 워크로드를 온프렘 k3s 클러스터로 옮기고 있었습니다. 노드는 충분히 컸습니다. CPU와 메모리는 남아 있었고 실제 사용률도 낮았습니다. 그런데 어느 순간 새 Pod가 더 이상 뜨지 않았습니다.
처음 보인 증상은 단순했습니다.
Too many pods
노드의 Pod capacity는 기본값인 110이었습니다. 이미 올라간 시스템 컴포넌트, GitOps, 모니터링, QA 애플리케이션, GitHub Actions runner를 합치면 110개는 금방 찹니다. 노드를 추가할 수 없는 조건이었기 때문에 단일 노드에서 최소 500개 이상의 Pod를 수용해야 했습니다.
이 작업은 단순히 숫자 하나를 600으로 바꾸는 일이 아니었습니다. Kubernetes에는 같은 방향의 제한이 두 개 있습니다.
- kubelet의
maxPods - 노드에 할당되는 PodCIDR 크기
둘 중 하나만 바꾸면 병목이 다른 곳으로 이동합니다.
첫 번째 제한: kubelet maxPods
kubelet은 노드 하나에 몇 개의 Pod를 둘 수 있는지 maxPods로 제한합니다. 기본값은 110입니다. 스케줄러는 이 값을 보고 노드에 더 이상 Pod를 배정하지 않습니다.
k3s에서는 설정 파일에 kubelet 인자를 넣어 조정할 수 있습니다.
kubelet-arg:
- "max-pods=600"
이 설정만 반영하면 노드의 capacity.pods와 allocatable.pods는 600으로 바뀝니다. 실제로 이 시점부터 Too many pods 에러는 사라졌습니다.
하지만 여기서 끝내면 안 됩니다.
두 번째 제한: PodCIDR
기존 노드는 10.42.0.0/24 PodCIDR을 가지고 있었습니다. /24는 주소가 256개뿐입니다. 네트워크 주소, 브리지 주소, 이미 점유된 주소를 빼면 실제로 쓸 수 있는 Pod IP는 이보다 적습니다. kubelet이 600개 Pod를 받아도 CNI가 줄 수 있는 IP가 부족하면 결국 Pod 생성이 막힙니다.
그래서 controller-manager의 node CIDR mask도 함께 바꿨습니다.
kube-controller-manager-arg:
- "node-cidr-mask-size=22"
/22는 1024개 주소를 제공합니다. 단일 노드에서 500개 이상의 Pod를 목표로 할 때 운영상 여유가 있는 크기입니다.
설정 반영만으로는 CIDR이 바뀌지 않았습니다
여기서 운영 함정이 나왔습니다. k3s를 재시작하고 설정이 반영된 뒤에도 기존 Node의 podCIDR는 그대로 /24였습니다.
capacity.pods = 600
podCIDR = 10.42.0.0/24
controller-manager는 새 설정으로 떠 있었지만, 이미 생성된 Node 오브젝트의 PodCIDR를 자동으로 다시 계산하지는 않았습니다. Kubernetes에서 Node의 PodCIDR는 한 번 설정되면 일반 patch로 변경할 수 없습니다.
서버 사이드 dry-run으로 patch를 시도해보면 다음처럼 거부됩니다.
node updates may not change podCIDR except from "" to valid
즉 기존 Node의 PodCIDR를 /24에서 /22로 직접 바꾸는 길은 막혀 있었습니다.
Node 재등록과 finalizer
다음 선택지는 Node 오브젝트를 삭제하고 kubelet이 다시 등록하게 만드는 것이었습니다. 단일 노드 클러스터라 drain은 의미가 없고, 점검 공지 후 중단을 감수하고 진행했습니다.
그런데 Node 삭제도 바로 끝나지 않았습니다. k3s의 관리 finalizer가 남아 Node가 삭제 중 상태로 멈췄습니다.
metadata.finalizers:
- wrangler.cattle.io/managed-etcd-controller
단일 etcd 멤버 환경에서 Node 삭제가 etcd 멤버 제거 흐름과 얽히며 finalizer가 남았습니다. 이 상태에서는 Node 오브젝트가 완전히 사라지지 않으므로 새 PodCIDR도 할당되지 않습니다.
finalizer를 제거해 Node 삭제를 완료한 뒤 k3s를 다시 시작했습니다. 그 후 kubelet이 Node를 새로 등록했고, controller-manager가 비어 있던 podCIDR에 새 /22 CIDR를 할당했습니다.
capacity.pods = 600
podCIDR = 10.42.0.0/22
flannel도 새 subnet을 반영했습니다.
FLANNEL_SUBNET=10.42.0.1/22
cni0=10.42.0.1/22
이 순간부터 Pod IP가 10.42.1.x, 10.42.2.x 대역으로 할당되는 것을 확인했습니다. IPAM 관점에서도 /24를 넘어선 것입니다.
테스트: 600개까지 스케줄은 됩니다
설정 후 임시 pause Pod를 대량 생성해 확인했습니다. 목표는 두 가지였습니다.
- 스케줄러가 더 이상 110개에서 막히지 않는지
- CNI가
/24밖의 IP를 실제로 할당하는지
결과적으로 Node 기준 Non-terminated Pods: 600까지 올라갔습니다. 기존 병목인 Too many pods는 사라졌고, Pod IP도 /22 범위에서 할당됐습니다.
다만 한 번에 수백 개 Pod를 밀어 넣는 것은 별개의 문제였습니다. kubelet과 containerd가 Pod sandbox를 만들고, 이미지를 확인하고, CNI를 붙이는 과정이 병목이 되면서 ContainerCreating이 길게 쌓였습니다. API 서버도 순간적으로 응답 지연을 보였습니다.
따라서 이번 작업의 결론은 “단일 노드에서 500개 이상 Pod를 받을 수 있다”이지, “한 번에 500개를 무제한 속도로 생성해도 된다”는 뜻은 아닙니다.
운영에서는 다음이 필요합니다.
- 대량 rollout은 batch 크기를 제한합니다.
- runner처럼 burst가 큰 워크로드는 최대 동시 실행 수를 별도로 관리합니다.
- readiness가 회복되는 속도까지 포함해 점검합니다.
- Pod 수만 보지 말고 container runtime, CNI, image pull 병목을 같이 봅니다.
무중단으로 가능했을까요?
이번 케이스에서는 사실상 어렵다고 판단했습니다.
maxPods만 바꾸는 것은 k3s 재시작으로 끝날 수 있습니다. 하지만 기존 Node의 PodCIDR를 /24에서 /22로 바꾸려면 Node 재등록이 필요했습니다. 단일 노드 클러스터에서는 Node 재등록 자체가 control plane, kubelet, Pod lifecycle에 영향을 줍니다. 특히 이미 돌고 있는 Pod의 sandbox와 CNI 상태가 재정렬되므로, 일정 수준의 중단 가능성을 전제로 봐야 합니다.
노드가 여러 개였다면 다른 선택지가 있습니다.
- 새 CIDR 정책으로 새 노드를 추가합니다.
- 기존 Pod를 새 노드로 옮깁니다.
- 오래된 노드를 drain하고 제거합니다.
하지만 “노드 추가 불가” 조건에서는 이 경로가 막힙니다. 단일 노드에서 네트워크 CIDR 자체를 바꾸는 작업은 maintenance window 안에서 처리하는 것이 현실적이었습니다.
배운 점
첫째, Pod capacity는 kubelet 숫자만 보지 않습니다. maxPods와 PodCIDR를 같이 봐야 합니다.
둘째, 이미 생성된 Node의 PodCIDR는 쉽게 바뀌지 않습니다. 설정 변경 후에도 Node 오브젝트가 기존 값을 유지할 수 있습니다.
셋째, 단일 노드 k3s에서는 Node 삭제가 k3s finalizer와 etcd 멤버 관리 흐름에 걸릴 수 있습니다. 이 경우 “삭제했으니 재등록되겠지”라고 보면 안 되고, 실제 Node 오브젝트의 deletionTimestamp, finalizers, podCIDR를 확인해야 합니다.
넷째, 500개 이상 “스케줄 가능”과 500개 이상 “안정적으로 동시에 생성 가능”은 다릅니다. 후자는 kubelet, containerd, CNI, image cache, API server 처리량까지 포함한 문제입니다.
최종 상태
이번 변경 후 클러스터는 다음 상태로 정리됐습니다.
Node Ready = True
capacity.pods = 600
allocatable.pods = 600
podCIDR = 10.42.0.0/22
flannel subnet = 10.42.0.1/22
cni0 = 10.42.0.1/22
prod 주요 리소스와 DB StatefulSet, tunnel, kube-system 컴포넌트도 정상화된 것을 확인했습니다. 남은 실패는 잘못된 이미지 태그나 runner 초기화 지연처럼 별도 이슈로 분리했습니다.
결국 핵심은 단순했습니다.
Pod 수 제한을 늘릴 때는 스케줄러가 보는 숫자와 CNI가 줄 수 있는 IP를 같은 크기로 맞춰야 합니다.
그 숫자가 이미 떠 있는 단일 노드의 Node 오브젝트에 박혀 있다면, 설정 파일만 바꿔서는 끝나지 않습니다.