我是 LEE,老李,一个在 IT 行业摸爬滚打 16 年的技术老兵。
事件背景
我们的几个核心 k8s 集群规则持续增长,网络稳定性和效能之间的平衡性成为目前主要需要攻克的问题。目前单个集群的节点 270+ 、Pod 40000+ ,这样的规模 Pod 之间的请求流量规模十分巨大,而且网络流量压力对传统的 k8s 网络组件压力很大。为了要解决这个问题,我把目标转向了基于 ebpf 的 cilium 组件,决定使用 cilium 来替换 kubelet 中的 kubenet 组件。
我选择了一个有点量的测试 k8s 集群,安装了 cilium 1.11.8 后,做了集群内部的网络测试,Pod 到节点以及 Pod 之间都是可以 ping 通,而且端口访问是正常。随后我们对外发布测试服务的时候,发现外部无法连接到测试 k8s 集群内的应用。这个事情让我百思不得其解,明明网络测试都是 OK 的,为什么对外发布的测试服务却无法正常访问。
现象获取
这个时候拉来了底层云平台供应商的小伙伴一起排查,也是毫无进展。此时我只能逐一层次网络测试排查,通过一个下午的排查,最后发现外部直接访问 Pod 提供的服务是 OK,但是通过 Service 中 Loadbalancer 模式发布的服务确实访问不通的。
我们登录到云供应商上的 Loadbalancer 服务上,查看对应服务实例的状态和日志。通过观察,发现 Loadbalancer upstream 的 RealServer 状态不停的翻滚,一会状态“健康”,一会状态“异常”。我想这个“现象”应该是导致外部流量没有办法通过 Loadbalancer 到后端服务的原因。
另外一个问题:我们直接访问 Node 和 Pod 的端口提供的服务都是正常的,但是 Loadbalancer 健康检测这个服务端口却是失败的?
一看究竟就必须要请出 tcpdump 大法,抓包复原 Loadbalancer 健康检测会话流。
健康检测会话流分析
正常情况
按照原供应商小伙伴提供的信息,同时结合抓包结果,我们得知正常的一个健康检测是:
- Loadbalancer --> RealServer: sync
- RealServer --> Loadbalancer: sync,ack
- Loadbalancer --> RealServer: rst,ack
公有云的 Loadbalancer 会与 RealServer 尝试建立 tcp 连接,1,2 步交换 sync 和 sync,ack,Loadbalancer 再收到了 sync,ack,就会向 RealServer 发送 rst,ack 关闭 tcp 连接,成功完成一次健康检测
异常情况
我们看过了正常的健康检测的样子,那么我们这边公有云的 Loadbalancer 健康异常检测如上图所示。
经过分析,总结出现异常的情况如下:
- Loadbalancer --> RealServer 发送了 sync
- Loadbalancer --> RealServer RealServer 没有回应 sync,ack,然后公有云 Loadbalancer 尝试重传 sync
- RealServer(另外 IP) --> Loadbalancer 另外一个 IP 的 RealServer 回应 sync,ack
感觉到了一丝丝不对的情况,我想心细的小伙伴应该看到了问题所在。问题出现在 RealServer --> Loadbalancer 这里,为什么回应 Loadbalancer 的 RealServer IP 地址不对,导致 Loadbalancer 认为正确的 RealServer 没有回应自己的检测,随之将此 RealServer 的状态标记成“异常”。
原理分析
在沉思片刻后,我觉得最复杂的问题导致的原因可能最简单,一定是 cilium 组件哪里设置有问题,因为公有云 Loadbalancer 到后端 Pod 的这样业务模型在其他的没有部署 cilium 的集群内都是正常。
既然知道数据流的情况,也知道出问题的组件,问题就好解决了。
cilium 网络架构
cilium 的安装命令
helm upgrade -i cilium cilium/cilium --version 1.11.8 --namespace kube-system \
--set tunnel=disabled \
--set ipam.mode=kubernetes \
--set nativeRoutingCIDR=10.192.32.0/19 \
--set loadBalancer.mode=hybrid \ # 此参数是问题所在
--set kubeProxyReplacement=probe \
--set prometheus.enabled=true \
--set operator.prometheus.enabled=true \
--set hubble.enabled=true \
--set hubble.metrics.enabled="{dns,drop,tcp,flow,port-distribution,icmp,http}"
庖丁解牛
结合 cilium 网络架构看来看去,最值得怀疑的地方就是 loadBalancer.mode=hybrid 这个参数,按照 cilium 官方的参考文档解释如下:
Cilium also supports a hybrid DSR and SNAT mode, that is, DSR is performed for TCP and SNAT for UDP connections. This has the advantage that it removes the need for manual MTU changes in the network while still benefiting from the latency improvements through the removed extra hop for replies, in particular, when TCP is the main transport for workloads.
The mode setting loadBalancer.mode allows to control the behavior through the options dsr, snat and hybrid. By default the snat mode is used in the agent.
By default, Cilium’s eBPF NodePort implementation operates in SNAT mode. That is, when node-external traffic arrives and the node determines that the backend for the LoadBalancer, NodePort or services with externalIPs is at a remote node, then the node is redirecting the request to the remote backend on its behalf by performing SNAT. This does not require any additional MTU changes at the cost that replies from the backend need to make the extra hop back that node in order to perform the reverse SNAT translation there before returning the packet directly to the external client.
This setting can be changed through the loadBalancer.mode Helm option to dsr in order to let Cilium’s eBPF NodePort implementation operate in DSR mode. In this mode, the backends reply directly to the external client without taking the extra hop, meaning, backends reply by using the service IP/port as a source. DSR currently requires Cilium to be deployed in Native-Routing, i.e. it will not work in either tunneling mode.
Note that usage of DSR mode might not work in some public cloud provider environments due to the Cilium-specific IP options that could be dropped by an underlying fabric. Therefore, in case of connectivity issues to services where backends are located on a remote node from the node that is processing the given NodePort request, it is advised to first check whether the NodePort request actually arrived on the node containing the backend. If this was not the case, then switching back to the default SNAT mode would be advised as a workaround.
一大堆英文的看完了,大概的意思是同时使用 NativeRouting 和 loadBalancer.mode=hybrid,cilium 内部的 Loadbalancer 工作在了 DSR 的模式下。 那么公有云的 Loadbalancer 对后端 RealServer 检测异常的问题就能很好解释了。整个异常的过程跟 DSR 工作模式完全一样。
处理方法
通过一通复杂的分析与验证后,我们在 cilium 的安装命令中去掉了 loadBalancer.mode=hybrid 参数,重新部署 cilium,然后重启相关的 Pod。
最终效果
公有云的 Loadbalancer 与 Node 节点之间健康检测恢复正常,对外发布的服务也能够正常的访问。