System/Container & Kubernetes

Cilium BGP와 ECMP

마늘김 2025. 4. 27. 05:21

 이전 포스팅에서 이어집니다. 'On-Premise 환경에서 Kubernetes LoadBalancer 구현'을 읽고 오시길 권장드립니다.

On-Premise 환경에서 Kubernetes LoadBalancer 구현

CSP에서 제공하는 Kubernetes 서비스는 클라우드 인프라에 잘 통합되어 있어 있습니다. 그래서 간단한 명령어나 웹 UI를 통해 쉽게 클러스터를 생성할 수 있고 사용할 수 있습니다. 특히 Load Balancer

tech-recipe.tistory.com

문제의 발견

1. BGP 연결이 수상하다!

여러 경로 중 가장 좋은 경로만을 선택하는 BGP

  OPNsense에서 BGP의 경로를 확인해 보면 위와 같이 나타납니다. Kubernetes 워커노드 3대가 모두 Peer로 연결되어 있긴 하지만 그중 172.16.200.1에 도달하는 가장 좋은 경로는 192.168.200.31로 인식하고 있습니다.

Gateway가 하나로 설정되어 있는 것을 확인

 System > Route > Status에서 확인해 보아도 LB IP(172.16.200.1)에 대한 라우팅 게이트웨이가 192.168.200.31로 설정되어 있는 것을 확인할 수 있었습니다. 그래서 혹시나 하는 생각에 Hubble UI를 통해 트래픽 정보를 확인해 보았습니다.

2. 역시... 하나의 노드에만 몰리는 트래픽

Source IP가 10.0.2.101로 고정되어 있는 상태

 LB IP인 172.16.200.1을 호출하면 이를 Hubble UI에서 확인할 수 있습니다. 문제는 그 어떤 호출이라 할지라도 Source IP 주소가 항상 일정한 10.0.2.101로 출력된다는 것이었습니다. 도대체 저 IP 주소(10.0.2.101)가 무엇일까? Cilium Daemonset 중에 하나의 Cilium Host IP가 아닐까 의심이 들었습니다. OPNsense의 라우팅 정보에서 192.168.100.31(Worker 노드 1번)을 가리키고 있으니 해당 노드의 Cilium Host IP를 확인해 보았습니다.

#Cilium Daemonset Pod 확인
kubectl get pods -n kube-system -o wide | grep 192.168.200.31

cilium-envoy-57mks                        1/1     Running   0             25h   192.168.200.31   bgp-k8s-wkr-01    <none>           <none>
cilium-operator-78dc5dd875-25zdr          1/1     Running   1 (25h ago)   25h   192.168.200.31   bgp-k8s-wkr-01    <none>           <none>
cilium-shwwn                              1/1     Running   0             8h    192.168.200.31   bgp-k8s-wkr-01    <none>           <none>

#cilium-shwwn의 IP 주소 확인
kubectl exec -n kube-system cilium-shwwn -it -- ip addr

Defaulted container "cilium-agent" out of: cilium-agent, config (init), mount-cgroup (init), apply-sysctl-overwrites (init), mount-bpf-fs (init), clean-cilium-state (init), install-cni-binaries (init)
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
2: ens160: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether 00:50:56:b1:7c:02 brd ff:ff:ff:ff:ff:ff
    altname enp3s0
    inet 192.168.200.31/24 brd 192.168.200.255 scope global ens160
       valid_lft forever preferred_lft forever
    inet6 fe80::250:56ff:feb1:7c02/64 scope link 
       valid_lft forever preferred_lft forever
3: cilium_net@cilium_host: <BROADCAST,MULTICAST,NOARP,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether 76:95:31:5d:21:30 brd ff:ff:ff:ff:ff:ff
    inet6 fe80::7495:31ff:fe5d:2130/64 scope link 
       valid_lft forever preferred_lft forever
4: cilium_host@cilium_net: <BROADCAST,MULTICAST,NOARP,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether 42:13:c0:3a:eb:b5 brd ff:ff:ff:ff:ff:ff
    inet 10.0.2.101/32 scope global cilium_host          #예상대로 확인된 Source IP 주소
       valid_lft forever preferred_lft forever
    inet6 fe80::4013:c0ff:fe3a:ebb5/64 scope link 
       valid_lft forever preferred_lft forever
5: cilium_vxlan: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UNKNOWN group default qlen 1000
    link/ether 22:20:cc:41:95:57 brd ff:ff:ff:ff:ff:ff
    inet6 fe80::2020:ccff:fe41:9557/64 scope link 
       valid_lft forever preferred_lft forever
9: lxc3235ea9e75ca@if8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether 42:59:2c:65:76:62 brd ff:ff:ff:ff:ff:ff link-netnsid 1
    inet6 fe80::4059:2cff:fe65:7662/64 scope link 
       valid_lft forever preferred_lft forever
