확장성 보고서
확장성 보고서 (Scalability report)
이 보고서는 CRD 모드(kvstore 없음)에서 200개 이상의 노드가 있는 클러스터에 Cilium을 실행할 계획인 사용자를 위한 문서예요. 개발 과정에서 대규모 클러스터에 Cilium을 배포했고, 테스트에 적합했던 옵션들이 여기에 정리되어 있습니다.
본문
이 보고서는 CRD 모드(kvstore 사용 불가)에서 200개 이상의 노드가 있는 클러스터에 Cilium을 실행할 계획인 사용자를 위한 것입니다. 개발 주기에서 대규모 클러스터에 Cilium을 배포했으며, 테스트에 적합했던 옵션은 다음과 같아요:
설정 (Setup)
helm template cilium \\
--namespace kube-system \\
--set endpointHealthChecking.enabled=false \\
--set healthChecking=false \\
--set ipam.mode=kubernetes \\
--set k8sServiceHost=<KUBE-APISERVER-LB-IP-ADDRESS> \\
--set k8sServicePort=<KUBE-APISERVER-LB-PORT-NUMBER> \\
--set prometheus.enabled=true \\
--set operator.prometheus.enabled=true \\
> cilium.yaml
--set endpointHealthChecking.enabled=false와--set healthChecking=false는 엔드포인트 health checking을 완전히 비활성화해요. 다만 방화벽 규칙이나 하이퍼바이저 설정으로 인한 잠재적 패킷 손실을 감지할 수 있으므로, 작은 클러스터(3-10 노드)에서는 이 기능들을 처음에 활성화하는 것이 권장됩니다.--set ipam.mode=kubernetes는 클라우드 제공자가kube-controller-manager에서 파드 CIDR 할당을 활성화했기 때문에"kubernetes"로 설정했어요.--set k8sServiceHost와--set k8sServicePort는kube-apiserver앞에 있는 로드 밸런서의 IP 주소로 설정했어요. 이를 통해 Cilium이kube-apiserver에 연결하기 위해 kube-proxy에 의존하지 않을 수 있습니다.--set prometheus.enabled=true와--set operator.prometheus.enabled=true는 클러스터 전체에서 메트릭을 프로빙하는 Prometheus 서버가 있었기 때문에 설정했습니다.
테스트 클러스터는 컨트롤러 노드 3개와 워커 노드 1000개로 구성되었습니다. 공식 Kubernetes 문서의 권장 설정을 따랐으며, 머신을 다음 설정으로 프로비저닝했습니다:
- 클라우드 제공자: Google Cloud
- 컨트롤러: 3x n1-standard-32 (32vCPU, 120GB 메모리, 50GB SSD, 커널 5.4.0-1009-gcp)
- 워커: 1000x custom-2-4096 (2vCPU, 4GB 메모리, 10GB HDD, 커널 5.4.0-1009-gcp) 1개 풀
- 메트릭: 1x n1-standard-32 (32vCPU, 120GB 메모리, 10GB HDD + 500GB HDD), prometheus와 grafana 파드 전용 노드
Note
3개 컨트롤러 노드 모두 GCE 로드 밸런서 뒤에 있었어요.
각 컨트롤러에는 etcd, kube-apiserver, kube-controller-manager, kube-scheduler 인스턴스가 포함되어 있었습니다.
워커에 설정된 CPU, 메모리, 디스크 크기는 사용 사례에 따라 다를 수 있어요. 더 많은 메모리나 CPU를 필요로 하는 파드가 있을 수 있으므로 요구사항에 맞게 워커를 설계해야 합니다.
테스트 중에는 etcd 옵션 quota-backend-bytes=17179869184를 설정해야 했는데, etcd가 약 2GiB의 할당 공간에 도달하면 실패했기 때문입니다.
Cilium이 kube-proxy가 제공하는 모든 기능을 수행할 수 있으므로 워커 노드를 kube-proxy 없이 프로비저닝했습니다. Cilium이 kube-proxy 없이 kube-apiserver에 접근할 수 있도록 kube-apiserver 앞에 로드 밸런서를 만들고, --set k8sServiceHost=<KUBE-APISERVER-LB-IP-ADDRESS>와 --set k8sServicePort=<KUBE-APISERVER-LB-PORT-NUMBER> 옵션으로 Cilium을 구성했습니다.
우리의 DaemonSet updateStrategy는 maxUnavailable이 2가 아니라 250 파드로 설정되어 있었지만, 이 값은 Cilium 롤링 업데이트를 수행할 때 요구사항에 크게 의존합니다.
단계 (Steps)
우리가 취한 각 단계에 대해 자세한 내용, 발견 사항, 예상 동작을 아래에 제공합니다.
1. EndpointSlice 기능을 활성화한 Kubernetes v1.18.3 설치
Kubernetes와 Cilium의 최신 기능을 테스트하기 위해 Kubernetes v1.18.3과 확장성을 개선하는 EndpointSlice 기능을 활성화해 테스트했습니다.
Kubernetes는 etcd 클러스터를 요구하므로 v3.4.9를 배포했습니다.
2. Prometheus, Grafana, Cilium 배포
etcd, kube-apiserver, cilium, cilium-operator 메트릭을 수집·분석하기 위해 Prometheus v2.18.1과 Grafana v7.0.1을 사용했어요.
3. 워커 노드 2개 프로비저닝
이를 통해 테스트 클러스터가 올바르게 프로비저닝되었고 모든 메트릭이 수집되고 있는지 이해할 수 있었습니다.
4. 각 네임스페이스에 25개 배포가 있는 5개 네임스페이스 배포
- 각 배포는 1개 레플리카(총 125개 파드)를 가집니다.
- Cilium이 소비하는 리소스만 측정하기 위해 모든 배포는 같은 기본 이미지
registry.k8s.io/pause:3.2를 사용했어요. 이 이미지는 CPU나 메모리 오버헤드가 없습니다. - 작은 클러스터에 적은 수의 파드를 프로비저닝해 Cilium의 CPU 사용량을 이해했습니다:

