[KANS] 3기 5주 – LoadBalancer(MetalLB), IPVS

CloudNet@팀에서 진행하는 쿠버네티스 네트워크 스터디 3기 참가글입니다.

‘쿠버네티스’ 네트워크에 대해서 직접 실습을 통해 장애 시 해결 능력을 갖추고, 학습 내용을 전파 및 공유합니다!


1. LoadBalancer

외부 로드 밸런서를 사용하여 서비스를 외부에 노출합니다. Kubernetes는 로드 밸런싱 구성 요소를 직접 제공하지 않으므로 구성 요소를 제공해야 하거나 Kubernetes 클러스터를 클라우드 제공업체와 통합할 수 있습니다.

Exposes the Service externally using an external load balancer. Kubernetes does not directly offer a load balancing component; you must provide one, or you can integrate your Kubernetes cluster with a cloud provider.

Service Type, k8s doc

k8s 상에서 어플리케이션을 외부로 노출하기 위해서는, LoadBalancer 타입이 일반적으로 사용된다.

k8s 이전 로드 발란서를 생각해보면, 별도의 물리장비를 사용하는 하드웨어 기반 로드발란서와, 서버내 nginx 같은 패키지를 사용하는 소프트웨어 기반 로드발란서가 있다. 이와 유사하게, k8s에서 또한 LoadBalancer 타입의 서비스를 구현할 때 소프트웨어 기반/ 하드웨어 장비 기반으로 나눌 수 있다. 그러나 k8s 클러스터가 구성되는 환경이 클라우드 기반인가/온프레미스 기반인지에 따라 상세 구현 방식에는 차이가 있다.

AWS와 같은 클라우드 환경 기반의 k8s 클러스터인 경우, LoadBalancer 는 AWS 관리형 LoadBalancer 서비스인 ELB(Elastic Load Balancing)을 이용해 하드웨어 기반의 로드발란서를 기반으로 작동하도록 설정 할 수 있다.(물론 ALB가 하드웨어 로드발란서냐.. 하는 문제는 있지만 여기서는 제외) 온프레미스 환경 기반의 k8s 클러스터의 경우 대표적으로 Citrix나 F5 네트워크와 같은 벤더사의 제품을 활용한 하드웨어 기반 로드발란서나, MetalLB, OpenELB, PorterLB, kube-vip, LoxiLB등과 같은 패키지를 사용한 소프트웨어 기반의 로드발란서를 구성할 수도 있다. 이러한 소프트웨어 기반의 LB는 클라우드환경의 클러스터에서도 동일하게 구성할 수 있다.

1.1. LoadBalancer Type 통신 흐름

요약:

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


1.2. 부하분산

노드에 파드가 없을 경우 ‘로드밸런서’에서 노드에 헬스 체크(상태 검사)가 실패하여 해당 노드로는 외부 요청 트래픽을 전달하지 않는다.

아래의 예시에서 Node 3의 경우 파드가 없어 ELB의 헬스체크에 응답하지 않기 때문에, 해당 노드로 트래픽이 흐르지 않는다.


1.3. LoadBalancer 유형의 단점

위와 같은 LoadBalancer 유형의 경우, 아래와 같은 단점이 있다.

  • 서비스(LoadBalancer) 생성 시 마다 LB(예 AWS NLB)가 생성되어 자원 활용이 비효율적임
  • 서비스(LoadBalancer)는 HTTP/HTTPS 처리에 일부 부족함(TLS 종료, 도메인 기반 라우팅 등)
  • 온프레미스 환경에서 제공의 어려움

위의 단점에 대해, 추후 소개될 k8s Ingress 오브젝트를 사용하면 효율적인 활용을 꾀할 수 있다. 또한 물리장비가 없는 환경에서도, MetalLB 혹은 OpenELB(구 PorterLB) 등의 도구를 사용해 소프트웨어 기반 로드밸런서를 구성해 사용할 수 있다.

2. MetalLB

MetalLB는 표준 라우팅 프로토콜을 사용하여 베어메탈 쿠버네티스 클러스터를 위한 로드 밸런서 구현체입니다.

MetalLB is a load-balancer implementation for bare metal Kubernetes clusters, using standard routing protocols.

MetalLB, docs


MetalLB는 BareMetalLoadBalancer의 약자로, 클라우드 환경에서 일반적으로 제공되는 로드밸런서 기능을 베어메탈 환경에서도 구현하도록 지원하는 프로젝트이다. 현재 beta 상태이며, CNCF 재단에는 210914 합류하여 현재 Sandbox 성숙도 상태이다.


Why? > 온프레미스 환경에서도 ‘걍’ 작동하는 네트워크 구현을 위해

native k8s에서는 네트워크 로드밸런서의 구현체(LoadBalancer 유형의 서비스 리소스)를 제공하지 않는다. k8s에서 제공하는 구현체 코드는 모두 GCP, AWS, Azure 등과 같은 IaaS 플랫폼과 호환을 위한 코드이며, 해당 IaaS 플랫폼이 아닌 환경의 경우 생성된 LoadBalancer 유형의 서비스 오브젝트는 영원한 pending 상태로 남는다.

Bare-metal(온프레미스) 환경의 운영자는 외부 트래픽을 유입하기위해 NodePort/externalIPs와 같은 다른 서비스 유형을 사용할 수 있지만, 이 두 옵션 모두 운영 환경에 사용하기엔 상당한 단점이 존재한다.(아마 노드의 퍼블릭노출?) 이로 인해 베어메탈 기반의 클러스터는 k8s 생태계에서 주류가 아닌 2차적인 지위로 밀려난다.

MetalLB 프로젝트는 표준 네트워크 장비와 통합되는 네트워크 로드 밸런서 구현을 제공하여 이 불균형을 해소하고, 온프레미스환경에서의 외부 트래픽 또한 가능한 “걍” 작동하는것을 목표로 한다.

Concepts > 주소 할당, 외부 전파(ARP, NDP, or BGP 프로토콜)

위에서 설명한바처럼 metalLB는 온프레미스환경의 클러스터네 네트워크 로드밸런서의 구현기능을 제공한다. 이를 위해 주소 할당(address allocation), 외부 전파(external announcement) 2개의 기능이 필요하다.

  • 주소 할당(address allocation)
    • 클라우드 > 플랫폼이 / 베어메탈 > MetalLB가 IP주소 할당
    • 사용할 IP 대역(ipaddresspools) 사전 생성 필요
      • MetalLB는 설정된 대역에서 서비스 생성/제거 시 개별 주소 할당 또는 해제
    • colocation(상면임대?)의 경우 할당받은 퍼블릭 IP대역에서 사용
    • 사설망으로 사용하려면, 사설 IP대역(RFC1918)의 IP범위 할당
    • 또는 둘다! > 사용가능한 ipaddresspools 의 등록이 중요하지 종류는 상관 X
  • 외부 전파(external announcement)
    • IP 주소가 서비스 리소스에 할당된 후, 해당 IP 주소가 등록되었음을 알림
    • 모드에 따라 ARP, NDP, or BGP 같은 표준 라우팅 프로토콜 사용
    • Layer 2 mode (ARP/NDP)
      • L2 모드에서는 개별 머신(노드?)가 서비스의 소유권을 점유
      • 표준 주소 발견 프로토콜(ARP for IPv4, NDP for IPv6) 사용
      • LAN의 관점에서, 해당 노드는 여러개의 IP를 가진것처럼 보임
    • BGP mode (BGP)
      • 클러스터의 모든 머신(노드?)가 가까운 라우터와 BGP Peering 세션 수립
      • 라우터에서 어떻게 트래픽을 전달(forward)할지 결정
      • 다수 노드간 진정한 로드밸런싱 / BGP 정책 메커니즘을 통한 세밀한 트래픽 제어 가능


MetalLB software components > controller(deploy) / speaker(daemonset)

MetalLB Operator를 사용해 MetalLB 인스턴스를 시작하면, kind: deployment의 controller와 kind: daemonset의 speaker를 생성한다.

  • controller
    • deployment 유형의 단일 파드 생성
    • 새로운 LoadBalancer 유형의 서비스 생성/제거 시 ipaddresspools에서 적절한 IP 할당/해제
  • speaker
    • daemonset 유형의 speaker 파드들을 각 노드에 생성
    • controller가 IP주소를 LoadBalancer service 리소스에 할당한 뒤,
    • L2 모드) 특정 speaker pod가 할당된 LoadBalancer IP announce
      • IPv4 환경에선 Address Resolution Protocol (ARP) 사용
      • IPv6 환경에선 Neighbor Discovery Protocol (NDP) 사용
    • BPG 모드) 각 speaker pod는 BGP Peer를 통해 할당된 LoadBalancer IP announce
    • LoadBalancer IP로의 요청이 해당 IP를 announce 하는 speaker의 노드에 라우팅
    • 노드가 패킷 수신 후, service에 등록된 endpoint(백엔드파드)로 패킷 전달
    • 같은 노드에 endpoint가 있는것이 이상적