35: lxc_health@if34: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether 96:b2:38:45:b0:9f brd ff:ff:ff:ff:ff:ff link-netnsid 2
    inet6 fe80::94b2:38ff:fe45:b09f/64 scope link 
       valid_lft forever preferred_lft forever

 위 결과에서 볼 수 있듯, 192.168.200.31(Worker 노드 1번)의 Cilium Host의 주소가 10.0.2.101인 것을 확인할 수 있었습니다. 이것이 의미하는 바는 172.16.200.1로 향하는 트래픽이 모두 Worker 노드 1번으로 집중되었다가 다시 Pod를 찾아간다는 뜻이었습니다.
 물론 Kubernetes의 Service 레벨에서는 Endpoint인 Pod에 대한 로드밸런싱이 이루어지고는 있었지만, 노드로 접근하는 트래픽에 대한 로드밸런싱은 전혀 이루어지고 있지 않다는 뜻이었고, Endpoint Pod가 해당 노드에 존재하지 않으면 한번 더 트래픽이 이동해야 한다는 의미였습니다.

왜 이런 현상이 발생하나?

 문제의 원인은 BGP가 라우팅 프로토콜이라는 것입니다. 이게 무슨 말인가 하면, BGP는 특정 네트워크에 대해서 최적의 라우팅 경로를 찾는 알고리즘을 가지고 있는 것이지 부하를 분산하는 알고리즘을 가지고 있지는 않다는 것이었습니다. 그러니까 LB IP(172.16.200.1)로 향하는 다양한 경로에 대한 정보(192.168.200.31, 32, 33으로 향하면 172.16.200.1에 도달할 수 있음)가 전달되었으나 그중 가장 최적의 경로에 대해서 학습하고 나면 계속 그 경로만을 고집한다는 것입니다. 물론 192.168.200.3에 접근할 수 없게 된다면 다른 우회 경로를 찾겠지만 그전까지는 바뀌지 않습니다.

해결 방안 1 - 상위 라우터 ECMP 설정

 Cilium BGP Control Plane의 공식 문서를 살펴보면 아래와 같은 내용이 있습니다.

 When your upstream router supports Equal Cost Multi Path (ECMP), you can use this feature to load-balance traffic to the Service across multiple nodes by advertising the same virtual IPs from multiple nodes.

 상위 라우터, 즉 OPNsense에서 ECMP(Equal Cost Multi Path)를 지원하면 여러 노드에서 같은 IP 주소에 대한 경로를 제공하여 노드 레벨의 로드밸런싱이 가능하다는 것입니다. 실제로 OPNsense에서는 이 기능을 지원하고 있습니다.

1. OPNsense ECMP 활성화

net.route.mulipath 활성화
LB IP로의 Routing 경로가 다중으로 설정

 OPNsense의 웹 UI에서 System > Settings > Tunables로 이동합니다. 여기에서 net.route.multipath 항목을 검색합니다. 나타나는 메뉴를 연필모양 아이콘을 눌러 편집합니다. Value값의 기본은 0인데, 이것을 1로 바꿔주면 라우팅에서 Multipath를 사용할 수 있게 됩니다. 이를 통해 ECMP를 활성화할 수 있습니다.

! Warnning
Many routers have a limit on the number of ECMP paths they can hold in their routing table (Juniper). When advertising the Service VIPs from many nodes, you may exceed this limit. We recommend checking the limit with your network administrator before using this feature.

maximum-ecmp | Junos OS | Juniper Networks

Syntax maximum-ecmp next-hops; Hierarchy Level [edit chassis] Description MX Series) Configure 16, 32, or 64, and 128 ECMP next hops for RSVP or LDP LSPs, or MPLS static LSPs that are configured using set protocols mpls static-label-switched-path. This com

www.juniper.net

 단, Multipath의 수는 라우터에 따라 그 한계가 있습니다. Cilium의 공식 문서에도 이와 같은 내용에 대해서 언급하며 경고하고 있습니다. 여러 개의 노드에서 서비스 VIP를 광고하는 경우 그 한계를 초과할 수 있습니다. 따라서 이 기능을 사용하기 전에 네트워크 장비가 지원하는 최대 ECMP의 수를 확인해야 합니다.

2. OPNsense Maximum-paths 확인 및 설정

 OPNsense의 FRR에서는 ECMP 최대 경로수가 64개 입니다. 이를 Maximum-paths라고 하는데 이를 확인하고 설정하는 방법은 아래와 같습니다. 우선 System > Settings > Administration 메뉴에 접근합니다. Secure Shell 섹션에서 Enable Secure Shell을 선택하여 활성화합니다. Root Login과 Authentication Method를 모두 활성화 화여 root 사용자의 비밀번호 접근을 활성화합니다. 마지막으로 SSH 접근 대상이 되는 인터페이스를 선택합니다. 이렇게 하면 ssh로 OPNsense의 CLI 환경에 접근할 수 있습니다. 그러나 이는 보안상 권장하지 않으므로 설정을 끝내고 비활성화하는 것을 추천합니다.

