Kubernetes是一个复杂的平台,其中各个部件协同工作。 Kubernetes网络是最关键(甚至不是最重要)之一。 kubernetes网络有很多层,即Pod网络,服务IP,外部IP等。在此过程中,kube-proxy扮演着重要的角色。在这里,我们尝试通过一些不同的CNI选项将Kubernetes与kube-proxy一起使用,我们将看到它是否具有任何好处,或者kube-proxy是否具有任何特定的限制。
kube-proxy简介
在各种kubernetes网络组件中,让我们看两个特别的组件-pod网络和服务网络。
每当我们设置Kubernetes时,我们总是指定不重叠的Pod CIDR和Service CIDR。创建Pod时,它具有自己的IP,例如下面的nginx容器具有容器IP — 102.121.46.193。
![4475bd4a-0714-eb11-8da9-e4434bdf6706.png](http://p02.5ceimg.com/content/4475bd4a-0714-eb11-8da9-e4434bdf6706.png)
容器IP是来自容器CIDR的虚拟网络的一部分,它充当每个节点上的网桥。我们可以通过登录主节点之一并打印路由表来查看。
![4875bd4a-0714-eb11-8da9-e4434bdf6706.png](http://p01.5ceimg.com/content/4875bd4a-0714-eb11-8da9-e4434bdf6706.png)
我们可以在上面看到子网102.121.46.192/26中的所有Ip都路由到工作程序节点10.11.86.114。如果我们登录到该工作节点列表,则将看到该特定节点上的网络接口,我们将看到预期的虚拟接口:
![4e75bd4a-0714-eb11-8da9-e4434bdf6706.png](http://p01.5ceimg.com/content/4e75bd4a-0714-eb11-8da9-e4434bdf6706.png)
这非常清楚。现在,让我们看一下定义为到达此pod的服务。
![5475bd4a-0714-eb11-8da9-e4434bdf6706.png](http://p01.5ceimg.com/content/5475bd4a-0714-eb11-8da9-e4434bdf6706.png)
分配给该服务的服务IP为102.66.12.133。但是,该特定IP并未在任何主机上的任何位置定义,也不是路由表的一部分。那么如何路由呢?
这是kube-proxy出现的地方。 kube-proxy组件侦听来自kubernetes的所有服务请求,并在iptables中为每个服务IP创建条目,以实现到Pod的正确路由。如果我们为上面的示例ips转储iptables并进行检查,我们将看到此情况:
![5675bd4a-0714-eb11-8da9-e4434bdf6706.png](http://p02.5ceimg.com/content/5675bd4a-0714-eb11-8da9-e4434bdf6706.png)
如我们所见,服务IP 102.66.12.133被“映射”到端口80上的容器IP 102.121.46.193。
为什么要替换 kube-proxy ?
那么,为什么要替换kube-proxy呢? kube-proxy组件已被广泛使用,就像事实上的部署一样,因此没有真正的理由无故删除它。上面与iptables相关的入门文章中也暗示了另一个原因。让我们做一个简单的实验。
在vSphere中,我们有一个1个主节点3个工作节点集群,并按如下方式部署了nginx:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
spec:
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
nodeSelector:
apptype: nginx
containers:
- name: nginx
image: nginx:1
ports:
- name: http
containerPort: 80
还定义了一个简单的集群IP服务:
apiVersion: v1
kind: Service
metadata:
name: nginx-svc
spec:
ports:
- name: http
port: 80
protocol: TCP
targetPort: 80
selector:
app: nginx
一旦部署了这些,就可以得到上面刚刚看到的Pod和服务:
# kubectl get pods -n benchmark -o wide
NAME READY STATUS RESTARTS AGE IP
nginx-8694799779-9xcdn 1/1 Running 0 34h 102.121.46.193
# kubectl get service nginx-svc -o wide
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
nginx-svc ClusterIP 102.66.12.133 <none> 80/TCP 34h app=nginx
我们已经在上一节中看到了将创建的iptables规则。现在让我们做一个“傻”测试。我们将使用相同的选择器添加100个服务,这些选择器指向具有不同服务名称的当前nginx部署。
# for i in {1..100}; do yq w test-nginx-service.yaml "metadata.name" "nginx-svc-"$i | kubectl apply -n benchmark -f -; done
service/nginx-svc-1 created
service/nginx-svc-2 created
service/nginx-svc-3 created
...
service/nginx-svc-100 created
现在,我们有100个服务Ips都指向同一个Pod。如果我们现在看一下iptables:
![5b75bd4a-0714-eb11-8da9-e4434bdf6706.png](http://p01.5ceimg.com/content/5b75bd4a-0714-eb11-8da9-e4434bdf6706.png)
我们可以看到kube-proxy为定义的每个新服务添加了iptables规则集。随着服务数量的增加,该列表将变得庞大,并且可能会影响性能(我们将在不久后看到),因为iptables处理是顺序的。规则在链中越向下,处理所需的时间就越长。那么我们可以删除kube-proxy并用不使用下面的iptables的替代方法替换它吗?是。解决方案(或潜在解决方案之一)在于使用eBPF(扩展的Berkeley数据包过滤器)。
什么是eBPF?
全面深入的技术理解超出了本实验的范围,甚至超出了我自己的技能范围,但以简单的术语来说,eBPF(扩展的Berkeley Packet Filter)是一种运行在linux机器内核中的虚拟机。它能够运行本地实时编译的“ bpf程序”,这些程序可以访问某些内核功能。换句话说,用户可以在运行时按需注入这些程序以在内核中运行。这些程序遵循bpf提供的特定指令集,并具有某些需要遵循的规则,并且将仅运行可以安全运行的程序。这与Linux模块不同,后者也可以在内核中运行,但是如果编写不正确,可能会导致内核出现问题。
我会将这些细节推迟到有关BPF的大量文章上。但是,该虚拟机可以连接到诸如网络设备之类的任何内核子系统,并且BPF程序是响应这些子系统上的事件而执行的。最古老,最受欢迎的Linux工具之一-tcpdump使用BPF。我很想说像智能nics等新技术都利用了BPF,但这对我来说只是一个疯狂的猜测。
使用eBPF用CNI驱动程序替换kube-proxy
cilium项目利用eBPF实施其网络策略,还提供了kube-proxy替代产品。 Calico项目还具有使用eBPF的技术预览,但是对于本实验,我们将仅使用Cilium。
Cilium kube-proxy 安装
这实际上非常简单。关于内核版本,存在某些先决条件。内核版本至少应为4.19,但建议版本> v5.3。我们只需要遵循此处概述的步骤https://docs.cilium.io/zh/v1.7/gettingstarted/kubeproxy-free/
对于1.15和更低版本的K8S,我们首先在主机上删除kube-proxy的守护程序集并清空iptables,然后再添加其他节点。
kubectl -n kube-system delete ds kube-proxy
iptables-restore <(iptables-save | grep -v KUBE)
对于1.16及更高版本,我们可以完全跳过kube-proxy:
kubeadm init --pod-network-cidr=10.217.0.0/16 --skip-phases=addon/kube-proxy
直接使用helm或生成yaml模板并安装cilium:
# helm template cilium --namespace kube-system --set global.k8sServiceHost=10.11.86.116 --set global.k8sServicePort=6443
--set global.bpf.preallocateMaps=true
--set global.kubeProxyReplacement=strict
--set global.nodePort.mode=snat
--set global.tunnel=disabled
--set global.autoDirectNodeRoutes=true
--set global.l7Proxy.enabled=false > cilium.yaml
# kubectl apply -f cilium.yaml
应用CNI驱动程序后,我们的kube-system名称空间将如下所示:
![5d75bd4a-0714-eb11-8da9-e4434bdf6706.png](http://p03.5ceimg.com/content/5d75bd4a-0714-eb11-8da9-e4434bdf6706.png)
如我们所见,没有kube-proxy pod。现在,我们可以像上面一样创建我们的nginx部署和单个服务,并检查iptables。
# kubectl get pods -n benchmark -o wide
NAME READY STATUS RESTARTS AGE IP
nginx-8694799779-f8h82 1/1 Running 0 2d19h 101.96.1.18
# kubectl get service nginx-svc -o wide
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
nginx-svc ClusterIP 101.67.9.101 <none> 80/TCP 2d19h app=nginx
![5f75bd4a-0714-eb11-8da9-e4434bdf6706.png](http://p05.5ceimg.com/content/5f75bd4a-0714-eb11-8da9-e4434bdf6706.png)
如我们所见,它为KUBE添加了一些基本规则,但没有KUBE-SERVICES存在。让我们像上面的测试一样添加100个服务,然后再次检查iptables。
![6475bd4a-0714-eb11-8da9-e4434bdf6706.png](http://p01.5ceimg.com/content/6475bd4a-0714-eb11-8da9-e4434bdf6706.png)
完全一样。
因此,我们摆脱了庞大的iptables规则,但是性能呢?让我们做一些测试。
简单性能测试
为了测试这两种设置的性能,我们在这两种设置上部署了一个运行ubuntu和apache bench的简单pod。我们确保apache工作台上的nodeSelector与运行nginx的节点不同,以便我们可以跨节点进行网络延迟。
此设置绝不是完全优化的生产设置,因此这里的结果是要注意的是,在不同的环境中情况可能有所不同,但是仍然可以在相同的基础上进行比较。
部署如下所示:
apiVersion: apps/v1
kind: Deployment
metadata:
name: apachebench
spec:
selector:
matchLabels:
app: apachebench
template:
metadata:
labels:
app: apachebench
spec:
nodeSelector:
apptype: apachebench
containers:
- name: ubuntu
image: ubuntu
command: ["/bin/sleep", "3650d"]
部署完成后,我们将得到如下内容:
![6675bd4a-0714-eb11-8da9-e4434bdf6706.png](http://p02.5ceimg.com/content/6675bd4a-0714-eb11-8da9-e4434bdf6706.png)
我们可以看到两个Pod都在不同的节点上。现在,我们只需在ubuntu pod上安装apache bench,然后运行一个简单的ab测试,即可从单个连接发送100000个请求。
# kubectl exec -it apachebench-7db7b745c6-9cf9z /bin/bash -n benchmark
root@apachebench-7db7b745c6-9cf9z:/# ab -n 100000 -c 1 http://nginx-svc/
This is ApacheBench, Version 2.3 <$Revision: 1807734 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
Benchmarking nginx-svc (be patient)
Completed 10000 requests
Completed 20000 requests
Completed 30000 requests
Completed 40000 requests
Completed 50000 requests
Completed 60000 requests
Completed 70000 requests
Completed 80000 requests
Completed 90000 requests
Completed 100000 requests
Finished 100000 requests
Server Software: nginx/1.17.10
Server Hostname: nginx-svc
Server Port: 80
Document Path: /
Document Length: 612 bytes
Concurrency Level: 1
Time taken for tests: 78.948 seconds
Complete requests: 100000
Failed requests: 0
Total transferred: 84600000 bytes
HTML transferred: 61200000 bytes
Requests per second: 1266.65 [#/sec] (mean)
Time per request: 0.789 [ms] (mean)
Time per request: 0.789 [ms] (mean, across all concurrent requests)
Transfer rate: 1046.47 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.1 0 6
Processing: 0 0 0.1 0 4
Waiting: 0 0 0.1 0 4
Total: 0 1 0.1 1 7
Percentage of the requests served within a certain time (ms)
50% 1
66% 1
75% 1
80% 1
90% 1
95% 1
98% 1
99% 1
100% 7 (longest request)
root@apachebench-7db7b745c6-9cf9z:/#
如果我们通过将群集中nginx服务的数量从1,100,1000,2000,3000,4000,5000增加到两个设置来运行此测试,并绘制比较图表,我们可以看到:
![6875bd4a-0714-eb11-8da9-e4434bdf6706.png](http://p02.5ceimg.com/content/6875bd4a-0714-eb11-8da9-e4434bdf6706.png)
正如我们看到的iptables(kube-proxy)和bpf设置开始时相对接近800微秒,但是随着服务数量的增加,iptables设置性能开始下降。bpf设置趋于或多或少保持恒定。现在显然随着我们添加更多不同种类的负载,这些负载甚至可能进一步不同,但是该图至少显示了在这种特定情况下,iptables的顺序处理如何在定义的大量规则上导致性能下降。
结论
我们已经看到了如何使用基于bpf的规则集摆脱基于kube-proxy和iptables的规则集。但是,这种性能下降在多大程度上真正影响了生产系统,这真的是必须做的吗?与往常一样,此类问题的答案是-这取决于:如果它是一个很小或中等的集群,并且没有很多服务或网络策略,那么kube-proxy版本可能会正常工作。实际上,它对许多kubernetes用户来说都是透明的。
但是,请想象一个大型集群,在一个运行大量服务的多租户系统中,拥有数百个租户。加上运行istio之类的Sidecar代理的应用程序,其本身大量使用iptables。如果遇到iptables瓶颈,则在这种情况下,所有租户的性能都会下降。因此,这是一个有趣的探索途径。 kube-proxy很有可能在某个时候开始在内部使用eBPF,并且所有这些更改都变得透明。
基于BPF的CNI驱动程序仍相对较新,它们在等待时可能会有自己的优化,因此很难说是否已准备好开箱即用,但这是值得期待的令人兴奋的领域。