📌 Notice
Kubernetes Advanced Networking Study (=KANS)
k8s 네트워크 장애 시, 네트워크 상세 동작 원리를 기반으로 원인을 찾고 해결하는 내용을 정리한 블로그입니다.
CloudNetaStudy
그룹에서 스터디를 진행하고 있습니다.
Gasida
님께 다시한번 🙇 감사드립니다.
EKS 관련 이전 스터디 내용은 아래 링크를 통해 확인할 수 있습니다.
Kubernetes에서 서비스를 외부로 노출하는 방법으로 ClusterIP, NodePort, LoadBalancer 타입의 동작 원리를 심층적으로 이해합니다.
ClusterIP와 NodePort, 그리고 LoadBalancer 서비스의 네트워크 흐름을 분석하고 iptables 및 IPVS의 규칙 설정 방식을 학습합니다.
MetalLB와 같은 로드 밸런서를 활용하여 Kubernetes에서 고가용성 네트워크를 구현하고 장애 상황에서의 대응 방법을 이해합니다.
외부 로드 밸런서를 사용하여 서비스를 외부에 노출합니다.
Kubernetes는 로드 밸런싱 구성 요소를 직접 제공하지 않습니다
you must provide one, or you can integrate your Kubernetes cluster with a cloud provider.
type: Loadbalancer
의 서비스를 구현하기 위해 Kubernetes는 일반적으로 type: nodePort
의 서비스를 요청하는 것과 동등한 내용을 변경하는 것으로 시작합니다. 그런 다음 클라우드 컨트롤러-관리자 구성 요소는 외부 로드 밸런서를 구성하여 할당된 노드 포트로 트래픽을 전달합니다.
클라우드 제공업체 구현에서 이를 지원하는 경우 로드 밸런싱 서비스를 구성하여 노드 포트 할당을 생략할 수 있습니다.
summary
- 외부 클라이언트가 '로드밸런서' 접속 시 부하분산 되어 노드 도달 후 iptables 룰로 목적지 파드와 통신됨
외부에서 로드밸런서 (부하분산) 처리 후 → 노드(NodePort) 이후 기본 과정은 NodePort 과정과 동일합니다.
출처 - CloudNet@
노드는 외부에 공개되지 않고 로드밸런서만 외부에 공개되어, 외부 클라이언트는 로드밸런서에 접속을 할 뿐 내부 노드의 정보를 알 수 없습니다.
로드밸런서가 부하분산
하여 파드가 존재하는 노드들에게 전달합니다. iptables 룰에서는 자신의 노드에 있는 파드만 연결한다. (externalTrafficPolicy: local
)
DNAT 2번 동작하게 됩니다.
쿠버네티스는 Service(LB Type) API 만 정의하고 실제 구현은 add-on 에 맡깁니다.
부하분산 최적화
노드에 파드가 없을 경우 '로드밸런서'에서 노드에 헬스 체크(상태 검사)가 실패하여 해당 노드로는 외부 요청 트래픽을 전달하지 않는다
출처 - CloudNet@
서비스(LoadBalancer) 생성 시 마다 LB(예 AWS NLB)가 생성
되어 자원 활용이 비효율적 ⇒ HTTP 경우 인그레스(Ingress) 를 통해 자원 활용 효율화 가능!
서비스(LoadBalancer)는 HTTP/HTTPS 처리에 일부 부족함(TLS 종료, 도메인 기반 라우팅 등) ⇒ 인그레스(Ingress) 를 통해 기능 동작 가능!
(참고) 온프레미스 환경에서 제공 불가능
??? ⇒ MetalLB 혹은 OpenELB(구 PorterLB) 를 통해서 온프레미스 환경에서 서비스(LoadBalancer) 기능 동작 가능!
MetalLB Docs : MetalLB is a load-balancer implementation for bare metal Kubernetes clusters, using standard routing protocols - link
MetalLB - Layer2 모드
- 리더 파드가 선출되고 해당 리더 파드가 생성된 노드로만 트래픽이 인입되어 해당 노드에서 iptables 분산되어 파드로 접속
- 권장 사용 환경 : 테스트 및 소규모의 환경(동일 네트워크 1개 사용)에서 클라이언트 IP 보존이 필요 없을 때
출처 - CloudNet@
MetalLB - BGP 모드
- speaker 파드가 BGP로 서비스 정보(EXTERNAL-IP)를 전파 후, 외부에서 라우터를 통해 ECMP 라우팅으로 부하 분산 접속
- 권장 사용 환경 : 규모가 있고, 클라이언트 IP보존과 장애 시 빠른 절체가 필요하며, 네트워크 팀 협조가 가능할때
출처 - CloudNet@
MetalLB 는 온프레미스 환경(IDC)에서 사용할 수 있는 서비스(로드밸런서 타입)입니다.
(클라우드 환경의 서비스(로드밸런서 타입)와는 동작이 조금 다릅니다!)
MetalLB는 BareMetalLoadBalancer 약자!
출처 - CloudNet@
L2 mode (ARP)
출처 - CloudNet@
BGP mode
출처 - CloudNet@
External IP
' 전파를 위해서 표준 프로토콜인 ARP(IPv4)/NDP(IPv6), BGP 를 사용합니다.External IP
' 전파합니다
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)도 있지만 사용하지 않은 이유
출처 - CloudNet@
제한 사항
single-node bottlenecking, potentially slow failover
- single-node bottlenecking : 서비스 1개 생성 사용 시, 모든 서비스 접근 트래픽은 리더 파드가 존재하는 노드로만 인입되어서 부하가 집중
- potentially slow failover : 리더(노드)가 장애 시 나머지 노드 리더가 선출되고, ARP 전파 및 갱신완료 전까지는 장애가 발생됨 (대략 10초~20초 정도)
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) 기준으로 동작합니다.
- 물론 라우터 장비에 따라 다양한 라우팅(분산) 처리가 가능합니다
출처 - CloudNet@
제한 사항
라우터에서 서비스로 인입이 되기 때문에, 라우터의 관련 설정이 중요한 만큼 네트워크팀과 협업을 적극 권장
- 노드(speaker) 파드 장애 시 BGP Timer 설정 등 구성하고 있는 네트워크 환경에 맞게 최적화 작업이 필요
- ECMP 부하 분산 접속 시 특정 파드에 몰리거나 혹은 세션 고정, flapping 등 다양한 환경에 대응이 필요
- BGP 라우팅 설정 및 라우팅 전파 관련 최적화 설정이 필요
기본 정보 확인
# 파드와 서비스 사용 가능 네트워크 대역 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:'
설치 방법 지원 : Kubernetes manifests, using Kustomize, or using Helm
간단하게 manifests 로 설치 진행! - https://github.com/metallb/metallb/tree/main/config/manifests
# 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
컨피그맵 생성 : 모드 및 서비스 대역 지정
서비스(External-IP) 대역을 노드가 속한 eth0의 대역이 아니여도 상관없다! → 다만 이 경우 GW 역할의 라우터에서 노드들로 라우팅 경로 지정 필요제 경우에는 브릿지의 네트워크 대역이 172.19.0.0/16으로 설정되어 그에 맞게 진행하였습니다.
# kind 설치 시 kind 이름의 도커 브리지가 생성된다 : 172.19.0.0/16 대역 docker network ls docker inspect kind # kind network 중 컨테이너(노드) IP(대역) 확인 : 172.19.0.2~ 부터 할당되며, control-plane 이 꼭 172.19.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.19.255.200-172.19.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 mypc2 arping -I eth0 -f -c 1 $SVC1EXIP docker exec -it mypc2 arping -I eth0 -f -c 1 $SVC2EXIP docker exec -it mypc2 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 mypc2 ip -c neigh # icmp는 지원하지 않는다. docker exec -it mypc2 ping -c 1 -w 1 -W 1 $SVC1EXIP docker exec -it mypc2 ping -c 1 -w 1 -W 1 $SVC2EXIP docker exec -it mypc2 ping -c 1 -w 1 -W 1 $SVC3EXIP
for i in $SVC1EXIP $SVC2EXIP $SVC3EXIP; do docker exec -it mypc2 ping -c 1 -w 1 -W 1 $i; done for i in 172.19.0.2 172.19.0.3 172.19.0.4 172.19.0.5; do docker exec -it mypc2 ping -c 1 -w 1 -W 1 $i; done # mypc/mypc2 에서 arp 테이블 정보 확인 >> SVC IP별로 리더 파드(스피커) 역할의 노드를 확인! docker exec -it mypc2 ip -c neigh | sort 172.19.0.2 dev eth0 lladdr 02:42:ac:12:00:02 REACHABLE 172.19.0.3 dev eth0 lladdr 02:42:ac:12:00:03 REACHABLE 172.19.0.4 dev eth0 lladdr 02:42:ac:12:00:04 REACHABLE 172.19.0.5 dev eth0 lladdr 02:42:ac:12:00:05 DELAY 172.19.255.200 dev eth0 lladdr 02:42:ac:12:00:04 STALE 172.19.255.201 dev eth0 lladdr 02:42:ac:12:00:04 STALE 172.19.255.202 dev eth0 lladdr 02:42:ac:12:00:02 STALE # mac 주소에 매칭되는 IP(노드) 찾기 kubectl get node -owide
# (옵션) 노드에서 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 # 매칭 사용 가능
이제 드디어 k8s 클러스터 외부에서 노드의 IP를 감추고(?) VIP/Port 를 통해 k8s 클러스터 내부의 애플리케이션에 접속 할 수 있습니다!
클라이언트(mypc, mypc2) → 서비스(External-IP) 접속 테스트
출처 - CloudNet@
# 현재 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 mypc2 curl -s $SVC1EXIP docker exec -it mypc2 curl -s $SVC1EXIP | grep Hostname for i in $SVC1EXIP $SVC2EXIP $SVC3EXIP; do docker exec -it mypc2 curl -s $i | grep Hostname ; done for i in $SVC1EXIP $SVC2EXIP $SVC3EXIP; do echo ">> Access Service External-IP : $i <<" ; docker exec -it mypc2 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 mypc2 zsh -c "for i in {1..100}; do curl -s $SVC1EXIP | grep Hostname; done | sort | uniq -c | sort -nr" docker exec -it mypc2 zsh -c "for i in {1..100}; do curl -s $SVC2EXIP | grep Hostname; done | sort | uniq -c | sort -nr" docker exec -it mypc2 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
클라이언트 → 서비스(External-IP) 접속 시 : 리더 파드가 존재하는 노드 인입 후 Service 에 매칭된 iptables rules 에 따라 랜덤 부하 분산되어서(SNAT) 파드로 접속됩니다.
장애 발생 시 동작
출처 - CloudNet@
위 그림처럼 장애 발생 전 워커노드1의 스피커파드가 SVC1, SVC3 의 리더 스피커 파드 역할을 하고 있는 상태에서, 워커노드1에 장애가 발생하게 되면 남아있는 스피커 파드들은 워커노드1의 장애를 인지하게 됩니다. 이후 장애가 발생한 스피커파드가 소유한 서비스의 ExternalIP 에 대한 리더 파드를 다시 선출하게 됩니다. 예를 들면 워커노드2가 SVC1, SVC3에 대한 리더 스피커파드가 선출이 되게 되면, 해당 파드는 GARP 로 자신이 SVC1, SVC3 ExternalIP에 대한 소유라고 전파를 하게 됩니다.
다만 장애 발생 시 문제가 있다는 인지를 확인하는 시간과 ARP 정보가 전파되는 시간이 필요하며 대략 20초~1분 이내의 장애 지속 시간이 발생하게 됩니다.
워커노드 중 1대를 중지하여 장애를 발생시키고, 장애시간을 확인해보겠습니다.
MetalLB Layer2 모드의 경우 노드 장애나 MetalLB 파드에 장애가 발생 시 10초~1분 정도의 장애 지속 시간이 소요됩니다. 만약 실무에서 MetalLB Layer2 모드 사용 시, 해당 장애 지속 시간이 운영하고 있는 애플리케이션 동작에 문제가 없는 지 꼭 사전 장애 관련 테스트를 진행하셔서, 최적화 및 장애 대응 방안을 수립하시기를 권장합니다.
리더 Speaker 파드가 존재하는 노드를 재부팅!
→ curl 접속 테스트 시 10~20초 정도의 장애 시간이 발생하였다
→ 이후 자동 원복 되며, 원복 시 5초 정도 장애 시간 발생!# 사전 준비 ## 지속적으로 반복 접속 SVC1EXIP=$(kubectl get svc svc1 -o jsonpath='{.status.loadBalancer.ingress[0].ip}') docker exec -it mypc2 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(노드) 찾기
실습을 위한 K8S 설치
실습 환경은 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 소개(정보 수집)
- IPVS 는 리눅스 커널에서 동작하는 소프트웨어 로드밸런서이다. 백엔드(플랫폼)으로 Netfilter 를 사용하며, TCP/UDP 요청을 처리 할 수 있다.
- iptables 의 rule 기반 처리의 성능 한계와 분산 알고리즘이 없어서, 최근에는 대체로 IPVS 를 사용한다.
Netfilter Hook Function
: 6개의 Netfilter Hook Function 을 사용 → Local Hook 사용 이유는 IPVS Dummy Interface 사용하기 때문
출처 - https://ssup2.github.io/theory_analysis/Linux_LVS_IPVS/
부하 분산 스케줄링
: kube-proxy 파라미터에 설정 적용 -ipvs-scheduler
출처 - https://net711.tistory.com/entry/lvs-리눅스-l4-만들기
IPSET
- iptables로 5000건 이상의 룰셋이 등록 되었을때 시스템의 성능이 급격하게 떨어지는 반면 룰을 줄일 수 있는 방법 중 'IP들의 집합' 으로 관리 ⇒ IPSET
- 아래는 ipvs proxy mode 사용 시 설정되는 기본 ipset 내용
출처 - https://kubernetes.io/blog/2018/07/09/ipvs-based-in-cluster-load-balancing-deep-dive/
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
목적지(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
실습 도식화
출처 - https://kschoi728.tistory.com/265
생성 및 확인 : 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"
이번 스터디를 통해 Kubernetes에서 클러스터 내부 서비스를 외부로 노출하는 다양한 방법과 그 작동 원리에 대해 깊이 있게 이해할 수 있었습니다. 특히 ClusterIP, NodePort, LoadBalancer 서비스 타입을 중심으로 실제 실습을 통해 통신 흐름과 iptables, IPVS 규칙이 어떻게 구성되고 동작하는지 분석하였습니다.
ClusterIP 서비스를 통해 클러스터 내부에서만 접근 가능한 서비스를 생성하고, iptables의 규칙을 통해 파드로 트래픽이 분산되는 과정을 확인하였습니다. 또한 NodePort와 LoadBalancer 서비스를 사용하여 클러스터 외부에서의 접근 방식을 실습하고, externalTrafficPolicy 설정을 통해 클라이언트의 IP를 보존하는 방법을 학습하였습니다.
MetalLB를 활용하여 온프레미스 환경에서도 Kubernetes 로드 밸런서를 구현할 수 있었으며, 이를 통해 고가용성 네트워크를 구성하고 장애 대응 방법을 탐구하였습니다. 이러한 과정에서 Kubernetes의 네트워킹이 내부적으로 어떻게 트래픽을 처리하고 분산하는지 명확히 이해할 수 있었습니다.
Kubernetes 공식 문서: https://kubernetes.io/docs/
MetalLB 공식 문서: https://metallb.universe.tf/
CloudNet@ 관련 자료: https://velog.io/@xgro
기타 참고 블로그 및 자료: https://kschoi728.tistory.com, https://ssup2.github.io