실습 환경 생성
AWS EC2 based Kind cluster
K8S v1.31.0 , CNI(Kindnet, Direct Routing mode) , IPTABLES proxy mode

  • AWS EC2 생성
# YAML 파일 다운로드
curl -O https://s3.ap-northeast-2.amazonaws.com/cloudformation.cloudneta.net/kans/kans-5w.yaml

# CloudFormation 스택 배포
# aws cloudformation deploy --template-file kans-5w.yaml --stack-name mylab --parameter-overrides KeyName=<My SSH Keyname> SgIngressSshCidr=<My Home Public IP Address>/32 --region ap-northeast-2
예시) aws cloudformation deploy --template-file kans-5w.yaml --stack-name mylab --parameter-overrides KeyName=nasir-personal SgIngressSshCidr=$(curl -s ipinfo.io/ip)/32 --region ap-northeast-2

# [모니터링] CloudFormation 스택 상태 : 생성 확인
while true; do 
  date
  AWS_PAGER="" aws cloudformation list-stacks \
    --stack-status-filter CREATE_IN_PROGRESS CREATE_COMPLETE CREATE_FAILED DELETE_IN_PROGRESS DELETE_FAILED \
    --query "StackSummaries[*].{StackName:StackName, StackStatus:StackStatus}" \
    --output table
  sleep 1
done

# CloudFormation 스택 배포 완료 후 작업용 EC2 IP 출력
aws cloudformation describe-stacks --stack-name mylab --query 'Stacks[*].Outputs[0].OutputValue' --output text --region ap-northeast-2

# EC2 SSH 접속
ssh -i ~/.ssh/kp-gasida.pem ubuntu@$(aws cloudformation describe-stacks --stack-name mylab --query 'Stacks[*].Outputs[0].OutputValue' --output text --region ap-northeast-2)
  • kind cluster 생성
#
cat <<EOT> kind-svc-2w.yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
featureGates:
  "InPlacePodVerticalScaling": true  #실행 중인 파드의 리소스 요청 및 제한을 변경할 수 있게 합니다.
  "MultiCIDRServiceAllocator": true  #서비스에 대해 여러 CIDR 블록을 사용할 수 있게 합니다.
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:  #API 서버에 추가 인수를 제공
        runtime-config: api/all=true  #모든 API 버전을 활성화
    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
- 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  #파드 IP를 위한 CIDR 범위를 정의합니다. 파드는 이 범위에서 IP를 할당받습니다.
  serviceSubnet: 10.200.1.0/24  #서비스 IP를 위한 CIDR 범위를 정의합니다. 서비스는 이 범위에서 IP를 할당받습니다.
EOT

# k8s 클러스터 설치
kind create cluster --config kind-svc-2w.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


# k8s v1.31.0 버전 확인
kubectl get node

# 노드 labels 확인
kubectl get nodes -o jsonpath="{.items[*].metadata.labels}" | jq

# kind network 중 컨테이너(노드) IP(대역) 확인
docker ps -q | xargs docker inspect --format '{{.Name}} {{.NetworkSettings.Networks.kind.IPAddress}}'

# 파드CIDR 과 Service 대역 확인 : CNI는 kindnet 사용
kubectl get cm -n kube-system kubeadm-config -oyaml | grep -i subnet
kubectl cluster-info dump | grep -m 2 -E "cluster-cidr|service-cluster-ip-range"

# MultiCIDRServiceAllocator : https://kubernetes.io/docs/tasks/network/extend-service-ip-ranges/
kubectl get servicecidr
NAME         CIDRS           AGE
kubernetes   10.200.1.0/24   2m13s

# 노드마다 할당된 dedicated subnet (podCIDR) 확인
kubectl get nodes -o jsonpath="{.items[*].spec.podCIDR}"
10.10.0.0/24 10.10.4.0/24 10.10.3.0/24 10.10.1.0/24

# kube-proxy configmap 확인
kubectl describe cm -n kube-system kube-proxy
...
mode: iptables
iptables:
  localhostNodePorts: null
  masqueradeAll: false
  masqueradeBit: null
  minSyncPeriod: 1s
  syncPeriod: 0s
...


# 노드 별 네트워트 정보 확인 : CNI는 kindnet 사용
for i in control-plane worker worker2 worker3; do echo ">> node myk8s-$i <<"; docker exec -it myk8s-$i cat /etc/cni/net.d/10-kindnet.conflist; echo; done
for i in control-plane worker worker2 worker3; do echo ">> node myk8s-$i <<"; docker exec -it myk8s-$i ip -c route; echo; done
for i in control-plane worker worker2 worker3; do echo ">> node myk8s-$i <<"; docker exec -it myk8s-$i ip -c addr; echo; done

# iptables 정보 확인
for i in filter nat mangle raw ; do echo ">> IPTables Type : $i <<"; docker exec -it myk8s-control-plane  iptables -t $i -S ; echo; done
for i in filter nat mangle raw ; do echo ">> IPTables Type : $i <<"; docker exec -it myk8s-worker  iptables -t $i -S ; echo; done
for i in filter nat mangle raw ; do echo ">> IPTables Type : $i <<"; docker exec -it myk8s-worker2 iptables -t $i -S ; echo; done
for i in filter nat mangle raw ; do echo ">> IPTables Type : $i <<"; docker exec -it myk8s-worker3 iptables -t $i -S ; echo; done

# 각 노드 bash 접속
docker exec -it myk8s-control-plane bash
docker exec -it myk8s-worker bash
docker exec -it myk8s-worker2 bash
docker exec -it myk8s-worker3 bash
----------------------------------------
exit
----------------------------------------

# kind 설치 시 kind 이름의 도커 브리지가 생성된다 : 172.18.0.0/16 대역
docker network ls
docker inspect kind

# arp scan 해두기
docker exec -it myk8s-control-plane arp-scan --interfac=eth0 --localnet

# mypc 컨테이너 기동 : kind 도커 브리지를 사용하고, 컨테이너 IP를 지정 없이 혹은 지정 해서 사용
docker run -d --rm --name mypc --network kind --ip 172.18.0.100 nicolaka/netshoot sleep infinity # IP 지정 실행 시
IP 지정 실행 시 에러 발생 시 아래 처럼 IP 지정 없이 실행
docker run -d --rm --name mypc --network kind nicolaka/netshoot sleep infinity # IP 지정 없이 실행 시
docker ps

# mypc2 컨테이너 기동 : kind 도커 브리지를 사용하고, 컨테이너 IP를 지정 없이 혹은 지정 해서 사용
docker run -d --rm --name mypc2 --network kind --ip 172.18.0.200 nicolaka/netshoot sleep infinity # IP 지정 실행 시
IP 지정 실행 시 에러 발생 시 아래 처럼 IP 지정 없이 실행
docker run -d --rm --name mypc2 --network kind nicolaka/netshoot sleep infinity # IP 지정 없이 실행 시
docker ps

# kind network 중 컨테이너(노드) IP(대역) 확인
docker ps -q | xargs docker inspect --format '{{.Name}} {{.NetworkSettings.Networks.kind.IPAddress}}'


# kube-ops-view 설치
helm repo add geek-cookbook https://geek-cookbook.github.io/charts/
helm install kube-ops-view geek-cookbook/kube-ops-view --version 1.2.2 --set service.main.type=NodePort,service.main.ports.http.nodePort=30000 --set env.TZ="Asia/Seoul" --namespace kube-system

# myk8s-control-plane 배치
kubectl -n kube-system edit deploy kube-ops-view
---
spec:
  ...
  template:
    ...
    spec:
      nodeSelector:
        mynode: control-plane
      tolerations:
      - key: "node-role.kubernetes.io/control-plane"
        operator: "Equal"
        effect: "NoSchedule"
---

# 설치 확인
kubectl -n kube-system get pod -o wide -l app.kubernetes.io/instance=kube-ops-view

# kube-ops-view 접속 URL 확인 (1.5 , 2 배율) : macOS 사용자
echo -e "KUBE-OPS-VIEW URL = http://localhost:30000/#scale=1.5"
echo -e "KUBE-OPS-VIEW URL = http://localhost:30000/#scale=2"

# kube-ops-view 접속 URL 확인 (1.5 , 2 배율) : Windows 사용자
echo -e "KUBE-OPS-VIEW URL = http://192.168.50.10:30000/#scale=1.5"
echo -e "KUBE-OPS-VIEW URL = http://192.168.50.10:30000/#scale=2"

