1 为什么需要kube-proxy
我们知道容器的特点是快速创建、快速销毁,Kubernetes Pod和容器一样只具有临时的生命周期,一个Pod随时有可能被终止或者漂移,随着集群的状态变化而变化,一旦Pod变化,则该Pod提供的服务也就无法访问,如果直接访问Pod则无法实现服务的连续性和高可用性,因此显然不能使用Pod地址作为服务暴露端口。
解决这个问题的办法和传统数据中心解决无状态服务高可用的思路完全一样,通过负载均衡和VIP实现后端真实服务的自动转发、故障转移。
这个负载均衡在Kubernetes中称为Service,VIP即Service ClusterIP,因此可以认为Kubernetes的Service就是一个四层负载均衡,Kubernetes对应的还有七层负载均衡Ingress,本文仅介绍Kubernetes Service。
这个Service就是由kube-proxy实现的,ClusterIP不会因为Podz状态改变而变,需要注意的是VIP即ClusterIP是个假的IP,这个IP在整个集群中根本不存在,当然也就无法通过IP协议栈无法路由,底层underlay设备更无法感知这个IP的存在,因此ClusterIP只能是单主机(Host Only)作用域可见,这个IP在其他节点以及集群外均无法访问。
Kubernetes为了实现在集群所有的节点都能够访问Service,kube-proxy默认会在所有的Node节点都创建这个VIP并且实现负载,所以在部署Kubernetes后发现kube-proxy是一个DaemonSet。
而Service负载之所以能够在Node节点上实现是因为无论Kubernetes使用哪个网络模型,均需要保证满足如下三个条件:
-
容器之间要求不需要任何NAT能直接通信;
-
容器与Node之间要求不需要任何NAT能直接通信;
-
容器看到自身的IP和外面看到它的IP必须是一样的,即不存在IP转化的问题。
至少第2点是必须满足的,有了如上几个假设,Kubernetes Service才能在Node上实现,否则Node不通Pod IP也就实现不了了。有人说既然kube-proxy是四层负载均衡,那kube-proxy应该可以使用haproxy、nginx等作为负载后端啊?
事实上确实没有问题,不过唯一需要考虑的就是性能问题,如上这些负载均衡功能都强大,但毕竟还是基于用户态转发或者反向代理实现的,性能必然不如在内核态直接转发处理好。因此kube-proxy默认会优先选择基于内核态的负载作为后端实现机制,目前kube-proxy默认是通过iptables实现负载的,在此之前还有一种称为userspace模式,其实也是基于iptables实现,可以认为当前的iptables模式是对之前userspace模式的优化。
本节接下来将详细介绍kube-proxy iptables模式的实现原理。
2 kube-proxy iptables模式实现原理
这里使用一个部署在k8中的一个应用作为示例,来了解整个报文的调用流程
node网段:10.216.4.0/24
pod网段:10.246.0.0/16
svc网段:10.254.0.0/16
操作node:10.216.4.21
测试应用名称:go-hello,已经部署好deployment, pod, svc服务
[root @k8s -server-t- 4 - 21 ~]#kubectl get svc -n tech-daily go-hello-svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE go-hello-svc ClusterIP 10.254 . 56.75 <none> 8081 /TCP 14d [root @k8s -server-t- 4 - 21 ~]#kubectl get pod -n tech-daily -o wide | grep go-hello go-hello-deployment-67bc4d947b-k42xl 3 / 3 Running 0 37h 10.246 . 23.126 k8s-server-t- 4 - 23 <none> go-hello-deployment-67bc4d947b-ngzs7 3 / 3 Running 0 13d 10.246 . 12.94 k8s-server-t- 34 - 12 <none> go-hello-deployment-67bc4d947b-wkvlm 3 / 3 Running 0 27m 10.246 . 19.175 k8s-server-t- 4 - 19 <none> |
防火墙的四表五链需要先了解
当用户在linux的命令行里面进行curl另外一台宿主机上的pod对应的ip的时候,属于linux本机资源开始的请求
此时在Node节点10.216.4.21上访问该Service服务,首先流量到达的是OUTPUT链,这里我们只关心nat表的OUTPUT链:
[root @k8s -server-t- 4 - 21 ~]#iptables-save -t nat | grep OUTPUT :OUTPUT ACCEPT [ 416 : 24960 ] -A OUTPUT -m comment --comment "kubernetes service portals" -j KUBE-SERVICES -A OUTPUT ! -d 127.0 . 0.0 / 8 -m addrtype --dst-type LOCAL -j DOCKER |
该链跳转到KUBE-SERVICES子链中:
[root @k8s -server-t- 4 - 21 ~]#iptables-save -t nat | grep KUBE-SERVICES | grep go-hello -A KUBE-SERVICES ! -s 172.16 . 0.0 / 16 -d 10.254 . 56.75 / 32 -p tcp -m comment --comment "tech-daily/go-hello-svc:http cluster IP" -m tcp --dport 8081 -j KUBE-MARK-MASQ -A KUBE-SERVICES -d 10.254 . 56.75 / 32 -p tcp -m comment --comment "tech-daily/go-hello-svc:http cluster IP" -m tcp --dport 8081 -j KUBE-SVC-CAMFZVZYIQQOXWCG |
我们发现里面有很多相同的规则,都是相关的两条:
第一条负责打标记MARK 0x4000/0x4000,后面会用到这个标记。
第二条规则跳到KUBE-SVC-CAMFZVZYIQQOXWCG子链。
其中KUBE-SVC-CAMFZVZYIQQOXWCG子链规则如下:
[root @k8s -server-t- 4 - 21 ~]#iptables-save -t nat | grep KUBE-SVC-CAMFZVZYIQQOXWCG -A KUBE-SVC-CAMFZVZYIQQOXWCG -m statistic --mode random --probability 0.33332999982 -j KUBE-SEP-4WNCI7OIWBDOLCN2 -A KUBE-SVC-CAMFZVZYIQQOXWCG -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-NHBL56EJB3OMKK3H -A KUBE-SVC-CAMFZVZYIQQOXWCG -j KUBE-SEP-RQJRFHXYJIRTVOGX |
这几条规则看起来复杂,其实实现的功能很简单:
1/3的概率跳到子链KUBE-SEP-4WNCI7OIWBDOLCN2
剩下概率的1/2,(1 – 1/3) * 1/2 == 1/3,即1/3的概率跳到子链KUBE-SEP-NHBL56EJB3OMKK3H
剩下1/3的概率跳到KUBE-SEP-RQJRFHXYJIRTVOGX
我们查看其中一个子链KUBE-SEP-4WNCI7OIWBDOLCN2规则:
[root @k8s -server-t- 4 - 21 ~]#iptables-save -t nat | grep KUBE-SEP-4WNCI7OIWBDOLCN2 -A KUBE-SEP-4WNCI7OIWBDOLCN2 -p tcp -m tcp -j DNAT --to-destination 10.246 . 12.94 : 8081 |
可见这条规则的目的是做了一次DNAT,DNAT目标为其中一个Endpoint,即Pod服务。
由此可见子链KUBE-SEP-4WNCI7OIWBDOLCN2的功能就是按照概率均等的原则DNAT到其中一个Endpoint IP,即Pod IP
接下来到filter的OUTPUT链
[root @k8s -server-t- 4 - 21 ~]#iptables-save | grep OUTPUT -A OUTPUT -m conntrack --ctstate NEW -m comment --comment "kubernetes service portals" -j KUBE-SERVICES -A OUTPUT -j KUBE-FIREWALL |
这里面主要是两个策略:
第一条是有service的ip,但是没有pod的,全部REJECT掉
第二条是标记为0x8000全部DROP
这两条都不符合,走默认策略ACCEPT
接着来到POSTROUTING链:
[root @k8s -server-t- 4 - 21 ~]#iptables-save -t nat | grep -- '-A POSTR' -A POSTROUTING -m comment --comment "kubernetes postrouting rules" -j KUBE-POSTROUTING [root @k8s -server-t- 4 - 21 ~]#iptables-save -t nat | grep -- 'KUBE-POSTROUTING' -A KUBE-POSTROUTING -m comment --comment "kubernetes service traffic requiring SNAT" -m mark --mark 0x4000 / 0x4000 -j MASQUERADE |
这两条规则只做一件事就是只要标记了 0x4000/0x4000的包就一律做MASQUERADE(SNAT),因此会把源IP改为10.216.4.21发送出去
NodePort
接下来研究下NodePort过程,这里使用ingress的Service进行分析:
[root @k8s -server-t- 4 - 22 ~]#kubectl get svc ingress-nginx -n ingress-nginx-daily NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE ingress-nginx NodePort 10.254 . 67.230 <none> 80 : 31553 /TCP, 443 : 31248 /TCP, 10254 : 32154 /TCP 335d |
其中Service的NodePort端口为31553。即每个node上面都有这个端口在监听,可以通过node的ip加端口进行访问,也可以通过svc的ip进行访问
假设有一个外部IP 10.16.10.1,通过10.216.4.22:31553访问服务。
首先到达PREROUTING链:
[root @k8s -server-t- 4 - 22 ~]#iptables-save -t nat | grep PREROUTING -A PREROUTING -m comment --comment "kubernetes service portals" -j KUBE-SERVICES [root @k8s -server-t- 4 - 22 ~]#iptables-save -t nat | grep -- '-A KUBE-SERVICES' ... -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 |
PREROUTING的规则非常简单,凡是发给自己的包,则交给子链 KUBE-NODEPORTS处理。注意前面省略了判断ClusterIP的部分规则。
KUBE-NODEPORTS规则如下:
[root @k8s -server-t- 4 - 22 ~]#iptables-save -t nat | grep NODEPORTS -A KUBE-NODEPORTS -p tcp -m comment --comment "ingress-nginx-daily/ingress-nginx:http" -m tcp --dport 31553 -j KUBE-MARK-MASQ -A KUBE-NODEPORTS -p tcp -m comment --comment "ingress-nginx-daily/ingress-nginx:http" -m tcp --dport 31553 -j KUBE-SVC-QUKMVOJ2WLHT7WTD |
这个规则首先给包打上标记 0x4000/0x4000,然后交给子链KUBE-SVC-QUKMVOJ2WLHT7WTD处理, KUBE-SVC-QUKMVOJ2WLHT7WTD刚刚已经见面过了,其功能就是按照概率均等的原则DNAT到其中一个Endpoint IP,即Pod IP
接着到了 FORWARD链
[root @k8s -server-t- 4 - 22 ~]#iptables-save | grep FORWARD -A FORWARD -m comment --comment "kubernetes forwarding rules" -j KUBE-FORWARD -A KUBE-FORWARD -m comment --comment "kubernetes forwarding rules" -m mark --mark 0x4000 / 0x4000 -j ACCEPT -A KUBE-FORWARD -s 172.16 . 0.0 / 16 -m comment --comment "kubernetes forwarding conntrack pod source rule" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT -A KUBE-FORWARD -d 172.16 . 0.0 / 16 -m comment --comment "kubernetes forwarding conntrack pod destination rule" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT |
FORWARD表在这里只是判断下,只允许打了标记 0x4000/0x4000的包才允许转发。
最后来到 POSTROUTING链,这里和ClusterIP就完全一样了,在 KUBE-POSTROUTING中做一次 MASQUERADE(SNAT)
上面是k8的service原理介绍,下面说说公司遇到的问题,以及相应的解决方案思路
问题
1、pod访问外部的时候源ip使用的是node的ip地址,这种情况在dubbo注册的时候会有问题,导致zookeeper显示的注册节点为node ip,显示不出真实的pod ip,排查问题比较费劲,这里需要显示pod的ip地址
2、需要对pod访问外部网络进行限制,只允许访问特定的ip和端口,并且需要将其自动化,研发人员可以通过工单的形式自动开通访问
为了解决第一个问题,需要先知道在哪个阶段ip地址做了更改
在POSTROUTING链中会对ip地址做snat,因此,需要在这里添加规则,当目标地址为内网ip的时候,直接返回,使用原始的ip进行通信
当访问外部网络的时候使用snat进行通信
[root @k8s -server-t- 4 - 21 ~]#iptables-save -t nat | grep POSTR :POSTROUTING ACCEPT [ 577 : 35234 ] :KUBE-POSTROUTING - [ 0 : 0 ] -A POSTROUTING -m comment --comment "kubernetes postrouting rules" -j KUBE-POSTROUTING -A POSTROUTING -s 10.246 . 21.0 / 24 -d 10.0 . 0.0 / 8 -j RETURN -A POSTROUTING -s 10.246 . 21.0 / 24 -d 172.18 . 0.0 / 16 -j RETURN |
为了解决限制pod访问外网的问题
第一种解决方案是pod使用自己的ip地址访问外网,然后在硬件防火墙上面做限制,根据源ip和目标ip进行限制,缺点是需要频繁和基础网络组进行沟通协调沟通,不利于自动化,最好是将限制放到应用这边,这样更加灵活和便于控制
第二种解决方案是pod访问内网使用自己的ip地址进行访问,但是访问外网的时候使用node的ip,这样基础网络组直接将node的ip进行放行,pod的控制放在应用这边进行控制
因为pod中是共享网络名称空间的,一个pod可以启动多个容器,一个容器来运行业务相关的进行,sidercar对应的容器运行一个自己开发的程序,来控制pod中的iptables规则,也可以达到限制网络访问的功能
我们这边使用第二种方式进行设计,防火墙功能使用master slave架构进行工作,每个pod中运行一个slave程序,监听端口来接收master的http请求,并根据请求执行相应的添加和删除规则功能
master来接收工单平台的请求,并根据不同的功能,从k8s中找到对应的业务的pod的ip列表,然后请求对应pod的ip来中转请求
为了解决pod漂移或者重启后配置同步的问题,将对应的规则配置保存到configmap中,并将其挂载到pod目录中,sidercar程序启动的时候先读取配置信息,将已有的规则读取并配置,来保证重启后规则依然有效。
评论前必须登录!
注册