k8s系列11-cilium部署KubeProxyReplacement模式

本文最后更新于:December 22, 2022 pm

本文主要介绍在使用了Cilium的K8S集群中如何开启KubeProxyReplacement功能来替代K8S集群原生的kube-proxy组件,同时还会介绍Cilium的几个特色功能如Maglev一致性哈希、DSR模式、Socket旁路和XDP加速等。

关于本文实操使用的K8S集群的部署过程可以参考上一篇文章k8s系列10-使用kube-router和cilium部署BGP模式的k8s集群。此前写的一些关于k8s基础知识和集群搭建的一些方案,有需要的同学可以看一下。

1、配置KubeProxyReplacement

1.1 检查集群状态

关于使用cilium替代kube-proxy的官方文档可以参考这里,需要注意的是我们这里是在已经部署好cilium和kube-proxy的K8S集群上面操作,因此会和官方的全新初始化安装稍有不同,但是大致原理类似。

首先检查一下我们现在的cilium状态,可以看到KubeProxyReplacement参数默认是设置为Disabled

1
2
3
$ kubectl -n kube-system exec ds/cilium -- cilium status | grep KubeProxyReplacement
Defaulted container "cilium-agent" out of: cilium-agent, mount-cgroup (init), apply-sysctl-overwrites (init), mount-bpf-fs (init), clean-cilium-state (init)
KubeProxyReplacement: Disabled

再检查一下集群中的kube-proxy组件状态,可以看到各个组件都工作正常。

1
2
3
4
5
6
7
8
9
10
11
$ kubectl get pods -n kube-system | grep kube-proxy
kube-proxy-86g7b 1/1 Running 1 (18h ago) 19h
kube-proxy-gqbd4 1/1 Running 1 (18h ago) 19h
kube-proxy-gtcqc 1/1 Running 1 (18h ago) 19h
kube-proxy-kdjr9 1/1 Running 1 (18h ago) 19h
kube-proxy-pbj8s 1/1 Running 1 (18h ago) 19h
kube-proxy-tvltv 1/1 Running 1 (18h ago) 19h
$ kubectl get ds -n kube-system | grep kube-proxy
kube-proxy 6 6 6 6 6 kubernetes.io/os=linux 45h
$ kubectl get cm -n kube-system | grep kube-proxy
kube-proxy 2 45h

1.2 删除kube-proxy

在删除kube-proxy之前我们先备份一下相关的配置

1
2
3
4
5
6
# 在master节点上备份kube-proxy相关的配置
$ kubectl get ds -n kube-system kube-proxy -o yaml > kube-proxy-ds.yaml
$ kubectl get cm -n kube-system kube-proxy -o yaml > kube-proxy-cm.yaml

# 在每台机器上面使用root权限备份一下iptables规则
$ iptables-save > kube-proxy-iptables-save.bak

接下来我们删除kube-proxy相关的daemonsetconfigmapiptables规则和ipvs规则。

1
2
3
4
5
6
7
8
9
# 删除掉kube-proxy这个daemonset
$ kubectl -n kube-system delete ds kube-proxy

# 删除掉kube-proxy的configmap,防止以后使用kubeadm升级K8S的时候重新安装了kube-proxy(1.19版本之后的K8S)
$ kubectl -n kube-system delete cm kube-proxy

# 在每台机器上面使用root权限清除掉iptables规则和ipvs规则
$ iptables-save | grep -v KUBE | iptables-restore
$ ipvsadm -C

1.3 配置cilium

删除kube-proxy之后此时的K8S集群应该会处于不正常工作的状态,不用紧张,现在我们开启ciliumkube-proxy-replacement功能来替代kube-proxy

因为我们集群已经部署了cililum,因此我们可以直接通过修改configmap的方式开启kube-proxy-replacement;修改cilium-config,将kube-proxy-replacement: disabled修改为kube-proxy-replacement: strict

1
2
3
4
5
6
7
$ kubectl edit cm -n kube-system cilium-config
configmap/cilium-config edited

