KANS 3기 5주차 - LoadBalancer(MetalLB), IPVS

Oasis·2024년 9월 30일

KANS

목록 보기
5/9

가시다님의 KANS [3기] 스터디 내용을 정리한 포스트 입니다.

1. LoadBalancer

Kubernetes의 서비스 오브젝트(Service Object) 중 LoadBalancer 타입은 외부에서 클러스터 내에 있는 서비스를 접근할 수 있도록 하는 방법 중 하나입니다. 이를 통해 클러스터 외부에서 서비스에 대한 요청을 보낼 수 있게 됩니다.

LoadBalancer서비스는 퍼블릭 클라우드 환경에서 동작하는 LoadBalancer 서비 스와 온프레미스환경에서 동작하는 LoadBalancer 서비스가 있습니다.

1.1 퍼블릭 클라우드 환경

크게 2가지 동작 방식이 있습니다.

  • 첫 번째 방식
    • 외부 클라이언트는 로드밸런서로 접속을 하고, 로드밸런서는 파드가 있는 노드들로 부하분산해서 전달함
    • 노드의 NodePort 로 인입 후에는 iptables 에 의해서 파드로 랜 덤 부하분산을 통해서 전달함
    • 부하분산 과정이 두 번 수행됨

  • 두 번째 방식
    • 로드밸런서에서 파드의 IP로 직접 부하분산 전달함
    • 로드밸런서 가 파드의 IP 정보를 알기 위해서, 별도의 로드밸런서 컨트롤러를 구성하고 해당 로 드밸런서 컨트롤러가 로드밸런서에게 파드의 IP를 동적으로 전달함
    • 부하분산 과정을 한 번만 수행하여 첫 번째 방식에 비해서 좀 더 효율적임
    • 해당 동작을 위해서는 AWS 로드밸런서 컨트롤러를 구성해야 함

1.2 온프레미스 환경

크게 하드웨어 기반과 소프트웨어 기반으로 방식이 있습니다. 엔터프라이즈 환경에서는 가용성등 측면에서 하드웨어 기반으로 보통 구성하게 됩니다.

  • 하드웨어 기반 방식
    • 클라우드 환경의 AWS LoadBalancer 서비스에 거의 동일하게 별도의 장비로 접속 후 노드에 NodePort 혹은 파드로 직접 전달하여 통신함
    • 대표적으로 시트릭스나 F5네트웍스의 제품이 있음

  • 소프트웨어 기반 방식
    • 온프레미스 환경에서 동작하는 LoadBalancer 서비스도 소프트웨어 기반의 동작은 별도의 네트워크 장비 없이, 소프트웨어로 동작함
    • 대표적으로 MetalLB, OpenELB, PubeLB, kube-vip 등이 있음

1.3 통신 흐름

  • 외부에서 로드밸런서 (부하분산) 처리 후 → 노드(NodePort) 이후 기본 과정은 NodePort 과정과 동일함
  • 노드는 외부에 공개되지 않고 로드밸런서만 외부에 공개되어, 외부 클라이언트는 로드밸랜서에 접속을 할 뿐 내부 노드의 정보를 알 수 없음
  • 로드밸런서가 부하분산하여 파드가 존재하는 노드들에게 전달한다, iptables 룰에서는 자신의 노드에 있는 파드만 연결함 (externalTrafficPolicy: local)
  • DNAT 2번 동작 : 첫번째(로드밸런서 접속 후 빠져 나갈때), 두번째(노드의 iptables 룰에서 파드IP 전달 시)
  • 외부 클라이언트 IP 보존(유지) : AWS NLB 는 타켓인스턴스일 경우 클라이언트 IP를 유지, iptables 룰 경우도 externalTrafficPolicy 로 클라이언트 IP를 보존
  • 쿠버네티스는 Service(LB Type) API 만 정의하고 실제 구현은 add-on 에 맡김
  • 노드에 파드가 없을 경우 '로드밸런서'에서 노드에 헬스 체크(상태 검사)가 실패하여 해당 노드로는 외부 요청 트래픽을 전달하지 않음

2. MetalLB

2.1 MetalLB 개요



  • MetalLB온프레미스 환경(IDC)에서 사용할 수 있는 서비스(로드밸런서 타입)이다
  • 서비스(로드 밸런서)의 'External IP' 전파를 위해서 표준 프로토콜인 ARP(IPv4)/NDP(IPv6), BGP 를 사용합니다.
    • 데몬셋으로 speaker 파드를 생성하여 'External IP' 전파합니다.
  • 일반적인 BGP 데몬과 다르게 BGP 업데이트(외부에서 광고하는 네트워크 대역)을 받지 않습니다.
  • 클라우드 플랫폼 호환 : 대부분의 클라우드 플랫폼(예 AWS, Azure, GCP 등)과 호환되지 않습니다. - 링크
  • CNI(네트워크 플러그인) 호환 : 일부 CNI와 연동에 이슈가 있습니다.- 링크
    • 예시) Calico IPIP(BGP)와 MetalLB BGP 모드를 상단 라우터와 동시 사용 시 문제 발생 - 링크

2.2 Layer2 모드

  • 모드 소개 : 리더 파드가 선출되고 해당 리더 파드가 생성된 노드로만 트래픽이 인입되어 해당 노드에서 iptables 분산되어 파드로 접속
    • 서비스(로드밸런서) 'External IP' 생성 시 speaker 파드 중 1개리더가 되고, 리더 speaker 파드가 존재하는 노드로 서비스 접속 트래픽이 인입되게 됩니다
      • 데몬셋으로 배포된 speaker 파드는 호스트 네트워크를 사용합니다 ⇒ "NetworkMode": "host"
    • 리더는 ARP(GARP, Gratuitous APR)로 해당 'External IP' 에 대해서 자신이 소유(?)라며 동일 네트워크에 전파를 합니다
    • 만약 리더(노드)가 장애 발생 시 자동으로 나머지 speaker 파드 중 1개가 리더가 됩니다.
      • 멤버 리스터 및 장애 발견은 hashicorp 의 memberlist 를 사용 - Gossip based membership and failure detection
    • Layer 2에서 멤버 발견 및 자동 절체에 Keepalived(VRRP)도 있지만 사용하지 않은 이유 - 링크
  • 제한 사항
    • single-node bottlenecking : 서비스 1개 생성 사용 시, 모든 서비스 접근 트래픽은 리더 파드가 존재하는 노드로만 인입되어서 부하가 집중
    • potentially slow failover : 리더(노드)가 장애 시 나머지 노드 리더가 선출되고, ARP 전파 및 갱신완료 전까지는 장애가 발생됨 (대략 10초~20초 정도)

