Service和kube-proxy是一对难兄难弟,它们集群中起到的作用如下:
简要说明:
- kube-proxy会watch Service和Endpoints对象
- 如果检测到Service和Endpoints变化后,会在自己的节点上设置iptables或ipvs规则
- 然后客户端就可以通过clusterip访问到具体的pod
从上面可以看出整个逻辑其实并不复杂,但是底层实现还是很复杂的,我们来一一道来。
Service的服务发现
目前Service支持两种服务发现机制,一种是环境变量的方式,还有一种的DNS的方式,现目前都采用的DNS方式。
环境变量方式
在这种方式下,kubelet在创建Pod的时候会自动将可用的Service以环境变量的形式添加到Pod中。这些环境变量以以下形式:
- {SVCNAME}_SERVICE_HOST
- {SVCNAME}_SERVICE_PORT
其中SVCNAME是service的名字,比如我们创建下面这个service,如果有-
,会被替换成_
。
比如有一个叫my-app的service,集群为其分配的svc ip为192.168.10.12,svc port为8080,那么就会产生如下的环境变量。
MY_APP_SERVICE_HOST=192.168.10.12
MY_APP_SERVICE_PORT=8080
然后客户端就可以使用MY_APP_SERVICE_HOST:MY_APP_SERVICE_PORT来访问pod了。
值得注意的是,在这种方式下,service虚先于pod启动,如果pod先启动,那么service的环境变量是不会被注入Pod的,而且随着集群规模的增加,就会造成环境变量泛滥,维护成本增加。
DNS方式
这是目前默认的服务发现方式,而且是用CoreDNS来实现。
这个DNS服务通过kubernetes的watchAPI不断的检测service,并为其添加一条DNS解析记录,如果DNS在整个集群范围都可用,那么任意Pod都能自动解析service的域名。
比如有一个叫my-service的service,工作在default命名空间,那么就会形成一条my-service.default的DNS解析记录,如果是在相同的命名空间下,可以直接通过my-service访问到该service对应的pod,如果是在其他的命名空间下,则可以通过my-service.default访问到该service对应的Pod。
但是用DNS这种服务发现方式也会有一些问题,最常见的就是缓存问题。DNS服务器通常会将查找到的结果缓存到本地,方便下次使用,如果对其进行了修改的话可能会导致下次访问得到的解析结果是错误的。
不过目前来看,这种方式还是非常受欢迎的,而且随着DNS周边也在不断的优化,比如DNSLocalCache等,相信以后会越来越好。
Service负载均衡
Service的负载均衡是通过iptabels或者IPVS来实现的,而iptables或IPVS规则则是由kube-proxy来实现的,所以kube-proxy其实是一个中间桥梁,它连接了k8s和Linux系统。下面来介绍以下这两种负载方式。
iptables方式
iptables是一个用户态程序,是面向用户的,它是通过配置底层的Netfilter来配置防火墙规则。Netfilter是Linux内核的网络包管理框架,工作在OSI的第三层。它提供了一整套的hook函数管理机制,使得诸如数据包过滤,网络地址转换(NAT)和基于协议类型的连接跟踪成为了可能。
Netfilter在内核中的位置如下图所示
接下来介绍kube-proxy是如何利用Iptables做负载均衡的。数据包在Iptables中的匹配流程如下图所示:
在Iptables模式下,kube-proxy通过在目标node节点上的Iptables中的NAT表的PREROUTIN和POSTROUTING链中创建一系列的自定义链(这些自定义链主要是”KUBE-SERVICE”链, “KUBE-POSTROUTING”链,每个服务对应的”KUBE-SVC-XXXXXX”链和”KUBE-SEP-XXXX”链),然后通过这些自定义链对流经到该Node的数据包做DNAT和SNAT操作从而实现路由,负载均衡和地址转化,如下图所示:
kube-proxy中,客户端的请求数据包在Iptables规则中具体的匹配过程为:
- PREROUTING链或者OUTPUT链(集群内的Pod通过clusterIP访问Service时经过OUTPUT链, 而当集群外主机通过NodePort方式访问Service时,通过PREROUTING链,两个链都会跳转到KUBE-SERVICE链)
- KUBE-SERVICES链(每一个Service所暴露的每一个端口在KUBE-SERVICES链中都会对应一条相应的规则,当Service的数量达到一定规模时,KUBE-SERVICES链中的规则的数据将会非常的大,而Iptables在进行查找匹配时是线性查找,这将耗费很长时间,时间复杂度O(n))
- KUBE-SVC-XXXXX链 (在KUBE-SVC-XXXXX链中(后面那串 hash 值由 Service 的虚 IP 生成),会以一定的概率匹配下面的某一条规则执行,通过statistic模块为每个后端设置权重,已实现负载均衡的目的,每个KUBE-SEP-XXXXX链代表Service后面的一个具体的Pod(后面那串 hash 值由后端 Pod 实际 IP 生成),这样便实现了负载均衡的目的)
- KUBE-SEP-XXXX链 (通过DNAT,将数据包的目的IP修改为服务端的Pod IP)
- POSTROUTING链
- KUBE_POSTROUTING链 (对标记的数据包做SNAT)
通过上面的这个设置便实现了基于Iptables实现了负载均衡。但是Iptbles做负载均衡存在一些问题:
- 规则线性匹配时延:
KUBE-SERVICES链挂了一长串KUBE-SVC-*链,访问每个service,要遍历每条链直到匹配,时间复杂度O(N) - 规则更新时延:
非增量式,需要先iptables-save拷贝Iptables状态,然后再更新部分规则,最后再通过 iptables-restore写入到内核。当规则数到达一定程度时,这个过程就会变得非常缓慢。 - 可扩展性:
当系统存在大量的Iptables规则链时,增加/删除规则会出现kernel lock,这时只能等待。 - 可用性: 服务扩容/缩容时, Iptables规则的刷新会导致连接断开,服务不可用。
IPVS方式
IPVS是LVS项目的一部分,是一款运行在Linux kernel当中的4层负载均衡器,性能异常优秀。使用调优后的内核,可以轻松处理每秒10万次以上的转发请求。
IPVS具有以下特点:
- 传输层Load Balancer, LVS负载均衡器的实现。
- 与Iptables同样基于Netfilter, 但是使用的是hash表。
- 支持TCP, UDP, SCTP协议,支持IPV4, IPV6。
- 支持多种负载均衡策略:
- rr: round-robin
- lc: least connection
- dh: destination hashing
- sh: source hashing
- sed: shortest expected delay
- nq: never queue
- rr: round-robin
- 支持会话保持
LVS的工作原理如下图所示:
其工作流程如下:
- 当客户端的请求到达负载均衡器的内核空间时,首先会达到PREROUTING链。
- 当内核发现请求的数据包的目的地址是本机时,将数据包送往INPUT链。
- 当数据包达到INPUT链时, 首先会被IPVS检查,如果数据包里面的目的地址及端口没有在IPVS规则里面,则这条数据包将被放行至用户空间。
- 如果数据包里面的目的地址和端口在IPVS规则里面,那么这条数据报文的目的地址会被修改为通过负责均衡算法选好的后后端服务器(DNAT),并发往POSROUTING链。
- 最后经由POSTROUTING链发往后端的服务器。
LVS主要由三种工作模式, 分别是NAT, DR, Tunnel模式,而在kube-proxy中,IPVS工作在NAT模式,所以下面主要对NAT模式进行介绍(还是分析上面的那张图):
- 客户端将请求发往前端的负载均衡器,请求报文源地址是CIP(客户端IP), 目的地址是VIP(负载均衡器前端地址)
- 负载均衡器收到报文之后,发现请求的是在规则里面存在的地址,那么它将请求的报文的目的地址改为后端服务器的RIP地址,并将报文根据响应的负责均衡策略发送出去
- 报文发送到Real Server后,由于报文的目的地址是自己,所有会响应请求,并将响应的报文返回给LVS
- 然后LVS将此报文的源地址修改本机的IP地址并发送给客户端
介绍完基本的工作原理之后,下面我们看看如何在kube-proxy中使用IPVS模式进行负载均衡。
首先需要在启动kube-proxy的参数中指定如下参数:
--proxy-mode=ipvs //将kube-proxy的模式设置为IPVS
--ipvs-scheduler=rr //设置ipvs的负载均衡算法,默认是rr
--ipvs-min-sync-period=5s // 刷新IPVS规则的最小时间间隔
--ipvs-sync-period=30s // 刷新IPVS规则的最大时间间隔
设置完这些参数之后,重启启动kube-proxy服务即可。当创建ClusterIP类型的Service时,IPVS模式的kube-proxy会做下面几件事儿:
- 创建虚拟网卡,默认是kube-ipvs0
- 绑定service IP地址到虚拟网卡kube-ipvs0
- 为每一个Service IP地址创建IPVS虚拟服务器
同时IPVS还支持会话保持功能,通过在创建Srevice对象时,指定service.spec.sessionAffinity参数为ClusterIP默认是None 和 指定service.spec.sessionAffinityConfig.clientIP.timeoutSeconds参数为需要的时间,默认是10800s。
下面是一个创建Service,指定会话保持的一个具体的例子:
kind: Service
apiVersion: v1
metadata:
name: nginx-service
spec:
type: ClusterIP
selector:
app: my-app
sessionAffinity: ClientIP
sessionAffinityConfig:
clientIP:
timeoutSeconds: 50
ports:
- name: http
protocol: TCP
port: 80
targetPort: 80
kube-proxy在使用iptables和ipvs实现对Service的负载均衡,但是通过iptables的实现方式,由于Iptables本身的特性,新增规则,更新规则是非增量式的,需要先iptables-save然后在内存中更新规则,在内核中修改规则,在iptables-restore,并且Iptables在进行规则查找匹配时是线性查找,这将耗费很长时间,时间复杂度O(n)。而使用IPVS的实现方式,其连接过程的时间复杂度是O(1)。基本就是说连接的效率与集群Service的数量是无关的。因此随着集群内部Service的不断增加,IPVS的性能优势就体现出来了。