$ kubectl get cm -n kube-system cilium-config -o yaml | grep kube-proxy-replacement
kube-proxy-replacement: strict

$ kubectl rollout restart ds/cilium deployment/cilium-operator -n kube-system

另外默认情况下cilium不会允许集群外的机器访问clusterip,需要开启这个功能的话可以在配置中添加bpf-lb-external-clusterip: "true"

1
2
$ kubectl get cm -n kube-system cilium-config -o yaml | grep bpf-lb-external-clusterip
bpf-lb-external-clusterip: "true"

如果是使用helm来初始化安装或者是更新的话可以考虑添加下面的这两个参数,最后的效果应该是一致的:

需要特别注意如果一开始就是用helm部署的话,这里要加上参数手动指定k8s的apiserver地址和端口。

1
2
3
4
5
6
7
8
9
10
# 使用helm来更新的话可以添加下面的这几个参数
--set kubeProxyReplacement=strict \
--set bpf.lbExternalClusterIP=true \
--set k8sServiceHost=k8s-cilium-apiserver.tinychen.io \
--set k8sServicePort=8443 \

# 检查一下更新之后的configmap
$ kubectl get cm -n kube-system cilium-config -o yaml | egrep "bpf-lb-external-clusterip|kube-proxy-replacement"
bpf-lb-external-clusterip: "true"
kube-proxy-replacement: strict

个人建议使用helm和configmap来管理cilium的配置这两种方式挑一种即可,不要两个方式混用,避免一些配置被覆盖

Cilium的参数比较多,在helm和configmap中的名字不一样,可以参考github上面的配置官方给出的helm说明

1.4 检测cilium

cilium重启完成之后检测相关的服务状态:

1
2
3
$ kubectl -n kube-system exec ds/cilium -- cilium status | grep KubeProxyReplacement
Defaulted container "cilium-agent" out of: cilium-agent, mount-cgroup (init), apply-sysctl-overwrites (init), mount-bpf-fs (init), clean-cilium-state (init)
KubeProxyReplacement: Strict [eth0 10.31.80.4 (Direct Routing)]

注意这时候cilium里面能够看到的Service Type应该还包含了LoadBalancerNodePort等类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
$ kubectl -n kube-system exec ds/cilium -- cilium service list
Defaulted container "cilium-agent" out of: cilium-agent, mount-cgroup (init), apply-sysctl-overwrites (init), mount-bpf-fs (init), clean-cilium-state (init)
ID Frontend Service Type Backend
1 10.32.169.6:80 ClusterIP 1 => 10.32.3.93:80 (active)
2 => 10.32.3.85:80 (active)
3 => 10.32.5.204:80 (active)
4 => 10.32.4.25:80 (active)
2 10.32.192.192:80 LoadBalancer 1 => 10.32.3.93:80 (active)
2 => 10.32.3.85:80 (active)
3 => 10.32.5.204:80 (active)
4 => 10.32.4.25:80 (active)
3 10.32.176.164:8080 ClusterIP 1 => 10.32.3.93:80 (active)
2 => 10.32.3.85:80 (active)
3 => 10.32.5.204:80 (active)
4 => 10.32.4.25:80 (active)
4 10.31.80.3:30088 NodePort 1 => 10.32.3.93:80 (active)
2 => 10.32.3.85:80 (active)
3 => 10.32.5.204:80 (active)
4 => 10.32.4.25:80 (active)
5 0.0.0.0:30088 NodePort 1 => 10.32.3.93:80 (active)
2 => 10.32.3.85:80 (active)
3 => 10.32.5.204:80 (active)
4 => 10.32.4.25:80 (active)
6 10.32.131.127:443 ClusterIP 1 => 10.31.80.6:4244 (active)
2 => 10.31.80.3:4244 (active)
3 => 10.31.80.1:4244 (active)
4 => 10.31.80.5:4244 (active)
5 => 10.31.80.4:4244 (active)
6 => 10.31.80.2:4244 (active)
7 10.32.171.0:80 ClusterIP 1 => 10.32.4.114:4245 (active)
8 10.32.128.10:53 ClusterIP 1 => 10.32.5.239:53 (active)
2 => 10.32.5.21:53 (active)
9 10.32.128.10:9153 ClusterIP 1 => 10.32.5.239:9153 (active)
2 => 10.32.5.21:9153 (active)
10 10.32.184.206:80 ClusterIP 1 => 10.32.3.138:8081 (active)
11 10.31.80.3:30081 NodePort 1 => 10.32.3.138:8081 (active)
12 0.0.0.0:30081 NodePort 1 => 10.32.3.138:8081 (active)
13 10.32.172.208:80 ClusterIP 1 => 10.32.3.93:80 (active)
2 => 10.32.3.85:80 (active)
3 => 10.32.5.204:80 (active)
4 => 10.32.4.25:80 (active)
14 10.32.192.0:80 LoadBalancer 1 => 10.32.3.93:80 (active)
2 => 10.32.3.85:80 (active)
3 => 10.32.5.204:80 (active)
4 => 10.32.4.25:80 (active)
15 10.32.188.57:8080 ClusterIP 1 => 10.32.5.220:8080 (active)
16 10.31.80.3:31243 NodePort 1 => 10.32.5.220:8080 (active)
17 0.0.0.0:31243 NodePort 1 => 10.32.5.220:8080 (active)
18 10.32.142.224:8080 ClusterIP 1 => 10.32.4.248:8080 (active)
19 0.0.0.0:31578 NodePort 1 => 10.32.4.248:8080 (active)
20 10.31.80.3:31578 NodePort 1 => 10.32.4.248:8080 (active)
21 10.32.128.1:443 ClusterIP 1 => 10.31.80.1:6443 (active)
2 => 10.31.80.2:6443 (active)
3 => 10.31.80.3:6443 (active)

