LabHub
배우기 러닝패스 코스

CCA — Cilium Certified Associate

We Removed kube-proxy and the Service Rules Vanished

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

목표

kube-proxy 없이 도는 진짜 Cilium 1.20.1 에서 서비스 부하 분산이 어느 BPF 맵과 어느 eBPF 프로그램으로 옮겨 갔는지 직접 읽습니다. iptables 가 비어 있음을 확인하고, 서비스 맵·conntrack 맵·파드 장치의 프로그램·NodePort·백엔드 없는 서비스의 응답까지 따라갑니다.

왜 중요한가

kube-proxy 의 iptables 모드는 서비스와 엔드포인트마다 규칙을 만들고, 규칙은 위에서부터 차례로 평가됩니다. 서비스가 늘면 규칙이 늘고, 바뀔 때마다 규칙을 다시 씁니다. Cilium 의 kube-proxy 대체는 서비스를 해시 맵 항목으로 두고, 파드와 노드 장치에 붙은 eBPF 프로그램이 패킷이 지나갈 때 맵을 찾아 목적지를 바꿉니다.

이 차이를 알아야 장애 때 어디를 볼지 압니다. "서비스가 안 된다" 는 신고에 iptables-save 를 뒤지면 아무것도 나오지 않습니다. 대신 서비스 맵의 백엔드 슬롯, conntrack 의 항목, 파드 장치에 프로그램이 붙어 있는지를 봐야 합니다.

VM 안의 개인 k3s(Cilium 1.20.1, kubeProxyReplacement=true)만 사용하세요. 환경 준비에 약 5분이 걸립니다. 세션이 끝나면 /root/cca-datapath 의 파일은 사라집니다.

