Kube Proxy
kube-proxy监听API server中service和endpoint的变化情况,并通过userspace、iptables、ipvs或winuserspace等proxier来为服务配置负载均衡(仅支持TCP和UDP)。
本文对kube-proxy做了一些总结说明,对其内部的实现原理进行了研究,并对userspace和iptables两种mode的缺点进行的描述,都通过例子说明了iptable的工作。在下一篇博文中,我将对k8s v1.5中kube-proxy的源码进行分析,有兴趣的同学可以关注。
kube-proxy和service背景
说到kube-proxy,就不得不提到k8s中service,下面对它们两做简单说明:
kube-proxy其实就是管理service的访问入口,包括集群内Pod到Service的访问和集群外访问service。 kube-proxy管理sevice的Endpoints,该service对外暴露一个Virtual IP,也成为Cluster IP, 集群内通过访问这个Cluster IP:Port就能访问到集群内对应的serivce下的Pod。 service是通过Selector选择的一组Pods的服务抽象,其实就是一个微服务,提供了服务的LB和反向代理的能力,而kube-proxy的主要作用就是负责service的实现。 service另外一个重要作用是,一个服务后端的Pods可能会随着生存灭亡而发生IP的改变,service的出现,给服务提供了一个固定的IP,而无视后端Endpoint的变化。
服务发现
k8s提供了两种方式进行服务发现:
环境变量: 当你创建一个Pod的时候,kubelet会在该Pod中注入集群内所有Service的相关环境变量。需要注意的是,要想一个Pod中注入某个Service的环境变量,则必须Service要先比该Pod创建。这一点,几乎使得这种方式进行服务发现不可用。
比如,一个ServiceName为redis-master的Service,对应的ClusterIP:Port为10.0.0.11:6379,则其对应的环境变量为:
[java] view plain copy
REDIS_MASTER_SERVICE_HOST=10.0.0.11
REDIS_MASTER_SERVICE_PORT=6379
REDIS_MASTER_PORT=tcp://10.0.0.11:6379
REDIS_MASTER_PORT_6379_TCP=tcp://10.0.0.11:6379
REDIS_MASTER_PORT_6379_TCP_PROTO=tcp
REDIS_MASTER_PORT_6379_TCP_PORT=6379
REDIS_MASTER_PORT_6379_TCP_ADDR=10.0.0.11
DNS:这也是k8s官方强烈推荐的方式。
可以通过cluster add-on的方式轻松的创建KubeDNS来对集群内的Service进行服务发现。
更多关于KubeDNS的内容,请查看: Kubernetes DNS Service技术研究。
发布(暴露)服务
k8s原生的,一个Service的ServiceType决定了其发布服务的方式。
1.ClusterIP:这是k8s默认的ServiceType。通过集群内的ClusterIP在内部发布服务。
2.NodePort:这种方式是常用的,用来对集群外暴露Service,你可以通过访问集群内的每个NodeIP:NodePort的方式,访问到对应Service后端的Endpoint。
3.LoadBalancer: 这也是用来对集群外暴露服务的,不同的是这需要Cloud Provider的支持,比如AWS等。 ExternalName:这个也是在集群内发布服务用的,需要借助KubeDNS(version >= 1.7)的支持,就是用KubeDNS将该service和ExternalName做一个Map,KubeDNS返回一个CNAME记录。
kube-proxy内部原理
kube-proxy当前实现了两种proxyMode:userspace和iptables。其中userspace mode是v1.0及之前版本的默认模式,从v1.1版本中开始增加了iptables mode,在v1.2版本中正式替代userspace模式成为默认模式。
kube-proxy转发的两种模式
kube-proxy在转发时主要有两种模式Userspace和Iptables。
使用Userspace模式(k8s版本为1.2之前默认模式),外部网络可以直接访问cluster IP。
使用Iptables模式(k8s版本为1.2之后默认模式),外部网络不能直接访问cluster IP。
从效率上看,Iptables会更高一些,但是需要Iptables version >=1.4.11.
userspace mode
userspace是在用户空间,通过kube-proxy来实现service的代理服务。废话不多说,其原理如下如图所示:
输入图片说明
可见,这种mode最大的问题是,service的请求会先从用户空间进入内核iptables,然后再回到用户空间,由kube-proxy完成后端Endpoints的选择和代理工作,这样流量从用户空间进出内核带来的性能损耗是不可接受的。这也是k8s v1.0及之前版本中对kube-proxy质疑最大的一点,因此社区就开始研究iptables mode。
Example
[java] view plain copy
$ kubectl get service
NAME LABELS SELECTOR IP(S) PORT(S)
kubernetes component=apiserver,provider=kubernetes 10.254.0.1 443/TCP
ssh-service1 name=ssh,role=service ssh-service=true 10.254.132.107 2222/TCP
$ kubectl describe service ssh-service1
Name: ssh-service1
Namespace: default
Labels: name=ssh,role=service
Selector: ssh-service=true
Type: LoadBalancer
IP: 10.254.132.107
Port: 2222/TCP
NodePort: 30239/TCP
Endpoints:
Session Affinity: None
No events.
NodePort的工作原理与ClusterIP大致相同,发送到某个NodeIP:NodePort的请求,通过iptables重定向到kube-proxy对应的端口(Node上的随机端口)上,然后由kube-proxy再将请求发送到其中的一个Pod:TargetPort。
这里,假如Node的ip为10.0.0.5,则对应的iptables如下:
[java] view plain copy
$ sudo iptables -S -t nat
...
-A KUBE-NODEPORT-CONTAINER -p tcp -m comment --comment "default/ssh-service1:" -m tcp --dport 30239 -j REDIRECT --to-ports 36463
-A KUBE-NODEPORT-HOST -p tcp -m comment --comment "default/ssh-service1:" -m tcp --dport 30239 -j DNAT --to-destination 10.0.0.5:36463
-A KUBE-PORTALS-CONTAINER -d 10.254.132.107/32 -p tcp -m comment --comment "default/ssh-service1:" -m tcp --dport 2222 -j REDIRECT --to-ports 36463
-A KUBE-PORTALS-HOST -d 10.254.132.107/32 -p tcp -m comment --comment "default/ssh-service1:" -m tcp --dport 2222 -j DNAT --to-destination 10.0.0.5:36463
可见:访问10.0.0.5:30239端口会被转发到node上的36463端口(随机监听端口)。而且在访问clusterIP 10.254.132.107的2222端口时,也会把请求转发到本地的36463端口。 36463端口实际被kube-proxy所监听,将流量进行导向到后端的pod上。
iptables mode
另一种mode是iptables,它完全利用内核iptables来实现service的代理和LB。是v1.2及之后版本默认模式,其原理图如下所示:
输入图片说明
iptables mode因为使用iptable NAT来完成转发,也存在不可忽视的性能损耗。另外,如果集群中存在上万的Service/Endpoint,那么Node上的iptables rules将会非常庞大,性能还会再打折扣。
这也导致,目前大部分企业用k8s上生产时,都不会直接用kube-proxy作为服务代理,而是通过自己开发或者通过Ingress Controller来集成HAProxy, Nginx来代替kube-proxy。
Example
iptables的方式则是利用了linux的iptables的nat转发进行实现。
[java] view plain copy
apiVersion: v1
kind: Service
metadata:
labels:
name: <a href="https://www.2cto.com/database/MySQL/" class="keylink" style="border:none;padding:0px;margin:0px;color:rgb(31,58,135);text-decoration:none;background:none;">mysql</a>
role: service
name: mysql-service
spec:
ports:
- port: 3306
targetPort: 3306
nodePort: 30964
type: NodePort
selector:
mysql-service: "true"
mysql-service对应的nodePort暴露出来的端口为30964,对应的cluster IP(10.254.162.44)的端口为3306,进一步对应于后端的pod的端口为3306。
mysql-service后端代理了两个pod,ip分别是192.168.125.129和192.168.125.131。先来看一下iptables。
[java] view plain copy
$iptables -S -t nat
...
-A PREROUTING -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
-A OUTPUT -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
-A POSTROUTING -m comment --comment "kubernetes postrouting rules" -j KUBE-POSTROUTING
-A KUBE-MARK-MASQ -j MARK --set-xmark 0x4000/0x4000
-A KUBE-NODEPORTS -p tcp -m comment --comment "default/mysql-service:" -m tcp --dport 30964 -j KUBE-MARK-MASQ
-A KUBE-NODEPORTS -p tcp -m comment --comment "default/mysql-service:" -m tcp --dport 30964 -j KUBE-SVC-67RL4FN6JRUPOJYM
-A KUBE-SEP-ID6YWIT3F6WNZ47P -s 192.168.125.129/32 -m comment --comment "default/mysql-service:" -j KUBE-MARK-MASQ
-A KUBE-SEP-ID6YWIT3F6WNZ47P -p tcp -m comment --comment "default/mysql-service:" -m tcp -j DNAT --to-destination 192.168.125.129:3306
-A KUBE-SEP-IN2YML2VIFH5RO2T -s 192.168.125.131/32 -m comment --comment "default/mysql-service:" -j KUBE-MARK-MASQ
-A KUBE-SEP-IN2YML2VIFH5RO2T -p tcp -m comment --comment "default/mysql-service:" -m tcp -j DNAT --to-destination 192.168.125.131:3306
-A KUBE-SERVICES -d 10.254.162.44/32 -p tcp -m comment --comment "default/mysql-service: cluster IP" -m tcp --dport 3306 -j KUBE-SVC-67RL4FN6JRUPOJYM
-A KUBE-SERVICES -m comment --comment "kubernetes service nodeports; NOTE: this must be the last rule in this chain" -m addrtype --dst-type LOCAL -j KUBE-NODEPORTS
-A KUBE-SVC-67RL4FN6JRUPOJYM -m comment --comment "default/mysql-service:" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-ID6YWIT3F6WNZ47P
-A KUBE-SVC-67RL4FN6JRUPOJYM -m comment --comment "default/mysql-service:" -j KUBE-SEP-IN2YML2VIFH5RO2T
首先如果是通过node的30964端口访问,则会进入到以下链:
[java] view plain copy
-A KUBE-NODEPORTS -p tcp -m comment --comment "default/mysql-service:" -m tcp --dport 30964 -j KUBE-MARK-MASQ
-A KUBE-NODEPORTS -p tcp -m comment --comment "default/mysql-service:" -m tcp --dport 30964 -j KUBE-SVC-67RL4FN6JRUPOJYM
然后进一步跳转到KUBE-SVC-67RL4FN6JRUPOJYM的链:
[java] view plain copy
-A KUBE-SVC-67RL4FN6JRUPOJYM -m comment --comment "default/mysql-service:" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-ID6YWIT3F6WNZ47P
-A KUBE-SVC-67RL4FN6JRUPOJYM -m comment --comment "default/mysql-service:" -j KUBE-SEP-IN2YML2VIFH5RO2T
这里利用了iptables的–probability的特性,使连接有50%的概率进入到KUBE-SEP-ID6YWIT3F6WNZ47P链,50%的概率进入到KUBE-SEP-IN2YML2VIFH5RO2T链。
KUBE-SEP-ID6YWIT3F6WNZ47P的链的具体作用就是将请求通过DNAT发送到192.168.125.129的3306端口。
[java] view plain copy
-A KUBE-SEP-ID6YWIT3F6WNZ47P -s 192.168.125.129/32 -m comment --comment "default/mysql-service:" -j KUBE-MARK-MASQ
-A KUBE-SEP-ID6YWIT3F6WNZ47P -p tcp -m comment --comment "default/mysql-service:" -m tcp -j DNAT --to-destination 192.168.125.129:3306
同理KUBE-SEP-IN2YML2VIFH5RO2T的作用是通过DNAT发送到192.168.125.131的3306端口。
[java] view plain copy
-A KUBE-SEP-IN2YML2VIFH5RO2T -s 192.168.125.131/32 -m comment --comment "default/mysql-service:" -j KUBE-MARK-MASQ
-A KUBE-SEP-IN2YML2VIFH5RO2T -p tcp -m comment --comment "default/mysql-service:" -m tcp -j DNAT --to-destination 192.168.125.131:3306
分析完nodePort的工作方式,接下里说一下clusterIP的访问方式。 对于直接访问cluster IP(10.254.162.44)的3306端口会直接跳转到KUBE-SVC-67RL4FN6JRUPOJYM。
[java] view plain copy
-A KUBE-SERVICES -d 10.254.162.44/32 -p tcp -m comment --comment "default/mysql-service: cluster IP" -m tcp --dport 3306 -j KUBE-SVC-67RL4FN6JRUPOJYM
接下来的跳转方式同NodePort方式。
参考链接
2.kubernetes的kube-proxy的转发规则分析
2.1概要
当service有了port和nodePort之后,就可以对内/外提供服务。那么其具体是通过什么原理来实现的呢?奥妙就在kube-proxy在本地node上创建的iptables规则。kube-proxy是kubernetes中设置转发规则的组件,通过iptables修改报文的流向。
本文所用的kube-proxy版本如下:
这里写图片描述
五个检查点:
这里写图片描述
通过“iptables -L -t [iptables表名]”可以看到,kube-proxy只修改了filter和nat表。
2.2 filter表
filter表中Chain:
这里写图片描述
由上图可以看到:
kube-proxy只设置了filter表中INPUT chain和OUTPUT chain,增加了KUBE-FIREWALL和KUBE-SERVICES两个规则链。
所有的出报文都要经过KUBE-SERVICES,如果一个Service没有对应的endpoint,则拒绝将报文发出(如图中KUBE-SERVICES链中的那两行所示)
注意在KUBE-FIREWALL中,所有标记了0x8000的包都会被丢弃,标记动作可以发生在其它的表中。
2.3 nat表
nat表中设置的规则比较多(先截取一部分看):
这里写图片描述
先无视其中的calico,单看kube-proxy,由上图可以看到:
kube-proxy设置了nat表中PREROUTING链、OUTPUT链、POSTROUTING 链,增加了KUBE-MARKDROP链和KUBE-MARK-MASQ、KUBE-NODEPORTS、KUBE-POSTROUTING、多个KUBE-SEP-XXX链、KUBE-SERVICES、多个KUBE-SVC-XXX链。
在PREROUTING阶段,将所有报文转发到KUBE-SERVICES
在OUTPUT阶段,将所有报文转发到KUBE-SERVICES
在POSTROUTING阶段,将所有报文转发到KUBE-POSTROUTING
先来看KUBE-SERVICES链:
这里写图片描述
可以看到,所有对service的clusterIP的访问都会先转到KUBE-MARK-MASQ链,然后再转到KUBE-SVC-XXX链。
(1)看一下KUBE-MARK-MASQ链做了什么:
这里写图片描述
即,在KUBE-MARK-MASQ链中为包set mark 0x4000, 有此标记的数据包会在KUBE-POSTROUTING链中统一做MASQUERADE
(2)接着包走到KUBE-SVC-XXX链,看一下在KUBE-SVC-XXX链中做了什么:
这里写图片描述
可以看到,每一个SERVICE,假如有多后端endpoint的话,按照一定规则(比如random)将报文提交到了某个的KUBE-SEP-XXX,KUBE-SEP-XXX链做了什么:
这里写图片描述
首先是又走了一下KUBE-MARK-MASQ链set mark 0x4000(之后在KUBE-POSTROUTING链中会做SNAT),最后在KUBE-SEP-XX中完整了最终的DNAT,将目的地址转换成了POD的IP和端口。