가시다님의 KANS [3기] 스터디 내용을 정리한 포스트 입니다.
Kubernetes의 서비스 오브젝트(Service Object) 중 LoadBalancer 타입은 외부에서 클러스터 내에 있는 서비스를 접근할 수 있도록 하는 방법 중 하나입니다. 이를 통해 클러스터 외부에서 서비스에 대한 요청을 보낼 수 있게 됩니다.
LoadBalancer서비스는 퍼블릭 클라우드 환경에서 동작하는 LoadBalancer 서비 스와 온프레미스환경에서 동작하는 LoadBalancer 서비스가 있습니다.
크게 2가지 동작 방식이 있습니다.
- 외부 클라이언트는 로드밸런서로 접속을 하고, 로드밸런서는 파드가 있는 노드들로 부하분산해서 전달함
- 노드의 NodePort 로 인입 후에는 iptables 에 의해서 파드로 랜 덤 부하분산을 통해서 전달함
- 부하분산 과정이 두 번 수행됨

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

크게 하드웨어 기반과 소프트웨어 기반으로 방식이 있습니다. 엔터프라이즈 환경에서는 가용성등 측면에서 하드웨어 기반으로 보통 구성하게 됩니다.
- 클라우드 환경의 AWS LoadBalancer 서비스에 거의 동일하게 별도의 장비로 접속 후 노드에 NodePort 혹은 파드로 직접 전달하여 통신함
- 대표적으로 시트릭스나 F5네트웍스의 제품이 있음

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




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

모드 소개: 리더 파드가 선출되고 해당 리더 파드가 생성된 노드로만 트래픽이 인입되어 해당 노드에서 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초 정도)

모드 소개: 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 라우팅 설정 및 라우팅 전파 관련 최적화 설정이 필요
# 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"]
# (옵션) 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 # 매칭 사용 가능
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 # 매칭 사용 가능

# 현재 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
-> 접속 안됨!

리더 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으로 옮겨진 것으로 확인된다.

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가 지원하는 주요 스케줄링 알고리즘은 아래와 같습니다.

실습 환경은 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

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
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
# 변수 지정
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로 분산되어 접속되는 것을 확인 할 수 있습니다.