단계

  1. kubectl apply -f /opt/fixtures/cca-datapath/web.yaml 로 cca-dp 네임스페이스에 web(레플리카 2)과 ClusterIP 서비스 web 을 띄우세요. 파드가 Ready 가 되면 VM 셸에서 iptables-save 를 읽어 /root/cca-datapath/iptables.json 에 기록합니다 — cluster_ip(서비스 web 의 ClusterIP), kube_svc_lines(KUBE-SVC 를 포함한 줄 수), cluster_ip_lines(그 ClusterIP 를 포함한 줄 수), cilium_lines(CILIUM 을 포함한 줄 수), kube_proxy_replacement(에이전트 cilium-dbg status 의 KubeProxyReplacement 값). 수는 숫자로 적습니다.
  2. 에이전트의 cilium-dbg service list -o json 에서 cca-dp/web 의 ClusterIP 항목을 찾고, cilium-dbg bpf lb list 에서 같은 프런트엔드의 슬롯을 확인하세요. /root/cca-datapath/svc.jsonservice_id(숫자), frontend(ClusterIP:80), backends(백엔드 ip:port 2개 목록), backend_pods(그 IP 를 가진 web 파드 이름 2개) 를 기록합니다.
  3. web 을 레플리카 4개로 늘리세요. 네 파드가 Ready 가 되고 bpf lb list 의 web 프런트엔드 백엔드 슬롯이 4개가 될 때까지 짧게 폴링한 뒤, /root/cca-datapath/scale.jsonbackends(지금 맵에 있는 ip:port 4개), new_backends(svc.json 에 없던 것 2개) 를 기록합니다. EndpointSlice 의 ready 주소와도 같아야 합니다.
  4. kubectl apply -f /opt/fixtures/cca-datapath/holder.yaml 로 holder 파드를 띄우세요. holder 는 출발 포트 40404 로 web 서비스(ClusterIP:80)에 TCP 연결 하나를 열고 쥐고 있으며, 로그에 앱이 getpeername 으로 본 상대 주소를 찍습니다. 세 곳에서 같은 연결을 보세요 — holder 로그, holder 안 netstat -tn, 에이전트의 cilium-dbg bpf ct list global:40404 항목. /root/cca-datapath/ct.jsonclient(holderIP:40404), app_peer(로그의 상대 주소 ip:port), socket_peer(holder netstat 의 Foreign Address), backend(conntrack OUT 항목의 목적지 ip:port), backend_pod(그 IP 의 web 파드 이름), svc_entries(:40404 를 가진 TCP SVC 항목 수, 숫자), socket_lb_coverage(cilium-dbg status --verbose 의 Socket LB Coverage) 를 기록합니다. 백엔드 파드 안 netstat 에도 holder 의 IP:40404 가 보여야 합니다.
  5. holder 의 엔드포인트 번호(kubectl -n cca-dp get cep holder 의 status.id)로 에이전트 cilium-dbg endpoint get <번호> -o json 을 읽어 호스트 쪽 장치 이름과 ifindex 를 찾고, VM 셸에서 bpftool net show dev <장치>tc filter show dev <장치> ingress 를 비교하세요. /root/cca-datapath/prog.jsonendpoint_id, interface, ifindex, attach(bpftool 이 보여 준 붙은 자리), program(프로그램 이름), policy_map(cilium-dbg map list 에서 찾은 이 엔드포인트의 정책 맵 이름), tc_filter_lines(tc filter 출력 줄 수, 숫자) 를 기록합니다.
  6. cca-dp 에 NodePort 서비스 web-np 를 만드세요 — selector app=web, port 80, targetPort 8080, nodePort 30780. 노드 InternalIP 의 30780 으로 VM 셸에서 요청해 200 을 확인하고, /root/cca-datapath/nodeport.jsonnode_ip, node_port, http_code(숫자), iptables_lines(iptables-save 에서 30780 을 포함한 줄 수), listen_sockets(ss -Htln 'sport = :30780' 줄 수), service_id(에이전트 목록의 0.0.0.0:30780 NodePort 항목 번호) 를 기록합니다.
  7. cca-dp 에 selector app=ghost, port 80 → targetPort 8080 인 ClusterIP 서비스 ghost 를 만드세요(그 라벨의 파드는 만들지 않습니다). curl 파드 probe(이미지 curlimages/curl:8.10.1@sha256:d9b4541e214bcd85196d6e92e2753ac6d0ea699f0af5741f8c6cccbfcf00ef4b, 명령 sleep 86400)를 띄워 그 안에서 ghost 의 ClusterIP 로 5초 제한 요청을 보냅니다. /root/cca-datapath/ghost.jsoncluster_ip, backends(에이전트 목록의 백엔드 수, 숫자), curl_exit(curl 종료 코드), seconds(curl 의 time_total, 숫자), no_backend_response(cilium-dbg config -a 의 ServiceNoBackendResponse 값) 를 기록합니다.
  8. /root/cca-datapath/report.txt키=값 일곱 줄을 씁니다 — kube_proxy_replacement, socket_lb_coverage(status --verbose 의 Socket LB Coverage), web_backends(지금 bpf lb list 의 web 백엔드 슬롯 수), holder_backend(지금 conntrack 에서 holder 연결이 향한 ip:port), holder_program(prog.json 의 program), nodeport_iptables_lines(지금 30780 을 포함한 iptables 줄 수), ghost_no_backend_response. 값은 모두 지금 상태와 앞 기록에 일치해야 합니다.

참고

kube-proxy 가 없는데 서비스는 누가 돌리나

kubectl apply -f /opt/fixtures/cca-datapath/web.yaml 로 cca-dp 네임스페이스에 web(레플리카 2)과 ClusterIP 서비스 web 을 띄우세요. 파드가 Ready 가 되면 VM 셸에서 iptables-save 를 읽어 /root/cca-datapath/iptables.json 에 기록합니다 — cluster_ip(서비스 web 의 ClusterIP), kube_svc_lines(KUBE-SVC 를 포함한 줄 수), cluster_ip_lines(그 ClusterIP 를 포함한 줄 수), cilium_lines(CILIUM 을 포함한 줄 수), kube_proxy_replacement(에이전트 cilium-dbg status 의 KubeProxyReplacement 값). 수는 숫자로 적습니다.