# kube-ops-view 접속 URL 확인 (1.5 , 2 배율) : AWS_EC2 사용자
echo -e "KUBE-OPS-VIEW URL = http://$(curl -s ipinfo.io/ip):30000/#scale=1.5"
echo -e "KUBE-OPS-VIEW URL = http://$(curl -s ipinfo.io/ip):30000/#scale=2"

기본 정보 확인, 파드 생성 및 접속 확인

  • 기본 정보 확인
# 파드와 서비스 사용 가능 네트워크 대역
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",

# kube-proxy 모드 확인 : iptables proxy 모드
kubectl describe  configmap -n kube-system kube-proxy | grep mode
mode: "iptables"

# iptables 정보 확인
for i in filter nat mangle raw ; do echo ">> IPTables Type : $i <<"; docker exec -it myk8s-control-plane  iptables -t $i -S ; echo; done
for i in filter nat mangle raw ; do echo ">> IPTables Type : $i <<"; docker exec -it myk8s-worker  iptables -t $i -S ; echo; done
for i in filter nat mangle raw ; do echo ">> IPTables Type : $i <<"; docker exec -it myk8s-worker2 iptables -t $i -S ; echo; done
for i in filter nat mangle raw ; do echo ">> IPTables Type : $i <<"; docker exec -it myk8s-worker3 iptables -t $i -S ; echo; done
  • 파드 생성
cat <<EOF | kubectl apply -f -
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
EOF
  • 파드 접속 확인
# 파드 정보 확인
kubectl get pod -owide

# 파드 IP주소를 변수에 지정
WPOD1=$(kubectl get pod webpod1 -o jsonpath="{.status.podIP}")
WPOD2=$(kubectl get pod webpod2 -o jsonpath="{.status.podIP}")
echo $WPOD1 $WPOD2

# 접속 확인
docker exec -it myk8s-control-plane  ping -i 1 -W 1 -c 1 $WPOD1
docker exec -it myk8s-control-plane  ping -i 1 -W 1 -c 1 $WPOD2
docker exec -it myk8s-control-plane  curl -s --connect-timeout 1 $WPOD1 | grep Hostname
docker exec -it myk8s-control-plane  curl -s --connect-timeout 1 $WPOD2 | grep Hostname
docker exec -it myk8s-control-plane  curl -s --connect-timeout 1 $WPOD1 | egrep 'Hostname|RemoteAddr|Host:'
docker exec -it myk8s-control-plane  curl -s --connect-timeout 1 $WPOD2 | egrep 'Hostname|RemoteAddr|Host:'

2.1. Layer 2 모드 (ARP/NDP)

모드 소개

  • 리더 파드가 선출되고
  • 해당 리더 파드가 생성된 노드로만 트래픽이 인입되어
  • 해당 노드에서 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)도 있지만 사용하지 않은 이유 – 링크
    • 클라이언트 관점에서는 memberlist/Keepalived 는 동일하게 보임
      • 장애 시 서비스IP가 한 노드에서 다른 노드 이동
      • 평상 시 노드에 하나이상의 IP가 할당 되어있음
    • VRRP 프로토콜의 제한점
      • VRRP에선 네트워크당 255개의 로드밸런서 제한 존재
        • memberlist에선 대역대의 모든 IP 사용 가능
      • Virtual Router ID가 필요
        • memberlist에선 해당 제한이 없어 구성 간편
    • memberlist의 단점
      • 결국 VRRP는 아니기 때문에 타 VRRP 라우터와 호환 불가
      • metalLB는 로드밸런싱과 장애조치에 관심 / 3자 LB 호환성은 관심X

L2 모드 제한 사항 > single-node bottlenecking, potentially slow failover

  • single-node bottlenecking
    • 서비스 1개 생성 사용 시, 모든 서비스 접근 트래픽은 리더 파드가 존재하는 노드로만 인입되어서 부하가 집중
    • > 공식 가이드는 아니지만, 외부 라우터에서 접근 시 ECMP 로 부하 분산이 가능
  • potentially slow failover
    • 리더(노드)가 장애 시 나머지 노드 리더가 선출되고, ARP 전파 및 갱신완료 전까지는 장애가 발생됨 (대략 10초~20초 정도)
    • > 공식 가이드는 아니지만, 외부 라우터에서 노드(파드) 장애 헬스 체크로 빠르게 절체 가능


MetalLB concepts for layer2 mode,
Chapter 26. Load balancing with MetalLB, Red Hat

  • 개략적인 내용은 위와 동일
  • IP를 통해 외부에서 노드(speaker)까지는 metalLB의 영역, service에서 endpoint까지는 CNI의 영역
    • In layer 2 mode, all traffic for a service IP address is routed through one node. After traffic enters the node, the service proxy for the CNI network provider distributes the traffic to all the pods for the service.
  • 엄밀히 말하면 L2 mode에는 모든 트래픽이 특정 단일 노드로 인입하기 때문에 로드밸런싱 기능은 구현되지 않았다. 대신 자동화된 failover 매커니즘이 있을뿐
    • Because all traffic for a service enters through a single node in layer 2 mode, in a strict sense, MetalLB does not implement a load balancer for layer 2. Rather, MetalLB implements a failover mechanism for layer 2 so that when a speaker pod becomes unavailable, a speaker pod on a different node can announce the service IP address.


  • 어플리케이션은 클러스터에서
    • 내부적으로 CNI가 172.130.0.0/16대역에서 지정한 IP로 접근가능
    • 외부적으로 metalLB가 ipaddresspools에서 지정한 192.168.100.200로 접근가능
  • Node 1, 3에는 어플리케이션을 위한 Pod 존재
  • speaker daemonset은 각 노드에서 실행 중
  • 각 speaker pod는 host-network 파드; pod의 IP는 호스트 노드의 IP와 동일
  • 노드1의 speaker pod는 ARP를 통해 192.168.100.200 externalIP announce
    • externalIP를 announce하는 speaker 파드는 반드시? 백엔드파드와 동일하여야함
  • 클라이언트의 트래픽은 host network(192.168.100.0/24)를 거쳐 192.168.100.200와 연결, 트래픽이 노드에 도달한 이후, 서비스 프록시는 설정된 externalTrafficPolicy에 따라 트래픽을 같은/다른 노드의 백엔드 파드로 전달 
    • externalTrafficPolicy:cluster(디폴트) > speaker pod 가 가동중인 노드중에서, 하나의 노드만 192.168.100.200를 광고하도록 결정, 해당 노드만 서비스에 대한 트래픽 수신
    • externalTrafficPolicy:local > speaker pod 가 가동중이면서, 서비스에 대한 endpoint(application-pod)가 하나라도 존재하는 노드 중에서, 하나의 노드만 192.168.100.200를 광고하도록 결정 > 위의 그림에서는 노드1,3이 가능
  • 만약 (결정된) 노드1 장애시, externalIP 주소는 다른노드로 장애 복구 동작 수행, application-pod와 service-endpoint가 있는 노드의 speaker 파드가 선정됨, > 위의 그림에서는 노드3만 가능

2.1.1. L2모드 MetalLB 구성

설치 방법 지원 : Kubernetes manifests, using Kustomize, or using Helm
> Kubernetes manifests 사용

L2모드 MetalLB 설치

  • manifest 설치
# Kubernetes manifests 로 설치
kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/refs/heads/main/config/manifests/metallb-native-prometheus.yaml
## 혹은 프로메테우스 미설치시 아래 manifests 로 설치
kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/refs/heads/main/config/manifests/metallb-native.yaml


# metallb crd 확인
kubectl get crd | grep metallb
bfdprofiles.metallb.io                      2024-09-28T15:24:06Z
bgpadvertisements.metallb.io                2024-09-28T15:24:06Z
bgppeers.metallb.io                         2024-09-28T15:24:06Z
communities.metallb.io                      2024-09-28T15:24:06Z
ipaddresspools.metallb.io                   2024-09-28T15:24:06Z
l2advertisements.metallb.io                 2024-09-28T15:24:06Z
servicel2statuses.metallb.io                2024-09-28T15:24:06Z

# 생성된 리소스 확인 : metallb-system 네임스페이스 생성, 파드(컨트롤러, 스피커) 생성, RBAC(서비스/파드/컨피그맵 조회 등등 권한들), SA 등
kubectl get-all -n metallb-system # kubectl krew 플러그인 get-all 설치 후 사용 가능
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}"

## metallb 컨트롤러는 디플로이먼트로 배포됨
kubectl get ds,deploy -n metallb-system