표시는 125개 파드의 생성을 시작한 시점입니다. 예상대로 두 Cilium 에이전트와 Cilium operator 모두에서 CPU 사용량이 약간 증가했습니다. 에이전트는 2vCPU 머신에서 6.8% CPU 사용량으로 정점을 찍었어요.

메모리 사용량 측면에서 Cilium 에이전트의 유의미한 메모리 증가는 보지 못했습니다. eBPF 메모리 측면에서는 새 파드를 위한 일부 eBPF 맵 초기화로 인해 증가하는 것을 확인합니다.
5. 노드 998개 추가 프로비저닝 (총 1000 노드)

첫 번째 표시는 노드 생성 작업, 두 번째 표시는 1000개 Cilium 파드가 준비 상태에 도달한 시점입니다. 새 노드가 클러스터에 프로비저닝될 때마다 각 Cilium 에이전트가 Kubernetes로부터 이벤트를 받기 때문에 CPU 사용량 증가는 예상됩니다. 모든 노드가 배포된 후 CPU 사용량은 2vCPU 노드 기준 평균 0.15%였습니다.

클러스터의 노드 수를 1000으로 늘렸으므로 모든 메트릭에서 메모리 사용량이 약간 증가하는 것은 예상됩니다. 다만 노드 수의 증가는 컨트롤 플레인과 데이터플레인 모두에서 Cilium의 메모리 소비에 유의미한 증가를 일으키지 않는다는 점을 지적하는 것이 중요해요.
6. 각 네임스페이스에 25개 배포 더 추가
이제 클러스터 전체에 5 namespaces * (25 old deployments + 25 new deployments) = 250개 배포가 생깁니다. 노드가 2개뿐이라 각 워커 노드에 125개 파드가 생기므로 처음부터 250개 배포를 설치하지 않았습니다. Kubernetes 문서에 따르면 노드당 권장 최대 파드 수는 100개예요.
7. 각 배포를 200 레플리카로 스케일 (총 50000 파드)
5개 네임스페이스에 50개 배포가 있으므로 250개의 서로 다른 고유 보안 identity가 생깁니다. Cilium이 선택하는 라벨의 카디널리티가 낮으면 클러스터 확장에 도움이 됩니다. 기본적으로 Cilium은 16k 보안 identity 한도를 가지지만, Cilium ConfigMap에서 bpf-policy-map-max로 늘릴 수 있어요.

