설치하고 나면 아무도 안 보는 구성 요소

CNI를 언제 마지막으로 들여다보셨나요? 대부분은 클러스터를 처음 만들 때 한 번 고르고, 그 뒤로는 업그레이드 공지가 올 때나 떠올리실 겁니다. 저도 그랬습니다. 그런데 CNI의 에이전트와 컨트롤러는 그동안에도 모든 노드에서 계속 CPU와 메모리를 쓰고 있습니다.

그렇다면 얼마나 쓰고 있을까요? 이 질문에 답하는 자료를 찾다가 흥미로운 사실을 알게 됐습니다. 처리량 벤치마크는 많은데, 상시 자원 사용량을 같은 조건에서 비교한 공개 자료는 찾을 수 없었습니다. 벤더 문서에도 없습니다. Cilium은 helm 차트에 자원 요청값을 넣지 않았고, Calico 메인테이너는 권장치를 공표해 달라는 요청을 거절했습니다. 그래서 검색하면 출처가 불분명한 수치가 먼저 나오는 경우가 많습니다.

이 공백이 실무에서 문제가 되는 이유는 시장 구도 때문입니다. 교육과 자격증은 Calico를 표준처럼 다루는데, 관리형 서비스의 기본값과 대규모 이관 사례는 Cilium으로 향하고 있습니다. 이관을 검토하는 팀이 마지막에 묻는 질문이 바로 “그래서 평소에 자원을 얼마나 더 쓰는데?“인데, 이관 사례집들은 이 지점에 침묵합니다. 답이 없으면 직접 재 보는 수밖에 없습니다.

그래서 측정했습니다. Calico Open Source, Cilium, Flannel, Antrea, kube-router 5종을 14개 구성으로 나누어 9일간 무인으로 돌렸고, 유효 측정 73회분을 모았습니다. 전체 수치와 재현 방법은 GitHub 저장소에 있습니다.

무엇을 어떻게 측정했나

측정 대상은 상시 비용입니다. 즉 네트워킹 스택(CNI 구성 요소 전부 + kube-proxy)이 평소와 부하 상황에서 쓰는 CPU와 메모리입니다. 처리량과 지연은 재지 않았습니다. 가상화 환경(VirtualBox 3노드)에서는 가상 스위치 성능이 섞여서 CNI 자체의 성능이라고 말할 수 없기 때문입니다. 부하는 자원 사용을 일으키는 자극으로만 씁니다.

구성 14개는 CNI마다 의미 있는 스위치를 하나씩 분리하도록 짰습니다. Calico는 설치 방식(operator/manifest)과 데이터플레인(iptables/eBPF), BGP 유무로 4개, Cilium은 기본에서 시작해 Hubble off, kube-proxy 대체(KPR), netkit까지 하나씩 쌓는 사슬로 4개, Flannel은 kube-proxy 모드(iptables/nftables)로 2개, Antrea는 FlowExporter 유무로 2개, kube-router는 전기능과 CNI 전용으로 2개입니다.

측정 구간은 6개입니다. 아무 일도 없는 idle에서 시작해 파드 60개, NetworkPolicy 100개, Service 200개를 차례로 쌓고, churn(20초마다 파드 10개를 지워 재생성을 반복시키는 구간)과 노드 drain/재합류까지 진행합니다. 구성을 바꿀 때마다 CNI가 없는 베이스 스냅샷으로 완전히 복원해서 이전 구성의 흔적이 남지 않게 했습니다.

수집에는 세 가지 원칙을 두었습니다. 측정 대상 클러스터에 수집용 컴포넌트를 설치하지 않을 것(kubelet cadvisor 메트릭을 API 서버 프록시로 폴링), 메모리는 working set 기준으로 보되 RSS를 병기할 것, 그리고 eBPF 계열이 쓰는 map 커널 메모리를 bpftool로 따로 잴 것. 세 번째가 특히 중요한데, 그 이유는 뒤에서 보겠습니다.

측정 결과: 왼쪽 아래로 갈수록 자원을 적게 씁니다

측정한 14개 구성을 idle 메모리(가로축)와 churn 구간 CPU(세로축)로 놓으면 다음 그림과 같습니다. 왼쪽 아래로 갈수록 평소에도 부하 중에도 자원을 적게 쓰는 구성입니다.