此时再查看ipvs规则可以看到为空,重启节点后对应的kube-ipvs0网卡也会消失,iptables中的相关规则也不会再生成。

1
2
3
4
5
6
7
8
9
10
11
12
$ ipvsadm -ln
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
-> RemoteAddress:Port Forward Weight ActiveConn InActConn

$ ip link show kube-ipvs0
Device "kube-ipvs0" does not exist.


$ iptables-save | grep KUBE-SVC
[ empty line ]

此前我们已经配置了该集群的podIP、clusterIP和loadbalancerIP均为集群外路由可达,即可ping通,可正常请求。此时已经开启了cilium的kube-proxy-replacement模式之后,只有pod IP是能正常ping通并请求的;loadbalancerIPclusterIP都是无法ping通,但是能正常请求;主要原因是cilium本身的eBPF代码默认并没有对loadbalancerIPclusterIP的icmp数据包进行处理,导致ping请求无法被响应。

kube-proxy + kube-router without kube-proxy + kube-router
集群内:Pod IP ping测试:Y
TCP测试:Y
UDP测试:Y
ping测试:Y
TCP测试:Y
UDP测试:Y
集群内:Cluster IP ping测试:Y
TCP测试:Y
UDP测试:Y
ping测试:N
TCP测试:Y
UDP测试:Y
集群内:LoadBalancer IP ping测试:Y
TCP测试:Y
UDP测试:Y
ping测试:N
TCP测试:Y
UDP测试:Y
集群外:Pod IP ping测试:Y
TCP测试:Y
UDP测试:Y
ping测试:Y
TCP测试:Y
UDP测试:Y
集群外:Cluster IP ping测试:Y
TCP测试:Y
UDP测试:Y
ping测试:N
TCP测试:Y
UDP测试:Y
集群外:LoadBalancer IP ping测试:Y
TCP测试:Y
UDP测试:Y
ping测试:N
TCP测试:Y
UDP测试:Y

2、一致性哈希

Cilium官方声称已经实现了完整的四七层代理,因此在切换到它的KubeProxyReplacement模式之后我们可以使用一些cilium的特有功能,比如一致性哈希。