2.3 BGP 모드

  • 모드 소개 : speaker 파드가 BGP로 서비스 정보(EXTERNAL-IP)를 전파 후, 외부에서 라우터를 통해 ECMP 라우팅으로 부하 분산 접속
    • speaker 파드에 BGP 가 동작하여 서비스 정보(EXTERNAL-IP)를 전파한다
      • 기본은 IP주소(32bit)를 전파하며, 설정으로 축약된 네트워크 정보를 전파할 수 있다 → bgp-advertisements 에 aggregation-length 설정
      • BGP 커뮤니티, localpref 등 BGP 관련 설정을 할 수 있다
      • IP 주소 마지막이 0 과 255 를 처리를 못하는 라우터 장비가 있을 경우 avoid-buggy-ips: true 옵션으로 할당되지 않게 할 수 있다
    • 외부 클라이언트에서 SVC(서비스, EXTERNAL-IP)로 접속이 가능하며, 라우터에서 ECMP 라우팅을 통해 부하 분산 접속 할 수 있다
      • 일반적으로 ECMP 는 5-tuple(프로토콜, 출발지IP, 목적지IP, 출발지Port, 목적지Port) 기준으로 동작합니다.
      • 물론 라우터 장비에 따라 다양한 라우팅(분산) 처리가 가능합니다
  • 제한 사항
    • 라우터에서 서비스로 인입이 되기 때문에, 라우터의 관련 설정이 중요한 만큼 네트워크팀과 협업을 적극 권장
    • 노드(speaker) 파드 장애 시 BGP Timer 설정 등 구성하고 있는 네트워크 환경에 맞게 최적화 작업이 필요
    • ECMP 부하 분산 접속 시 특정 파드에 몰리거나 혹은 세션 고정, flapping 등 다양한 환경에 대응이 필요
    • BGP 라우팅 설정 및 라우팅 전파 관련 최적화 설정이 필요

2.4 MeltalLB 설치 및 접속 테스트

2.4.1 MeltalLB 설치

  • MetalLB 설치
# Kubernetes manifests 로 설치
#kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.14.8/config/manifests/metallb-native.yaml
kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/refs/heads/main/config/manifests/metallb-native-prometheus.yaml

# metallb crd 확인
kubectl get crd | grep metallb
bfdprofiles.metallb.io                      2024-10-01T05:18:12Z
bgpadvertisements.metallb.io                2024-10-01T05:18:12Z
bgppeers.metallb.io                         2024-10-01T05:18:12Z
communities.metallb.io                      2024-10-01T05:18:12Z
ipaddresspools.metallb.io                   2024-10-01T05:18:12Z
l2advertisements.metallb.io                 2024-10-01T05:18:12Z
servicel2statuses.metallb.io                2024-10-01T05:18:12Z

# 생성된 리소스 확인 : metallb-system 네임스페이스 생성, 파드(컨트롤러, 스피커) 생성, RBAC(서비스/파드/컨피그맵 조회 등등 권한들), SA 등
kubectl get all,configmap,secret,ep -n metallb-system

# 파드 내에 kube-rbac-proxy 컨테이너는 프로메테우스 익스포터 역할 제공
kubectl get pods -n metallb-system -l app=metallb -o jsonpath="{range .items[*]}{.metadata.name}{':\n'}{range .spec.containers[*]}{'  '}{.name}{' -> '}{.image}{'\n'}{end}{end}"
controller-679855f7d7-7qqrd:
  kube-rbac-proxy -> gcr.io/kubebuilder/kube-rbac-proxy:v0.12.0
  controller -> quay.io/metallb/controller:main
speaker-649hg:
  kube-rbac-proxy -> gcr.io/kubebuilder/kube-rbac-proxy:v0.12.0
  speaker -> quay.io/metallb/speaker:main
speaker-9hlfx:
  kube-rbac-proxy -> gcr.io/kubebuilder/kube-rbac-proxy:v0.12.0
  speaker -> quay.io/metallb/speaker:main
speaker-fbdng:
  kube-rbac-proxy -> gcr.io/kubebuilder/kube-rbac-proxy:v0.12.0
  speaker -> quay.io/metallb/speaker:main
speaker-wcdvg:
  kube-rbac-proxy -> gcr.io/kubebuilder/kube-rbac-proxy:v0.12.0
  speaker -> quay.io/metallb/speaker:main

## metallb 컨트롤러는 디플로이먼트로 배포됨
kubectl get ds,deploy -n metallb-system
NAME                     DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR            AGE
daemonset.apps/speaker   4         4         4       4            4           kubernetes.io/os=linux   15m

NAME                         READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/controller   1/1     1            1           15m

## 데몬셋으로 배포되는 metallb 스피커 파드의 IP는 네트워크가 host 모드이므로 노드의 IP를 그대로 사용
kubectl get pod -n metallb-system -o wide
NAME                          READY   STATUS    RESTARTS   AGE     IP           NODE                  NOMINATED NODE   READINESS GATES
controller-679855f7d7-7qqrd   2/2     Running   0          13m   10.10.4.4    myk8s-worker2         <none>           <none>
speaker-649hg                 2/2     Running   0          13m   172.18.0.2   myk8s-worker2         <none>           <none>
speaker-9hlfx                 2/2     Running   0          13m   172.18.0.5   myk8s-control-plane   <none>           <none>
speaker-fbdng                 2/2     Running   0          13m   172.18.0.3   myk8s-worker          <none>           <none>
speaker-wcdvg                 2/2     Running   0          13m   172.18.0.4   myk8s-worker3         <none>           <none>
  • 컨피그맵 생성 : 모드 및 서비스 대역 지정