첫 번째 표시는 배포 스케일 업 작업, 두 번째 표시는 50000 파드가 준비 상태에 도달한 시점입니다.
- 새 파드가 스케줄·시작될 때 각 노드에서 Cilium 에이전트가 Kubernetes 이벤트를 받으므로 Cilium CPU 사용량이 증가하는 것이 예상됩니다.
- 모든 Cilium 에이전트의 평균 CPU 소비는 2vCPU 머신 기준 3.38%였어요. 대략 15:23분 경 하나의 Cilium 에이전트가 27.94% CPU 사용량을 기록했습니다.
- Cilium Operator는 파드가 생성되는 동안 안정적인 5% CPU 소비를 보였습니다.

워커 노드 수를 늘릴 때 본 동작과 유사하게, 새 파드를 추가해도 Cilium 메모리 소비가 증가합니다.
- 파드 수를 250에서 50000으로 늘리면서 한 Cilium 에이전트의 최대 메모리 사용량은 573MiB, 평균은 438MiB였습니다.
- eBPF 메모리 사용량은 최대 462.7MiB였습니다.
- 이는 클러스터의 새 파드당 각 Cilium 에이전트의 메모리가 10.5KiB씩 증가한다는 뜻이에요.
8. 한 네임스페이스에 250개 정책 배포
여기서 L4 네트워크 정책 125개와 L7 정책 125개를 만들었습니다. 각 정책은 이 네임스페이스의 모든 파드를 선택하고 이 네임스페이스의 다른 파드로 트래픽을 보내는 것이 허용되었어요. 250개 정책 각각은 서로 서로소인(disjoint) 포트 집합에 대한 접근을 허용합니다. 결국 10000개 파드를 선택하는 250개의 서로 다른 정책이 생깁니다.
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
name: "l4-rule-#"
namespace: "namespace-1"
spec:
endpointSelector:
matchLabels:
my-label: testing
fromEndpoints:
matchLabels:
my-label: testing
egress:
- toPorts:
- ports:
- port: "[0-125]+80" # from 80 to 12580
protocol: TCP
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
name: "l7-rule-#"
namespace: "namespace-1"
spec:
endpointSelector:
matchLabels:
my-label: testing
fromEndpoints:
matchLabels:
my-label: testing
ingress:
- toPorts:
- ports:
- port: '[126-250]+80' # from 12680 to 25080
protocol: TCP
rules:
http:
- method: GET
path: "/path1$"
- method: PUT
path: "/path2$"
headers:
- 'X-My-Header: true'

이 경우 한 Cilium 에이전트가 15초 동안 100% CPU 사용량으로 뛰었고, 90초 동안 평균 최고치는 40%였습니다.

예상대로 정책 수를 늘리는 것은 Cilium의 메모리 사용량에 유의미한 영향을 주지 않습니다. eBPF 정책 맵은 파드가 초기화되면 일정한 크기를 갖기 때문이에요.


첫 번째 표시는 kubectl create로 CiliumNetworkPolicies를 만들 때의 시점입니다. 250개 정책을 순차적으로 만들었기 때문에 수렴 시간을 제대로 계산할 수 없어요. 그러려면 specs 필드( spec 필드 대신) 아래에 여러 정책 규칙이 정의된 단일 CNP를 사용할 수 있습니다.
그래도 마지막 Cilium 에이전트가 Policy Revision을 증가시킨 시점(각 Cilium 에이전트는 CiliumNetworkPolicy(CNP)을 받을 때마다 개별적으로 증가)을 15:45:44에서 15:45:46 사이에서 볼 수 있고, "Endpoint regeneration time"의 99번째 백분위를 확인해 마지막 엔드포인트가 재생성된 시점을 알 수 있습니다. 이 방식으로 5초 미만이 걸렸음을 확인했습니다. 또한 엔드포인트에 정책이 적용되는 최대 시간이 600ms 미만인 것을 검증할 수 있어요.
9. CiliumClusterwideNetworkPolicies(CCNP)용 250개 정책 배포
이 정책들과 앞서 설치한 정책들의 차이는 이 정책들이 모든 네임스페이스의 모든 파드를 선택한다는 것입니다. 요약하면 이제 1000개 노드 클러스터에서 10000개 파드를 선택하는 250개 네트워크 정책과 50000개 파드를 선택하는 250개 네트워크 정책을 갖게 됩니다. 이전 단계와 마찬가지로 L4 정책 125개와 L7 정책 125개를 배포합니다.