在负载均衡算法中使用传统的哈希算法时,增减一个后端节点都会导致几乎所有的哈希规则进行重新映射,使得原先的客户端会被转发到新的后端节点,这对于缓存服务器来说是非常不友好的。因此在这种场景下一般会使用一致性哈希算法,其特点是当哈希表槽位数(大小)的改变平均只需要对K/n 个关键字重新映射,其中K是关键字的数量,n是槽位数量。也就是说使用了一致性哈希算法之后,可以大幅度减小后端节点变化带来的哈希映射关系变动,从而使得请求转发更加的均匀稳定。

一致性哈希算法有很多种具体实现,而Cilium主要是通过实现谷歌此前开源的Maglev的一种变体来达到一致性哈希的效果,这提高了发生故障时的弹性并提供了更好的负载平衡属性。因为添加到集群的节点将在整个集群中为给定的 5 元组做出一致的后端选择,而无需与其他节点同步状态。Cilium声称可以在后端出现变动的时候将影响控制到1%以内。

这里需要解释一下这两个特定于 Maglev 的参数:maglev.tableSizemaglev.hashSeed

maglev.tableSize用来指定maglev查找单个服务的表的大小,maglev建议该值(M) 应远大于实际的最大后端节点数量 (N),实际上我们为了实现后端出现变动的时候将影响控制到1%以内的目标,需要将M的值设置成大于N的100倍才比较合理,另外M必须是一个质数,Cilium默认将M设置为16381(对应最大后端节点数量在160左右)。下表是cilium给出的一些可以使用的参考值,我们可以根据自己的实际业务进行调整:

maglev.tableSize value
251
509
1021
2039
4093
8191
16381
32749
65521
131071

maglev.hashSeed则是用来设置maglev算法的seed值,官方推荐设置,这样就不需要依赖内置的seed值。该值每台机器需要一致,这样才能保证每台机器的哈希结果一致。maglev.hashSeed应该是一个base64编码的12位随机字符串,可以使用head -c12 /dev/urandom | base64 -w0命令生成。

配置maglev算法和hash相关参数,主要配置三个值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 首先随机生成一个SEED值用于哈希算法
$ head -c12 /dev/urandom | base64 -w0
djthA7ezcQmtolON

$ kubectl get cm -n kube-system cilium-config -o yaml | egrep "bpf-lb-algorithm|bpf-lb-maglev-hash-seed|bpf-lb-maglev-table-size"
bpf-lb-algorithm: maglev
bpf-lb-maglev-hash-seed: djthA7ezcQmtolON
bpf-lb-maglev-table-size: "65521"


SEED=$(head -c12 /dev/urandom | base64 -w0)
# 使用helm来更新的话可以添加下面的参数
--set loadBalancer.algorithm=maglev \
--set maglev.tableSize=65521 \
--set maglev.hashSeed=$SEED \

注意开启了maglev一致性哈希之后,因为需要维护哈希表,所以cilium的ds进程会占用更多的内存;同时需要注意的是maglev一致性哈希只会对集群外部的流量生效(即通过nodeport、externalIP等方式进来的流量),因为对于集群内部的东西流量,往往都是直接转发到对应的pod上面,不需要经过中间的选择转发环节,也就没有maglev一致性哈希的工作环节了。当然,Cilium的一致性哈希支持XDP加速

3、Direct Server Return (DSR)

默认情况下,Cilium 的 eBPF NodePort转发是通过SNAT模式实现的,也就是说,当节点外部的流量通过诸如LoadBalancer、NodePort等方式进入集群内,并且需要转发到非本node的pod上面时,该node将通过执行SNAT转换将请求转发到对应的后端pod上面。这不需要对数据包的MTU进行变更,代价是后端pod回复数据包的时候需要再次经由node进行reverse SNAT再把请求发回给客户端。