## 데몬셋으로 배포되는 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-2dvbm   2/2     Running   0          9m17s   10.10.2.6    myk8s-worker3         <none>           <none>
speaker-jfvh9                 2/2     Running   0          9m17s   172.18.0.2   myk8s-worker          <none>           <none>
speaker-l2tdn                 2/2     Running   0          9m17s   172.18.0.5   myk8s-worker3         <none>           <none>
speaker-pzs8z                 2/2     Running   0          9m17s   172.18.0.3   myk8s-worker2         <none>           <none>
speaker-vfsdj                 2/2     Running   0          9m17s   172.18.0.4   myk8s-control-plane   <none>           <none>

## metallb 데몬셋으로 배포되는 스피커 파드는 hostNetwork 를 사용함
kubectl get ds -n metallb-system -o yaml
...
          ports:
          - containerPort: 7472
            name: monitoring
            protocol: TCP
          - containerPort: 7946
            name: memberlist-tcp
            protocol: TCP
          - containerPort: 7946
            name: memberlist-udp
            protocol: UDP
				...
        hostNetwork: true
...

## 스피커 파드의 Ports 정보 확인 : Host Ports 같이 확인
kubectl describe pod -n metallb-system -l component=speaker| grep Ports:
    Ports:         7472/TCP, 7946/TCP, 7946/UDP
    Host Ports:    7472/TCP, 7946/TCP, 7946/UDP


# (참고) 상세 정보 확인
kubectl get sa,cm,secret -n metallb-system
kubectl describe role -n metallb-system
kubectl describe deploy controller -n metallb-system
kubectl describe ds speaker -n metallb-system
  • configmap 생성 > 모드 및 서비스 대역 지정
    • 서비스(External-IP) 대역을 노드가 속한 eth0의 대역이 아니여도 상관없다! → 다만 이 경우 GW 역할의 라우터에서 노드들로 라우팅 경로 지정 필요
# kind 설치 시 kind 이름의 도커 브리지가 생성된다 : 172.18.0.0/16 대역
docker network ls
docker inspect kind
# kind network 중 컨테이너(노드) IP(대역) 확인 : 172.18.0.2~ 부터 할당되며, control-plane 이 꼭 172.18.0.2가 안될 수 도 있음
docker ps -q | xargs docker inspect --format '{{.Name}} {{.NetworkSettings.Networks.kind.IPAddress}}'

# IPAddressPool 생성 : LoadBalancer External IP로 사용할 IP 대역
## MetalLB는 서비스를 위한 외부 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 사용 허용
## Kubernetes 클러스터 내의 서비스가 외부 네트워크에 IP 주소를 광고하는 방식을 정의

kubectl explain l2advertisements.metallb.io

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"]    

서비스 생성 및 확인

  • 서비스(LoadBalancer 타입) 생성
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
  • 서비스 확인 및 리더 Speaker 파드 확인
# arp scan 해두기
docker exec -it myk8s-control-plane arp-scan --interfac=eth0 --localnet

# 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        121m
service/svc1         LoadBalancer   10.200.1.69    172.18.255.200   80:30485/TCP   3m37s
service/svc2         LoadBalancer   10.200.1.218   172.18.255.201   80:31046/TCP   3m37s
service/svc3         LoadBalancer   10.200.1.81    172.18.255.202   80:30459/TCP   3m37s

NAME                   ENDPOINTS                   AGE
endpoints/kubernetes   172.18.0.5:6443             31m
endpoints/svc1         10.10.1.6:80,10.10.3.6:80   8m4s
endpoints/svc2         10.10.1.6:80,10.10.3.6:80   8m4s
endpoints/svc3         10.10.1.6:80,10.10.3.6:80   8m4s

# LoadBalancer 타입은 기본적으로 NodePort를 포함 사용. NodePort는 ClusterIP를 포함 사용.
## 클라우드사업자 LB Type이나 온프레미스환경 HW LB Type 경우 LB 사용 시 NodePort 미사용 설정 가능
kubectl describe svc svc1

## 아래 처럼 LB VIP 별로 이던 speaker 배포된 노드가 리더 역할을 하는지 확인 가능
kubectl describe svc | grep Events: -A5
...
Events:
  Type    Reason        Age   From                Message
  ----    ------        ----  ----                -------
  Normal  IPAllocated   40m   metallb-controller  Assigned IP ["172.18.255.201"]
  Normal  nodeAssigned  40m   metallb-speaker     announcing from node "myk8s-worker" with protocol "layer2"
...

