[cilium] LB IPAM에 Cilium L2 Announement 적용하기

L2 Announcements는 로컬 영역 네트워크에서 서비스가 접근 가능하도록 만드는 기능입니다. 주로 BGP 기반 라우팅이 없는 사내/캠퍼스 네트워크 같은 온프레미스 배포를 위해 설계되었습니다.

 

이 기능을 사용하면 ExternalIP 및/또는 LoadBalancer IP에 대한 ARP 쿼리에 응답합니다. 이 ARP 응답을 주는건 클러스터 중 리더로 선택 된 노드가 자신의 MAC 주소로 응답을 줍니다. 그 노드는 서비스 로드밸런싱 기능으로 트래픽을 분산 처리하며, 클러스터의 north/south 로드밸런서 역할을 합니다.

 

테스트 환경에서 NodePort를 쓰는것과 L2 Announcement를 쓰는걸 비교해보면 NodePort는 클라이언트가 어떤 호스트로 보낼지 결정해야하고 노드가 다운되면 해당 IP+Port 조합은 쓸 수 없게되는 반면 L2 Announcement는 서비스 VIP가 다른 노드로 자동 마이그레이션되어 계속 동작하며 고유 IP 를 가질 수 있어 동일한 포트 번호를 여러 서비스가 사용할 수 있다는 점에 장점입니다. 

리더 선출 과정

ARP/NDP 특성상, 호스트는 IP당 최신 응답의 MAC 주소 하나만 저장합니다. 즉, 클러스터에서한 노드만 해당 IP의 요청에 응답할 수 있습니다.

이를 구현하기 위해 각 Cilium 에이전트는 자신이 속한 노드에서 정책에 의해 선택된 서비스를 계산하고, 서비스마다 리더 선출에 참여합니다. Kubernetes Lease 메커니즘을 사용하며, 서비스마다 하나의 Lease 로 매핑됩니다. Lease 보유자가 선택된 인터페이스에서 요청에 응답하기 시작합니다.

Lease는 선착순입니다. 먼저 획득한 노드가 리더가 되며, 이로 인해 트래픽 분산이 비대칭이 될 수 있습니다.

클라이언트 레이트 리밋 산정

쿠버네티스에서 리더 선출은 지속적으로 API 트래픽을 생성합니다. 트래픽의 양은 Lease Duration(리더 유효 시간, 한번 리더가 정해지고 유효하다고 생각되는 시간), Renew Deadline(리즈 갱신 시간), 그리고 이 기능을 사용하는 서비스 수에 따라 달라집니다.

한 노드의 레이트 리밋(QPS) = “한 노드가 ‘리더’로 맡은 서비스 수 × 리스 갱신 주기”

정리

  • 필요한 지속 처리량 계산: 필요 QPS ≈ (그 노드가 리더인 서비스 수) × (1 / Renew Deadline)
    • 한 노드에 몇개의 서비스가 리더인지 평균치로 계산하기: 서비스 수 S ÷ 후보 노드 수 N
    • 한 노드로 몰릴 것을 예상한다면: 전체 서비스 수
  • qps = 위 값에 여유 1.5배 내외 (서비스가 한 노드에몰릴 수 있기때문에.. )
  • burst = qps의 1.5~3배 (잠깐 몰릴 때 막히지 않게)

운영 팁

  • 로그에 throttling(client-side throttling)이 보이면 qps/burst를 올리세요.
  • Failover를 빠르게 하려고 Renew Deadline를 줄이면 QPS가 선형으로 증가합니다(오버헤드 ↑).
  • 정책이 노드 집합을 분리한다면, 각 집합별로 위 계산을 따로 해서 더 높은 쪽에 맞추는 게 안전합니다.

장애 조치

리더 선출에 참여한 노드들은 리더가 leaseDurationSeconds 동안 갱신하지 않으면, API 서버에 자신을 새 리더로 만들도록 요청합니다. 가장 먼저 처리된 요청이 승자가 되며, 나머지는 거부됩니다.

새로운 노드가 리더가 되면 설정된 인터페이스에서 Gratuitous ARP Reply 를 전송합니다. 이를 수용하는 클라이언트는 ARP 테이블을 즉시 갱신하여 새 리더로 트래픽을 보냅니다. 일부 클라이언트는 ARP 스푸핑 위험 때문에 Gratuitous ARP를 수용하지 않을 수 있습니다. 이런 경우 내부 ARP 캐시 TTL이 만료될 때까지 더 긴 다운타임을 겪을 수 있습니다.

참고! 아직 IPv6 지원이 없으므로 Unsolicited Neighbor Advertisement 는 전송되지 않고, ARP 메시지만 전송됩니다.

 

L2 Announcement 활성화하기 

사전 요구 사항 

  • kube-proxy 대체 모드가 활성화되어 있어야 합니다.
  • L2 Aware LB를 알릴 모든 인터페이스 장치가 활성화되어 있고, --devices 플래그 또는 Helm의 devices 옵션에 포함되어야 합니다(명시적으로 설정한 경우).
  • externalIPs 를 사용할 계획이라면 Helm 옵션 externalIPs.enabled=true를 설정해야 합니다. 설정하지 않으면 external IP에 대한 서비스 로드밸런싱이 비활성화됩니다. (1.17.6 부터는 옵션이 사라짐, kube-proxy 대체 모드만 활성화 해도 자동 인식 됨)

