目录
-
Kubernetes 网络模型
-
Cilium 对 Kubernetes Service 负载均衡的实现,以及我们的一些实践经验
-
一些新的 BPF 内核扩展
-
Pod 会因为某些原因重建,而 Kubernetes 无法保证它每次都会分到同一个 IP 地址 。例如,如果 Node 重启了,Pod 很可能就会分到不同的 IP 地址,这对客户端来说个 大麻烦。
-
没有内置的负载均衡。即,客户端选择一个 PodIP 后,所有的请求都会发送到这个 Pod,而不是分散到不同的后端 Pod。
-
宿主机的端口资源是所有 Pod 共享的,任何一个端口只能被一个 Pod 使用 ,因此在每台 Node 上,任何一个服务最多只能有一个 Pod(每个 backend 都是一 致的,因此需要使用相同的 HostPort)。对用户非常不友好。
-
和 PodIP 方式一样,没有内置的负载均衡。
-
已经有了服务(Service)的概念,多个 Pod 属于同一个 Service,挂掉一个时其 他 Pod 还能继续提供服务。
-
客户端不用关心 Pod 在哪个 Node 上,因为集群内的所有 Node 上都开了这个端口并监听在那里,它们对全局的 backend 有一致的视图。
-
已经有了负载均衡,每个 node 都是 LB。
在宿主机 netns 内访问这些服务时,通过 localhost:NodePort 就行了,无需 DNS 解析。
-
大部分实现都是基于 SNAT,当 Pod 不在本节点时,导致 packet 中的真实客户端 IP 地址信息丢失,监控、排障等不方便。
-
Node 做转发使得转发路径多了一跳,延时变大。
-
External IP 在 Kubernetes 的控制范围之外,是由底层的网络平台提供的。例如,底层网 络通过 BGP 宣告,使得 IP 能到达某些 nodes。
-
由于这个 IP 是在 Kubernetes 的控制之外,对 Kubernetes 来说就是黑盒,因此 从集群内访问 external IP 是存在安全隐患的,例如 external IP 上可能运行了 恶意服务,能够进行中间人攻击。因此,Cilium 目前不支持在集群内通过 external IP 访问 Service。
-
externalIPs 在 Kubernetes 的控制之外,使用方式是从某个地方申请一个 external IP, 然后填到 Service 的 Spec 里;这个 external IP 是存在安全隐患的,因为并不是 Kubernetes 分配和控制的;
-
LoadBalancer 在 Kubernetes 的控制之内,只需要声明 这是一个 LoadBalancer 类型的 Service,Kubernetes 的 cloud-provider 组件 就会自动给这个 Service 分配一个外部可达的 IP,本质上 cloud-provider 做的事 情就是从某个 LB 分配一个受信任的 VIP 然后填到 Service 的 Spec 里。
-
有专门的 LB 节点作为统一入口。
-
LB 节点再将流量转发到 NodePort。
-
NodePort 再将流量转发到 backend pods。
-
LoadBalancer 由云厂商实现,无需用户安装 BGP 软件、配置 BGP 协议等来宣告 VIP 可达性。
-
开箱即用,主流云厂商都针对它们的托管 Kubernetes 集群实现了这样的功能。
-
在这种情况下,Cloud LB 负责检测后端 Node(注意不是后端 Pod)的健康状态。
-
存在两层 LB:LB 节点转发和 node 转发。
-
使用方式因厂商而已,例如各厂商的 annotations 并没有标准化到 Kubernetes 中,跨云使用会有一些麻烦。
-
Cloud API 非常慢,调用厂商的 API 来做拉入拉出非常受影响。
-
ClusterIP 使用的 IP 地址段是在创建 Kubernetes 集群之前就预留好的;
-
ClusterIP 不可路由(会在出宿主机之前被拦截,然后 DNAT 成具体的 PodIP);
-
只能在集群内访问(For in-cluster access only)。
-
LoadBalancer
-
NodePort
-
ClusterIP
-
在每个 node 上运行一个 cilium-agent;
-
cilium-agent 监听 Kubernetes apiserver,因此能够感知到 Kubernetes 里 Service 的变化;
-
根据 Service 的变化动态更新 BPF 配置。
-
运行在 socket 层的 BPF 程序
-
运行在 tc/XDP 层的 BPF 程序
-
connect + sendmsg 做正向变换(translation)
-
recvmsg + getpeername 做反向变换
-
bpf_get_socket_cookie(),主要用于 UDP sockets,我们希望每个 UDP flow 都能选中相同的 backend pods。
-
bpf_get_netns_cookie(),用在两个地方:
-
用于区分 host netns 和 pod netns,例如检测到在 host netns 执行 bind 时,直接拒绝(reject);
-
用于 serviceSessionAffinity,实现在某段时间内永远选择相同的 backend pods。
-
missing driver support
-
high rate of cache-misses
-
Cilium XDP 模式:能够处理全部的 10Mpps 入向流量,将它们转发到其他节点上的 backend pods。
-
Cilium TC 模式:可以处理大约 2.8Mpps,虽然它的处理逻辑和 Cilium XDP 是类似的(除了 BPF helpers)。
-
kube-proxy iptables 模式:能处理 2.4Mpps,这是 Kubernetes 的默认 Service 负载均衡实现。
-
kube-proxy IPVS 模式:性能更差一些,因为它的 per-packet overhead 更大一 些,这里测试的 Service 只对应一个 backend pod。当 Service 数量更多时, IPVS 的可扩展性更好,相比 iptables 模式的 kube-proxy 性能会更好,但仍然没 法跟我们基于 TC BPF 和 XDP 的实现相比(no comparison at all)。
-
TPROXY 需要由内核协议栈完成:我们目前的 L7 proxy 功能会用到这个功能。
-
Kubernetes 默认安装了一些 iptables rule,用来检测从连接跟踪的角度看是非法的连接 (‘invalid’ connections on asymmetric paths),然后 netfilter 会 drop 这些连接 的包。我们最开始时曾尝试将包从宿主机 tc 层直接 redirect 到 veth,但应答包却要 经过协议栈,因此形成了非对称路径,流量被 drop。因此目前进和出都都要经过协议栈。
-
Pod 的出向流量在进入协议栈后,在 socket buffer 层会丢掉 socket 信息 (skb->sk gets orphaned at ip_rcv_core()),这导致包从主机设备发出去时, 我们无法在 FQ leaf 获得 TCP 反压(TCP back-pressure)。
-
转发和处理都是 packet 级别的,因此有 per-packet overhead。
-
bpf_redirect_neigh()
-
bpf_redirect_peer()
-
首先会查找路由,ip_route_output_flow()
-
将 skb 和匹配的路由条目(dst entry)关联起来,skb_dst_set()
-
然后调用到 neighbor 子系统,ip_finish_output2()
-
填充 neighbor 信息,即 src/dst MAC 地址
-
保留 skb->sk 信息,因此物理网卡上的 qdisc 都能访问到这个字段
-
这就是 Pod 出向的处理过程。
-
首先会获取对应的 veth pair,dev = ops->ndo_get_peer_dev(dev),然后获取 veth 的对端(在另一个 netns)
-
然后,skb_scrub_packet()
-
设置包的 dev 为容器内的 dev,skb->dev = dev
-
重新调度一次,sch_handle_ingress(),这不会进入 CPU 的 backlog queue:
-
goto another_round
-
no CPU backlog queue