LoxiLB를 사용해서 무중단(hitless) 기능을 제공하는 HA(high-available) 로드밸런서를 구성하고, 이를 베어메탈 kubernetes에 배포하는 방법에 대해 설명합니다.
서론
이 글을 읽으시는 분들 대부분이 아시다시피 k8s의 워크로드는 서비스라는 개념으로 추상화되어 있습니다. k8s 서비스는 사용자가 k8s 파드로 접근하기 위한 엑세스 포인트를 네트워크 서비스로 추상화하여 제공하며, 어떤 파드가 서비스와 연결되는지는 label을 사용해서 정의됩니다.
주로 사용되는 서비스 타입은 다음 세 가지입니다.
ClusterIP
NodePort
LoadBalancer
타입을 지정하지 않고 k8s 서비스를 생성하면 기본적으로 ClusterIP 타입으로 생성됩니다. 이 타입은 k8s 클러스터 내부에서만 사용되는 서비스며, 외부로 노출되지 않는 cluster IP만 서비스에 부여합니다. 외부 엑세스를 허용하기 위해서는 LoadBalancer 서비스를 사용해야 합니다.
이 포스트에서는 LoadBalancer 서비스에 관해서만 논의합니다. 하지만 LoadBalancer 서비스를 사용하기 위해서는 k8s와 별개로 LoadBalancer 서비스를 실제로 제공/구현하는 애플리케이션이 필요합니다. 이런 애플리케이션은 다음과 같은 기능이 요구됩니다.
k8s API server Hook up을 통한 이벤트 감지
서비스에 외부에서 접근 가능한 글로벌 IP 제공
RR (Round Robin), WRR (Weighted Round Robin), Sticky 등 다양한 엔드포인트 선택 알고리즘 제공
상태(state) 동기화 기반 HA(high-availability) 구성, hitless & fast failover 기능 제공
기존 라우팅 프로토콜(일반적으로 BGP)를 지원, 서비스의 외부 공개 & 외부 서비스 정보 수집 가능
엔드포인트 스케일링 및 헬스체크 기능
로드밸런서 커넥션 & 관련 상태 관련 로그 제공
여기서 위에 언급한 기능 중에 상태(state) 동기화와 클러스터링, 그리고 무중단 & fast-failover의 필요성에 대해 좀 더 짚고 넘어가겠습니다.
로드밸런싱에서 state는 active인 세션의 상태를 지칭합니다. 예를 들어서 tcp 상태나, conntrack, 5-tuple information, statistics 같은 정보들이 state에 포함됩니다. http2나 websocket이 등장하기 전부터 대부분 응용 프로그램들은 long-lived tcp connections를 유지해야 했습니다. 연결이 끊어지면 다시 세션을 연결하는데 시간이 오래 걸려 장애를 유발하거나, 최악의 경우 재전송을 끊임없이 시도하느라 쓸데없는 트래픽을 생성하기도 합니다. 5G나 RAN 환경과 같이 크리티컬한 애플리케이션 같은 경우 이런 부작용은 심각한 장애를 유발할 수 있습니다.
이러한 상황을 막기 위해 로드밸런서를 클러스터 형태로 배포하여 각 state에 대해 분산화 관리 기능을 갖추도록 구성해야 합니다. 로드밸런서 노드 하나에 장애가 발생할 경우, 클러스터 내 다른 로드밸런서 노드가 사용자 정보, 애플리케이션 혹은 서비스 엔드포인트에 대한 정보들을 인계받아 대신 세션 포워딩을 수행하게 됩니다. 인계받은 노드 역시 state를 유지하고 있기 때문에 이 세션은 끊어지지 않습니다.
Topology
해당 포스트에서 사용할 토폴로지는 다음과 같습니다.
토폴로지에서 쿠버네티스는 총 4개의 노드를 가집니다.
그리고 LoxiLB는 2개의 노드(llb1, llb2)를 사용해서 로드밸런서 클러스터를 구성합니다. 이는 무중단 failover 기능을 위한 HA 구성에 필요한 최소사양입니다. 각 LoxiLB 인스턴스는 keepalive를 사용해서 서로 정보를 교환하고 특정 노드의 상태를 정의합니다.
로드밸런서 클러스터는 미리 정의된 keepalive 인스턴스를 여러개 가지고 있습니다. k8s LoadBalancer 서비스가 생성되면, keepalive 인스턴스 중 하나가 서비스에 매핑됩니다. 매핑된 keepalive 인스턴스의 virtual IP address는 서비스의 reachable point로 활용됩니다.
간단히 설명하기 위해서, 로드밸런서 클러스터에 이름이 default인 keepalive 인스턴스가 한 개만 있다고 가정하겠습니다. k8s에서 생성된 모든 서비스는 이 default 인스턴스와 매핑될 것입니다. 서비스의 External IP는 default 인스턴스의 virtual IP를 Next-Hop 으로 삼아 LoxiLB의 BGP Peer로 advertise 됩니다.
LoxiLB의 내부 동작
LoxiLB는 eBPF 기반으로 동작하며 eBPF maps를 활용하여 로드밸런서 세션을 유지합니다. 또한 kprobes에 연결된 eBPF 프로그램을 활용하여 eBPF maps의 변경사항을 모니터링하고 golang의 RPC 패키지(gRPC는 아닙니다) 를 사용해 클러스터 전체 맵의 동기화를 수행합니다.
gRPC 대신 golang RPC를 사용한 이유는 추후 다른 토픽에서 다룰 예정입니다.
k8s 설치
위에서 구성한 토폴로지를 만들기 위해서 먼저 k8s 설치부터 시작합니다. 이 포스트에서는 미리 준비된 Vagrantfile을 사용하겠습니다. 해당 Vagrantfile은 토폴로지대로 4개의 노드에 k8s를 설치합니다.
$ sudo apt install vagrant virtualbox
#Setup disksize
$ vagrant plugin install vagrant-disksize
#Run with Vagrantfile
$ vagrant up --no-provision
$ vagrant provision
vagrant provision 명령어가 완료되면 4개의 노드가 생성됩니다. 각 node의 정보는 다음과 같습니다.
노드 | 역할 | IP |
k8slx-01 | master | 192.168.59.211 |
k8slx-02 | master | 192.168.59.212 |
ks8lx-03 | worker | 192.168.59.213 |
k8slx-04 | worker | 192.168.59.214 |
노드 접속은 vagrant ssh 명령어를 사용합니다. 예를 들어, k8slx-01 노드에 접속하기 위해서는 다음 명령어를 입력합니다.
$ vagrant ssh k8s-01
Install LoxiLB
LoxiLB 설치는 docker가 필요합니다. 토폴로지에서는 두 개의 LoxiLB 노드로 LB 클러스터를 구성하고 있으므로 VM 역시 두 개가 필요합니다.
Ubuntu 20.04 VM을 두 개 생성합니다. VM은 kubernetes와 외부 라우터 양쪽과 통신하기 위해 두 개의 네트워크 어댑터(호스트 네트워크)를 추가로 필요로 합니다. Note: 네트워크 설정에서 deny의 promiscuous mode를 “allow all”로 설정해야 합니다.
다음은 양쪽 VM에 전부 LoxiLB Docker가 요구하는 라이브러리를 설치합니다.
#Install necessary packages in LoxiLB VMs
$ sudo apt update
$ sudo apt install -y net-tools bridge-utils docker.io vim ssh docker-compose
$ sudo service ssh restart
LB 클러스터 구성에서 LoxiLB는 gobgp와 keepalive와 함께 실행됩니다. 따라서, gobgp 및 keepalive의 설정 파일을 토폴로지 구성에 맞게 수정할 필요가 있습니다. 이 토폴로지에서 사용한 config file은 링크에서 다운로드 할 수 있습니다. (네트워크 설정에 따라 config file의 IP 주소를 변경해야 할 수 있습니다)
다음은 LoxiLB docker 컨테이너를 실행합니다.
$ sudo docker-compose up -d
위 명령어를 실행하면 macvlan 드라이버를 사용한 두 개의 도커 네트워크가 생성되고, LoxiLB 컨테이너가 생성됩니다. 컨테이너는 두 네트워크에 연결되어 있습니다. 정상적으로 컨테이너가 실행되면 LoxiLB 노드의 Keepalive 상태를 확인할 수 있습니다. 양쪽 VM 모두에서 다음 명령을 실행하여 상태를 확인합니다.
$ sudo docker exec -it loxilb cat /etc/shared/keepalive.state INSTANCE default is in MASTER state vip 192.168.58.150
Install kube-loxilb
LoxiLB는 kube-loxilb를 제공합니다. kube-loxilb는 LoxiLB를 쿠버네티스의 기본 로드밸런서로 제공하는 역할을 수행합니다. kube-loxilb를 사용하기 위해서는 github 에서 다운로드한 다음 네트워크 구성에 따라 몇 가지 설정을 변경해야 합니다.
$ cd kube-loxilb/manifest/
$ vi kube-loxilb.yaml
Kube-loxilb는 API를 통해 LoxiLB 로드밸런서 룰을 설정합니다. 따라서 Kube-loxilb 설정은 LoxiLB의 올바른 IP 주소를 가지고 있어야 합니다. yaml 파일을 열고 아래 예시처럼 configmap에서 apiServerURL을 찾아 IP를 LoxiLB 도커 컨테이너 IP로 교체합니다.
terminationGracePeriodSeconds: 0
containers:
- name: kube-loxilb
image: ghcr.io/loxilb-io/kube-loxilb:latest
imagePullPolicy: Always
command:
- /bin/kube-loxilb
args:
- --loxiURL=http://12.12.12.1:11111,http://14.14.14.1:11111
- --externalCIDR=123.123.123.1/24
- --setBGP=true
#- --setLBMode=1
#- --config=/opt/loxilb/agent/kube-loxilb.conf
resources:
requests:
cpu: "100m"
memory: "50Mi"
yaml 파일을 수정한 다음, k8slx-01 노드에서 다음 명령어로 쿠버네티스에 설정을 추가합니다.
$ sudo kubectl apply -f kube-loxilb.yaml
serviceaccount/kube-loxilb created clusterrole.rbac.authorization.k8s.io/kube-loxilb created clusterrolebinding.rbac.authorization.k8s.io/kube-loxilb created deployment.apps/kube-loxilb created
다음 명령어로 Kube-loxilb 배포 상태를 확인할 수 있습니다.
$ sudo kubectl get pods -n kube-system | grep loxi
loxi-cloud-controller-manager-55xqt 1/1 Running 0 21s
loxi-cloud-controller-manager-rvrgz 1/1 Running 0 21s
External Router configuration
토폴로지에서 외부 라우터는 bird BGP를 사용하고 있습니다. 외부 라우터로 사용할 VM에서 다음 명령어로 bird BGP를 설치합니다.
sudo apt-get install bird2 –yes
토폴로지 구성을 위해 사용한 config 파일은 여기에서 찾을 수 있습니다. config file을 /etc/bird/bird.conf 경로에 저장한 후 다음 명령어로 bird BGP 서비스를 다시 시작해야 합니다.
## Change bird.conf as needed
sudo systemctl restart bird
Kubernetes Nodes Networking
k8s는 네트워크를 구성하기 위해 L2, L3, VxLAN 등 다양한 방법을 제공합니다. 이 포스트에서는 k8s의 기본 CNI로 calico를 사용하며 모드는 Direct / NoEncapMode (unencapsulated) mode로 설정합니다. 동시에 노드 간 경로 구성을 위해 BGP를 사용합니다.
calico BGP 설정에 필요한 yaml 파일은 여기에서 찾을 수 있습니다. k8slx-01 노드에서 아래 명령어로 해당 yaml 파일을 사용해 BGP를 설정합니다.
$ sudo calicoctl apply -f calico-bgp-config.yaml
Verify BGP connections
생성된 BGP 토폴로지 및 커넥션 상태를 확인하는 명령어는 다음과 같습니다.
LoxiLB 노드에서 확인:
$ sudo docker exec -it loxilb gobgp neigh
Peer AS Up/Down State |#Received Accepted
192.168.58.1 65001 01:07:18 Establ | 4 4
192.168.59.211 64512 01:07:22 Establ | 1 1
192.168.59.212 64512 01:07:22 Establ | 1 1
192.168.59.213 64512 01:07:22 Establ | 1 1
192.168.59.214 64512 01:07:22 Establ | 1 1
k8s calico 에서 확인
$ vagrant ssh k8slx-01
………
$vagrant@node1:~$ sudo calicoctl node status
Calico process is running.
IPv4 BGP status
+----------------+-------------------+-------+------------+-------------+
| PEER ADDRESS | PEER TYPE | STATE | SINCE | INFO |
+----------------+-------------------+-------+------------+-------------+
| 192.168.59.212 | node-to-node mesh | up | 2023-01-04 | Established |
| 192.168.59.213 | node-to-node mesh | up | 2023-01-04 | Established |
| 192.168.59.214 | node-to-node mesh | up | 2023-01-04 | Established |
| 192.168.59.101 | global | up | 07:33:22 | Established |
| 192.168.59.111 | global | up | 07:17:56 | Established |
+----------------+-------------------+-------+------------+-------------+
kube-loxilb 설정에서 setBGP를 true로 설정했습니다. 이 옵션은 BGP를 사용해 k8s 서비스 IP를 외부 라우터에 advertise 하도록 합니다.
setLBMode 설정은 LoxiLB의 NAT 모드에 관련된 설정입니다. 0에서 2까지 값을 설정할 수 있습니다. 각 모드에 대한 설명은 아래와 같습니다.
0 - default (only DNAT)
1 - onearm (source IP is changed to load balancer’s interface IP)
2 - fullNAT (sourceIP is changed to virtual IP).
LB 클러스터의 keepalive 인스턴스들은 각자 서비스와 매핑됩니다. 해당 서비스의 트래픽은 keepalive 인스턴스의 MASTER 노드로 흐릅니다. 이때 각각의 서비스가 매핑된 keepalive 인스턴스들의 MASTER 노드는 서로 다를 수 있으므로, 서비스 트래픽은 자동으로 LB 클러스터의 각 노드로 분산됩니다. LoxiLB 클러스터링은 이런 방식으로 무중단 클러스터링 기능과 트래픽 분산을 제공합니다. 이때 트래픽 분산을 최적화하기 위해서 두 가지 설정을 선택할 수 있습니다.
source IP 주소를 보존하는 경우 (default mode).
가상 IP 주소를 source IP로 사용하는 경우 (fullNAT mode).
iperf 서비스 테스트
k8slx-01 노드에서 iperf.yaml 파일을 사용해 iperf 파드와 iperf 서비스를 생성합니다.
$ vagrant@node1:~$ sudo kubectl apply -f iperf.yaml
service/iperf-service created
pod/iperf1 created
pod/iperf2 created
다음 명령어로 iperf-service가 생성되었음을 확인합니다.
$ sudo kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
iperf-service LoadBalancer 10.233.5.183 123.123.123.1 55001:32573/TCP 24s
kubernetes ClusterIP 10.233.0.1 <none> 443/TCP 1d
iperf-service가 LoadBalancer 타입으로 생성되었으며, external IP로 123.123.123.1을 할당받았음을 확인할 수 있습니다.
LoxiLB 노드에서도 다음로드밸런서 룰이 생성되었음을 확인할 수 있습니다.
$ sudo docker exec -it loxilb loxicmd get lb -o wide
Traffic check
이제 외부 노드에서 서비스의 external IP로 엑세스가 가능한지 확인합니다. 확인에는 iperf를 사용합니다.
netlox@netlox-VirtualBox:~$ sudo iperf -c 123.123.123.1 -t 5 -i 1 -p 55001
------------------------------------------------------------
Client connecting to 123.123.123.1, TCP port 55001
TCP window size: 654 KByte (default)
------------------------------------------------------------
[ 3] local 192.168.58.2 port 39644 connected with 123.123.123.1 port 55001
[ ID] Interval Transfer Bandwidth
[ 3] 0.0- 1.0 sec 90.5 MBytes 759 Mbits/sec
[ 3] 1.0- 2.0 sec 74.2 MBytes 623 Mbits/sec
[ 3] 2.0- 3.0 sec 40.8 MBytes 342 Mbits/sec
[ 3] 3.0- 4.0 sec 45.8 MBytes 384 Mbits/sec
[ 3] 4.0- 5.0 sec 51.8 MBytes 434 Mbits/sec
[ 3] 0.0- 5.0 sec 303 MBytes 508 Mbits/sec
Quick Demo
iperf 서비스의 무중단 failover를 영상을 통해서 확인할 수 있습니다. 해당 영상에서는 LB 클러스터를 구성하는 두 개의 LoxiLB 인스턴스가 있습니다. 한쪽 LoxiLB 인스턴스가 MASTER 일 때, 다른 하나는 BACKUP 가 됩니다. 클라이언트는 로드밸런서 서비스의 external IP 및 port를 사용하여 iperf 파드에 엑세스합니다. 이 때 MASTER LoxiLB 인스턴스는 해당 세션에 대해 connection tracking을 수행합니다. 동시에 해당 세션의 state는 BACKUP LoxiLB 인스턴스에도 동기화됩니다.
MASTER LoxiLB 인스턴스가 중지되면 BACKUP 인스턴스가 새로운 MASTER가 됩니다. 동시에 클라이언트와 서비스 간의 연결은 그대로 유지된다는 것을 알 수 있습니다.
Cleanup
다음 명령으로 LoxiLB 인스턴스에서 LoxiLB 관련 설정을 정리할 수 있습니다.
loxilb@loxilb-VirtualBox:~$ sudo docker-compose down
Comments