kube-proxy(iptables 모드)는 서비스마다 KUBE-SVC·KUBE-SEP 체인을 만들어 목적지를 바꿉니다. 그 흔적이 있는지 줄 수로 세어 보세요. grep -c 는 하나도 없을 때 종료 코드 1 을 내므로 set -e 아래에서는 조심하세요. 에이전트 명령은 kubectl -n kube-system exec ds/cilium -c cilium-agent -- cilium-dbg ... 로 실행합니다.

서비스 번호표를 BPF 맵에서 찾는다

에이전트의 cilium-dbg service list -o json 에서 cca-dp/web 의 ClusterIP 항목을 찾고, cilium-dbg bpf lb list 에서 같은 프런트엔드의 슬롯을 확인하세요. /root/cca-datapath/svc.jsonservice_id(숫자), frontend(ClusterIP:80), backends(백엔드 ip:port 2개 목록), backend_pods(그 IP 를 가진 web 파드 이름 2개) 를 기록합니다.

서비스 목록 JSON 의 spec.flags 에 이름·네임스페이스·유형이, spec.frontend-address 와 spec.backend-addresses 에 주소가 있습니다. bpf lb list 의 한 프런트엔드에는 괄호 속 번호 0 인 줄(서비스 자체)과 1 부터 시작하는 백엔드 줄이 있습니다. 파드 IP 는 kubectl get pod -o wide 와 맞춰 보세요.

레플리카를 늘리자 맵이 먼저 안다

web 을 레플리카 4개로 늘리세요. 네 파드가 Ready 가 되고 bpf lb list 의 web 프런트엔드 백엔드 슬롯이 4개가 될 때까지 짧게 폴링한 뒤, /root/cca-datapath/scale.jsonbackends(지금 맵에 있는 ip:port 4개), new_backends(svc.json 에 없던 것 2개) 를 기록합니다. EndpointSlice 의 ready 주소와도 같아야 합니다.

쿠버네티스는 EndpointSlice 를 갱신하고, 에이전트는 그것을 보고 서비스 맵의 백엔드 슬롯을 다시 씁니다. iptables 규칙 수천 줄을 다시 쓰는 대신 맵 항목 몇 개만 바뀝니다. 고정 sleep 대신 슬롯 수를 세는 루프를 쓰세요.

앱은 서비스에 붙었다고 믿지만 소켓은 이미 파드에 붙어 있다

kubectl apply -f /opt/fixtures/cca-datapath/holder.yaml 로 holder 파드를 띄우세요. holder 는 출발 포트 40404 로 web 서비스(ClusterIP:80)에 TCP 연결 하나를 열고 쥐고 있으며, 로그에 앱이 getpeername 으로 본 상대 주소를 찍습니다. 세 곳에서 같은 연결을 보세요 — holder 로그, holder 안 netstat -tn, 에이전트의 cilium-dbg bpf ct list global:40404 항목. /root/cca-datapath/ct.jsonclient(holderIP:40404), app_peer(로그의 상대 주소 ip:port), socket_peer(holder netstat 의 Foreign Address), backend(conntrack OUT 항목의 목적지 ip:port), backend_pod(그 IP 의 web 파드 이름), svc_entries(:40404 를 가진 TCP SVC 항목 수, 숫자), socket_lb_coverage(cilium-dbg status --verbose 의 Socket LB Coverage) 를 기록합니다. 백엔드 파드 안 netstat 에도 holder 의 IP:40404 가 보여야 합니다.

kube-proxy 대체의 소켓 LB 는 파드가 connect() 를 부르는 순간 cgroup 에 붙은 eBPF 가 목적지를 백엔드로 바꿉니다. 그래서 패킷에는 처음부터 서비스 주소가 없고, 앱에게는 getpeername 을 되돌려 서비스 주소를 보여 줍니다. 세 관측이 서로 다른 주소를 말하는 이유를 이 순서로 설명해 보세요. 백엔드가 보는 출발지가 노드 IP 로 바뀌었는지도 확인합니다.

파드 옆 장치에 붙은 프로그램과 그 파드만의 정책 맵