# kind 설치 시 kind 이름의 도커 브리지가 생성된다 : 172.18.0.0/16 대역
docker network ls
docker inspect kind

# kind network 중 컨테이너(노드) IP(대역) 확인
docker ps -q | xargs docker inspect --format '{{.Name}} {{.NetworkSettings.Networks.kind.IPAddress}}'
/myk8s-worker3 172.18.0.4
/myk8s-worker 172.18.0.3
/myk8s-control-plane 172.18.0.5
/myk8s-worker2 172.18.0.2

# IPAddressPool 생성 : LoadBalancer External IP로 사용할 IP 대역
kubectl explain ipaddresspools.metallb.io

cat <<EOF | kubectl apply -f -
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
  name: my-ippool
  namespace: metallb-system
spec:
  addresses:
  - 172.18.255.200-172.18.255.250
EOF

kubectl get ipaddresspools -n metallb-system
NAME        AUTO ASSIGN   AVOID BUGGY IPS   ADDRESSES
my-ippool   true          false             ["172.18.255.200-172.18.255.250"]

# L2Advertisement 생성 : 설정한 IPpool을 기반으로 Layer2 모드로 LoadBalancer IP 사용 허용
kubectl explain l2advertisements.metallb.io
NAME        AUTO ASSIGN   AVOID BUGGY IPS   ADDRESSES
my-ippool   true          false             ["172.18.255.200-172.18.255.250"]

cat <<EOF | kubectl apply -f -
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
  name: my-l2-advertise
  namespace: metallb-system
spec:
  ipAddressPools:
  - my-ippool
EOF

kubectl get l2advertisements -n metallb-system
NAME              IPADDRESSPOOLS   IPADDRESSPOOL SELECTORS   INTERFACES
my-l2-advertise   ["my-ippool"]    
  • 로그 확인 : 아래 로그 -f 모니터링 해두기
# (옵션) metallb-speaker 파드 로그 확인
kubectl logs -n metallb-system -l app=metallb -f
kubectl logs -n metallb-system -l component=speaker --since 1h
kubectl logs -n metallb-system -l component=speaker -f

# (옵션) kubectl krew 플러그인 stern 설치 후 아래 명령 사용 가능
kubectl stern -n metallb-system -l app=metallb
kubectl stern -n metallb-system -l component=speaker --since 1h
kubectl stern -n metallb-system -l component=speaker # 기본 설정이 follow
kubectl stern -n metallb-system speaker  # 매칭 사용 가능

2.4.2 서비스 생성 및 확인

  • 서비스 생성
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Service
metadata:
  name: svc1
spec:
  ports:
    - name: svc1-webport
      port: 80
      targetPort: 80
  selector:
    app: webpod
  type: LoadBalancer  # 서비스 타입이 LoadBalancer
---
apiVersion: v1
kind: Service
metadata:
  name: svc2
spec:
  ports:
    - name: svc2-webport
      port: 80
      targetPort: 80
  selector:
    app: webpod
  type: LoadBalancer
---
apiVersion: v1
kind: Service
metadata:
  name: svc3
spec:
  ports:
    - name: svc3-webport
      port: 80
      targetPort: 80
  selector:
    app: webpod
  type: LoadBalancer
EOF
  • 서비스 확인 및 리더 스피커 파드 확인
# arp scan 해두기
docker exec -it myk8s-control-plane arp-scan --interfac=eth0 --localnet

