摘要:在 Kubernetes 中,Service 是一个 L4(TCP/UDP/SCTP) 负载均衡器,它使用 DNAT 将入站流量重定向到后端 Pod。
重定向操作由位于每个节点上的 kube-proxy 执行。
在这篇文章中,我们将介绍在 iptables 模式下,kube-proxy 是如何处理流量转发的。
mode 模式
kube-proxy 通过监听 kube-apiserver 添加和删除 Service 和 Endpoints 事件来触发路由规则的配置,目前支持三种不同的操作模式:userspace、iptables、ipvs。
userspace
在 userspace 模式下,对于每个服务,它在本地节点上打开一个端口(随机选择)。与此“代理端口”的任何连接都会被代理到服务的后端 Pod。因为它速度慢且过时,它不常用。
iptables
基于 Netfilter 构建。对于每个服务,它都会在节点上配置 iptables 规则,这些规则捕获到服务的 clusterIP 和端口的流量,并将该流量重定向到服务的后端 Pod,这是大多数平台的默认模式。
ipvs
ipvs 模式调用 netlink 接口相应地创建 IPVS 规则,并定期将 IPVS 规则与 Kubernetes 服务和 Endpoint 同步,它要求 Linux 内核加载 IPVS 模块。
externalTrafficPolicy
externalTrafficPolicy 表示服务是否希望将外部流量路由到节点本地或集群范围的 Endpoint,有两个可用选项:集群(默认)和本地。
Cluster
Cluster 是默认选项,并具有以下属性:
- 发送到类型是 NodePort 或 LoadBalancer 服务的数据包由节点的 IP 进行 SNAT。
- 如果外部数据包被路由到没有 pod 的节点,代理会将其转发到另一台主机上的 pod,这可能会导致额外的消耗。
- kubernetes 在集群内部进行均衡,这意味着在进行均衡时会考虑 pod 的数量,因此具有良好的整体负载均衡能力。
Local
Local 具有以下属性:
- 仅适用于服务类型是 NodePort 和 LoadBalancer。
- 数据包不会针对集群间或外部流量进行 SNAT。
- 当数据包到达没有对应 Pod 的节点时,它会被丢弃,这避免了节点之间的额外消耗。
- kubernetes 执行节点内的平衡,这意味着代理仅将负载分配给本地节点上的 pod,由外部负载来解决负载不平衡问题。
iptables 规则
以下是某个节点 Node 上的 iptables。
root@tanjunchen-worker:/# iptables-save
*mangle
:PREROUTING ACCEPT [58400:81718346]
:INPUT ACCEPT [58400:81718346]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [46534:3716583]
:POSTROUTING ACCEPT [46534:3716583]
:KUBE-IPTABLES-HINT - [0:0]
:KUBE-KUBELET-CANARY - [0:0]
:KUBE-PROXY-CANARY - [0:0]
COMMIT
*filter
:INPUT ACCEPT [219:37830]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [199:14977]
:KUBE-EXTERNAL-SERVICES - [0:0]
:KUBE-FIREWALL - [0:0]
:KUBE-FORWARD - [0:0]
:KUBE-KUBELET-CANARY - [0:0]
:KUBE-NODEPORTS - [0:0]
:KUBE-PROXY-CANARY - [0:0]
:KUBE-PROXY-FIREWALL - [0:0]
:KUBE-SERVICES - [0:0]
-A INPUT -m conntrack --ctstate NEW -m comment --comment "kubernetes load balancer firewall" -j KUBE-PROXY-FIREWALL
-A INPUT -m comment --comment "kubernetes health check service ports" -j KUBE-NODEPORTS
-A INPUT -m conntrack --ctstate NEW -m comment --comment "kubernetes externally-visible service portals" -j KUBE-EXTERNAL-SERVICES
-A INPUT -j KUBE-FIREWALL
-A FORWARD -m conntrack --ctstate NEW -m comment --comment "kubernetes load balancer firewall" -j KUBE-PROXY-FIREWALL
-A FORWARD -m comment --comment "kubernetes forwarding rules" -j KUBE-FORWARD
-A FORWARD -m conntrack --ctstate NEW -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
-A FORWARD -m conntrack --ctstate NEW -m comment --comment "kubernetes externally-visible service portals" -j KUBE-EXTERNAL-SERVICES
-A OUTPUT -m conntrack --ctstate NEW -m comment --comment "kubernetes load balancer firewall" -j KUBE-PROXY-FIREWALL
-A OUTPUT -m conntrack --ctstate NEW -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
-A OUTPUT -j KUBE-FIREWALL
-A KUBE-FIREWALL ! -s 127.0.0.0/8 -d 127.0.0.0/8 -m comment --comment "block incoming localnet connections" -m conntrack ! --ctstate RELATED,ESTABLISHED,DNAT -j DROP
-A KUBE-FIREWALL -m comment --comment "kubernetes firewall for dropping marked packets" -m mark --mark 0x8000/0x8000 -j DROP
-A KUBE-FORWARD -m conntrack --ctstate INVALID -j DROP
-A KUBE-FORWARD -m comment --comment "kubernetes forwarding rules" -m mark --mark 0x4000/0x4000 -j ACCEPT
-A KUBE-FORWARD -m comment --comment "kubernetes forwarding conntrack rule" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
COMMIT
*nat
:PREROUTING ACCEPT [1:60]
:INPUT ACCEPT [1:60]
:OUTPUT ACCEPT [1:60]
:POSTROUTING ACCEPT [1:60]
:DOCKER_OUTPUT - [0:0]
:DOCKER_POSTROUTING - [0:0]
:KIND-MASQ-AGENT - [0:0]
:KUBE-KUBELET-CANARY - [0:0]
:KUBE-MARK-DROP - [0:0]
:KUBE-MARK-MASQ - [0:0]
:KUBE-NODEPORTS - [0:0]
:KUBE-POSTROUTING - [0:0]
:KUBE-PROXY-CANARY - [0:0]
:KUBE-SEP-6E7XQMQ4RAYOWTTM - [0:0]
:KUBE-SEP-PUHFDAMRBZWCPADU - [0:0]
:KUBE-SEP-QKX4QX54UKWK6JIY - [0:0]
:KUBE-SEP-SF3LG62VAE5ALYDV - [0:0]
:KUBE-SEP-WXWGHGKZOCNYRYI7 - [0:0]
:KUBE-SEP-ZP3FB6NMPNCO4VBJ - [0:0]
:KUBE-SEP-ZXMNUKOKXUTL2MK2 - [0:0]
:KUBE-SERVICES - [0:0]
:KUBE-SVC-ERIFXISQEP7F7OF4 - [0:0]
:KUBE-SVC-JD5MR3NA4I4DYORP - [0:0]
:KUBE-SVC-NPX46M4PTMTKRN6Y - [0:0]
:KUBE-SVC-TCOU7JCQXEZGVUNU - [0:0]
-A PREROUTING -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
-A PREROUTING -d 172.18.0.1/32 -j DOCKER_OUTPUT
-A OUTPUT -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
-A OUTPUT -d 172.18.0.1/32 -j DOCKER_OUTPUT
-A POSTROUTING -m comment --comment "kubernetes postrouting rules" -j KUBE-POSTROUTING
-A POSTROUTING -d 172.18.0.1/32 -j DOCKER_POSTROUTING
-A POSTROUTING -m addrtype ! --dst-type LOCAL -m comment --comment "kind-masq-agent: ensure nat POSTROUTING directs all non-LOCAL destination traffic to our custom KIND-MASQ-AGENT chain" -j KIND-MASQ-AGENT
-A DOCKER_OUTPUT -d 172.18.0.1/32 -p tcp -m tcp --dport 53 -j DNAT --to-destination 127.0.0.11:44591
-A DOCKER_OUTPUT -d 172.18.0.1/32 -p udp -m udp --dport 53 -j DNAT --to-destination 127.0.0.11:58922
-A DOCKER_POSTROUTING -s 127.0.0.11/32 -p tcp -m tcp --sport 44591 -j SNAT --to-source 172.18.0.1:53
-A DOCKER_POSTROUTING -s 127.0.0.11/32 -p udp -m udp --sport 58922 -j SNAT --to-source 172.18.0.1:53
-A KIND-MASQ-AGENT -d 10.244.0.0/16 -m comment --comment "kind-masq-agent: local traffic is not subject to MASQUERADE" -j RETURN
-A KIND-MASQ-AGENT -m comment --comment "kind-masq-agent: outbound traffic is subject to MASQUERADE (must be last in chain)" -j MASQUERADE
-A KUBE-MARK-DROP -j MARK --set-xmark 0x8000/0x8000
-A KUBE-MARK-MASQ -j MARK --set-xmark 0x4000/0x4000
-A KUBE-POSTROUTING -m mark ! --mark 0x4000/0x4000 -j RETURN
-A KUBE-POSTROUTING -j MARK --set-xmark 0x4000/0x0
-A KUBE-POSTROUTING -m comment --comment "kubernetes service traffic requiring SNAT" -j MASQUERADE --random-fully
-A KUBE-SEP-6E7XQMQ4RAYOWTTM -s 10.244.0.3/32 -m comment --comment "kube-system/kube-dns:dns" -j KUBE-MARK-MASQ
-A KUBE-SEP-6E7XQMQ4RAYOWTTM -p udp -m comment --comment "kube-system/kube-dns:dns" -m udp -j DNAT --to-destination 10.244.0.3:53
-A KUBE-SEP-PUHFDAMRBZWCPADU -s 10.244.0.4/32 -m comment --comment "kube-system/kube-dns:metrics" -j KUBE-MARK-MASQ
-A KUBE-SEP-PUHFDAMRBZWCPADU -p tcp -m comment --comment "kube-system/kube-dns:metrics" -m tcp -j DNAT --to-destination 10.244.0.4:9153
-A KUBE-SEP-QKX4QX54UKWK6JIY -s 172.18.0.3/32 -m comment --comment "default/kubernetes:https" -j KUBE-MARK-MASQ
-A KUBE-SEP-QKX4QX54UKWK6JIY -p tcp -m comment --comment "default/kubernetes:https" -m tcp -j DNAT --to-destination 172.18.0.3:6443
-A KUBE-SEP-SF3LG62VAE5ALYDV -s 10.244.0.4/32 -m comment --comment "kube-system/kube-dns:dns-tcp" -j KUBE-MARK-MASQ
-A KUBE-SEP-SF3LG62VAE5ALYDV -p tcp -m comment --comment "kube-system/kube-dns:dns-tcp" -m tcp -j DNAT --to-destination 10.244.0.4:53
-A KUBE-SEP-WXWGHGKZOCNYRYI7 -s 10.244.0.4/32 -m comment --comment "kube-system/kube-dns:dns" -j KUBE-MARK-MASQ
-A KUBE-SEP-WXWGHGKZOCNYRYI7 -p udp -m comment --comment "kube-system/kube-dns:dns" -m udp -j DNAT --to-destination 10.244.0.4:53
-A KUBE-SEP-ZP3FB6NMPNCO4VBJ -s 10.244.0.3/32 -m comment --comment "kube-system/kube-dns:metrics" -j KUBE-MARK-MASQ
-A KUBE-SEP-ZP3FB6NMPNCO4VBJ -p tcp -m comment --comment "kube-system/kube-dns:metrics" -m tcp -j DNAT --to-destination 10.244.0.3:9153
-A KUBE-SEP-ZXMNUKOKXUTL2MK2 -s 10.244.0.3/32 -m comment --comment "kube-system/kube-dns:dns-tcp" -j KUBE-MARK-MASQ
-A KUBE-SEP-ZXMNUKOKXUTL2MK2 -p tcp -m comment --comment "kube-system/kube-dns:dns-tcp" -m tcp -j DNAT --to-destination 10.244.0.3:53
-A KUBE-SERVICES -d 10.96.0.10/32 -p udp -m comment --comment "kube-system/kube-dns:dns cluster IP" -m udp --dport 53 -j KUBE-SVC-TCOU7JCQXEZGVUNU
-A KUBE-SERVICES -d 10.96.0.10/32 -p tcp -m comment --comment "kube-system/kube-dns:dns-tcp cluster IP" -m tcp --dport 53 -j KUBE-SVC-ERIFXISQEP7F7OF4
-A KUBE-SERVICES -d 10.96.0.10/32 -p tcp -m comment --comment "kube-system/kube-dns:metrics cluster IP" -m tcp --dport 9153 -j KUBE-SVC-JD5MR3NA4I4DYORP
-A KUBE-SERVICES -d 10.96.0.1/32 -p tcp -m comment --comment "default/kubernetes:https cluster IP" -m tcp --dport 443 -j KUBE-SVC-NPX46M4PTMTKRN6Y
-A KUBE-SERVICES -m comment --comment "kubernetes service nodeports; NOTE: this must be the last rule in this chain" -m addrtype --dst-type LOCAL -j KUBE-NODEPORTS
-A KUBE-SVC-ERIFXISQEP7F7OF4 ! -s 10.244.0.0/16 -d 10.96.0.10/32 -p tcp -m comment --comment "kube-system/kube-dns:dns-tcp cluster IP" -m tcp --dport 53 -j KUBE-MARK-MASQ
-A KUBE-SVC-ERIFXISQEP7F7OF4 -m comment --comment "kube-system/kube-dns:dns-tcp -> 10.244.0.3:53" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-ZXMNUKOKXUTL2MK2
-A KUBE-SVC-ERIFXISQEP7F7OF4 -m comment --comment "kube-system/kube-dns:dns-tcp -> 10.244.0.4:53" -j KUBE-SEP-SF3LG62VAE5ALYDV
-A KUBE-SVC-JD5MR3NA4I4DYORP ! -s 10.244.0.0/16 -d 10.96.0.10/32 -p tcp -m comment --comment "kube-system/kube-dns:metrics cluster IP" -m tcp --dport 9153 -j KUBE-MARK-MASQ
-A KUBE-SVC-JD5MR3NA4I4DYORP -m comment --comment "kube-system/kube-dns:metrics -> 10.244.0.3:9153" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-ZP3FB6NMPNCO4VBJ
-A KUBE-SVC-JD5MR3NA4I4DYORP -m comment --comment "kube-system/kube-dns:metrics -> 10.244.0.4:9153" -j KUBE-SEP-PUHFDAMRBZWCPADU
-A KUBE-SVC-NPX46M4PTMTKRN6Y ! -s 10.244.0.0/16 -d 10.96.0.1/32 -p tcp -m comment --comment "default/kubernetes:https cluster IP" -m tcp --dport 443 -j KUBE-MARK-MASQ
-A KUBE-SVC-NPX46M4PTMTKRN6Y -m comment --comment "default/kubernetes:https -> 172.18.0.3:6443" -j KUBE-SEP-QKX4QX54UKWK6JIY
-A KUBE-SVC-TCOU7JCQXEZGVUNU ! -s 10.244.0.0/16 -d 10.96.0.10/32 -p udp -m comment --comment "kube-system/kube-dns:dns cluster IP" -m udp --dport 53 -j KUBE-MARK-MASQ
-A KUBE-SVC-TCOU7JCQXEZGVUNU -m comment --comment "kube-system/kube-dns:dns -> 10.244.0.3:53" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-6E7XQMQ4RAYOWTTM
-A KUBE-SVC-TCOU7JCQXEZGVUNU -m comment --comment "kube-system/kube-dns:dns -> 10.244.0.4:53" -j KUBE-SEP-WXWGHGKZOCNYRYI7
COMMIT
当创建 Service 或 Endpoint 时,iptables 链会对 pod 和服务之间执行各种过滤和 NAT 操作,具体的 iptables 链如下所示:
- KUBE-SERVICES 是服务数据包的入口点,它的作用是匹配目标 IP:Port 并将数据包分派到相应的 KUBE-SVC-* 链。
- KUBE-SVC-* 充当负载均衡器,将数据包分发到 KUBE-SEP-* 链。KUBE-SEP-* 的数量等于 service 后面的 endpoint 数量,选择哪个 KUBE-SEP-* 是随机确定的。
- KUBE-SEP-* 代表服务 endpoint,它只是进行 DNAT,将服务 IP:Port 替换为 pod 的 endpoint IP:Port。
- KUBE-MARK-MASQ 向源自集群外部、发往服务的数据包添加 Netfilter 标记,带有此标记的数据包将在 POSTROUTING 规则中进行更改,以使用源网络地址转换 (SNAT),并将节点的 IP 地址作为其源 IP 地址。
- KUBE-MARK-DROP 向此时尚未启用目标 NAT 的数据包添加 Netfilter 标记,这些数据包将在 KUBE-FIREWALL 链中被丢弃。
- KUBE-FIREWALL 链对 LoadBalancer 服务生效,它将目标 IP 与服务的负载均衡器 IP 进行匹配,并将数据包分发到相应的 KUBE-SVC-* 链(externalTrafficPolicy: Cluster)或 KUBE-XLB-* 链(externalTrafficPolicy:Local)。
- KUBE-NODEPORTS 链对 NodePort 和 LoadBalancer 服务生效,外部源可以通过节点端口访问服务。它匹配节点端口,并将数据包分发到相应的 KUBE-SVC-* 链(externalTrafficPolicy:Cluster)或 KUBE-XLB-* 链(externalTrafficPolicy:Local)。
- 当 externalTrafficPolicy 设置为 Local 时,KUBE-XLB-* 链可以正常工作,如果节点没有保留相关 endpoint,则数据包将被丢弃。
Kubernetes 上的 kube-proxy 总体流程如下所示:
案例
现在我们已经了解了 kube-proxy 的基本概念,让我们在实践中认识一下它们。
环境准备
测试 demo 如下所示:
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: echo
name: echo
spec:
replicas: 1
selector:
matchLabels:
app: echoserver
template:
metadata:
labels:
app: echoserver
spec:
containers:
- image: ealen/echo-server:latest
imagePullPolicy: Always
name: echoserver
ports:
- containerPort: 80
env:
- name: PORT
value: "80"
登录到 node 上进行调试的工具,Yaml 如下所示:
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: node-shell-debug
spec:
selector:
matchLabels:
app: node-shell-debug
template:
metadata:
labels:
app: node-shell-debug
sidecar.istio.io/inject: "false"
spec:
containers:
- args:
- -t
- "1"
- -m
- -u
- -i
- -n
- sleep
- "140000000"
command:
- nsenter
image: docker.io/tanjunchen/node-shell:dev
imagePullPolicy: Always
name: shell
securityContext:
privileged: true
hostIPC: true
hostNetwork: true
hostPID: true
环境如下所示:
NodePort
Kubernetes 通过部署 NodePort 类型的服务以 nodeIP:nodePort 的方式暴露服务,kubernetes 创建了一个 ClusterIP 服务,NodePort 服务将路由到该 ClusterIP 服务,同时在所有 Node 上打开特定端口(节点端口),以及任何来自该服务的流量发送到此端口的数据将被转发到目标 pod。
用于执行 NodePort 流量的 iptables 规则的入口点是 ‘-A KUBE-SERVICES -m comment –comment “kubernetes service nodeports; this must be the last rule in this chain”-m addrtype –dst-type LOCAL -j KUBE-NODEPORTS’,这条规则是 KUBE-SERVICES 链中的最后一条,它表明如果访问数据包不匹配之前的规则链,都会被转发到 KUBE-NODEPORTS 链中进行进一步处理,登录到节点上查看 iptables 规则,如下所示:
kubectl exec -it node-shell-debug-hqx8g bash
kubectl exec [POD] [COMMAND] is DEPRECATED and will be removed in a future version. Use kubectl exec [POD] -- [COMMAND] instead.
groups: cannot find name for group ID 11
To run a command as administrator (user "root"), use "sudo <command>".
See "man sudo_root" for details.
root@ig-m6f5hd49-2ie613z6-ffcfq1uq:/# iptables -t nat -L KUBE-SERVICES
Chain KUBE-SERVICES (2 references)
target prot opt source destination
KUBE-MARK-MASQ all -- !10.0.0.0/16 anywhere /* Kubernetes service cluster ip + port for masquerade purpose */ match-set KUBE-CLUSTER-IP dst,dst
KUBE-NODE-PORT all -- anywhere anywhere ADDRTYPE match dst-type LOCAL
ACCEPT all -- anywhere anywhere match-set KUBE-CLUSTER-IP dst,dst
在下面的小节中,我们将讨论两种类型的 NodePort 服务(externalTrafficPolicy):Cluster、Local。 没有自定义的 NodePort 服务将创建一个 externalTrafficPolicy: Cluster 服务,如下所示:
apiVersion: v1
kind: Service
metadata:
name: echo-np
spec:
type: NodePort
ports:
- port: 8711
targetPort: 8080
selector:
app: echoserver
创建服务,如下所示:
root@ig-m6f5hd49-2ie613z6-ffcfq1uq:/# ps aux | grep kube-proxy
root 22228 0.1 0.5 746608 43004 ? Ssl Sep15 3:01 /usr/local/bin/kube-proxy --config=/etc/kubernetes/proxy.yaml --logtostderr=true --v=2
root 683881 0.0 0.0 3436 720 pts/0 S+ 17:15 0:00 grep --color=auto kube-proxy
从上面我们可以看到,端口 30642 被分配给服务 “echo-np”,在每个节点上,kube-proxy 为其分配一个监听端口。
从 iptables 的角度来看,每两组链和规则分别添加到链 KUBE-SERVICES 和 KUBE-NODE-PORT 中:
iptables -t nat -L KUBE-SERVICES
Chain KUBE-SERVICES (2 references)
target prot opt source destination
KUBE-MARK-MASQ all -- !10.0.0.0/16 anywhere /* Kubernetes service cluster ip + port for masquerade purpose */ match-set KUBE-CLUSTER-IP dst,dst
KUBE-NODE-PORT all -- anywhere anywhere ADDRTYPE match dst-type LOCAL
ACCEPT all -- anywhere anywhere match-set KUBE-CLUSTER-IP dst,dst
使用 “externalTrafficPolicy: Local” 将保留源 IP 并丢弃来自没有本地端点的节点的数据包,如下所示:
apiVersion: v1
kind: Service
metadata:
name: echo-local
spec:
ports:
- port: 8711
targetPort: 8080
selector:
app: echo
type: NodePort
externalTrafficPolicy: Local
KUBE-NODEPORTS 的链在具有本地端点和没有本地端点的节点之间是不同的。
ClusterIP
通过不同的配置,Service ClusterIP 服务可以分为 5 中类型:normal、not headless、session affinity、external ip、no endpoints、headless。
具体流量转发规则跟上述类似,在此不在重复,有兴趣的可以继续探索 kube-proxy 中的 iptables 奥秘!
参考
- https://ealenn.github.io/Echo-Server/pages/quick-start/kubernetes.html
- https://msazure.club/kubernetes-services-and-iptables/
- https://serenafeng.github.io/2020/03/26/kube-proxy-in-iptables-mode/