kubectl get svc svc1 -o json | jq
...
  "spec": {
    "allocateLoadBalancerNodePorts": true,
  ...
  "status": {
    "loadBalancer": {
      "ingress": [
        {
          "ip": "172.18.255.202",
          "ipMode": "VIP" # https://kubernetes.io/blog/2023/12/18/kubernetes-1-29-feature-loadbalancer-ip-mode-alpha/
        }                 # https://kubernetes.io/docs/concepts/services-networking/service/#load-balancer-ip-mode


# 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 툴 사용
## Unicast reply from 172.18.255.200: 해당 IP 주소에서 응답을 받았음을 의미합니다. 
## Sent 1 probes (1 broadcast(s)): 하나의 ARP 요청을 보냈고, 브로드캐스트 방식으로 요청을 전송했음을 나타냅니다.
## Received 1 response(s): 하나의 응답을 수신했음을 나타냅니다.
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
docker exec -it mypc ip -c neigh

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 DELAY 
172.18.255.200 dev eth0 lladdr 02:42:ac:12:00:04 STALE 
172.18.255.201 dev eth0 lladdr 02:42:ac:12:00:04 STALE 
172.18.255.202 dev eth0 lladdr 02:42:ac:12:00:02 STALE 

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  # 매칭 사용 가능

결과

  • nodeIP와 speaker daemonset IP 동일 확인
  • speaker 파드의 Ports=Host Ports 확인
  • 생성된 ipaddresspools, l2advertisements 확인
  • LoadBalancer 서비스 생성 확인 > EXTERNAL-IP 정상 부여
  • 각 svc 별로 할당된 리더노드 확인
  • 각 svc에 대해 arping
    • from 172.18.0.6 (mypc)가
    • 각 svc의 IP(172.18.255.20x)에 대해 ARPING시
    • 각 svc의 IP [MACaddr] 반환
    • mypc에서 각 svc, nodeIP에 등록된 MAC addr 확인 > 동일
    • 각 node IP와 각 서비스의 리더 speaker IP 확인 > 동일

2.1.2. 서비스 접속 테스트

이제 드디어 k8s 클러스터 외부에서 노드의 IP를 감추고(?) VIP/Port 를 통해 k8s 클러스터 내부의 애플리케이션에 접속 할 수 있다!

클라이언트(mypc, mypc2) → 서비스(External-IP) 접속 테스트

  • 클라이언트(mypc, mypc2) → 서비스(External-IP) 접속 테스트
# 현재 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.82   172.18.255.202   80:30246/TCP   49m

# 컨트롤노드에서 각각 접속 확인 실행 해보자
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

결과

  • svc의 externalIP를 통해 통신 가능
  • RemoteAddr은 노드의 ip가 찍힘 확인
  • LoadBalancer 타입의 경우, NodePort 및 ClusterIP 포함 확인

2.1.3. Failover 테스트

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

  • 노드1의 speaker 파드가 SVC1, SVC3의 리더 스피커 역할 수행 중
  • 노드1 down
  • 리더파드 재선출 > 노드2의 스피커 파드로 리더 변경 후 GARP 전파
    • 변경 완료까지 약 20초~1분 정도의 장애 지속

L2mode failover

  • 사전 준비
# 사전 준비
## 지속적으로 반복 접속
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
혹은
docker stop <svc1 번 리더 Speaker 파드가 존재하는 노드(실제는 컨테이너)> --signal 15
docker stop myk8s-worker --signal 15

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"
Hostname: webpod1
RemoteAddr: 172.18.0.2:25432
2024-09-29 06:31:07

2024-09-29 06:31:09

Hostname: webpod2
RemoteAddr: 172.18.0.2:26011
2024-09-29 06:31:10

2024-09-29 06:31:12

2024-09-29 06:31:14
...

# 변경된 리더 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/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(노드) 찾기

결과

  • node1(svc1) down 후 잠시 끊겼다가 이어지는 모습
  • arping으로 조회시 svc1의 리더 speaker가 노드2로 변경되었음 확인
  • k describe svc svc1 의 Events 기록에도 변경됨 확인
  • node1 복구 시 svc1의 리더 speaker가 노드1로 원복됨

2.2. BGP 모드 (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) 기준으로 동작합니다.
    • 물론 라우터 장비에 따라 다양한 라우팅(분산) 처리가 가능합니다


BGP 모드 제한사항 > 라우터에서 서비스로 인입이 되기 때문에, 라우터의 관련 설정이 중요한 만큼 네트워크팀과 협업을 적극 권장

  • 노드(speaker) 파드 장애 시 BGP Timer 설정 등 구성하고 있는 네트워크 환경에 맞게 최적화 작업이 필요
  • ECMP 부하 분산 접속 시 특정 파드에 몰리거나 혹은 세션 고정, flapping 등 다양한 환경에 대응이 필요
  • BGP 라우팅 설정 및 라우팅 전파 관련 최적화 설정이 필요


MetalLB concepts for BGP mode,
Chapter 26. Load balancing with MetalLB,
Red Hat
  • 개략적인 내용은 위와 동일
  • 각 speaker pod는 BGP Peer를 통해 라우터와 연결되어있음, 라우터가 LoadBalancer IP에 대한 트래픽을 수신시 해당 IP를 advertise한 speaker 중 하나를 선택하여 전달
  • 매번 라우터가 새로운 트래픽을 받을때마다 노드와 새로운 연결을 맺음. 일반적으로 라우터 제조사는 장비에 사용가능한 노드로 트래픽을 균등분산 할 수 있는 알고리즘을 내장함 > 부하분산
  • 노드 장애 시 새로운 노드와의 연결 수행


  • 어플리케이션은 클러스터에서
    • 내부적으로 CNI가 172.130.0.0/16대역에서 지정한 IP로 접근가능
    • 외부적으로 metalLB가 ipaddresspools에서 지정한 203.0.113.200로 접근가능
  • Node 2, 3에는 어플리케이션을 위한 Pod 존재
  • speaker daemonset은 각 노드에서 실행 중
  • 각 speaker pod는 host-network 파드; pod의 IP는 호스트 노드의 IP와 동일
  • 각 speaker pod는 모든 BGP 피어와 BGP 세션 시작, LoadBalancer IP 주소나 aggregated routes를 BGP 피어에게 advertise. 위의 그림에서 speaker pod들과 R1 라우터는 하나의 시스템에 속해있어 BGP 세션 수립
  • LoadBalancer IP를 advertise 하는 speaker pod가 있는 모든 노드는 트래픽 수신 가능
    • externalTrafficPolicy:cluster(디폴트) > speaker pod 가 작동 중인 모든 노드가 203.0.113.200를 advertise > 트래픽 수신 가능
    • externalTrafficPolicy:local > speaker pod 가 가동중이면서, 서비스에 대한 endpoint(application-pod)가 하나라도 존재하는 노드만 203.0.113.200를 advertise가능. 위의 그림에서는 노드2,3이 가능
  • BGPpeer custom resource 를 생성해, 노드 셀렉터를 추가함으로써 특정 speaker pod가 BGP 세션을 맺을 BGP peer 대상 제어 가능
  • R1과 같은 모든 라우터는 BGP를 사용하도록 구성하여 BGP peer로 설정 가능
  • 클라이언트의 트래픽은 host network(10.0.1.0/24)의 노드 중 하나로 라우팅 됨. 트래픽이 노드에 도달한 이후, 서비스에 설정된 externalTrafficPolicy에 따라, 서비스 프록시(CNI)가 같거나 다른 노드에 있는 어플리케이션 파드로 트래픽 전달
  • 노드 장애시, 라우터는 장애를 감지하고 다른 노드와 연결을 생성함. MetalLB는 좀더 빠른 장애 감지를 위해 BGP Peer에 대해 Bidirectional Forwarding Detection (BFD) profile를 구성하고 사용할 수 있음

3.1.1. BGP 모드 MetalLB 구성

BGP 모드를 사용하기 위해서는 BGP 프로토콜을 지원하는 별도의 라우터 장비가 필요하므로 현재 스터디에선 진행하지 않았다. 별도 구성과정만 찾아보고 정리하여둠

  • ConfigMap을 사용한 구성 방법
# k8s cm
cat <<EOF | kubectl replace --force -f -
apiVersion: v1
kind: ConfigMap
metadata:
  namespace: metallb-system
  name: config
data:
  config: |
    peers:
    - peer-address: 192.168.10.254
      peer-asn: 64513
      my-asn: 64512
    address-pools:
    - name: default
      protocol: bgp
      avoid-buggy-ips: true
      addresses:
      - 172.20.1.0/24
EOF

# avoid-buggy-ips: true ⇒ Handling buggy networks
# https://metallb.universe.tf/configuration/#handling-buggy-networks

# 리눅스 라우터에 BPG 설정 예시
router bgp 64513
	bgp router-id 192.168.10.254
	maximum-paths 4
	network 10.1.1.0/24
	neighbor 192.168.10.10  remote-as 64512
	neighbor 192.168.10.101 remote-as 64512
	neighbor 192.168.10.102 remote-as 64512
  ...

기 설치된 MetalLB환경에 위와 같은 configmap을 추가하여 구성한다. 아마 MetalLB Operator가 해당 cm을 참고하여 적절한 manifests로 구성하는듯?

L2 mode에서는 다음의 과정으로 구성이 완료되었다.

  1. 사용할 IP 대역대인 IPAddressPool CRD 생성
  2. 생성한 IPAddressPool 을 포함한 L2Advertisement CRD 생성

BGP mode에서는 다음의 과정을 수행하여야 하는듯 하다

  1. 연결할 라우터를 BGPPeer CRD로 등록
  2. 사용할 IP 대역대인 IPAddressPool CRD 생성
  3. 생성한 IPAddressPool 을 포함한 BGPAdvertisement CRD 생성
  4. (필요시) 헬스체크에 필요한 BFDProfile 생성 후 BGPPeer의 라우터정보에 포함

How to Use MetalLB in BGP Mode, Red Hat Blog
configsamples, metallb GitHub repo

3. External IPs

하나 이상의 클러스터 노드로 라우팅되는 외부 IP가 있다면, Kubernetes 서비스는 이러한 외부 IP를 통해 노출될 수 있다. 네트워크 트래픽이 클러스터로 들어올 때, 외부 IP(목적지 IP)와 해당 서비스와 일치하는 포트가 있는 경우, Kubernetes에 구성된 정책과 경로가 트래픽을 해당 서비스의 엔드포인트 중 하나로 라우팅한다.

If there are external IPs that route to one or more cluster nodes, Kubernetes Services can be exposed on those externalIPs. When network traffic arrives into the cluster, with the external IP (as destination IP) and the port matching that Service, rules and routes that Kubernetes has configured ensure that the traffic is routed to one of the endpoints for that Service.

External IPs, k8s docs
  • ExternalIP 서비스는 특정 노드IP(포트)로 인입한 트래픽을 컨테이너로 보내(ClusterIP 를 사용), 외부에서 접속할 수 있게 합니다
    • NodePort 와 거의 유사하니, 특별한 이유가 없으면 NodePort (ExternalTrafficPolicy 등 옵션 사용 가능) 를 사용하자
  • ExternalIP 는 노드 1개 혹은 일부 노드들을 지정할 수 있다
  • ExternalTrafficPolicy 정책을 사용할 수 없다 → 즉 외부에서 ExternalIP 로 접근 시 무조건 Node의 IP로 DNAT 되어서 Client IP 수집할 수 없다
설정 항목설명
spec.externalIPs노드 IP 주소(ExternalIP)
spec.ports[].portExternalIP 와 ClusterIP 에서 수신할 포트 번호
spec.ports[].targetPort목적지 컨테이너 포트 번호
k8s api overview

ExternalIP 실습

  • externalIPs service 배포
cat <<EOF | kubectl create -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: deploy-echo
spec:
  replicas: 2
  selector:
    matchLabels:
      app: deploy-websrv
  template:
    metadata:
      labels:
        app: deploy-websrv
    spec:
      terminationGracePeriodSeconds: 0
      containers:
      - name: ndks-websrv
        image: k8s.gcr.io/echoserver:1.5
        ports:
        - containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: svc-externalip
spec:
  type: ClusterIP
  externalIPs:
    - 192.168.10.101
    - 192.168.10.102
  ports:
    - name: svc-webport
      port: 9000
      targetPort: 8080
  selector:
    app: deploy-websrv
EOF
  • 결과 확인
# 확인 : ExternalIP 도 결국 ClusterIP를 사용(포함)
kubectl get svc svc-externalip
NAME             TYPE        CLUSTER-IP     EXTERNAL-IP                     PORT(S)    AGE
svc-externalip   ClusterIP   10.96.101.49   192.168.10.101,192.168.10.102   9000/TCP   46s

kubectl describe svc svc-externalip
Name:              svc-externalip
Namespace:         default
Labels:            <none>
Annotations:       <none>
Selector:          app=deploy-websrv
Type:              ClusterIP
IP Family Policy:  SingleStack
IP Families:       IPv4
IP:                10.96.101.49
IPs:               10.96.101.49
External IPs:      192.168.10.101,192.168.10.102
Port:              svc-webport  9000/TCP
TargetPort:        8080/TCP
Endpoints:         172.16.158.18:8080,172.16.184.17:8080
Session Affinity:  None
Events:            <none>

# ExternalTrafficPolicy 설정이 없음
kubectl get svc svc-externalip -o yaml
apiVersion: v1
kind: Service
metadata:
  creationTimestamp: "2022-02-24T07:32:47Z"
  name: svc-externalip
  namespace: default
  resourceVersion: "1454"
  uid: a27a8ca6-44ef-4363-ba3f-9894b4af5a54
spec:
  clusterIP: 10.110.23.53
  clusterIPs:
  - 10.110.23.53
  externalIPs:
  - 192.168.10.101
  - 192.168.10.102
  internalTrafficPolicy: Cluster
  ipFamilies:
  - IPv4
  ipFamilyPolicy: SingleStack
  ports:
  - name: svc-webport
    port: 9000
    protocol: TCP
    targetPort: 8080
  selector:
    app: deploy-websrv
  sessionAffinity: None
  type: ClusterIP
status:
  loadBalancer: {}

결과

  • ClusterIP유형이면서, EXTERNAL-IP 등록 확인

4. IPVS Proxy 모드

IPVS 모드에서는 kube-proxy가 커널 IPVS 및 iptables API를 사용하여 서비스 IP에서 엔드포인트 IP로 트래픽을 리디렉션하는 규칙을 생성합니다.

IPVS 프록시 모드는 iptables 모드와 유사한 netfilter 훅 함수를 기반으로 하지만, 기본 데이터 구조로 해시 테이블을 사용하고 커널 공간에서 작동합니다. 즉, IPVS 모드의 kube-proxy는 iptables 모드보다 낮은 대기 시간으로 트래픽을 리디렉션하며, 프록시 규칙 동기화 성능이 훨씬 우수합니다. iptables 프록시 모드와 비교할 때, IPVS 모드는 네트워크 트래픽의 높은 처리량을 지원합니다.

IPVS는 백엔드 Pod로의 트래픽을 균형 있게 분산시키기 위한 여러 가지 옵션을 제공합니다. 여기에는 다음과 같은 알고리즘이 포함됩니다: (생략)


In ipvs mode, kube-proxy uses the kernel IPVS and iptables APIs to create rules to redirect traffic from Service IPs to endpoint IPs.

The IPVS proxy mode is based on netfilter hook function that is similar to iptables mode, but uses a hash table as the underlying data structure and works in the kernel space. That means kube-proxy in IPVS mode redirects traffic with lower latency than kube-proxy in iptables mode, with much better performance when synchronizing proxy rules. Compared to the iptables proxy mode, IPVS mode also supports a higher throughput of network traffic.

IPVS provides more options for balancing traffic to backend Pods; these are: (생략)

IPVS proxy mode, k8s docs
  • IPVS 는 리눅스 커널에서 동작하는 소프트웨어 로드밸런서이다. 백엔드(플랫폼)으로 Netfilter 를 사용하며, TCP/UDP 요청을 처리 할 수 있다.
  • iptables 의 rule 기반 처리의 성능 한계와 분산 알고리즘이 없어서, 최근에는 대체로 IPVS 를 사용한다.

rule 기반 처리의 성능 한계
> iptables는 O(n)의 시간복잡도를 갖고, ipvs는 O(1)의 시간복잡도를 갖아 대상 규칙(=k8s services 오브젝트) 수가 많아질 수록, 일정 임계점 이후로는 iptables의 성능이 느려진다. (service 5000개)


IPVS, iptables and kube-proxy

Netfilter

IPVS와 iptables 모두 Linux 커널에서 기반한 packet-filtering framework인 netfilter에 기반하였기 때문에 넷필터이야기를 안하고 넘어갈 수 없다.

Netfilter는 Linux 커널 내부에 있는 일련의 훅을 지칭하며, 특정 커널 모듈이 커널의 네트워킹 스택에 콜백 함수를 등록할 수 있도록 한다. 이러한 함수들은 Linux 네트워크 스택을 통과하는 각 패킷에 대해 호출된다. 호출된 함수는 사전 정의된 규칙에 따라, 패킷을 수정하거나, 필터링 하는 등의 방식으로 적용된다.


IPtables

iptables 시스템 구성에서, 패킷이 네트워크 장치에 수신되면, 다음의 단계를 거친다.

1. 먼저 Prerouting hook을 통과하여 목적지가 로컬프로세스인지, 다른 장치인지 판별

로컬 프로세스가 대상이라면)
2. Input hook을 통해 로컬 프로세스로 전달

다른 장치가 대상이라면)
2. Forward 후크를 통과
3. Postrouting 후크를 통과

로컬에서 처리된 다음 다른 장치로 전달되야한다면)
2. Input hook을 통해 로컬 프로세스로 전달
3. 처리 이후 Output 후크 통과
4. Postrouting hook 통과

각 진행단계마다 필요한 후크들을 모두 거쳐야하며, iptables에는 여러 테이블이 있고, 각 테이블에 다수 CHAIN이 존재하고, 각 CHAIN에는 다수의 rules이 존재한다. 따라서 rule의 수 및 순서에 따라 복잡도/효율성이 달라진다.

Figure 2. Diagram from “Linux Firewalls Using iptables”


IPVS

IPVS 또한 netfilter 프레임워크를 기반으로하여 작동하지만, ip_vs_in 와 같은 별도 함수를 사용하여 일부 전달 과정을 생략/우회할 수 있어 성능이 개선되었다.

Figure 1. Interaction of LVS with Netfilter

IPVS 시스템 구성에서, 패킷이 넷필터 훅을 통과하는 과정에서 몇몇 함수가 트리거되고, 일부 후크를 건너뛰는 식으로 작동한다.

예를 들어 패킷이 로컬프로세스를 거쳐 외부로 나가는 경우, 패킷이 처음 PREROUTING hook에 도달하면 먼저 INPUT 체인으로 전달된다. 그 뒤 ip_vs_in 함수는 LOCAL_IN 체인에 연결되어 패킷을 검사하고, 일치하는 IPVS 규칙을 찾으면 (INPUT 체인을 우회하여) 직접 POSTROUTING 체인을 트리거한다. 자세한 내용은 원문을보라는데.. director가 무엇인지 + 다른 함수들은 어떤역할을 하는지 잘 모르겠다

결론은 IPVS 시스템은 기존 netfilter hook을 기반으로, 일부 규칙을 우회하는 함수를 적용하고 트리거하여 복잡도를 줄이고 속도를 개선하였다.


이후부터는 구체적으로 어느정도 차이가 나는지 구체적인 데이터를 살펴보겠다


Scale Kubernetes to support 50000 services, 2017 kubecon – Youtube, Slideshare

slide13, IPTables Service Routing Performance

리눅스 워커노드에 등록된 KUBE-SERVICES chain에 등록된 규칙에 따른 서비스 접근 레이턴시이다. 5000services 까지는 선형적으로 증가하다 그 이상부터는 급격히 응답속도의 지연을 확인할 수 있다.

slide 21, IPVS vs. IPTables Network Bandwidth

동일 조건(각 4개의 포트가 개방된 services 생성)에서 대역폭 비교시 서비스 5000개부터 급격히 성능차이가 발생함을 확인할 수 있다.


강연발표자인 HUAWEI쪽 자료를 살펴보니 Complexity 이야기를 계속이야기하는데.. 좀더 설명해줬으면…

Comparing kube-proxy modes: iptables or IPVS?, 2019 TIGERA

project Calico를 관리하는 TIGERA쪽의 벤치마크 결과도 비슷한 결론을 확인할 수 있다.

Round-Trip Response Times

이 차트는 두 가지 주요 사항을 보여줍니다:

  1. iptables와 IPVS 간의 평균 왕복 응답 시간 차이는 1,000개의 서비스(10,000개의 백엔드 파드)를 초과하기 전까지는 무의미할 정도로 미미합니다.
  2. 평균 왕복 응답 시간의 차이는 keepalive 연결을 사용하지 않을 때만 눈에 띕니다. 즉, 매 요청마다 새로운 연결을 사용할 때만 차이가 나타납니다.
Total CPU

이 차트는 두 가지 주요 사항을 보여줍니다:

  1. iptables와 IPVS 간의 CPU 사용량 차이는 1,000개의 서비스(10,000개의 백엔드 파드)를 초과하기 전까지는 상대적으로 미미합니다.
  2. 10,000개의 서비스(100,000개의 백엔드 파드)에서 iptables의 CPU 사용량 증가는 약 코어의 35%이며, IPVS의 경우 약 코어의 8%입니다.


부하 분산 스케줄링: kube-proxy 파라미터에 설정 적용 -ipvs-scheduler

[Linux] LVS로 L4 서버 만들기


IPSET

  • iptables로 5000건 이상의 룰셋이 등록 되었을때 시스템의 성능이 급격하게 떨어지는 반면 룰을 줄일 수 있는 방법 중 ‘IP들의 집합’ 으로 관리 ⇒ IPSET
  • 아래는 ipvs proxy mode 사용 시 설정되는 기본 ipset 내용
IPVS-Based In-Cluster Load Balancing Deep Dive


왜 kube-proxy의 default 동작은 ipvs가 아닌 iptables인가?

> iptables가 현재 시점에서 가장 안정적이고 널리 사용되는 프록시이기 때문


IPVS 문서에서 언급되는 IPTables 대비 IPVS의 장점은 다음과 같다.

  1. IPVS는 대규모 클러스터에 더 나은 확장성과 성능을 제공합니다.
  2. IPVS는 IPTABLES보다 더 정교한 부하 분산 알고리즘(최소 부하, 최소 연결, 지역성, 가중치 등)을 지원합니다.
  3. IPVS는 서버 상태 점검, 연결 재시도 등을 지원합니다.


더 나은 성능/기능임에도 불구하고 default k8s 설치시 기본적으로 iptables이 구성되는이유는 성숙도와 안정성, 광범위한 호환성일 것이다.

IPVS를 사용하기 위해서는 일정 수준 이상의 커널업데이트가 필요하고, 추가적인 설정이 필요할 수 있다. 이에 비해 iptables는 모든 리눅스 노드에서 사용가능하고, 대규모 구성이 아닌이상 크게 성능차이가 나지않으며, 많은 관리자들에게 친숙한 도구이기 때문일것이다.

실습 환경 생성
AWS EC2 based Kind cluster
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


# kube-proxy configmap 확인
kubectl describe cm -n kube-system kube-proxy
...
mode: ipvs
ipvs: # 아래 각각 옵션 의미 조사해보자!
  excludeCIDRs: null
  minSyncPeriod: 0s
  scheduler: ""
  strictARP: true  # MetalLB 동작을 위해서 true 설정 변경 필요
  syncPeriod: 0s
  tcpFinTimeout: 0s
  tcpTimeout: 0s
  udpTimeout: 0s
...

# strictARP: true는 ARP 패킷을 보다 엄격하게 처리하겠다는 설정입니다.
## IPVS 모드에서 strict ARP가 활성화되면, 노드의 인터페이스는 자신에게 할당된 IP 주소에 대해서만 ARP 응답을 보내게 됩니다. 
## 이는 IPVS로 로드밸런싱할 때 ARP 패킷이 잘못된 인터페이스로 전달되는 문제를 방지합니다.
## 이 설정은 특히 클러스터 내에서 여러 노드가 동일한 IP를 갖는 VIP(Virtual IP)를 사용하는 경우 중요합니다.


# 노드 별 네트워트 정보 확인 : kube-ipvs0 네트워크 인터페이스 확인
for i in control-plane worker worker2 worker3; do echo ">> node myk8s-$i <<"; docker exec -it myk8s-$i ip -c route; echo; done
for i in control-plane worker worker2 worker3; do echo ">> node myk8s-$i <<"; docker exec -it myk8s-$i ip -c addr; echo; done
for i in control-plane worker worker2 worker3; do echo ">> node myk8s-$i <<"; docker exec -it myk8s-$i ip -br -c addr show kube-ipvs0; echo; done
>> node myk8s-control-plane <<
kube-ipvs0       DOWN           10.200.1.1/32 10.200.1.10/32 

>> node myk8s-worker <<
kube-ipvs0       DOWN           10.200.1.10/32 10.200.1.1/32 

>> node myk8s-worker2 <<
kube-ipvs0       DOWN           10.200.1.1/32 10.200.1.10/32 

>> node myk8s-worker3 <<
kube-ipvs0       DOWN           10.200.1.10/32 10.200.1.1/32 

for i in control-plane worker worker2 worker3; do echo ">> node myk8s-$i <<"; docker exec -it myk8s-$i ip -d -c addr show kube-ipvs0; echo; done
>> node myk8s-control-plane <<
11: kube-ipvs0: <BROADCAST,NOARP> mtu 1500 qdisc noop state DOWN group default 
    link/ether 9e:1d:ca:21:c6:d1 brd ff:ff:ff:ff:ff:ff promiscuity 0  allmulti 0 minmtu 0 maxmtu 0 
    dummy numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535 tso_max_size 65536 tso_max_segs 65535 gro_max_size 65536 
    inet 10.200.1.10/32 scope global kube-ipvs0
       valid_lft forever preferred_lft forever
    inet 10.200.1.1/32 scope global kube-ipvs0
       valid_lft forever preferred_lft forever

>> node myk8s-worker <<
11: kube-ipvs0: <BROADCAST,NOARP> mtu 1500 qdisc noop state DOWN group default 
    link/ether fa:21:0a:00:a7:6c brd ff:ff:ff:ff:ff:ff promiscuity 0  allmulti 0 minmtu 0 maxmtu 0 
    dummy numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535 tso_max_size 65536 tso_max_segs 65535 gro_max_size 65536 
    inet 10.200.1.1/32 scope global kube-ipvs0
       valid_lft forever preferred_lft forever
    inet 10.200.1.10/32 scope global kube-ipvs0
       valid_lft forever preferred_lft forever

>> node myk8s-worker2 <<
11: kube-ipvs0: <BROADCAST,NOARP> mtu 1500 qdisc noop state DOWN group default 
    link/ether ba:e9:75:56:db:00 brd ff:ff:ff:ff:ff:ff promiscuity 0  allmulti 0 minmtu 0 maxmtu 0 
    dummy numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535 tso_max_size 65536 tso_max_segs 65535 gro_max_size 65536 
    inet 10.200.1.10/32 scope global kube-ipvs0
       valid_lft forever preferred_lft forever
    inet 10.200.1.1/32 scope global kube-ipvs0
       valid_lft forever preferred_lft forever

>> node myk8s-worker3 <<
11: kube-ipvs0: <BROADCAST,NOARP> mtu 1500 qdisc noop state DOWN group default 
    link/ether a2:1d:9c:e6:ad:84 brd ff:ff:ff:ff:ff:ff promiscuity 0  allmulti 0 minmtu 0 maxmtu 0 
    dummy numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535 tso_max_size 65536 tso_max_segs 65535 gro_max_size 65536 
    inet 10.200.1.1/32 scope global kube-ipvs0
       valid_lft forever preferred_lft forever
    inet 10.200.1.10/32 scope global kube-ipvs0
       valid_lft forever preferred_lft forever

# kube-ipvs0 에 할당된 IP(기본 IP + 보조 IP들) 정보 확인 
kubectl get svc,ep -A
NAMESPACE     NAME         TYPE        CLUSTER-IP    EXTERNAL-IP   PORT(S)                  AGE
default       kubernetes   ClusterIP   10.200.1.1    <none>        443/TCP                  3m8s
kube-system   kube-dns     ClusterIP   10.200.1.10   <none>        53/UDP,53/TCP,9153/TCP   3m7s

# ipvsadm 툴로 부하분산 되는 정보 확인 : 서비스의 IP와 서비스에 연동되어 있는 파드의 IP 를 확인
## Service IP(VIP) 처리를 ipvs 에서 담당 -> 이를 통해 iptables 에 체인/정책이 상당 수준 줄어듬
for i in control-plane worker worker2 worker3; do echo ">> node myk8s-$i <<"; docker exec -it myk8s-$i ipvsadm -Ln ; echo; done

## IPSET 확인
docker exec -it myk8s-worker ipset -h
docker exec -it myk8s-worker ipset -L


# iptables 정보 확인 : 정책 갯수를 iptables proxy 모드와 비교해보자
for i in filter nat mangle raw ; do echo ">> IPTables Type : $i <<"; docker exec -it myk8s-control-plane  iptables -t $i -S ; echo; done
for i in filter nat mangle raw ; do echo ">> IPTables Type : $i <<"; docker exec -it myk8s-worker  iptables -t $i -S ; echo; done
for i in filter nat mangle raw ; do echo ">> IPTables Type : $i <<"; docker exec -it myk8s-worker2 iptables -t $i -S ; echo; done
for i in filter nat mangle raw ; do echo ">> IPTables Type : $i <<"; docker exec -it myk8s-worker3 iptables -t $i -S ; echo; done

# 각 노드 bash 접속
docker exec -it myk8s-control-plane bash
docker exec -it myk8s-worker bash
docker exec -it myk8s-worker2 bash
docker exec -it myk8s-worker3 bash
----------------------------------------
exit
----------------------------------------


# mypc 컨테이너 기동 : kind 도커 브리지를 사용하고, 컨테이너 IP를 직접 지정 혹은 IP 지정 없이 배포
docker run -d --rm --name mypc --network kind --ip 172.18.0.100 nicolaka/netshoot sleep infinity
혹은
docker run -d --rm --name mypc --network kind nicolaka/netshoot sleep infinity

docker ps
  • IPVS 정보 확인
# kube-proxy 로그 확인 :  기본값 부하분산 스케줄러(RoundRobin = RR)
kubectl stern -n kube-system -l k8s-app=kube-proxy --since 2h | egrep '(ipvs|IPVS)'
...
kube-proxy-2qmw7 kube-proxy I0929 07:41:23.512897       1 server_linux.go:230] "Using ipvs Proxier"
kube-proxy-2qmw7 kube-proxy I0929 07:41:23.514074       1 proxier.go:364] "IPVS scheduler not specified, use rr by default" ipFamily="IPv4"
kube-proxy-2qmw7 kube-proxy I0929 07:41:23.514157       1 proxier.go:364] "IPVS scheduler not specified, use rr by default" ipFamily="IPv6"
kube-proxy-v5lcv kube-proxy I0929 07:41:23.523480       1 proxier.go:364] "IPVS scheduler not specified, use rr by default" ipFamily="IPv6"