OPNsense SSH 접근 설정

 이제 SSH를 사용하여 root@<SSH로 접근할 인터페이스 IP 주소>로 OPNsense에 접속합니다. 비밀번호를 입력하는것은 리눅스의 그것과 매우 흡사합니다. 로그인을 하고 나면 OPNsense를 최초로 설치했을 때 보았던 화면이 나옵니다.

  0) Logout                              7) Ping host
  1) Assign interfaces                   8) Shell
  2) Set interface IP address            9) pfTop
  3) Reset the root password            10) Firewall log
  4) Reset to factory defaults          11) Reload all services
  5) Power off system                   12) Update from console
  6) Reboot system                      13) Restore a backup

Enter an option: 8  #8 입력하여 Shell 실행

 '8'을 입력하여 Shell을 실행합니다. 그리고 아래 명령어를 입력합니다. 이 명령어들은 스위치의 콘솔에서 사용하는 명령어와 그 형식이 같습니다.

vtysh     #shell에서 이 명령어를 입력하면 FRR CLI 모드로 전환

Hello, this is FRRouting (version 8.5.7).
Copyright 1996-2005 Kunihiro Ishiguro, et al.

configure terminal
route bgp <Router의 AS 번호>    #예) route bgp 65551
address-family ipv4 unicast
maximum-paths ?

#아래 처럼 출력
  (1-64)  Number of paths     #OPNsense의 경우 ECMP는 64개가 한계
  ibgp    iBGP-multipath
  
maximum-paths <1-64 사이 숫자 입력>    #예)maximum-paths 16
exit     #bgp router 설정에서 빠져나옴
do write     #설정 내용 저장
do show running-config     #설정 내용 확인

#아래 처럼 출력
Building configuration...

Current configuration:
!
frr version 8.5.7
frr defaults traditional
hostname <hostname>
log syslog notifications
!
router bgp 65551
 no bgp ebgp-requires-policy
 no bgp default ipv4-unicast
 neighbor 192.168.200.31 remote-as 65000
 neighbor 192.168.200.31 update-source vlan0.200
 neighbor 192.168.200.32 remote-as 65000
 neighbor 192.168.200.32 update-source vlan0.200
 neighbor 192.168.200.33 remote-as 65000
 neighbor 192.168.200.33 update-source vlan0.200
 !
 address-family ipv4 unicast
  neighbor 192.168.200.31 activate
  neighbor 192.168.200.32 activate
  neighbor 192.168.200.33 activate
  maximum-paths 16      #ECMP 최대 경로 수가 16으로 설정 됨
 exit-address-family
exit
!
end

 위 절차를 통해 OPNsense는 ECMP 경로의 최대 수가 64개 임을 확인할 수 있었고, 이를 설정하는 방법도 확인 하였습니다.

해결 방안 2 - 외부 Load Balancer 개발

 Kubernetes Load Balancer는 궁극적으로는 Node Port와 같습니다. 아래의 내용을 보시면 이해할 수 있을 것입니다. LoadBalancer 서비스이지만 Node Port를 노출하고 있고, <Kubernetes Noe IP>:<Node Port>로 호출하면 웹 서비스가 응답하는 것을 확인할 수 있습니다.

#Service 확인
kubectl describe svc test-service

Name:                     test-service
Namespace:                default
Labels:                   color=blue
Annotations:              <none>
Selector:                 app=test-app
Type:                     LoadBalancer
IP Family Policy:         SingleStack
IP Families:              IPv4
IP:                       10.109.208.124
IPs:                      10.109.208.124
LoadBalancer Ingress:     172.16.200.1 (VIP)
Port:                     <unset>  80/TCP
TargetPort:               80/TCP
NodePort:                 <unset>  30199/TCP       #실제로 NodePort를 노출하고 있음
Endpoints:                10.0.0.184:80,10.0.5.16:80
Session Affinity:         None
External Traffic Policy:  Cluster
Internal Traffic Policy:  Cluster
Events:                   <none>

#Kubernetes노드에 NodePort로 호출 응답 확인
curl 192.168.200.31:30199

<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>
NodePort로 웹서비스에 접근이 가능

 이런 작동 원리를 이용하여 다음과 같은 Kubernetes에 통합된 외부 LB 서비스를 설계할 수 있을 것 같습니다.

외부 LB 아키텍처
  • Serivce의 상태를 감지하는 Operator를 Kubernetes에 배포
  • Loadbalancer 서비스가 생성되면 Operator가 이를 감지하고 인프라에 LB 구현체 프로비저닝을 요청
  • LB 구현체는 외부에서 접근할 수 있는 IP를 하나 부여받고 이를 Kubernetes LB Service의 EXTERNAL-IP와 바인딩
  • Operator은 LB Service의 Node Port를 감지하고 프로비저닝 된 LB 구현체에게 <Kubernetes 노드 IP>:<LB Servie Node Port>를 백앤드로 설정하도록 명령

 이러한 한계가 있음에도 불구하고 On-Premise 환경에서 Cilium BGP는 Kubernetes Loadbalaner를 구현하는데 매우 좋은 방법이 아닐까 합니다. 특별한 기능 개발이나, 물리적인 장비의 추가 없이도 기존 장비의 기능을 활용하여 Kubernetes의 가상 네트워크를 물리 네트워크로 확장하게 해주는 매우 유용한 기능이 아닐 수 없습니다.