概述
k8s service 可以看做是多个 Pod 的负载均衡。有以下几种 service:
- LoadBalancer
- ClusterIP
- NodePort
- ExternalName
在 service 的演进中,从最初的 userspace 的方案变成 iptables 和 ipvs 的方案,其中,ipvs 主要是解决了 iptables 的性能问题。这篇文章主要分析 iptables 如何实现 service 的负载均衡。
ClusterIP
ClusterIP 是提供在集群中访问 Service 的方案,通常每个 Service 都会分配一个 VIP,然后为多个 Pod 提供负载均衡。这里我们创建两个副本的 nginx 部署,以及一个 nginx service。具体信息如下:
$ kubectl get endpoints nginx
NAME ENDPOINTS AGE
nginx 172.17.0.4:80,172.17.0.5:80 65m
$ kubectl get service nginx
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
nginx ClusterIP 10.111.67.225 <none> 80/TCP 65m
在集群中访问 nginx.default.svc.cluster.local
时,DNS 会将这个地址解析到 Service 的 IP 上,也就是 10.111.67.225
。下面我们看看 iptables 是如何将访问这个地址的流量转到真实的 Pod 上的。
首先看一下 nat 表上的 OUTPUT 链:
$ iptables -t nat -nL OUTPUT
Chain OUTPUT (policy ACCEPT)
target prot opt source destination
KUBE-SERVICES all -- 0.0.0.0/0 0.0.0.0/0 /* kubernetes service portals */
DOCKER all -- 0.0.0.0/0 !127.0.0.0/8 ADDRTYPE match dst-type LOCAL
第一条规则会匹配所有的流量,然后跳到 KUBE-SERVICES
这条链上。我们看一下 KUBE-SERVICES
的具体内容:
$ iptables -t nat -nL KUBE-SERVICES
Chain KUBE-SERVICES (2 references)
target prot opt source destination
KUBE-SVC-NPX46M4PTMTKRN6Y tcp -- 0.0.0.0/0 10.96.0.1 /* default/kubernetes:https cluster IP */ tcp dpt:443
KUBE-SVC-P4Q3KNUAWJVP4ILH tcp -- 0.0.0.0/0 10.111.67.225 /* default/nginx:http cluster IP */ tcp dpt:80
KUBE-SVC-TCOU7JCQXEZGVUNU udp -- 0.0.0.0/0 10.96.0.10 /* kube-system/kube-dns:dns cluster IP */ udp dpt:53
KUBE-SVC-ERIFXISQEP7F7OF4 tcp -- 0.0.0.0/0 10.96.0.10 /* kube-system/kube-dns:dns-tcp cluster IP */ tcp dpt:53
KUBE-SVC-JD5MR3NA4I4DYORP tcp -- 0.0.0.0/0 10.96.0.10 /* kube-system/kube-dns:metrics cluster IP */ tcp dpt:9153
KUBE-NODEPORTS all -- 0.0.0.0/0 0.0.0.0/0 /* kubernetes service nodeports; NOTE: this must be the last rule in this chain */ ADDRTYPE match dst-type LOCAL
这里前面的 KUBE-SVC-*
都是根据 destination, protocol 和目的端口号来匹配的,根据我们的 service 地址和端口号以及协议,可以定位到 KUBE-SVC-P4Q3KNUAWJVP4ILH
这条规则可以匹配,然后跳到这条链上。我们接着看这条链定义了什么:
$ iptables -t nat -nL KUBE-SVC-P4Q3KNUAWJVP4ILH
Chain KUBE-SVC-P4Q3KNUAWJVP4ILH (1 references)
target prot opt source destination
KUBE-SEP-GL7IUDQTUTXSADHR all -- 0.0.0.0/0 0.0.0.0/0 /* default/nginx:http */ statistic mode random probability 0.50000000000
KUBE-SEP-VMO3WCKZND6ZICDD all -- 0.0.0.0/0 0.0.0.0/0 /* default/nginx:http */
有两条规则,根据第一条规则后面的内容,我们可以知道这就是使用 iptables 实现负载均衡的地方了。第一条规则有 50% 的匹配几率。如果匹配到了其中一条,就会跳到另外一个链上。比如:
$ iptables -t nat -nL KUBE-SEP-GL7IUDQTUTXSADHR
Chain KUBE-SEP-GL7IUDQTUTXSADHR (1 references)
target prot opt source destination
KUBE-MARK-MASQ all -- 172.17.0.4 0.0.0.0/0 /* default/nginx:http */
DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 /* default/nginx:http */ tcp to:172.17.0.4:80
其中第一条规则的 source 是 Pod 的 IP,在访问 Service 时目前还不会匹配,于是我们看第二条规则,将目的 IP 和 Port 改写成 172.17.0.4:80,也就是我们的 Pod IP,这样流量就经过负载均衡指向了我们的 Pod了。
NodePort
我们将上面的 Service 改成 NodePort
nginx NodePort 10.111.67.225 <none> 80:30000/TCP 34h
然后查询机器上的 30000 端口。
$ ss -lp | grep 30000
tcp LISTEN 0 0 0.0.0.0:30000 0.0.0.0:* users:(("kube-proxy",pid=4006,fd=8))
可以看到, kube-proxy
监听了 30000 端口,同时我们看 nat 表上的 PREROUTING
链。
KUBE-SERVICES all -- 0.0.0.0/0 0.0.0.0/0 /* kubernetes service portals */
再看 KUBE-SERVICES
KUBE-SVC-TCOU7JCQXEZGVUNU udp -- 0.0.0.0/0 10.96.0.10 /* kube-system/kube-dns:dns cluster IP */ udp dpt:53
KUBE-SVC-ERIFXISQEP7F7OF4 tcp -- 0.0.0.0/0 10.96.0.10 /* kube-system/kube-dns:dns-tcp cluster IP */ tcp dpt:53
KUBE-SVC-JD5MR3NA4I4DYORP tcp -- 0.0.0.0/0 10.96.0.10 /* kube-system/kube-dns:metrics cluster IP */ tcp dpt:9153
KUBE-SVC-NPX46M4PTMTKRN6Y tcp -- 0.0.0.0/0 10.96.0.1 /* default/kubernetes:https cluster IP */ tcp dpt:443
KUBE-SVC-P4Q3KNUAWJVP4ILH tcp -- 0.0.0.0/0 10.111.67.225 /* default/nginx:http cluster IP */ tcp dpt:80
KUBE-NODEPORTS all -- 0.0.0.0/0 0.0.0.0/0 /* kubernetes service nodeports; NOTE: this must be the last rule in this chain */ ADDRTYPE match dst-type LOCAL
最后一条 KUBE-NODEPORTS
可以匹配到,这里有个匹配条件,那就是 ADDRTYPE match dst-type LOCAL
。注意这里的 LOCAL
指的是本机网卡上存在的地址,也就是这条数据是发到本机,那么就能匹配。
KUBE-NODEPORTS
的规则如下:
KUBE-MARK-MASQ tcp -- 0.0.0.0/0 0.0.0.0/0 /* default/nginx:http */ tcp dpt:30000
KUBE-SVC-P4Q3KNUAWJVP4ILH tcp -- 0.0.0.0/0 0.0.0.0/0 /* default/nginx:http */ tcp dpt:30000
第一条规则是替换源地址为本机出口的网卡地址。第二条规则如下:
KUBE-SEP-F3MS6OIYSABTYGOY all -- 0.0.0.0/0 0.0.0.0/0 /* default/nginx:http */ statistic mode random probability 0.50000000000
KUBE-SEP-VMO3WCKZND6ZICDD all -- 0.0.0.0/0 0.0.0.0/0 /* default/nginx:http */
这里我们在 ClusterIP
中就分析了实现方法,因此这里忽略。
LoadBalancer
LoadBalancer 本身不是由 Kubernetes 提供的,其原理说起来也不难,我们先创建一个 LoadBalancer 的 Service 看看:
nginx LoadBalancer 10.111.67.225 <pending> 80:32014/TCP 34h
这里因为我的本地集群没有 LoadBalancer,所以一直处于 Pending 状态。但是我们可以看到,这里还有一个 80:32014
。和上面的 NodePort 输出一致。也就是说创建 LoadBalancer 时,会在 Pod 所在的机器上开启 NodePort,然后由外部的 LoadBalancer 将负载均衡过的流量带到机器的指定的 NodePort 上。
一些有意思的参数
这里顺便多提几个有意思的Service 参数
externalTrafficPolicy
:可选值有 Local
和 Cluster
。
- Local: 流量只会被导向本机的 Pod,这样就少一次包的转发,提高性能。但是缺点是如果容易导致负载不均衡。
- Cluster: 在集群范围内转发流量
如果能保证 Pod 均匀的分布在不同的节点上,那么外部的 LoadBalancer 配合 Local 的 externalTrafficPolicy 可以带来更好的性能。
sessionAffinity
: 会话亲和性,可以设置为 ClientIP,来达到将同一个 IP 的会话转发到相同的 Pod 上。其也是通过 iptables 实现的。
KUBE-SEP-Q7ZFI57LOFFPF3HN all -- 0.0.0.0/0 0.0.0.0/0 /* test/nginx-session-affinity:http */ recent: CHECK seconds: 10800 reap name: KUBE-SEP-Q7ZFI57LOFFPF3HN side: source mask: 255.255.255.255
KUBE-SEP-LWUZWBNY6M3CYJ2M all -- 0.0.0.0/0 0.0.0.0/0 /* test/nginx-session-affinity:http */ recent: CHECK seconds: 10800 reap name: KUBE-SEP-LWUZWBNY6M3CYJ2M side: source mask: 255.255.255.255
KUBE-SEP-Q7ZFI57LOFFPF3HN all -- 0.0.0.0/0 0.0.0.0/0 /* test/nginx-session-affinity:http */ statistic mode random probability 0.50000000000
KUBE-SEP-LWUZWBNY6M3CYJ2M all -- 0.0.0.0/0 0.0.0.0/0 /* test/nginx-session-affinity:http */
这个 iptables 的前两条规则就是在做 iptables 的检查。