상시 비용 산점도: idle 메모리 대 churn CPU

이 그림에서 세 가지가 바로 보입니다. 첫째, 가장 가벼운 Flannel+nftables (209MiB)와 가장 무거운 Cilium KPR 구성(map 포함 2,400MiB대)이 가로축에서 10배 이상 벌어져 있습니다. 둘째, 세로 방향은 kube-router 전기능(Ku1) 하나만 왼쪽 위로 튀어 있습니다. 셋째, 나머지 구성들의 churn CPU는 150~470mC 대역에 모여 있어서, 상시 비용의 실질 축은 CPU가 아니라 메모리라는 것을 알 수 있습니다.

발견 1. 조건을 가르는 축은 메모리 사용량입니다

idle CPU는 가장 무거운 구성도 클러스터 합 127mC(0.13코어)에 그쳤습니다. 어떤 CNI를 골라도 평소 CPU가 문제 될 가능성은 낮다는 뜻입니다. 하지만 메모리 사용량은 다릅니다. 노드당 메모리가 4GB인 소형 환경이라면 네트워킹 스택이 100MiB를 쓰는지 800MiB를 쓰는지에 따라 워크로드에 남는 메모리가 달라지게 됩니다. CNI를 자원 관점에서 고른다면 봐야 할 축은 메모리입니다.

여기에 함정이 하나 있습니다. eBPF 계열 CNI가 쓰는 map 커널 메모리는 프로세스 메트릭 바깥에 있어서 kubectl top에 잡히지 않습니다. 실측값은 노드 합 기준으로 Cilium 기본 412MiB, Cilium KPR 712MiB, Calico eBPF 521MiB였습니다. Calico eBPF는 프로세스 메모리만 보면 iptables 구성보다 오히려 작기 때문에(920 대 1005MiB), map을 빼고 비교하면 판단이 반대로 나올 수 있습니다. eBPF CNI의 메모리를 비교할 때는 bpftool 계측까지 포함해야 합니다.

발견 2. kube-proxy를 nftables 모드로 바꾸면 메모리가 70% 줄어듭니다

개인적으로 이번 측정에서 가장 실용적인 결과라고 생각합니다. nftables 모드는 쿠버네티스가 iptables 모드의 성능 문제를 해결하려고 만든 후속 모드로, 1.33에서 GA가 됐지만 호환성 때문에 1.36에서도 기본값은 여전히 iptables입니다. 공식 블로그 NFTables mode for kube-proxy는 지연 개선을 수치로 보여주지만, kube-proxy 프로세스 자체의 자원 사용량은 다루지 않습니다.

같은 Flannel에서 kube-proxy 모드만 바꾼 두 구성을 비교하면 다음과 같습니다.

kube-proxy 메모리 사용량: iptables 대 nftables

idle에서 157.5MiB가 46.8MiB로 70% 줄었고, Service 200개가 걸린 상태(-65%)와 churn 구간(-54%)에서도 같은 방향이 유지됐습니다. CNI를 바꾸지 않고 얻을 수 있는 절감으로는 이번 측정에서 가장 컸습니다. kube-proxy를 쓰고 계시다면 nftables 모드 전환을 검토해 보시기 바랍니다.

발견 3. kube-router 전기능 모드는 churn을 겪고 나면 CPU가 내려오지 않습니다

kube-router는 파드 네트워킹, NetworkPolicy, IPVS 서비스 프록시를 데몬 하나로 처리하는 통합형입니다. 전기능 모드(Ku1)는 idle에서 전 구성 중 가장 가벼웠습니다(2mC / 215MiB). 그런데 churn이 시작되면 클러스터 합 3,355mC(노드당 약 1.1코어)까지 올라가고, churn이 끝난 다음 구간에서도 내려오지 않았습니다. 5회 반복에서 매번 같은 값이 나왔습니다.