# 기본 모드 정보 확인
kubectl get cm -n kube-system kube-proxy -o yaml | egrep 'mode|strictARP|scheduler'
      scheduler: ""
      strictARP: true
    mode: ipvs

# ipvsadm 툴로 부하분산 되는 정보 확인 : RR 부하분산 스케줄러 확인
for i in control-plane worker worker2 worker3; do echo ">> node myk8s-$i <<"; docker exec -it myk8s-$i ipvsadm -Ln ; echo; done
...
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
  -> RemoteAddress:Port           Forward Weight ActiveConn InActConn
TCP  10.200.1.1:443 rr
  -> 172.18.0.2:6443              Masq    1      3          0    
...

# 커널 파라미터 확인
# (심화 옵션) strictARP - 링크 설정(유사한)이유
# --ipvs-strict-arp : Enable strict ARP by setting arp_ignore to 1 and arp_announce to 2
# arp_ignore : ARP request 를 받았을때 응답 여부 - 0(ARP 요청 도착시, any Interface 있으면 응답), 1(ARP 요청을 받은 Interface 가 해당 IP일때만 응답)
# arp_announce : ARP request 를 보낼 때 'ARP Sender IP 주소'에 지정 값 - 0(sender IP로 시스템의 any IP 가능), 2(sender IP로 실제 전송하는 Interface 에 IP를 사용)
docker exec -it myk8s-worker tree /proc/sys/net/ipv4/conf/kube-ipvs0
docker exec -it myk8s-worker cat /proc/sys/net/ipv4/conf/kube-ipvs0/arp_ignore
docker exec -it myk8s-worker cat /proc/sys/net/ipv4/conf/kube-ipvs0/arp_announce

