kubernetes源码-kube-proxy 原理和源码分析(二)
kube-proxy 原理和源码分析,源码为 kubernetes 的 release-1.22 分支 .
写在前面
由于篇幅和精力的原因,我们这里就先拿 ipvs 模式来做分析,如果后续有精力,可以考虑是不是要去看其他模式的代码。
ipvs 模式
上一篇我们提到 userspace 由于经常在内核态和用户态之间频繁切换,从而导致性能的巨大的消耗,所以,正常情况下是不使用 userspace 模式的; kubernetes 默认使用的是 iptables 模式,不过,如果集群规模较大的时候,iptables 规则的同步和规则查找规则的过程也是很耗性能的。
我们先拆解一下步骤:
-
首先,我们先看看 ipvs 模式的构造函数。
-
然后前面我们也了解到了事件函数会因为不同的模式有不同的实现逻辑,我们再看监听函数。
-
最后,我们再看 syncProxyRules 代理规则同步。
NewProxier
平时用的 ipv4 比较多,我们先看只有 ipv4 协议栈的构造函数 proxier, err = ipvs.NewProxier()
。
-
检查内核参数 bridge-nf-call-iptables 是否等于 1 (主要是解决 Service 同节点通信问题),启用 contrack 保留连接跟踪信息。
-
解析 kernelVersion ,设置 net.ipv4.vs.conn_reuse_mode=0 。(当 net.ipv4.vs.conn_reuse_mode=0 时,ipvs 不会对新连接进行重新负载,而是复用之前的负载结果,将新连接转发到原来的 rs 上;当 net.ipv4.vs.conn_reuse_mode=1 时,ipvs 则会对新连接进行重新调度。)
-
设置 net.ipv4.vs.expire_nodest_conn=1 ,当 rs 的 weight 被设置成 0 后,流量立刻被摘掉。实现快速平滑的摘除 rs 。
-
设置 net.ipv4.vs.expire_quiescent_template=1 ,默认值为0,当 rs 的 weight 值=0(如,健康检测失败,应用程序将 RS weight 置0)时,会话保持的新建连接还会继续调度到该 rs 上;如果配置为1,则马上将会话保持的连接模板置为无效,重新调度新的 rs ,如果有会话保持的业务,建议该值配置为1。
-
从 kubernetes 角度来说,kube-proxy 需要在保证性能的前提下,找到一种能让新连接重新调度的方式。目前从内核代码中可以看到,需要将参数设置如下:
-
net.ipv4.vs.conntrack=0
-
net.ipv4.vs.conn_reuse_mode=1
-
net.ipv4.vs.expire_nodest_conn=1
-
-
设置 net.ipv4.ip_forward=1 ,启用ip转发。
-
net.ipv4.conf.all.arp_ignore=1 ,只回答目标IP地址是本机上来访网络接口(网卡)IP地址段的 ARP 查询请求 。比如 eth0=192.168.0.1/24, eth1=10.1.1.1/24, 那么 eth0 收到来自 10.1.1.2 地址发起的对 192.168.0.1/24 的查询会回应,eth0 收到对 10.1.1.1/24 的 arp 查询不会回应。
-
net.ipv4.conf.all.arp_announce=2 ,始终使用与目标IP地址对应的最佳本地 IP 地址作为 ARP 请求的源 IP 地址。在此模式下将忽略 IP 数据包的源 IP 地址并尝试选择能与目标 IP 地址通信的本机地址。首要是选择所有网络接口中子网包含该目标 IP 地址的本机 IP 地址。如果没有合适的地址,将选择当前的网络接口或其他的有可能接受到该 ARP 回应的网络接口来进行发送 ARP 请求,并把发送 ARP 请求的网络接口卡的 IP 地址设置为 ARP 请求的源 IP 。
-
为了使其他节点不对 local 网卡上的 Service External IP 进行 ARP Reply,节点需要设置 arp_ignore=1 以及 arp_announce=2 ,或者是设置 Kube-proxy 的 –ipvs-strict-arp 参数为 true 。
-
如果 tcpTimeout > 0 || tcpFinTimeout > 0 || udpTimeout > 0 ,则配置 ipvs 超时时间为这些参数的值。
-
生成 masquerade 标志用于SNAT规则。检查ip协议簇,并对节点ip进行分类记录,检查 ipvs 转发策略。
-
设置定时(1分钟)优雅清理 rs 任务。
|
|
事件函数实现
在上一篇,我们在核心逻辑 Run() 函数里面遗留了事件函数这么一段还没去看,我们现在看一下这一部分。
|
|
service 事件函数
我们跟随 NewServiceConfig() 构造函数,看看它是怎么实现的。
|
|
我们看看 handleAddService、 handleUpdateService、 handleDeleteService 里面是什么样的? 原来,它最终是去调 Proxier 的接口来实现的。
|
|
-
我们看看 Proxier 的这些接口具体是什么样的逻辑。
-
OnServiceAdd()、 OnServiceDelete 直接调用 OnServiceUpdate() 。
-
OnServiceUpdate() 内部调用 proxier.serviceChanges.Update(oldService, service) 生成 serviceMap (以命名空间、端口名称、端口协议作为 map 的 key ,ServicePort (ip,端口,协议)接口类型做为值) 对比对象有没有发生变化,需不需要生成 proxy 规则,最再后去调用 proxier.Sync() 接口同步处理。
-
serviceMap e.g. :
{{"ns", "cluster-ip", "TCP"}: {"172.16.55.10", 1234, "TCP"}}
|
|
endpoints 事件函数
同理,endpointSlice 类型的同步逻辑也是观察到有变化就去调用 proxier.Sync() 接口同步处理。
|
|
proxier.Sync()
|
|
proxier.syncRunner.Run()
proxier.syncRunner = async.NewBoundedFrequencyRunner("sync-runner", proxier.syncProxyRules, minSyncPeriod, syncPeriod, burstSyncs)
1.发送消息给通道 bfr.run 触发 tryRun() 。 2.最终也是去触发 proxier.syncProxyRules() 接口。
|
|
proxier.syncProxyRules()
proxier.Sync() 最终是调用 proxier.syncRunner.Run() 接口做规则同步。
-
serviceConfig.Run(wait.NeverStop) 是去调用 Proxier 对象的 OnServiceSynced() 接口实现的。
-
endpointSliceConfig.Run(wait.NeverStop) 是去调用 Proxier 对象的 OnEndpointSlicesSynced() 接口实现的。
-
2 者最终都是调用 proxier.syncProxyRules() ,注意:这里只有在 2 者都完成同步后,才去无条件执行一次。
serviceConfig.Run()
|
|
endpointSliceConfig.Run()
|
|
s.Proxier.SyncLoop() 函数
- 从上面 service 和 endpointslice 对象的监听函数可以看出,只要一监听到 service 和 endpointslice 事件,就触发 syncProxyRules 规则同步,还记得前面的核心逻辑 Run() 函数吗?它启动的 s.Proxier.SyncLoop() 最终也是去调用 syncProxyRules 接口去同步代理规则的,我们先看看 s.Proxier.SyncLoop() 函数的逻辑,再解析 proxier.syncProxyRules() 的逻辑。
|
|
proxier.syncRunner.Loop()
跟 proxier.syncRunner.Run() 不同的是,proxier.syncRunner.Loop() 跟它字面意思一样,一直循环执行的,默认时间间隔是 30s。
|
|
Loop() 接口会分别根据条件运行 bfr.stop()、 tryRun()、 doRetry() 函数
-
接受到 <-stop 信号,运行 bfr.stop() 停止同步。
-
接收到 <-bfr.timer.C() 或 <-bfr.run 信号,运行 bfr.tryRun() 进行同步。
-
接收到 <-bfr.retry 信号,运行 bfr.doRetry() 。
|
|
未完
看完 s.Proxier.SyncLoop() 函数后,接下来要开始看 proxier.syncProxyRules() 函数了,我们先暂停一下,因为 proxier.syncProxyRules() 函数内容实在是太长了,我们不得不分开来讲。