제한사항

  • 현재 IPv6/NDP는 지원하지 않습니다.
  • L3→L2 변환 프로토콜 특성상, 특정 IP에 대한 모든 ARP 요청은 한 노드가 받게 되므로 트래픽이 클러스터에 도달하기 전에는 로드밸런싱이 불가합니다.
  • 현재 트래픽 재분산 메커니즘이 없어, 동일 정책 내 노드 간 부하 비대칭이 발생할 수 있습니다.
  • 서비스의 externalTrafficPolicy: Local 과 호환되지 않습니다. 파드가 없는 노드에서 서비스 IP를 광고할 수 있어 트래픽 드롭이 생길 수 있습니다.
  • 이 기능을 사용하면 API 서버에 부하가 발생할 수 있기 때문에 레이트 리밋을 거는것이 좋습니다. 몇을 적용하는게 좋은지는 위에 있습니다. 
helm upgrade cilium cilium/cilium --version 1.18.0 \
   --namespace kube-system \
   --reuse-values \
   --set l2announcements.enabled=true \
   --set k8sClientRateLimit.qps={QPS} \
   --set k8sClientRateLimit.burst={BURST} \
   --set kubeProxyReplacement=true \
   --set k8sServiceHost=${API_SERVER_IP} \
   --set k8sServicePort=${API_SERVER_PORT}

 

정책 설정 살펴보기

아래와 같이 설정 할 수 있습니다. 주석 참고해주세요.

apiVersion: "cilium.io/v2alpha1"
kind: CiliumL2AnnouncementPolicy
metadata:
  name: policy1
spec:
  serviceSelector: # <-- 이 정책을 어떤 서비스에 적용할지 
    matchLabels:
      color: blue
  nodeSelector:  # <-- 어떤 노드를 리더로 선택할지
    matchExpressions:
      - key: node-role.kubernetes.io/control-plane
        operator: DoesNotExist
  interfaces: # <-- 어떤 인터페이스를 통해 광고를 할지 
  - eth0      # <- 정규표현식도 사용가능
  # externalIPs 혹은 loadBalancerIPs 둘중 하나는 활성화를 해야함
  externalIPs: true  # .spec.externalIPs 에 있는 모든 IP 를 광고, 
                     # Helm 옵션 externalIPs.enabled=true 를 설정해 
                     # external IP에 대한 서비스 로드밸런싱을 활성화해야 함 
  loadBalancerIPs: true # .status.loadbalancer.ingress 에 있는 모든 IP 를 광고

 

실제 테스트해보기

기본 동작 테스트

정상적으로 L2Announcement가 활성화 되었는지는 다음과 같이 확이 가능합니다.

$ kubectl -n kube-system exec ds/cilium -- cilium-dbg config --all | grep EnableL2Announcements

 

 

그리고 KubeProxyReplacement가 활성화 되어야합니다. 

cilium status | grep KubeProxyReplacement

 

 

마지막으로 정책이 적용되었을 때 lease가 생성이 되어야합니다. 정책 이름으로 생성이 되었고 리더가 kind-worker2인것을 알 수 있습니다.

 

이제 마지막으로 agent내부에서 l2 announce 상태를 확인해보겠습니다.

cilium-dbg shell -- db/show l2-announce

 

여기서 잘보이면 잘 적용이 된 것 입니다.

 

2025.08.09 - [K8S/🔥 network study🔥] - [cilium] LB-IPAM 로드밸런서 아이피를 관리하기

LB IPAM으로 적용한 서비스를 보면 status의 loadbalancer.ingress에 vip가 적용되어있습니다.

 

이제 같은 네트워크 대역에 컨테이너를 생성하고 arping 테스트를 해보겠습니다. 

docker run -d --rm --name client --cap-add=NET_ADMIN --network kind nicolaka/netshoot tail -f /dev/null

 

추가된 아이피가 같은 대역대가 아니라서 다른 곳으로 나가려고 할 수 있기 때문에 라우팅 테이블을 추가해줍니다. (이거 추가 안해도 arp통신은 잘됨)

docker exec -it client bash 
 
$ ip route add 20.0.20.0/32 dev eth0 scope link

 

이제 arping을 확인해보겠습니다.

arping -I eth0 20.0.20.0

 

응답이 AE:DC:F7:8D:A4:4E 에서 오고 있습니다. 

 

이 맥 주소가 kind-worker2인지도 확인해보겠습니다. 디버깅 명령어로 확인했을 때 Selected가 true인 것만 l2 announce 로 사용가능하며 현재 kind-worker2의 eth0인터페이스의 맥 주소와 실제 응답 준 맥주소가 동일한 것을 확인할 수 있습니다. 

cilium-dbg shell -- db/show devices

 

리더 노드를 죽여보자 

이번엔 Gratuitous ARP Reply가 오는지 확인해보기 위해 현재 리더노드를 죽여보도록하겠습니다. 

클라이언트 컨테이너의 현재 arp 테이블을 확인해보겠습니다. 20.0.20.0의 맥 주소가 잘 보입니다. 

ip neigh show dev eth0

 

이 상태로 tcpdump를 떠두고 리더 노드를 죽여보겠습니다. 리더 노드를 죽이니 일정 시간 후 리더가 잘 변경이 되었습니다. 

 

그리고 클라이언트에는 다음과 같이 GARP 통신응답이 온것을 알 수 있습니다. 

tshark -i eth0 -f "arp" -Y "arp.opcode==2 && arp.src.proto_ipv4==arp.dst.proto_ipv4"

 

통신은 하지않았지만 arp 테이블도 변경이 된걸 확인해볼 수 있습니다. 

 

마지막으로 통신도 잘되는지 확인했을 때 아주 통신이 잘 되는 것을 확인했습니다.