holder 의 엔드포인트 번호(kubectl -n cca-dp get cep holder 의 status.id)로 에이전트 cilium-dbg endpoint get <번호> -o json 을 읽어 호스트 쪽 장치 이름과 ifindex 를 찾고, VM 셸에서 bpftool net show dev <장치>tc filter show dev <장치> ingress 를 비교하세요. /root/cca-datapath/prog.jsonendpoint_id, interface, ifindex, attach(bpftool 이 보여 준 붙은 자리), program(프로그램 이름), policy_map(cilium-dbg map list 에서 찾은 이 엔드포인트의 정책 맵 이름), tc_filter_lines(tc filter 출력 줄 수, 숫자) 를 기록합니다.

Cilium 은 파드마다 호스트 쪽 veth(lxc…)에 프로그램을 붙이고, 파드마다 따로 정책 맵을 둡니다. 맵 이름의 끝 숫자와 엔드포인트 번호를 비교해 보세요. 최신 커널에서는 프로그램을 tcx 로 붙이는데, 옛 tc 명령은 tcx 부착을 보여 주지 않습니다.

리슨하는 프로세스가 없는데 NodePort 가 답한다

cca-dp 에 NodePort 서비스 web-np 를 만드세요 — selector app=web, port 80, targetPort 8080, nodePort 30780. 노드 InternalIP 의 30780 으로 VM 셸에서 요청해 200 을 확인하고, /root/cca-datapath/nodeport.jsonnode_ip, node_port, http_code(숫자), iptables_lines(iptables-save 에서 30780 을 포함한 줄 수), listen_sockets(ss -Htln 'sport = :30780' 줄 수), service_id(에이전트 목록의 0.0.0.0:30780 NodePort 항목 번호) 를 기록합니다.

NodePort 는 포트를 여는 프로세스가 아니라 노드 장치에 붙은 eBPF 프로그램이 받아 서비스 맵으로 보냅니다. 그래서 ss 로도 iptables 로도 보이지 않는 포트가 응답합니다. 서비스 목록 JSON 에서 frontend-address 의 ip 가 0.0.0.0 이고 flags.type 이 NodePort 인 항목을 고르세요.

파드가 하나도 없는 서비스는 기다리지 않는다

cca-dp 에 selector app=ghost, port 80 → targetPort 8080 인 ClusterIP 서비스 ghost 를 만드세요(그 라벨의 파드는 만들지 않습니다). curl 파드 probe(이미지 curlimages/curl:8.10.1@sha256:d9b4541e214bcd85196d6e92e2753ac6d0ea699f0af5741f8c6cccbfcf00ef4b, 명령 sleep 86400)를 띄워 그 안에서 ghost 의 ClusterIP 로 5초 제한 요청을 보냅니다. /root/cca-datapath/ghost.jsoncluster_ip, backends(에이전트 목록의 백엔드 수, 숫자), curl_exit(curl 종료 코드), seconds(curl 의 time_total, 숫자), no_backend_response(cilium-dbg config -a 의 ServiceNoBackendResponse 값) 를 기록합니다.

백엔드가 없을 때 데이터패스가 패킷을 조용히 버리면 클라이언트는 제한 시간까지 기다리고, 거절 응답을 돌려주면 곧바로 실패합니다. curl 종료 코드 7 과 28 이 무엇을 뜻하는지 비교하세요. curl 의 -w '%{time_total}' 은 실패해도 찍힙니다.

kube-proxy 가 하던 일의 새 주소록

/root/cca-datapath/report.txt키=값 일곱 줄을 씁니다 — kube_proxy_replacement, socket_lb_coverage(status --verbose 의 Socket LB Coverage), web_backends(지금 bpf lb list 의 web 백엔드 슬롯 수), holder_backend(지금 conntrack 에서 holder 연결이 향한 ip:port), holder_program(prog.json 의 program), nodeport_iptables_lines(지금 30780 을 포함한 iptables 줄 수), ghost_no_backend_response. 값은 모두 지금 상태와 앞 기록에 일치해야 합니다.

앞 단계의 파일을 다시 쓰는 대신 지금 상태를 다시 읽어 적으세요. holder 가 재시작해 다시 연결했다면 백엔드가 달라졌을 수 있으니 ct.json 도 새로 만들어야 합니다.