# all 은 모든 인터페이스에 영항을 줌, 단 all 과 interface 값이 다를때 우선순위는 커널 파라미터 별로 다르다 - 링크
docker exec -it myk8s-worker sysctl net.ipv4.conf.all.arp_ignore
docker exec -it myk8s-worker sysctl net.ipv4.conf.all.arp_announce
docker exec -it myk8s-worker sysctl net.ipv4.conf.kube-ipvs0.arp_ignore
docker exec -it myk8s-worker sysctl net.ipv4.conf.kube-ipvs0.arp_announce
docker exec -it myk8s-worker sysctl -a | grep arp_ignore
docker exec -it myk8s-worker sysctl -a | grep arp_announce

# IPSET 확인
docker exec -it myk8s-worker ipset -h
docker exec -it myk8s-worker ipset -L

결과

  • kube-proxy cm의 mode, strictARP 정보 확인
  • ipvs의 동작을 위해 각 노드에 kube-ipvs0 dummy 인터페이스 생성
  • 각 노드의 ipvsadm 확인 > 간소화된 체인/규칙과 rr

4.1. 서비스 접속 테스트

ipvs mode상에 테스트 파드/서비스를 배포한뒤 접근여부와 작동과정을 확인해볼것이다.

실습 pod/svc 생성

  • 목적지(backend) 파드(Pod) 생성 : 3pod.yaml
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
  • 클라이언트(TestPod) 생성 : netpod.yaml
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) 생성 : svc-clusterip.yaml
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