Cilium提供了一个DSR模式来对此场景进行优化,即当数据包转发给后端pod之后,由pod直接返回给客户端,而不再经由node进行转发回复,这样可以减少一跳的转发,并且减少了一次NAT转换。DSR模式需要cilium工作在Native-Routing模式,如果是使用了隧道模式,那么将无法正常工作。

DSR模式相较于SNAT模式的另一个优势就是能够保留客户端的源IP,即后端服务能够根据客户端IP进行更灵活的控制策略。考虑到一个后端pod实际上可能会被多个SVC同时使用,Cilium会在IPv4 Options/IPv6 Destination Option extension header中编码特定的cilium信息,将对应的service IP/port信息传递给后端pod,对应的代价就是报文用来传递实际业务数据的MTU会减小。对于 TCP 服务,Cilium 只对 SYN 数据包的service IP/port进行编码,而不会对后续数据包进行编码。

Cilium还支持混合DSR和SNAT模式,即对TCP连接进行DSR,对UDP连接进行SNAT。当workload主要使用TCP进行数据传输的时候,这可以有效地折中优化转发链路减小MTU两者的影响。

注意在某些公有云环境中DSR模式可能会不生效,因为底层网络可能会丢弃Cilium特定的IP数据包。如果处理请求的pod不在接受nodeport请求的node上面,那么出现连接问题的时候优先确定数据包是否转发到了对应pod所在的节点上。如果不是这种情况,则建议切换回默认 SNAT 模式作为解决方法。此外,在某些实施源/目标 IP 地址检查(例如 AWS)的公共云提供商环境中,必须禁用检查才能使 DSR 模式工作。

设置loadBalancer.modedsr,将对所有服务(TCP+UDP)都使用DSR模式。

1
2
3
4
5
6
$ kubectl get cm -n kube-system cilium-config -o yaml | grep bpf-lb-mode
bpf-lb-mode: dsr
$ kubectl rollout restart ds/cilium deployment/cilium-operator -n kube-system

# 使用helm来更新的话可以添加下面的参数
--set loadBalancer.mode=dsr

设置loadBalancer.modehybrid,将对TCP服务使用DSR模式,UDP服务使用SNT模式。

1
2
3
4
5
6
$ kubectl get cm -n kube-system cilium-config -o yaml | grep bpf-lb-mode
bpf-lb-mode: hybrid
$ kubectl rollout restart ds/cilium deployment/cilium-operator -n kube-system

# 使用helm来更新的话可以添加下面的参数
--set loadBalancer.mode=hybrid

设置完成之后我们部署一个测试服务,直接返回remote_addrremote_port,用来验证DSR模式是否正常工作:

key value
PodIP 10.32.3.126
LoadBalanceIP 10.32.192.80
ClientIP 10.31.88.1
NodeIP 10.31.80.1-6

首先我们查看snat模式下的情况:

当在集群外通过LoadBalanceIP、ClusterIP、NodePort等方式访问的时候,转发路径如下:

1
Client --> Node --> Pod --> Node --> Client

此时客户端和pod之间的通信来回都是需要经过Node进行NAT转换。

1
2
3
4
5
6
7
8
# bpf-lb-mode: snat
# 我们直接访问podIP,是可以返回客户端的真实IP和端口的
$ curl 10.32.3.126
10.31.88.1:49940
# bpf-lb-mode: snat
# 通过LoadBalancerIP访问,此时返回的IP和端口是node的,而不是真实访问的客户端的
$ curl 10.32.192.80
10.31.80.1:50066

开启了DSR模式之后的转发路径变成下面这样:

1
Client --> Node --> Pod --> Client

此时Pod响应客户端的请求不需要再经由Node转发,而是由Pod直接返回给客户端。

1
2
3
4
5
6
# bpf-lb-mode: dsr
# 无论是通过LoadBalancerIP还是podIP访问,都是可以返回客户端的真实IP和端口的
$ curl 10.32.3.126
10.31.88.1:34220
$ curl 10.32.192.80
10.31.88.1:52650

4、Socket 旁路&XDP加速

4.1 Socket LoadBalancer Bypass in Pod Namespace