이전 250개 CNP 생성과 유사하게 CCNP 생성 중에도 CPU 사용량이 증가했습니다. 정책이 실제로 더 많은 파드를 선택했음에도 CPU 사용량은 비슷했어요.

노드에서 실행되는 모든 파드가 생성된 250개 CCNP 모두에 의해 선택되므로 Endpoint regeneration time이 증가하며 3초를 조금 넘는 수준으로 정점을 찍었습니다.
10. 10000개 파드 "실수로" 삭제
이 단계에서 10000개의 무작위 파드를 "실수로" 삭제했어요. 그러면 Kubernetes가 10000개의 새 파드를 재생성하므로, 배포된 모든 네트워크 정책에 대한 수렴 시간을 이해하는 데 도움이 됩니다.


- 첫 번째 표시는 파드가 "삭제된" 시점, 두 번째 표시는 Kubernetes가 10k 파드 재생성을 마친 시점입니다.
- 파드가 클러스터에 스케줄되는 동안 CPU 사용량이 약간 증가하는 것 외에, eBPF 메모리 사용량에서 흥미로운 데이터 포인트를 보았습니다. 각 엔드포인트는 전용 eBPF 맵을 하나 이상 가질 수 있으므로, eBPF 메모리 사용량은 노드에서 실행되는 파드 수에 정비례합니다. 노드당 파드 수가 줄면 eBPF 메모리 사용량도 줄어듭니다.

우리는 시간에 따라 정책이 적용된 Cilium 엔드포인트 수를 보고 모든 엔드포인트가 재생성되는 데 걸린 시간을 유추했습니다. 운 좋게도 정책이 적용 중인 Cilium 엔드포인트가 몇 개인지 보여주는 또 다른 메트릭이 있었어요:

11. 테스트 실행 동안의 컨트롤 플레인 메트릭
이 테스트의 초점은 대규모에서 Cilium 에이전트 리소스 소비를 연구하는 것이었습니다. 하지만 etcd 메트릭과 k8s-컨트롤러의 CPU 사용량 같은 컨트롤 플레인 노드의 일부 메트릭도 모니터링했으며, 그 모습을 다음 그림에 제시합니다.

전체 확장성 테스트 동안 3개 etcd 인스턴스의 메모리 소비.

3개 컨트롤러 노드의 CPU 사용량, etcd 클러스터의 요청 유형별 평균 지연, etcd로의 초당 연산 수.

모든 etcd 메트릭, 왼쪽에서 오른쪽으로, 위에서 아래로: 데이터베이스 크기, 디스크 동기화 시간, 클라이언트 인바운드 트래픽, 클라이언트 아웃바운드 트래픽, 피어 인바운드 트래픽, 피어 아웃바운드 트래픽.
마지막 의견 (Final Remarks)
이 실험들은 CRD 모드에서 etcd에 의존하지 않고 완전히 실행되는 대규모 클러스터의 Cilium에 대한 더 나은 이해를 개발하는 데 도움이 되었습니다. eBPF 맵의 메모리 풋프린트를 더욱 최적화하고 Cilium 에이전트의 메모리 풋프린트를 줄이는 데는 여전히 할 일이 남아 있어요. 다음 Cilium 버전에서 이를 다룰 예정입니다.
또한 200개 이상의 노드가 있는 클러스터에서 CRD 모드로 Cilium을 실행하는 것이 확장 가능하다고 판단할 수 있어요. 다만 컨트롤 플레인 업그레이드 중에 발생할 수 있듯이 Cilium이 kube-apiserver와의 연결을 잃을 때의 동작을 검증하기 위해 더 많은 테스트를 실행해야 한다는 점을 지적할 가치가 있습니다. 이것 역시 다음 Cilium 버전의 초점이 될 것입니다.