생성 및 확인 : IPVS Proxy 모드

# 생성
kubectl apply -f 3pod.yaml,netpod.yaml,svc-clusterip.yaml

# 파드와 서비스 사용 네트워크 대역 정보 확인 
kubectl cluster-info dump | grep -m 2 -E "cluster-cidr|service-cluster-ip-range"

# 확인
kubectl get pod -owide
kubectl get svc svc-clusterip
kubectl describe svc svc-clusterip
kubectl get endpoints svc-clusterip
kubectl get endpointslices -l kubernetes.io/service-name=svc-clusterip

# 노드 별 네트워트 정보 확인 : kube-ipvs0 네트워크 인터페이스 확인
## ClusterIP 생성 시 kube-ipvs0 인터페이스에 ClusterIP 가 할당되는 것을 확인
for i in control-plane worker worker2 worker3; do echo ">> node myk8s-$i <<"; docker exec -it myk8s-$i ip -c addr; echo; done
for i in control-plane worker worker2 worker3; do echo ">> node myk8s-$i <<"; docker exec -it myk8s-$i ip -br -c addr show kube-ipvs0; echo; done
for i in control-plane worker worker2 worker3; do echo ">> node myk8s-$i <<"; docker exec -it myk8s-$i ip -d -c addr show kube-ipvs0; echo; done

# 변수 지정
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
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

# 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

# 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 0x6343ff52
Size in memory: 456
References: 3
Number of entries: 5
Members:
10.200.1.1,tcp:443
10.200.1.10,tcp:53
10.200.1.10,udp:53
10.200.1.245,tcp:9000
10.200.1.10,tcp:9153

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

#
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

# 변수 지정
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 모니터링 실행 : ClusterIP 접속 시 아래 처럼 연결 정보 확인됨
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 변수 지정 : svc-clusterip 의 ClusterIP주소
SVC1=$(kubectl get svc svc-clusterip -o jsonpath={.spec.clusterIP})
echo $SVC1

# TCP 80,9000 포트별 접속 확인 : 출력 정보 의미 확인
kubectl exec -it net-pod -- curl -s --connect-timeout 1 $SVC1:9000
kubectl exec -it net-pod -- curl -s --connect-timeout 1 $SVC1:9000 | grep Hostname
kubectl exec -it net-pod -- curl -s --connect-timeout 1 $SVC1:9000 | grep Hostname

# 서비스(ClusterIP) 부하분산 접속 확인 : 부하분산 비률 확인
kubectl exec -it net-pod -- zsh -c "for i in {1..10};   do curl -s $SVC1:9000 | grep Hostname; done | sort | uniq -c | sort -nr"
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"
kubectl exec -it net-pod -- zsh -c "for i in {1..1000}; do curl -s $SVC1:9000 | grep Hostname; done | sort | uniq -c | sort -nr"
혹은
kubectl exec -it net-pod -- zsh -c "for i in {1..100};   do curl -s $SVC1:9000 | grep Hostname; sleep 1; done"
kubectl exec -it net-pod -- zsh -c "for i in {1..100};   do curl -s $SVC1:9000 | grep Hostname; sleep 0.1; done"
kubectl exec -it net-pod -- zsh -c "for i in {1..10000}; do curl -s $SVC1:9000 | grep Hostname; sleep 0.01; done"

# 반복 접속
kubectl exec -it net-pod -- zsh -c "while true; do curl -s --connect-timeout 1 $SVC1:9000 | egrep 'Hostname|RemoteAddr|Host:'; date '+%Y-%m-%d %H:%M:%S' ; echo '--------------' ;  sleep 1; done"

결과

  • 각 노드의 kube-ipvs0 인터페이스에 신규 생성한 svc의 ClusterIP 추가됨
  • 모든노드에 rr로 부하분산될것임을 ipvsadm을 사용해 확인
  • ipset으로 다수 ip routing 규칙이 관리됨을 확인
  • SVC1로 요청시 접근 성공
    • RemoteAddr: 요청을 보낸 netpod의 IP
    • Host: 요청을 수신한 svc의 IP
    • Hostname, IP: 실제 요청을 처리한 pod의 정보
  • 서비스(ClusterIP) 부하분산 접속시 iptables 대비 균일하게 분산됨 확인

Leave a Reply