Cilium对Scoket旁路的全称是Socket LoadBalancer Bypass in Pod Namespace,即对于同一个namspace下的服务,如果namespace中的某个pod通过LoadBalancer来访问同namespace下的另一个服务,实际上请求是会被转发到同namespace中的另一个pod,但是在底层的socket看来,还是由该客户端pod对LoadBalancerIP发起请求产生的socket连接,以及后续需要的NAT转换等操作。

开启了旁路功能之后,可以绕过上述的流程,直接把请求转发给对应的后端pod,缩短转发链路从而提高性能。

1
2
3
4
5
6
$ kubectl get cm -n kube-system cilium-config -o yaml | egrep "bpf-lb-sock-hostns-only"
bpf-lb-sock-hostns-only: "true"
$ kubectl rollout restart ds/cilium deployment/cilium-operator -n kube-system

# 使用helm来更新的话可以添加下面的参数
--set socketLB.hostNamespaceOnly=true

4.2 LoadBalancer & NodePort XDP Acceleration

Cilium还支持对外部流量(ExternalIP、NodePort等)服务使用XDP加速从而提高性能表现,由于XDP加速本身依赖Linux内核,同时也对网卡的型号和驱动有要求,因此最好先确定机器对应的网卡驱动(使用ethtool -i eth0查看)和内核版本是否符合需求。官方有给出一份支持列表,对于绝大部分高速网卡和大多数的虚拟机网卡驱动(virtio_net)都是支持的。

1
2
3
4
5
6
7
8
9
10
11
$ kubectl get cm -n kube-system cilium-config -o yaml | egrep "bpf-lb-sock-hostns-only"
bpf-lb-acceleration: native
$ kubectl rollout restart ds/cilium deployment/cilium-operator -n kube-system

# 使用helm来更新的话可以添加下面的参数
--set loadBalancer.acceleration=native

# 更新完成之后我们可以通过下面的命令来检查效果
$ kubectl -n kube-system exec ds/cilium -- cilium status --verbose | grep XDP
Defaulted container "cilium-agent" out of: cilium-agent, mount-cgroup (init), apply-sysctl-overwrites (init), mount-bpf-fs (init), clean-cilium-state (init)
XDP Acceleration: Native

5、Native-Routing-masquerade

注意该功能与是否开启KubeProxyReplacement模式无关,只需要集群开启了Native-routing模式,即确保podIP在集群外路由可达,即可配置该功能。

默认情况下Cilium会开启IP伪装功能,即pod在访问集群外的服务时,源IP会被伪装成所在node节点的IP而不是本身的真实IP,我们可以根据实际需求来进行调整,通过调整enable-ipv(4|6)-masquerade参数可以分别控制IPv4/IPv6网络下的IP伪装功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ kubectl get cm -n kube-system cilium-config -o yaml | egrep "enable-ipv(4|6)-masquerade"
enable-ipv4-masquerade: "true"
enable-ipv6-masquerade: "true"
# 当集群内的pod访问集群外部的服务,会返回所在node的IP
$ kubectl -n nginx-quic exec -it deployments/nginx-quic-deployment -- curl ipport.tinychen.com
10.31.80.6:44372
# 当集群内的pod访问集群内部的服务,则返回pod自身的真实IP
$ kubectl -n nginx-quic exec -it deployments/nginx-quic-deployment -- curl 10.32.192.80
10.32.5.96:42688

$ kubectl get cm -n kube-system cilium-config -o yaml | egrep "enable-ipv(4|6)-masquerade"
enable-ipv4-masquerade: "false"
enable-ipv6-masquerade: "false"
# 关闭masquerade功能之后,访问集群内外部的服务均可以返回pod自身的IP
$ kubectl -n nginx-quic exec -it deployments/nginx-quic-deployment -- curl ipport.tinychen.com
10.32.5.96:58512
$ kubectl -n nginx-quic exec -it deployments/nginx-quic-deployment -- curl 10.32.192.80
10.32.5.96:36324