TL;DR: 文章主要涵盖了 Kubernetes 中的服务负载均衡,重点包括 kube-proxy、iptables、IPVS 的实现。
Kubernetes Service中的负载均衡
Kubernetes Service 并不是真实存在的, 没有进程监听Service的 IP 地址和端口。
可以通过访问 Kubernetes 集群中的任何节点并执行
netstat -ntlp
来检查是否存在这种情况,连IP地址都找不到。
Service的 IP 地址由控制器管理器中的控制平面(control plane)分配并存储在数据库(etcd)中,然后另一个组件kube-proxy使用相同的 IP 地址。
Kube-proxy 读取所有Service 的 IP 地址列表,并在每个节点中写入 iptables 规则的集合。 这些规则的意思是:"如果看到此服务 IP 地址,请重写请求并选择其中一个 Pod 作为目的地"。 Service IP 地址仅用作占位符 --- 这就是没有进程监听该 IP 地址或端口的原因。
iptables 是轮询吗? 答案是否定的,iptables 主要用于防火墙,它不是为负载均衡而设计的。
但是我们可以制定一组智能规则,使 iptables 的行为类似于负载均衡器。 这正是 Kubernetes 中使用的情况。
如果你有三个 Pod,kube-proxy 会编写以下规则: 选择 Pod 1 作为目的地的可能性为 33%。否则,转到下一条规则 选择 Pod 2 作为目的地的概率为 50%。否则,转到以下规则 选择 Pod 3 作为目的地(无概率) 复合概率是 Pod 1、Pod 2 和 Pod 3 都有三分之一的机会(33%)被选中。
实现的整体流程
-
增加了一个Service或者Pod,则使用informer通知 dnsController,通过CoreDNS做服务注册,注册后informer也通知kube-proxy写入到IPVS/iptables 中
-
当一个请求过来之后,通过DNS查询来发现Service/Pod
-
Kube-proxy 通过 ipvs/iptables 的规则来重写Service的目的地址,实现负载均衡。
kube-proxy 的实现
kube-proxy 以 daemonSet的形式运行在集群中,维护每个节点的iptables信息。
ProxyServer
kube-proxy 通过命令行启动后,构建ProxyServer,这里默认策略是Iptables ,所以我们重点看构建出来的 iptables.Proxier
go
func newProxyServer(config *kubeproxyconfig.KubeProxyConfiguration, master string, initOnly bool) (*ProxyServer, error) {
s := &ProxyServer{Config: config}
s.Proxier, err = s.createProxier(config, dualStackSupported, initOnly)
}
func (s *ProxyServer) createProxier(config *proxyconfigapi.KubeProxyConfiguration, dualStack, initOnly bool) (proxy.Provider, error) {
if config.Mode == proxyconfigapi.ProxyModeIPTables {
if dualStack {
proxier, err = iptables.NewDualStackProxier()
} else {
proxier, err = iptables.NewProxier()
}
} else if config.Mode == proxyconfigapi.ProxyModeIPVS {
if dualStack {
proxier, err = ipvs.NewDualStackProxier()
} else {
proxier, err = ipvs.NewProxier()
}
}
return proxier, nil
}
构建完成后,通过 Run()
方法启动服务监听,通过 informer
获取到服务最新的 Service 和 Endpoints ,并注册 EventHandler
来进行更新处理
go
func (s *ProxyServer) Run() error {
informerFactory := informers.NewSharedInformerFactoryWithOptions(s.Client, s.Config.ConfigSyncPeriod.Duration,
informers.WithTweakListOptions(func(options *metav1.ListOptions) {
options.LabelSelector = labelSelector.String()
}))
// 监听Service的变化
serviceConfig := config.NewServiceConfig(informerFactory.Core().V1().Services(), s.Config.ConfigSyncPeriod.Duration)
serviceConfig.RegisterEventHandler(s.Proxier)
go serviceConfig.Run(wait.NeverStop)
// 监听endPoints的变化
endpointSliceConfig := config.NewEndpointSliceConfig(informerFactory.Discovery().V1().EndpointSlices(), s.Config.ConfigSyncPeriod.Duration)
endpointSliceConfig.RegisterEventHandler(s.Proxier)
go endpointSliceConfig.Run(wait.NeverStop)
go s.Proxier.SyncLoop()
return err
}
// 上面注册的Service Event Handler
func (proxier *Proxier) OnServiceAdd(service *v1.Service) {
proxier.OnServiceUpdate(nil, service)
}
func (proxier *Proxier) OnServiceUpdate(oldService, service *v1.Service) {
if proxier.serviceChanges.Update(oldService, service) && proxier.isInitialized() {
proxier.Sync()
}
}
func (proxier *Proxier) OnServiceDelete(service *v1.Service) {
proxier.OnServiceUpdate(service, nil)
}
Proxier
具体干活的是 proixer
,构建出来后就会启动同步循环,负责更新endpointsMap
和 svcPortMap
go
func NewProxier()(*Proxier, error){
proxier.syncRunner = async.NewBoundedFrequencyRunner("sync-runner", proxier.syncProxyRules, minSyncPeriod, time.Hour, burstSyncs)
}
// 启动同步循环
func (proxier *Proxier) SyncLoop() {
proxier.syncRunner.Loop(wait.NeverStop)
}
// BoundedFrequencyRunner 负责周期性执行任务或者是收到run信号后执行
func (bfr *BoundedFrequencyRunner) Loop(stop <-chan struct{}) {
klog.V(3).Infof("%s Loop running", bfr.name)
bfr.timer.Reset(bfr.maxInterval)
for {
select {
case <-stop:
bfr.stop()
klog.V(3).Infof("%s Loop stopping", bfr.name)
return
case <-bfr.timer.C():
bfr.tryRun()
case <-bfr.run:
bfr.tryRun()
case <-bfr.retry:
bfr.doRetry()
}
}
}
func (bfr *BoundedFrequencyRunner) Run() {
select {
case bfr.run <- struct{}{}:
default:
}
}
// 详细更新逻辑可以查看这个方法的代码,ipvs和 Iptables都有各自维护自己表信息的逻辑,都是执行命令进行更新维护
func (proxier *Proxier) syncProxyRules(){}
proxy.EndpointsMap
中就定义了ServicePortName的名字 map[ServicePortName][]Endpoint
, 所以在更新的时候可以找到所有后端能提供服务的 endpoints
我们了解了 kube-proxy
如何维护 iptables之后,接下来来看看iptables 具体如何实现负载均衡的
通过一张图我们来回顾一下整个流程
我们通过ipvs proxier的实现类图,可以看到最终也是通过命令来执行规则的维护变更,这里不展开看代码,有兴趣的看 pkg/proxy/ipvs
或者 pkg/proxy/iptables
包下代码继续深入了解
我们了解了 kube-proxy
如何维护 iptables之后,接下来来看看iptables 具体如何实现负载均衡的
iptables 实现负载均衡
netfilter 是真正的防火墙框架,位于内核空间中,iptables 用于操作 netfilter这个内核框架 ,netfilter / iptables 组成了Linux下的包过滤防火墙。
数据包经过防火墙实际上是经过了一组规则,匹配条件 和动作形成了一组规则。
不同阶段会有不同的链,如:PREROUTING、FORWARD、POSTROUTING
每条链上有多组规则,只要命中匹配条件,就会执行相应的动作。如匹配到Service目的地址是 10.244.10.1 就进行地址转换的动作。
iptables 动作有如下:
- ACCEPT:允许数据包通过。
- DROP:直接丢弃数据包,不给任何回应信息,这时候客户端会感觉自己的请求泥牛入海了,过了超时时间才会有反应。
- REJECT:拒绝数据包通过,必要时会给数据发送端一个响应的信息,客户端刚请求就会收到拒绝的信息。
- SNAT:源地址转换,解决内网用户用同一个公网地址上网的问题。
- MASQUERADE:是SNAT的一种特殊形式,适用于动态的、临时会变的ip上。
- DNAT:目标地址转换。
- REDIRECT:在本机做端口映射。
- LOG:在/var/log/messages文件中记录日志信息,然后将数据包传递给下一条规则,也就是说除了记录以外不对数据包做任何其他操作,仍然让下一条规则去匹配。
每条链上有一串规则,有些规则很相似,我们希望能够放在一起的话也会更方便管理,所以把规则的集合抽象成了表。
filter表:负责过滤功能,防火墙;内核模块:iptables_filter
nat表:network address translation,网络地址转换功能;内核模块:iptable_natmangle表:拆解报文,做出修改,并重新封装 的功能;iptable_mangle
raw表:关闭nat表上启用的连接追踪机制;iptable_raw
分离表和链的设计使得 iptables
更加灵活,能够根据需要对不同类型的数据包采取不同的处理方式。
数据包经过iptables 流程图如下:
由于地址的转发主要用到了 NAT (网络地址转化),我们重点来关注 nat 表
环境准备
使用 kubectl
创建出对应的资源 ,yaml文件如下:
yaml
apiVersion: v1
kind: Namespace
metadata:
name: nginx-test
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
namespace: nginx-test
spec:
selector:
matchLabels:
app: nginx
replicas: 3
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.14.2
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: nginx-service
namespace: nginx-test
spec:
selector:
app: nginx
ports:
- name: http
port: 80
targetPort: 80 # nginx pod 对应的端口
---
apiVersion: v1
kind: Service
metadata:
name: nginx-service-np
namespace: nginx-test
spec:
type: NodePort
selector:
app: nginx
ports:
- name: http
port: 80
targetPort: 80 # nginx pod 对应的端口
nodePort: 36666 # 暴露的端口
提交后创建出来的信息如下:
- Service ClusterIP:10.104.60.209
- Service NodePort的ClusterIp: 10.104.122.7
- nginx pod的三个ip地址:10.244.1.3:80, 10.244.2.234:80, 10.244.2.235:80
外部访问的NodePort的包会经过的数据链为:PREROUTING、FORWARD、POSTROUTING
通过命令行可以查看 nat 表的规则
iptables结果列说明
pkts (packets) : 代表已经匹配的数据包数量。这个字段显示有多少个数据包符合规则。
bytes : 代表已经匹配的数据包的总字节数。这个字段显示了规则匹配的数据包总共有多少字节。
target : 代表规则匹配后采取的动作。可能的值包括 ACCEPT(允许通过)、DROP(丢弃数据包)、RETURN(返回上一层处理)等。
prot (protocol) : 代表规则所应用的网络协议。可能的值包括 tcp、udp、icmp 等,也可以是 all 表示匹配所有协议。
opt (options) : 代表附加选项,显示特定规则的其他设置或条件。
in (input) : 代表数据包进入防火墙的网络接口。
out (output) : 代表数据包离开防火墙的网络接口。
source : 代表规则匹配的数据包的源地址或地址范围。
destination : 代表规则匹配的数据包的目标地址或地址范围。
首先查看 PREROUTING的链
shell
$ iptables -h
# --list -L [chain [rulenum]] List the rules in a chain or all chains 列出所有规则
# --numeric -n numeric output of addresses and ports 将地址和端口数字化,方便我们查看
$ iptables -nvL PREROUTING -t nat # 查看PREROUTING的链
Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
224M 14G KUBE-SERVICES all -- * * 0.0.0.0/0 0.0.0.0/0 /* kubernetes service portals */
224M 14G DOCKER all -- * * 0.0.0.0/0 0.0.0.0/0 ADDRTYPE match dst-type LOCAL
$ iptables -nvL KUBE-SERVICES -t nat # 查看 KUBE_SERVICES的链 ,这里可以看到我们上面的两个Service 规则
pkts bytes target prot opt in out source destination
0 0 KUBE-MARK-MASQ tcp -- * * !10.244.0.0/16 10.104.60.209 /* nginx-test/nginx-service:http cluster IP */ tcp dpt:80
0 0 KUBE-SVC-JLLMT4ARS7XWDBFY tcp -- * * 0.0.0.0/0 10.104.60.209 /* nginx-test/nginx-service:http cluster IP */ tcp dpt:80
0 0 KUBE-MARK-MASQ tcp -- * * !10.244.0.0/16 10.104.122.7 /* nginx-test/nginx-service-np:http cluster IP */ tcp dpt:80
0 0 KUBE-SVC-D66WLXX5FKD63X4W tcp -- * * 0.0.0.0/0 10.104.122.7 /* nginx-test/nginx-service-np:http cluster IP */ tcp dpt:80
$ iptables -nvL KUBE-NODEPORTS -t nat |grep nginx # 查看NodePort规则的链
pkts bytes target prot opt in out source destination
2 104 KUBE-MARK-MASQ tcp -- * * 0.0.0.0/0 0.0.0.0/0 /* nginx-test/nginx-service-np:http */ tcp dpt:36666
2 104 KUBE-SVC-D66WLXX5FKD63X4W tcp -- * * 0.0.0.0/0 0.0.0.0/0 /* nginx-test/nginx-service-np:http */ tcp dpt:36666
$ iptables -nvL KUBE-MARK-MASQ -t nat # 这里我们看到它只有一条规则,使用mark命令,对数据包设置标记 0x4000,在KUBE-POSTROUTING链上有MARK标记的数据包进行一次MASQUERADE,即**SNAT,会用节点ip替换源ip地址。**
pkts bytes target prot opt in out source destination
5 284 MARK all -- * * 0.0.0.0/0 0.0.0.0/0 MARK or 0x4000
$ iptables -nvL KUBE-SVC-D66WLXX5FKD63X4W -t nat # NodePort描述的Service,自定义的KUBE-SVC-D66WLXX5FKD63X4链通过概率实现了负载均衡
Chain KUBE-SVC-D66WLXX5FKD63X4W (2 references)
pkts bytes target prot opt in out source destination
1 52 KUBE-SEP-SPX6SLDO7E3RZ3D6 all -- * * 0.0.0.0/0 0.0.0.0/0 /* nginx-test/nginx-service-np:http */ statistic mode random probability 0.33333333349
0 0 KUBE-SEP-D67LAGTWHT6R6X4R all -- * * 0.0.0.0/0 0.0.0.0/0 /* nginx-test/nginx-service-np:http */ statistic mode random probability 0.50000000000
1 52 KUBE-SEP-FKVKJM7KELS26OUG all -- * * 0.0.0.0/0 0.0.0.0/0 /* nginx-test/nginx-service-np:http */
# 任意选取一条链进行查看,执行的动作是 DNAT,即目的地址的转换,将Service 的地址转换成具体 Pod的地址。
$ iptables -nvL KUBE-SEP-SPX6SLDO7E3RZ3D6 -t nat
Chain KUBE-SEP-SPX6SLDO7E3RZ3D6 (1 references)
pkts bytes target prot opt in out source destination
0 0 KUBE-MARK-MASQ all -- * * 10.244.1.3 0.0.0.0/0 /* nginx-test/nginx-service-np:http */
1 52 DNAT tcp -- * * 0.0.0.0/0 0.0.0.0/0 /* nginx-test/nginx-service-np:http */ tcp to:10.244.1.3:80
$ iptables -nvL KUBE-SEP-D67LAGTWHT6R6X4R -t nat
Chain KUBE-SEP-D67LAGTWHT6R6X4R (1 references)
pkts bytes target prot opt in out source destination
0 0 KUBE-MARK-MASQ all -- * * 10.244.2.234 0.0.0.0/0 /* nginx-test/nginx-service-np:http */
0 0 DNAT tcp -- * * 0.0.0.0/0 0.0.0.0/0 /* nginx-test/nginx-service-np:http */ tcp to:10.244.2.234:80
$ iptables -nvL KUBE-SEP-FKVKJM7KELS26OUG -t nat
Chain KUBE-SEP-FKVKJM7KELS26OUG (1 references)
pkts bytes target prot opt in out source destination
0 0 KUBE-MARK-MASQ all -- * * 10.244.2.235 0.0.0.0/0 /* nginx-test/nginx-service-np:http */
0 0 DNAT tcp -- * * 0.0.0.0/0 0.0.0.0/0 /* nginx-test/nginx-service-np:http */ tcp to:10.244.2.235:80
在经过了PREROUTING链后,接下来会判断目的ip地址不是本机的ip地址,如果不是,则接下来会经过FORWARD链。在FORWARD链上,仅做了一件事情,就是将前面打了0x4000的数据包允许转发。
shell
$ iptables -nvL FORWARD
Chain FORWARD (policy DROP 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
203M 222G KUBE-FORWARD all -- * * 0.0.0.0/0 0.0.0.0/0 /* kubernetes forwarding rules */
66 3600 KUBE-SERVICES all -- * * 0.0.0.0/0 0.0.0.0/0 ctstate NEW /* kubernetes service portals */
$ iptables -nvL KUBE-FORWARD # 标记 0x4000 的则接收
Chain KUBE-FORWARD (1 references)
pkts bytes target prot opt in out source destination
0 0 DROP all -- * * 0.0.0.0/0 0.0.0.0/0 ctstate INVALID
0 0 ACCEPT all -- * * 0.0.0.0/0 0.0.0.0/0 /* kubernetes forwarding rules */ mark match 0x4000/0x4000
12201 15M ACCEPT all -- * * 0.0.0.0/0 0.0.0.0/0 /* kubernetes forwarding conntrack pod source rule */ ctstate RELATED,ESTABLISHED
0 0 ACCEPT all -- * * 0.0.0.0/0 0.0.0.0/0 /* kubernetes forwarding conntrack pod destination rule */ ctstate RELATED,ESTABLISHED
最后处理结束后,POSTROUTING 会对数据包做一次 SNAT ,从而客户端拿不到Service的真实地址
shell
$ iptables -nvL POSTROUTING -t nat # 可以看到查看 KUBE-POSTROUTING的链
Chain POSTROUTING (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
22M 1354M KUBE-POSTROUTING all -- * * 0.0.0.0/0 0.0.0.0/0 /* kubernetes postrouting rules */
19 1176 MASQUERADE all -- * !docker0 172.17.0.0/16 0.0.0.0/0
22M 1339M FLANNEL-POSTRTG all -- * * 0.0.0.0/0 0.0.0.0/0 /* flanneld masq */
$ iptables -nvL KUBE-POSTROUTING -t nat # 查看到去除了0x4000的标记,并且对数据包执行 MASQUERADE 操作,即SNAT,会用节点ip替换源ip地址。
Chain KUBE-POSTROUTING (1 references)
pkts bytes target prot opt in out source destination
4098 251K RETURN all -- * * 0.0.0.0/0 0.0.0.0/0 mark match ! 0x4000/0x4000
8 480 MARK all -- * * 0.0.0.0/0 0.0.0.0/0 MARK xor 0x4000
8 480 MASQUERADE all -- * * 0.0.0.0/0 0.0.0.0/0 /* kubernetes service traffic requiring SNAT */ random-fully
SNAT 依赖 conntrack 记录的信息,将Service的地址重新替换回来,然后进行回包。另外这里如果不做MASQUERADE,流量发到目的的pod后,pod回包时目的地址为发起端的源地址,而发起端的源地址很可能是在k8s集群外部的,此时pod发回的包是不能回到发起端的。
至此一个数据包经过iptables实现了负载均衡,我们用以下图来进行总结:
iptables也存在比较明显的缺点:
- iptables规则特别乱,一旦出现问题非常难以排查
- 由于iptables规则是串行执行,算法复杂度为O(n),一旦iptables规则多了后,性能将非常差。
- iptables规则提供的负载均衡功能非常有限,不支持较为复杂的负载均衡算法。
所以有了IPVS来对这些缺点进行弥补。
IPVS
详细的内容可以参考 :github.com/kubernetes/...
IPVS(IP 虚拟服务器)实现传输层负载均衡,作为 Linux 内核的一部分。
IPVS模式与IPTABLES模式的区别如下:
- IPVS 为大型集群提供了更好的可扩展性和性能。
- IPVS 支持比 IPTABLES 更复杂的负载平衡算法(最小负载、最少连接、局部性、加权等)。
- IPVS支持服务器健康检查、连接重试等。
IPVS proxier 使用 IPTABLES 进行数据包过滤、SNAT 。具体来说,IPVS proxier 将使用 ipset 来存储需要 DROP 或需要进行SNAT的流量,以确保 IPTABLES 规则的数量恒定,无论我们有多少服务,如果没有ipvs,每一个Service都会有相应的转发规则,大集群下性能会变差。
恭喜!至此我们已经学习了短连接的负载均衡,下一篇文章我会带大家看看长连接的负载均衡。
Reference
- learnk8s.io/kubernetes-...
- learnk8s.io/etcd-kubern...
- etcd.io/docs/v3.5/l...
- xigang.github.io/2019/07/21/...
- nigelpoulton.com/demystifyin...
- github.com/coredns/cor...
- 源码相关内容
- kube-proxy 源码分析: xiaorui.cc/archives/73...
- informer 源码分析: xiaorui.cc/archives/73...
- shareInformer源码分析:xiaorui.cc/archives/73...
- coredns :github.com/rfyiamcool/...
- CNI flannel: github.com/rfyiamcool/...
- github.com/kubernetes/...
- iptables
- 概念介绍:www.zsythink.net/archives/11...
- 动作 DNAT和SNAT:www.zsythink.net/archives/17...
- 让iptables 配置的像负载均衡器:scalingo.com/blog/iptabl...
- conntrack 实现 :arthurchiao.art/blog/conntr...
- iptables 规则分析:kuring.me/post/kube-p...
- ipvs: www.linuxvirtualserver.org/software/ip...