Kubernetes 1.33에서 kube-proxy의 nftables 모드가 GA로 승격됐다. 우리 팀도 iptables 모드에서 nftables 모드로 옮기는 논의를 시작했는데, 막상 "왜 nftables가 iptables보다 빠른가"라는 질문에 답이 잘 안 나왔다. "룰 개수가 줄어드니까 빠르지" 정도로 부족했다. 그래서 KEP-3866과 실제 룰 덤프를 뜯어보며 정리했다. 이 글은 그 결과다.
기본적으로 아직 iptables가 kube-proxy의 기본 모드로 남아있고, 명시적으로 --proxy-mode nftables를 줘야 nftables 모드가 켜진다. 커널은 5.13 이상이 필요하다. 이 부분은 공식 블로그에도 명확히 적혀 있다.
왜 iptables 모드가 느렸는가
먼저 iptables 모드의 구조를 짧게 다시 훑자. kube-proxy iptables 모드는 Service마다 KUBE-SVC-\<hash\> 체인을 만들고, 그 밑에 endpoint별로 KUBE-SEP-\<hash\> 체인을 매단다. 매칭은 -j KUBE-SERVICES 체인에서 시작해 각 Service 체인으로 점프하고, endpoint 선택은 각 KUBE-SVC 체인 안의 statistic module로 확률 분기시킨다.
여기서 세 가지 병목이 있다.
첫째는 선형 매칭. iptables는 룰을 순서대로 평가한다. Service가 5,000개면 최악의 경우 5,000번 비교를 돌아야 매칭이 결정된다. 커널 컨텍스트에서 이 비교가 매 패킷마다 발생한다. 사실 KUBE-SERVICES 체인은 destination IP로 분기하니 대부분은 잘라내지만, 그래도 O(N)의 특성 자체는 남는다.
둘째는 iptables-restore의 원자성 비용. kube-proxy는 룰 변경을 배치해서 iptables-restore --noflush로 한 번에 밀어 넣는다. 문제는 restore가 커널의 xt_table 락을 잡고 통째로 rule set을 교체한다는 점이다. 룰이 커질수록 락 시간이 늘어난다. Service가 몇 만 개인 클러스터에서 restore 한 번에 몇 초씩 걸리는 사례가 실제로 있었다. 그동안 다른 iptables 커맨드는 다 대기한다.
셋째는 incremental update의 부재. Endpoint 하나만 바뀌어도 관련 서비스의 룰들을 다시 생성해서 restore 스트림에 실어야 한다. 부분 업데이트가 힘든 자료구조라서 어쩔 수 없다.
nftables가 뒤집은 지점
nftables는 자료구조 자체가 다르다. 룰은 여전히 있지만, 매칭의 상당수를 map과 set으로 처리한다. 그리고 커널 내부는 pseudo-VM(nft VM) 위에서 룰을 실행한다. iptables가 매치 모듈들의 fixed pipeline이라면, nftables는 명령어 시퀀스를 인터프리터가 도는 구조에 가깝다.
kube-proxy nftables 모드의 실제 룰을 덤프해 보면 대충 이런 그림이 나온다.
table ip kube-proxy {
map service-ips {
type ipv4_addr . inet_proto . inet_service : verdict
elements = {
10.96.0.10 . tcp . 53 : jump service-XYZ,
10.96.0.1 . tcp . 443 : jump service-ABC,
...
}
}
chain services {
ip daddr . meta l4proto . th dport vmap @service-ips
}
}
핵심은 vmap(verdict map)이다. 튜플을 키로 해서 곧바로 어느 서비스 체인으로 점프할지 O(1)에 가까운 lookup으로 결정한다. 내부는 rbtree 혹은 hash 기반 자료구조라서 서비스 개수가 늘어도 매칭 비용이 로그 스케일 이하로 안정적이다. iptables의 선형 스캔이 사라지는 지점이다.
Endpoint 선택 역시 numgen random mod N vmap { 0: jump ep0, 1: jump ep1, ... } 같은 형태로 표현된다. 확률 분기가 여러 룰의 chain이 아니라 하나의 verdict map으로 압축된다.
Incremental update와 원자적 트랜잭션
nftables는 netlink 기반의 트랜잭션 API를 쓴다. kube-proxy는 필요한 delta만 준비해서 하나의 트랜잭션으로 커널에 보낸다. Endpoint 하나가 추가되면 관련 set/map에 element를 추가하고, 지워지면 element를 제거한다. 룰 자체는 그대로다.
이게 중요한 이유는 두 가지다.
하나는 락 홀드 시간이 짧아진다는 점. 트랜잭션 처리 시간이 룰 사이즈가 아니라 delta 사이질 비례한다. 서비스 만 개짜리 클러스터에서 endpoint 하나 바뀌었다고 전체를 밀어 넣지 않는다.
다른 하나는 부분 실패가 없다는 점. netlink 트랜잭션 내에서 어떤 원소가 실패하면 전체가 롤백된다. 룰이 반쯤 반영된 상태로 남는 코너 케이스가 원천적으로 사라진다.
여담이지만 커널 5.13 요구는 이 트랜잭션과 set 원소별 timeout 등의 API 스펙에 걸려 있는 것으로 보인다. 그 이전 커널은 nftables 자체는 있지만 kube-proxy가 쓰는 특정 조합을 지원하지 못한다.
`sync_runner`와 정합성 보장
kube-proxy는 informer로 Service/EndpointSlice 이벤트를 받고, 짧은 backoff로 룰을 재합성한다. iptables 모드에서는 매 sync가 사실상 rule set 재생성에 가까웠지만, nftables 모드에서는 이 재합성이 map/set의 diff 계산으로 바뀐다. 코드상으로도 NFTablesProxier가 last-applied 상태를 캐시하고, 새 목표 상태와 비교해서 add/delete 목록만 만들어 커널에 보낸다.
한 가지 주의할 점은 초기 sync다. 큰 클러스터에서 kube-proxy가 처음 뜰 때는 map/set에 elements를 통째로 넣어야 하므로 트랜잭션이 커진다. 이때는 iptables 모드의 initial restore와 비슷하거나 살짝 더 낫다. 이 구간의 CPU/메모리 스파이크는 여전히 리소스 예산에 반영해야 한다.
그럼 IPVS는 어떻게 되나
IPVS 모드는 다르다. IPVS는 L4 로드밸런서를 커널 자료구조(ipvs)로 직접 구현하는 방식이고, 룰 매칭이 아니다. 부하가 심한 경우 IPVS가 여전히 유리한 지점이 있다. 특히 endpoint가 많고 커넥션 recycle이 잦은 워크로드에서.
다만 IPVS 모드도 부수 룰(iptables 마스킹, healthcheck 등)을 iptables로 얹기 때문에 iptables의 스케일 문제에서 완전히 자유롭지 않다. 최근에는 IPVS 대신 nftables + Cilium 같은 eBPF datapath로 옮기는 팀도 늘고 있다. 우리도 kube-proxy를 nftables로 옮기고, 그 다음에 Cilium kube-proxy replacement 검증을 이어가려고 한다.
옮기기 전 확인할 것
몇 가지만 짚자면. nft 커맨드가 노드에 설치돼 있어야 디버깅이 편하다. 모니터링 지표 이름 몇 개가 바뀌었다(예: kubeproxy_sync_proxy_rules_iptables_* → ..._nftables_* 계열). 그리고 EndpointSlice controller 설정, kernel version, custom iptables 룰과의 충돌 여부(특히 CNI가 얹는 룰)를 노드마다 점검해야 한다. AKS는 최근에야 nftables 모드 프리뷰가 열렸고, EKS/GKE도 지원 시점이 배포판마다 다르니 관리형 클러스터라면 벤더 문서를 먼저 봐야 한다.
우리 팀은 아직 스테이징에만 nftables 모드를 켜 두고 2주 관찰 중이다. sync 시간, conntrack drop, service 접속 실패율 같은 지표를 iptables 모드와 병렬 비교하고 있다. 지금까지는 지표 상 개선이 명확하지만, 프로덕션 반영은 다음 KubeCon 발표들을 조금 더 보고 결정할 생각이다.
'IT > Kubernets' 카테고리의 다른 글
| distroless 컨테이너, kubectl debug 없으면 진짜 답 없다 (0) | 2026.08.27 |
|---|---|
| Kubernetes DRA로 GPU 스케줄링하는 법 — 1.34 GA 이후 실전 가이드 (1) | 2026.08.27 |
| HPA 진동 잡느라 이틀 태운 이야기 (0) | 2026.08.26 |
| kubectl events --for, 이거 모르는 분 꽤 많더라 (0) | 2026.08.23 |
| CronJob startingDeadlineSeconds 잘못 만졌다가 새벽 배치가 3일간 안 돌던 이야기 (0) | 2026.08.23 |