Interface: eth0, type: EN10MB, MAC: 02:42:ac:12:00:05, IPv4: 172.18.0.5
Starting arp-scan 1.10.0 with 65536 hosts (https://github.com/royhills/arp-scan)
172.18.0.1	02:42:6c:bd:62:0b	(Unknown: locally administered)
172.18.0.2	02:42:ac:12:00:02	(Unknown: locally administered)
172.18.0.3	02:42:ac:12:00:03	(Unknown: locally administered)
172.18.0.4	02:42:ac:12:00:04	(Unknown: locally administered)


# LoadBalancer 타입의 서비스 생성 확인 : EXTERNAL-IP가 서비스 마다 할당되며, 실습 환경에 따라 다를 수 있음
## LoadBalancer 타입의 서비스는 NodePort 와 ClusterIP 를 포함함 - 'allocateLoadBalancerNodePorts : true' 기본값
## ExternalIP 로 접속 시 사용하는 포트는 PORT(S) 의 앞에 있는 값을 사용 (아래의 경우는 TCP 80 임)
## 만약 노드의 IP에 NodePort 로 접속 시 사용하는 포트는 PORT(S) 의 뒤에 있는 값을 사용 (아래는 30485 임)
kubectl get service,ep
NAME                 TYPE           CLUSTER-IP     EXTERNAL-IP      PORT(S)        AGE
service/kubernetes   ClusterIP      10.200.1.1     <none>           443/TCP        21h
service/svc1         LoadBalancer   10.200.1.34    172.18.255.200   80:31280/TCP   42s
service/svc2         LoadBalancer   10.200.1.85    172.18.255.201   80:32760/TCP   42s
service/svc3         LoadBalancer   10.200.1.146   172.18.255.202   80:30205/TCP   42s

NAME                   ENDPOINTS                   AGE
endpoints/kubernetes   172.18.0.5:6443             21h
endpoints/svc1         10.10.2.5:80,10.10.4.3:80   42s
endpoints/svc2         10.10.2.5:80,10.10.4.3:80   42s
endpoints/svc3         10.10.2.5:80,10.10.4.3:80   42s

# LoadBalancer 타입은 기본적으로 NodePort를 포함 사용. NodePort는 ClusterIP를 포함 사용.
## 클라우드사업자 LB Type이나 온프레미스환경 HW LB Type 경우 LB 사용 시 NodePort 미사용 설정 가능
kubectl describe svc svc1
Namespace:                default
Labels:                   <none>
Annotations:              metallb.universe.tf/ip-allocated-from-pool: my-ippool
Selector:                 app=webpod
Type:                     LoadBalancer
IP Family Policy:         SingleStack
IP Families:              IPv4
IP:                       10.200.1.34
IPs:                      10.200.1.34
LoadBalancer Ingress:     172.18.255.200 (VIP)
Port:                     svc1-webport  80/TCP
TargetPort:               80/TCP
NodePort:                 svc1-webport  31280/TCP
Endpoints:                10.10.4.3:80,10.10.2.5:80
Session Affinity:         None
External Traffic Policy:  Cluster
Internal Traffic Policy:  Cluster
Events:
  Type    Reason        Age   From                Message
  ----    ------        ----  ----                -------
  Normal  IPAllocated   67s   metallb-controller  Assigned IP ["172.18.255.200"]
  Normal  nodeAssigned  67s   metallb-speaker     announcing from node "myk8s-worker" with protocol "layer2"

## 아래 처럼 LB VIP 별로 이던 speaker 배포된 노드가 리더 역할을 하는지 확인 가능
kubectl describe svc | grep Events: -A5
Events:                   <none>


Name:                     svc1
Namespace:                default
Labels:                   <none>
--
Events:
  Type    Reason        Age   From                Message
  ----    ------        ----  ----                -------
  Normal  IPAllocated   89s   metallb-controller  Assigned IP ["172.18.255.200"]
  Normal  nodeAssigned  89s   metallb-speaker     announcing from node "myk8s-worker" with protocol "layer2"

--
Events:
  Type    Reason        Age   From                Message
  ----    ------        ----  ----                -------
  Normal  IPAllocated   89s   metallb-controller  Assigned IP ["172.18.255.201"]
  Normal  nodeAssigned  89s   metallb-speaker     announcing from node "myk8s-worker" with protocol "layer2"

--
Events:
  Type    Reason        Age   From                Message
  ----    ------        ----  ----                -------
  Normal  IPAllocated   89s   metallb-controller  Assigned IP ["172.18.255.202"]
  Normal  nodeAssigned  89s   metallb-speaker     announcing from node "myk8s-worker3" with protocol "layer2"

kubectl get svc svc1 -o json | jq


# metallb CRD인 servicel2status 로 상태 정보 확인
kubectl explain servicel2status
kubectl get servicel2status -n metallb-system
kubectl describe servicel2status -n metallb-system
kubectl get servicel2status -n metallb-system -o json --watch # watch 모드


# 현재 SVC EXTERNAL-IP를 변수에 지정
SVC1EXIP=$(kubectl get svc svc1 -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
SVC2EXIP=$(kubectl get svc svc2 -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
SVC3EXIP=$(kubectl get svc svc3 -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
echo $SVC1EXIP $SVC2EXIP $SVC3EXIP

# mypc/mypc2 에서 현재 SVC EXTERNAL-IP를 담당하는 리더 Speaker 파드 찾는법 : arping 툴 사용
docker exec -it mypc arping -I eth0 -f -c 1 $SVC1EXIP
docker exec -it mypc arping -I eth0 -f -c 1 $SVC2EXIP
docker exec -it mypc arping -I eth0 -f -c 1 $SVC3EXIP

$for i in $SVC1EXIP $SVC2EXIP $SVC3EXIP; do docker exec -it mypc arping -I eth0 -f -c 1 $i; done
ARPING 172.18.255.200 from 172.18.0.6 eth0
Unicast reply from 172.18.255.200 [02:42:AC:12:00:03]  0.874ms
Sent 1 probes (1 broadcast(s))
Received 1 response(s)
ARPING 172.18.255.201 from 172.18.0.6 eth0
Unicast reply from 172.18.255.201 [02:42:AC:12:00:03]  1.206ms
Sent 1 probes (1 broadcast(s))
Received 1 response(s)
ARPING 172.18.255.202 from 172.18.0.6 eth0
Unicast reply from 172.18.255.202 [02:42:AC:12:00:04]  3.856ms
Sent 1 probes (1 broadcast(s))
Received 1 response(s)

docker exec -it mypc ip -c neigh
172.18.0.4 dev eth0 lladdr 02:42:ac:12:00:04 STALE
172.18.255.202 dev eth0 lladdr 02:42:ac:12:00:04 REACHABLE
172.18.0.3 dev eth0 lladdr 02:42:ac:12:00:03 STALE
172.18.255.201 dev eth0 lladdr 02:42:ac:12:00:03 REACHABLE

docker exec -it mypc ping -c 1 -w 1 -W 1 $SVC1EXIP
docker exec -it mypc ping -c 1 -w 1 -W 1 $SVC2EXIP
docker exec -it mypc ping -c 1 -w 1 -W 1 $SVC3EXIP
for i in $SVC1EXIP $SVC2EXIP $SVC3EXIP; do docker exec -it mypc ping -c 1 -w 1 -W 1 $i; done
for i in 172.18.0.2 172.18.0.3 172.18.0.4 172.18.0.5; do docker exec -it mypc ping -c 1 -w 1 -W 1 $i; done

# mypc/mypc2 에서 arp 테이블 정보 확인 >> SVC IP별로 리더 파드(스피커) 역할의 노드를 확인!
docker exec -it mypc ip -c neigh | sort
172.18.0.2 dev eth0 lladdr 02:42:ac:12:00:02 REACHABLE
172.18.0.3 dev eth0 lladdr 02:42:ac:12:00:03 REACHABLE
172.18.0.4 dev eth0 lladdr 02:42:ac:12:00:04 REACHABLE
172.18.0.5 dev eth0 lladdr 02:42:ac:12:00:05 REACHABLE
172.18.255.200 dev eth0 lladdr 02:42:ac:12:00:03 STALE
172.18.255.201 dev eth0 lladdr 02:42:ac:12:00:03 REACHABLE
172.18.255.202 dev eth0 lladdr 02:42:ac:12:00:04 REACHABLE

kubectl get node -owide # mac 주소에 매칭되는 IP(노드) 찾기

# (옵션) 노드에서 ARP 패킷 캡쳐 확인
docker exec -it myk8s-control-plane tcpdump -i eth0 -nn arp
docker exec -it myk8s-worker        tcpdump -i eth0 -nn arp
docker exec -it myk8s-worker2       tcpdump -i eth0 -nn arp
docker exec -it myk8s-worker3       tcpdump -i eth0 -nn arp


# (옵션) metallb-speaker 파드 로그 확인
kubectl logs -n metallb-system -l app=metallb -f
kubectl logs -n metallb-system -l component=speaker --since 1h
kubectl logs -n metallb-system -l component=speaker -f

# (옵션) kubectl krew 플러그인 stern 설치 후 아래 명령 사용 가능
kubectl stern -n metallb-system -l app=metallb
kubectl stern -n metallb-system -l component=speaker --since 1h
kubectl stern -n metallb-system -l component=speaker # 기본 설정이 follow
kubectl stern -n metallb-system speaker  # 매칭 사용 가능

2.4.3 서비스 접속 테스트

# 현재 SVC EXTERNAL-IP를 변수에 지정
SVC1EXIP=$(kubectl get svc svc1 -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
SVC2EXIP=$(kubectl get svc svc2 -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
SVC3EXIP=$(kubectl get svc svc3 -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
echo $SVC1EXIP $SVC2EXIP $SVC3EXIP

# mypc/mypc2 에서 접속 테스트
docker exec -it mypc curl -s $SVC1EXIP
docker exec -it mypc curl -s $SVC1EXIP | grep Hostname
for i in $SVC1EXIP $SVC2EXIP $SVC3EXIP; do docker exec -it mypc curl -s $i | grep Hostname ; done
for i in $SVC1EXIP $SVC2EXIP $SVC3EXIP; do echo ">> Access Service External-IP : $i <<" ; docker exec -it mypc curl -s $i | grep Hostname ; echo ; done

## RemoteAddr 주소는 어떻게 나오나요?  그럴까요?
##  NodePort 기본 동작과 동일하게 인입한 노드의 인터페이스로 SNAT 되어서 최종 파드로 전달됨
for i in $SVC1EXIP $SVC2EXIP $SVC3EXIP; do echo ">> Access Service External-IP : $i <<" ;docker exec -it mypc curl -s $i | egrep 'Hostname|RemoteAddr|Host:' ; echo ; done

# 부하분산 접속됨
docker exec -it mypc zsh -c "for i in {1..100}; do curl -s $SVC1EXIP | grep Hostname; done | sort | uniq -c | sort -nr"
docker exec -it mypc zsh -c "for i in {1..100}; do curl -s $SVC2EXIP | grep Hostname; done | sort | uniq -c | sort -nr"
docker exec -it mypc zsh -c "for i in {1..100}; do curl -s $SVC3EXIP | grep Hostname; done | sort | uniq -c | sort -nr"

# 지속적으로 반복 접속
docker exec -it mypc zsh -c "while true; do curl -s --connect-timeout 1 $SVC1EXIP | egrep 'Hostname|RemoteAddr'; date '+%Y-%m-%d %H:%M:%S' ; echo ;  sleep 1; done"
docker exec -it mypc zsh -c "while true; do curl -s --connect-timeout 1 $SVC2EXIP | egrep 'Hostname|RemoteAddr'; date '+%Y-%m-%d %H:%M:%S' ; echo ;  sleep 1; done"
docker exec -it mypc zsh -c "while true; do curl -s --connect-timeout 1 $SVC3EXIP | egrep 'Hostname|RemoteAddr'; date '+%Y-%m-%d %H:%M:%S' ; echo ;  sleep 1; done"

# LoadBalancer Type은 기본값으로 NodePort 포함. NodePort 서비스는 ClusterIP 를 포함
# NodePort:PORT 및 CLUSTER-IP:PORT 로 접속 가능!
kubectl get svc svc1
NAME   TYPE           CLUSTER-IP    EXTERNAL-IP      PORT(S)        AGE
svc1   LoadBalancer   10.200.1.34   172.18.255.200   80:31280/TCP   83m

# 컨트롤노드에서 각각 접속 확인 실행 해보자
docker exec -it myk8s-control-plane curl -s 127.0.0.0:30246 # NodePor Type
docker exec -it myk8s-control-plane curl -s 10.200.1.82     # ClusterIP Tpye
-> 접속 안됨!

2.4.4 Failover 테스트

리더 Speaker 파드가 존재하는 노드를 재부팅하면 curl 접속 테스트 시 10~20초 정도의 장애 시간이 발생합니다. 이후 자동 원복 되며, 원복 시 5초 정도 장애 시간 발생하게 됩니다.

# 사전 준비
## 지속적으로 반복 접속
SVC1EXIP=$(kubectl get svc svc1 -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
docker exec -it mypc zsh -c "while true; do curl -s --connect-timeout 1 $SVC1EXIP | egrep 'Hostname|RemoteAddr'; date '+%Y-%m-%d %H:%M:%S' ; echo ;  sleep 1; done"

## 상태 모니터링
watch -d kubectl get pod,svc,ep

## 실시간 로그 확인
kubectl logs -n metallb-system -l app=metallb -f
혹은
kubectl stern -n metallb-system -l app=metallb


# 장애 재연
## 리더 Speaker 파드가 존재하는 노드(실제는 컨테이너)를 중지
docker stop <svc1  리더 Speaker 파드가 존재하는 노드(실제는 컨테이너)> --signal 9
docker stop myk8s-worker --signal 9

# myk8s-worker 노드를 중지함


docker ps -a
docker ps -a | grep worker$

## 지속적으로 반복 접속 상태 모니터링
### curl 연속 접속 시도 >> 대략 10초 이내에 정상 접근 되었지만, 20초까지는 불안정하게 접속이 되었다
### 실제로는 다른 노드의 speaker 파드가 리더가 되고, 이후 다시 노드(컨테이너)가 정상화되면, 다시 리더 speaker 가 됨
docker exec -it mypc zsh -c "while true; do curl -s --connect-timeout 1 $SVC1EXIP | egrep 'Hostname|RemoteAddr'; date '+%Y-%m-%d %H:%M:%S' ; echo ;  sleep 1; done"

myk8s-worker 노드가 중지되면서 webpod1로 접속이 안되다가 약 10초 후 부터는 webpod2로만 접속이 됨.

# 변경된 리더 Speaker 파드 확인
# mypc/mypc2 에서 현재 SVC EXTERNAL-IP를 담당하는 리더 Speaker 파드 찾기
for i in $SVC1EXIP $SVC2EXIP $SVC3EXIP; do docker exec -it mypc ping -c 1 -w 1 -W 1 $i; done

# mypc/mypc2 에서 arp 테이블 정보 확인 >> SVC IP별로 리더 파드(스피커) 역할의 노드를 확인!
docker exec -it mypc ip -c neigh | sort
kubectl get node -owide # mac 주소에 매칭되는 IP(노드) 찾기


# 장애 원복(노드 정상화)
## 노드(실제 컨테이너) 정상화 
docker start <svc1  리더 Speaker 파드가 존재하는 노드(실제는 컨테이너)>
docker start myk8s-worker

# 변경된 리더 Speaker 파드 확인
# mypc에서 현재 SVC EXTERNAL-IP를 담당하는 리더 Speaker 파드 찾기
for i in $SVC1EXIP $SVC2EXIP $SVC3EXIP; do docker exec -it mypc ping -c 1 -w 1 -W 1 $i; done

# mypc에서 arp 테이블 정보 확인
docker exec -it mypc ip -c neigh | sort
kubectl get node -owide # mac 주소에 매칭되는 IP(노드) 찾기

mypc에서 arp 테이블 정보 확인 결과 기존 myk8s-woker 노드에 있던 리더 파드(스피커) 역할이 myk8s-worker3으로 옮겨진 것으로 확인된다.

3. IPVS

3.1 IPVS 개요

IPVS(Internet Protocol Virtual Server)는 Linux 커널에서 제공하는 고성능 로드 밸런싱 기술입니다. Kubernetes에서는 클러스터 내부에서 서비스 간의 로드 밸런싱을 더욱 효율적으로 수행하기 위해 kube-proxy의 구현 방식 중 하나로 IPVS를 사용할 수 있습니다. IPVS는 대규모 네트워크 트래픽을 처리하는 데 유리하며, 레이어 4(TCP/UDP)에서 동작하는 네트워크 트래픽의 부하를 분산하는 역할을 합니다.

  • 로드 밸런싱 동작 방식
    • IPVS는 클라이언트의 요청을 가상 IP(VIP, Virtual IP)를 통해 받아, 이를 백엔드 서버(Pods 또는 노드)로 분배합니다.
    • 여러 로드 밸런싱 알고리즘을 지원하여, 트래픽을 여러 파드나 노드로 효율적으로 분산할 수 있습니다.
    • Round-robin: 순서대로 요청을 백엔드 서버에 분배
    • Least connections: 가장 적은 연결을 가진 서버에 요청을 분배
    • Source hashing: 클라이언트의 IP 주소를 해싱하여 특정 서버에 요청을 보냄
    • 로드 밸런싱 작업을 커널 레벨에서 처리하기 때문에, 매우 빠르고 효율적인 성능을 제공합니다.

  • IPVS vs. iptables
    Kubernetes에서 기본적으로 kube-proxy는 iptables 기반으로 동작합니다. 하지만 IPVS는 성능과 확장성 면에서 iptables보다 더 우수한 특징을 가집니다.
    • iptables는 규칙 기반으로 패킷을 처리하는 방식이며, 많은 규칙이 쌓일수록 성능이 저하될 수 있습니다.
    • IPVS는 해시 테이블을 사용해 네트워크 패킷을 더 빠르게 처리할 수 있으며, 대규모의 서비스나 파드를 처리할 때 더 높은 성능을 발휘합니다.

  • IPVS의 장점
    • 성능 향상: iptables 기반보다 트래픽 처리가 훨씬 빠르며, 커널 레벨에서 로드 밸런싱을 수행하기 때문에 효율적입니다.
    • 다양한 로드 밸런싱 알고리즘 지원: 여러 로드 밸런싱 방식을 제공해 다양한 트래픽 분배 전략을 구현할 수 있습니다.
    • 스케일링: 많은 서비스 및 파드를 효율적으로 관리할 수 있어, 대규모 시스템에 적합합니다.
    • 상태 정보 유지: IPVS는 각 연결에 대해 상태 정보를 유지하기 때문에 연결이 지속되는 동안 요청을 같은 백엔드 서버로 보낼 수 있습니다.

  • Kubernetes에서의 IPVS 사용
    Kubernetes에서 kube-proxy는 IPVS 모드를 지원합니다. 이를 활성화하면 kube-proxy가 iptables 대신 IPVS를 사용하여 클러스터 내부에서 서비스 간 트래픽을 라우팅합니다.
    IPVS 모드를 활성화하려면 다음과 같이 kube-proxy 설정 파일에서 mode를 ipvs로 지정할 수 있습니다.

$kube-proxy --proxy-mode=ipvs

또한, IPVS를 사용하기 위해서는 Linux 커널에서 IPVS 관련 모듈들이 로드되어 있어야 합니다.

IPVS 모듈은 다음과 같습니다.
• ip_vs
• ip_vs_rr (round-robin 로드 밸런싱)
• ip_vs_wrr (weighted round-robin)
• ip_vs_sh (source hashing)

부하 분산 스케쥴링을 위한 kube-proxy 파라미터 설정은 아래와 같습니다.

$ kube-proxy --proxy-mode=ipvs --ipvs-scheduler=rr

IPVS가 지원하는 주요 스케줄링 알고리즘은 아래와 같습니다.

3.2 IPVS 설치 및 실행

3.2.1 실습 환경 및 설치(AWS EC2 환경)

실습 환경은 K8S v1.31.0 , CNI(Kindnet, Direct Routing mode) , IPVS proxy mode

  • 노드(실제로는 컨테이너) 네트워크 대역 : 172.18.0.0/16
  • 파드 사용 네트워크 대역 : 10.10.0.0/16 ⇒ 각각 10.10.1.0/24, 10.10.2.0/24, 10.10.3.0/24, 10.10.4.0/24
  • 서비스 사용 네트워크 대역 : 10.200.1.0/24


# 파일 작성
cat <<EOT> kind-svc-2w-ipvs.yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
featureGates:
  "InPlacePodVerticalScaling": true
  "MultiCIDRServiceAllocator": true
nodes:
- role: control-plane
  labels:
    mynode: control-plane
    topology.kubernetes.io/zone: ap-northeast-2a
  extraPortMappings:
  - containerPort: 30000
    hostPort: 30000
  - containerPort: 30001
    hostPort: 30001
  - containerPort: 30002
    hostPort: 30002
  - containerPort: 30003
    hostPort: 30003
  - containerPort: 30004
    hostPort: 30004
  kubeadmConfigPatches:
  - |
    kind: ClusterConfiguration
    apiServer:
      extraArgs:
        runtime-config: api/all=true
    controllerManager:
      extraArgs:
        bind-address: 0.0.0.0
    etcd:
      local:
        extraArgs:
          listen-metrics-urls: http://0.0.0.0:2381
    scheduler:
      extraArgs:
        bind-address: 0.0.0.0
  - |
    kind: KubeProxyConfiguration
    metricsBindAddress: 0.0.0.0
    ipvs:
      strictARP: true
- role: worker
  labels:
    mynode: worker1
    topology.kubernetes.io/zone: ap-northeast-2a
- role: worker
  labels:
    mynode: worker2
    topology.kubernetes.io/zone: ap-northeast-2b
- role: worker
  labels:
    mynode: worker3
    topology.kubernetes.io/zone: ap-northeast-2c
networking:
  podSubnet: 10.10.0.0/16
  serviceSubnet: 10.200.1.0/24
  kubeProxyMode: "ipvs"        
EOT

# k8s 클러스터 설치
kind create cluster --config kind-svc-2w-ipvs.yaml --name myk8s --image kindest/node:v1.31.0
docker ps

# 노드에 기본 툴 설치
docker exec -it myk8s-control-plane sh -c 'apt update && apt install tree psmisc lsof wget bsdmainutils bridge-utils net-tools dnsutils ipset ipvsadm nfacct tcpdump ngrep iputils-ping arping git vim arp-scan -y'
for i in worker worker2 worker3; do echo ">> node myk8s-$i <<"; docker exec -it myk8s-$i sh -c 'apt update && apt install tree psmisc lsof wget bsdmainutils bridge-utils net-tools dnsutils ipset ipvsadm nfacct tcpdump ngrep iputils-ping arping -y'; echo; done

# mypc 컨테이너 기동
docker run -d --rm --name mypc --network kind nicolaka/netshoot sleep infinity

# docker ps
CONTAINER ID   IMAGE                  COMMAND                  CREATED              STATUS              PORTS                                                             NAMES
b1be438f6aa6   nicolaka/netshoot      "sleep infinity"         About a minute ago   Up About a minute                                                                     mypc
e0fb8d275e9c   kindest/node:v1.31.0   "/usr/local/bin/entr…"   21 hours ago         Up 21 hours         0.0.0.0:30000-30004->30000-30004/tcp, 127.0.0.1:37877->6443/tcp   myk8s-control-plane
1ed7e45f8227   kindest/node:v1.31.0   "/usr/local/bin/entr…"   21 hours ago         Up 21 hours                                                                           myk8s-worker2
ac04f73d3f25   kindest/node:v1.31.0   "/usr/local/bin/entr…"   21 hours ago         Up 21 hours                                                                           myk8s-worker3
473c6e451a07   kindest/node:v1.31.0   "/usr/local/bin/entr…"   21 hours ago         Up 21 hours                                                                           myk8s-worker

3.2.2 파드 및 서비스 생성 및 확인

  • 목적지 파드 생성
cat <<EOT> 3pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: webpod1
  labels:
    app: webpod
spec:
  nodeName: myk8s-worker
  containers:
  - name: container
    image: traefik/whoami
  terminationGracePeriodSeconds: 0
---
apiVersion: v1
kind: Pod
metadata:
  name: webpod2
  labels:
    app: webpod
spec:
  nodeName: myk8s-worker2
  containers:
  - name: container
    image: traefik/whoami
  terminationGracePeriodSeconds: 0
---
apiVersion: v1
kind: Pod
metadata:
  name: webpod3
  labels:
    app: webpod
spec:
  nodeName: myk8s-worker3
  containers:
  - name: container
    image: traefik/whoami
  terminationGracePeriodSeconds: 0
EOT
  • 클라이언트 파드 생성
cat <<EOT> netpod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: net-pod
spec:
  nodeName: myk8s-control-plane
  containers:
  - name: netshoot-pod
    image: nicolaka/netshoot
    command: ["tail"]
    args: ["-f", "/dev/null"]
  terminationGracePeriodSeconds: 0
EOT
  • 서비스(clusterIP) 생성
cat <<EOT> svc-clusterip.yaml
apiVersion: v1
kind: Service
metadata:
  name: svc-clusterip
spec:
  ports:
    - name: svc-webport
      port: 9000        # 서비스 IP 에 접속 시 사용하는 포트 port 를 의미
      targetPort: 80    # 타킷 targetPort 는 서비스를 통해서 목적지 파드로 접속 시 해당 파드로 접속하는 포트를 의미
  selector:
    app: webpod         # 셀렉터 아래 app:webpod 레이블이 설정되어 있는 파드들은 해당 서비스에 연동됨
  type: ClusterIP       # 서비스 타입
EOT
  • 파드 및 서비스 배포 및 확인
kubectl apply -f 3pod.yaml,netpod.yaml,svc-clusterip.yaml

# 파드와 서비스 사용 네트워크 대역 정보 확인 
kubectl cluster-info dump | grep -m 2 -E "cluster-cidr|service-cluster-ip-range"
                            "--service-cluster-ip-range=10.200.1.0/24",
                            "--cluster-cidr=10.10.0.0/16",
                            
# 확인
$kubectl get pod -owide
NAME      READY   STATUS    RESTARTS   AGE   IP          NODE                  NOMINATED NODE   READINESS GATES
net-pod   1/1     Running   0          23m   10.10.0.5   myk8s-control-plane   <none>           <none>
webpod1   1/1     Running   0          23m   10.10.2.2   myk8s-worker          <none>           <none>
webpod2   1/1     Running   0          23m   10.10.4.2   myk8s-worker2         <none>           <none>
webpod3   1/1     Running   0          23m   10.10.1.2   myk8s-worker3         <none>           <none>

$kubectl get svc svc-clusterip
AME            TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)    AGE
svc-clusterip   ClusterIP   10.200.1.201   <none>        9000/TCP   23m

$kubectl describe svc svc-clusterip
Name:                     svc-clusterip
Namespace:                default
Labels:                   <none>
Annotations:              <none>
Selector:                 app=webpod
Type:                     ClusterIP
IP Family Policy:         SingleStack
IP Families:              IPv4
IP:                       10.200.1.201
IPs:                      10.200.1.201
Port:                     svc-webport  9000/TCP
TargetPort:               80/TCP
Endpoints:                10.10.2.2:80,10.10.1.2:80,10.10.4.2:80
Session Affinity:         None
Internal Traffic Policy:  Cluster
Events:                   <none>

$kubectl get endpoints svc-clusterip
NAME            ENDPOINTS                                AGE
svc-clusterip   10.10.1.2:80,10.10.2.2:80,10.10.4.2:80   24m

$kubectl get endpointslices -l kubernetes.io/service-name=svc-clusterip
NAME                  ADDRESSTYPE   PORTS   ENDPOINTS                       AGE
svc-clusterip-rbbms   IPv4          80      10.10.2.2,10.10.1.2,10.10.4.2   24m

# 변수 지정
CIP=$(kubectl get svc svc-clusterip -o jsonpath="{.spec.clusterIP}")
CPORT=$(kubectl get svc svc-clusterip -o jsonpath="{.spec.ports[0].port}")
echo $CIP $CPORT

# ipvsadm 툴로 부하분산 되는 정보 확인
## 10.200.1.216(TCP 9000) 인입 시 3곳의 목적지로 라운드로빈(rr)로 부하분산하여 전달됨을 확인 : 모든 노드에서 동일한 IPVS 분산 설정 정보 확인
## 3곳의 목적지는 각각 서비스에 연동된 목적지 파드 3개이며, 전달 시 출발지 IP는 마스커레이딩 변환 처리
$docker exec -it myk8s-control-plane ipvsadm -Ln -t $CIP:$CPORT

# ipvsadm 툴로 부하분산 되는 현재 연결 정보 확인 : 추가로 --rate 도 있음
docker exec -it myk8s-control-plane ipvsadm -Ln -t $CIP:$CPORT --stats
for i in control-plane worker worker2 worker3; do echo ">> node myk8s-$i <<"; docker exec -it myk8s-$i ipvsadm -Ln -t $CIP:$CPORT --stats ; echo; done

docker exec -it myk8s-control-plane ipvsadm -Ln -t $CIP:$CPORT --rate
for i in control-plane worker worker2 worker3; do echo ">> node myk8s-$i <<"; docker exec -it myk8s-$i ipvsadm -Ln -t $CIP:$CPORT --rate ; echo; done

# iptables 규칙 확인 : ipset list 를 활용
$docker exec -it myk8s-control-plane iptables -t nat -S | grep KUBE-CLUSTER-IP
-A KUBE-SERVICES ! -s 10.10.0.0/16 -m comment --comment "Kubernetes service cluster ip + port for masquerade purpose" -m set --match-set KUBE-CLUSTER-IP dst,dst -j KUBE-MARK-MASQ
-A KUBE-SERVICES -m set --match-set KUBE-CLUSTER-IP dst,dst -j ACCEPT

# ipset list 정보를 확인 : KUBE-CLUSTER-IP 이름은 아래 6개의 IP:Port 조합을 지칭
# 예를 들면 ipset list 를 사용하지 않을 경우 6개의 iptables 규칙이 필요하지만, ipset 사용 시 1개의 규칙으로 가능
$docker exec -it myk8s-control-plane ipset list KUBE-CLUSTER-IP
Name: KUBE-CLUSTER-IP
Type: hash:ip,port
Revision: 7
Header: family inet hashsize 1024 maxelem 65536 bucketsize 12 initval 0x25dc366b
Size in memory: 456
References: 3
Number of entries: 5
Members:
10.200.1.201,tcp:9000
10.200.1.10,tcp:53
10.200.1.10,tcp:9153
10.200.1.10,udp:53
10.200.1.1,tcp:443

3.2.3 IPVS 정보 확인 및 서비스 접속 확인

  • IPVS 정보 확인
# 변수 지정
CIP=$(kubectl get svc svc-clusterip -o jsonpath="{.spec.clusterIP}")
CPORT=$(kubectl get svc svc-clusterip -o jsonpath="{.spec.ports[0].port}")
echo $CIP $CPORT

$for i in control-plane worker worker2 worker3; do echo ">> node myk8s-$i <<"; docker exec -it myk8s-$i ipvsadm -Ln -t $CIP:$CPORT ; echo; done

부하 분산이 되도록 rr 설정과 3개의 파드로 구성되어 있는 것을 볼 수 있다.

  • 서비스 접속 확인
#하단 콘솔에서 컨트롤플레인 노드에서 ipvsadm 모니터링 실행
watch -d "docker exec -it myk8s-control-plane ipvsadm -Ln -t $CIP:$CPORT --stats; echo; docker exec -it myk8s-control-plane ipvsadm -Ln -t $CIP:$CPORT --rate"

#상단 콘솔에서 서비스 IP로 반복 접속하여 부하분산이 되는지 확인
$kubectl exec -it net-pod -- zsh -c "for i in {1..100};  do curl -s $SVC1:9000 | grep Hostname; done | sort | uniq -c | sort -nr"

클라이언트에서 서비스 IP로 100번을 반복접속을 시도하였고 약 1/3 비율로 균등하게 3개의 pod로 분산되어 접속되는 것을 확인 할 수 있습니다.

0개의 댓글