원인 범위를 좁히려고 별도로 재현해 봤습니다. 파드 재시작도, OOM도, 오류 로그도 없었고 CPU는 kube-router 내부의 사용자 공간 루프가 쓰고 있었습니다. 가장 눈에 띄는 부분은 같은 상태라도 이력에 따라 CPU가 달라진다는 점입니다. churn 전에는 같은 오브젝트 규모(Service 200개, 엔드포인트 12,008개)에서 77mC였는데, churn을 한 번 겪고 나면 같은 규모에서 3,300mC가 유지되고, 부하 오브젝트를 삭제하면 90초 안에 idle 수준으로 내려옵니다. 서비스 프록시를 kube-proxy에 맡긴 분리 구성(Ku2)은 같은 부하에서 정상이었으므로 원인은 IPVS 서비스 프록시 쪽일 가능성이 높지만, 내부의 어느 동작이 원인인지까지는 확인하지 않았습니다. 파드 교체가 잦은 클러스터에 전기능 모드를 고려하신다면 이 동작을 알고 계시는 게 좋겠습니다.

발견 4. Calico는 데이터플레인보다 설치 방식이 메모리를 더 바꿉니다

같은 iptables 데이터플레인이라도 operator 방식 설치는 manifest 방식보다 idle 메모리를 533MiB 더 씁니다. Typha 2개, calico-apiserver 2개, csi-node-driver, tigera-operator가 추가로 상주하기 때문입니다. 그에 반해 데이터플레인을 eBPF로 바꿨을 때의 프로세스 메모리 차이는 85MiB에 그칩니다. 어느 데이터플레인을 쓰는지보다 어떤 방식으로 설치하는지가 상주 메모리에는 더 크게 작용하는 셈입니다. 물론 operator가 주는 관리 편의가 있으니, 533MiB를 그 편의의 비용으로 이해하시면 됩니다.

발견 5. 관측 기능은 켜 두어도 부담이 매우 작습니다

Cilium의 Hubble(helm 기본값, relay와 ui 없음)을 끈 구성과 켠 구성의 차이는 에이전트 메모리 약 22MiB였고 CPU 차이는 반복 편차 안이었습니다. Antrea의 FlowExporter도 +5~10MiB로 같은 경향이었습니다. 관측 스택의 실질 비용은 기능 토글이 아니라 relay, ui, 수집기 같은 추가 컴포넌트에서 발생한다고 볼 수 있습니다. 기본 구성 수준의 관측 기능이라면 자원 때문에 끌 이유는 없어 보입니다.

수치를 읽을 때 주의하실 점

세 가지만 짚고 넘어가는 게 좋겠습니다. 첫째, 위에서 말한 대로 eBPF map은 kubectl top에 잡히지 않습니다. 둘째, working set과 RSS는 구성 요소에 따라 5배까지 다릅니다(cilium-agent는 1,137 대 236MiB). 다른 자료와 수치를 비교하실 때는 어떤 메트릭 기준인지부터 확인하셔야 합니다. 셋째, CPU 절대값은 호스트 상태에 따라 측정 시기 사이에 20~33% 달라지는 것을 확인했습니다. 이번 측정은 14개 구성을 반복 회차 안에서 순환 배치해서 구성 간 비교가 이 변동의 영향을 받지 않도록 했지만, 표의 CPU 절대값 자체는 순위와 대략의 크기로 읽으시는 게 맞습니다.

한계도 분명합니다. 가상화 위 3노드 소규모 측정이라 절대값을 대규모 클러스터로 그대로 확장하면 안 되고, 처리량과 지연은 측정 대상이 아니며, 암호화와 관측 스택의 추가 컴포넌트는 범위 밖입니다.

정리하며

측정을 시작할 때의 질문은 “CNI는 평소에 자원을 얼마나 쓸까"였습니다. 답은 “CPU는 어느 것이든 무시할 수준이고, 메모리 사용량은 구성에 따라 8배까지 갈린다"입니다. 그리고 그 메모리를 제대로 세려면 kubectl top 밖에 있는 eBPF map까지 봐야 하고, CNI를 바꾸지 않아도 kube-proxy 모드 전환만으로 70%를 줄일 수 있다는 것까지 확인했습니다.

측정 조건 14개의 전체 표, 구성 요소별 상세 수치, 재현용 하네스는 GitHub 저장소에 공개해 두었으니 참고하시기 바랍니다. 환경이 다른 곳에서 재현해 보신 결과가 있다면 꼭 알려주시기 바랍니다.