Cilium:基于 BPF/XDP 实现 Kubernetes Service 负载均衡
Kubernetes 网络模型
Cilium 对 Kubernetes Service 负载均衡的实现,以及我们的一些实践经验
一些新的 BPF 内核扩展
没有内置的负载均衡。即,客户端选择一个 PodIP 后,所有的请求都会发送到这个 Pod,而不是分散到不同的后端 Pod。
宿主机的端口资源是所有 Pod 共享的,任何一个端口只能被一个 Pod 使用 ,因此在每台 Node 上,任何一个服务最多只能有一个 Pod(每个 backend 都是一 致的,因此需要使用相同的 HostPort)。对用户非常不友好。
和 PodIP 方式一样,没有内置的负载均衡。
已经有了服务(Service)的概念,多个 Pod 属于同一个 Service,挂掉一个时其 他 Pod 还能继续提供服务。
客户端不用关心 Pod 在哪个 Node 上,因为集群内的所有 Node 上都开了这个端口并监听在那里,它们对全局的 backend 有一致的视图。
已经有了负载均衡,每个 node 都是 LB。
在宿主机 netns 内访问这些服务时,通过 localhost:NodePort 就行了,无需 DNS 解析。
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 不可路由(会在出宿主机之前被拦截,然后 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()
保留 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