引言
本文介绍 Kubernetes 网络中 Service 网路部分的实现方案,更多关于 Kubernetes 的介绍均收录于<Kubernetes系列文章>中。
Service 网络
service network 比较特殊,每个新创建的 service 会被分配一个 service IP,它实际上只是一个存在于 iptables 中的路由规则,并没有对应的虚拟网卡。还记得我们在试玩环节创建的 kubia service 吗?我们只要在集群中的任意机器查看 iptables 就能发现端倪,这里需要声明的是我当时在创建 Kubernetes 集群时指定的 PodSubnet 为 192.168.128.0/18
,serviceSubnet 为 192.168.192.0/18
。
我们知道,linux 系统中有 route,用来和其他网络连接。如果没有它,linux 就无法和外部通信了。
iptables 是一系列的规则,用来对内核中的数据进行处理。没有它,linux 可以正常工作,有了它,linux 可以对网络中的数据的操作更加多样化,可见 iptables 是一个辅助角色,可以让 linux 的网络系统更强大。
iptables 其实不是真正的防火墙,我们可以把它理解成一个客户端代理,用户通过 iptables 这个代理,将用户的安全设定执行到对应的"安全框架"中,这个"安全框架"才是真正的防火墙,这个框架的名字叫 netfilter。
netfilter 才是防火墙真正的安全框架(framework),netfilter位于内核空间。
iptables 其实是一个命令行工具,位于用户空间,我们用这个工具操作真正的框架。
Netfilter是Linux操作系统核心层内部的一个数据包处理模块,它具有如下功能:
- 网络地址转换(Network Address Translate)
- 数据包内容修改
- 数据包过滤的防火墙功能
在 iptables 中 有两个概念:链和表,iptables 中定义有5条链,说白了就是5个钩子函数,它们是在数据包经过内核的过程中有五处关键地方,分别是 PREROUTING、INPUT、OUTPUT、FORWARD、POSTROUTING,因为每个钩子函数中可以定义多条规则,每当数据包到达一个钩子函数时,iptables 就会从钩子函数中第一条规则开始检查,看该数据包是否满足规则所定义的条件。如果满足,系统就会根据该条规则所定义的方法处理该数据包;否则 iptables 将继续检查下一条规则,如果该数据包不符合钩子函数中任一条规则,iptables 就会根据该函数预先定义的默认策略来处理数据包。iptables 中还有一个表的概念,每个表(tables)对应了一个特定的功能,有filter表(实现包过滤)、nat表(实现网络地址转换)、mangle表(实现包修改)、raw表(实现数据跟踪),不同的表中包含了不同的钩子函数,不同表的同一钩子函数具有一定的优先级:raw-->mangle-->nat-->filter。一条链上可定义不同功能的规则,检查数据包时将根据上面的优先级顺序检查。
总结一下,数据包先经过 PREOUTING,由该链确定数据包的走向:
- 目的地址是本地,则发送到 INPUT,让INPUT决定是否接收下来送到用户空间,流程为①--->②;
- 若满足 PREROUTING 的 nat 表上的转发规则,则发送给 FORWARD,然后再经过 POSTROUTING 发送出去,流程为: ①--->③--->④--->⑥
- 主机发送数据包时,流程则是⑤--->⑥
此处列出一些 iptables 常用的动作,在后面分析 iptables 时您将看到 Kubernetes 对各种动作的使用:
- ACCEPT:允许数据包通过。
- DROP:直接丢弃数据包,不给任何回应信息,这时候客户端会感觉自己的请求泥牛入海了,过了超时时间才会有反应。
- REJECT:拒绝数据包通过,必要时会给数据发送端一个响应的信息,客户端刚请求就会收到拒绝的信息。
- SNAT:源地址转换,解决内网用户用同一个公网地址上网的问题。
- MASQUERADE:是SNAT的一种特殊形式,适用于动态的、临时会变的ip上。
- DNAT:目标地址转换。
- REDIRECT:在本机做端口映射。
- MARK:打标记。
我们先回顾一下 kubia service 的端口和 service ip, 可以看到它的 service endpoint 是 192.168.199.234:8080,因为采用了 NodePort 类型的 service,所以我们也可以通过所有 pod 所处宿主机的 32681 端口访问该服务。
kubectl get service -o wide
#NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
#kubia NodePort 192.168.199.234 <none> 8080:32681/TCP 17h app=kubia
然后我们先看一下 iptables 的 nat 表内容,因为它控制了网络地址转换
iptables -t nat -nL
#Chain PREROUTING (policy ACCEPT)
#target prot opt source destination
#KUBE-SERVICES all -- 0.0.0.0/0 0.0.0.0/0 /* kubernetes service portals */
# 注入 KUBE-SERVICES 链
#
#Chain OUTPUT (policy ACCEPT)
#target prot opt source destination
#KUBE-SERVICES all -- 0.0.0.0/0 0.0.0.0/0 /* kubernetes service portals */
# 注入 KUBE-SERVICES 链
在 PREROUTING 链和 OUTPUT 链中,Kubernetes 注入了自己的链,该链匹配所有 ip 的包。然后,我们看下 KUBE-SERVICES 链都做了什么。
#Chain KUBE-SERVICES (2 references)
#target prot opt source destination
#KUBE-MARK-MASQ tcp -- !192.168.128.0/18 192.168.199.234 /* default/kubia: cluster IP */ tcp dpt:8080
#KUBE-SVC-L5EAUEZ74VZL5GSC tcp -- 0.0.0.0/0 192.168.199.234 /* default/kubia: cluster IP */ tcp dpt:8080
#KUBE-NODEPORTS all -- 0.0.0.0/0 0.0.0.0/0 /* kubernetes service nodeports; NOTE: this must be the last rule in this chain */ ADDRTYPE match dst-type LOCAL
在 KUBE-SERVICES 链中可以看到,当我们的 destination 为 192.168.199.234 tcp 8080 时,会先判断是不是 Pod 子网,如果不是则会先进入 KUBE-MARK-MASQ 链,然后才会进入 KUBE-SVC-L5EAUEZ74VZL5GSC 链,最后一条规则是 KUBE-NODEPORTS 匹配目标地址是 local 的流量。
接下来我们分别看一看 KUBE-SERVICES 下层的三个链分别是干什么用的。
#Chain KUBE-MARK-MASQ (25 references)
#target prot opt source destination
#MARK all -- 0.0.0.0/0 0.0.0.0/0 MARK or 0x4000
KUBE-MARK-MASQ 链的作用,只是打上标记 MARK or 0x4000
,该标记的作用一会儿再说。
#Chain KUBE-SVC-L5EAUEZ74VZL5GSC (2 references)
#target prot opt source destination
#KUBE-SEP-DHEI6HLVRKMGWFA5 all -- 0.0.0.0/0 0.0.0.0/0 statistic mode random probability 0.16667000018
#KUBE-SEP-U2WLSCWTWVRL43BI all -- 0.0.0.0/0 0.0.0.0/0 statistic mode random probability 0.20000000019
#KUBE-SEP-T3I6FPLS4WOL5Q3I all -- 0.0.0.0/0 0.0.0.0/0 statistic mode random probability 0.25000000000
#KUBE-SEP-SRYJEUDBHDCQM4KY all -- 0.0.0.0/0 0.0.0.0/0 statistic mode random probability 0.33332999982
#KUBE-SEP-VVYACXWHEDETJZUB all -- 0.0.0.0/0 0.0.0.0/0 statistic mode random probability 0.50000000000
#KUBE-SEP-5A4LPBRVHCEID4EO all -- 0.0.0.0/0 0.0.0.0/0
在 KUBE-SVC-L5EAUEZ74VZL5GSC 链中就对应了我们 kubia 服务的的 6 个 pod,可以看到他在这一层做了一个负载均衡,第一个 KUBE-SEP 分配了 1/6 的份额,第二个 KUBE-SEP 分配了剩余份额中的 1/5 ,以此类推,最终达到的效果就是每个 KUBE-SEP 分配 1/Pod count 的份额,我们挑选第一个 pod 对应的 KUBE-SEP-DHEI6HLVRKMGWFA5 看一下。
#Chain KUBE-SEP-DHEI6HLVRKMGWFA5 (1 references)
#target prot opt source destination
#KUBE-MARK-MASQ all -- 192.168.131.21 0.0.0.0/0
#DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp to:192.168.131.21:8080
如果源 ip 和 pod ip 地址相同的话,则同样打上 mark,然后将目标地址(DNAT)改为该 Pod 的 ip + 服务端口,to:192.168.131.21:8080,到这里就完成了路由工作。
#Chain KUBE-NODEPORTS (1 references)
#target prot opt source destination
#KUBE-MARK-MASQ tcp -- 0.0.0.0/0 0.0.0.0/0 /* default/kubia: */ tcp dpt:32681
#KUBE-SVC-L5EAUEZ74VZL5GSC tcp -- 0.0.0.0/0 0.0.0.0/0 /* default/kubia: */ tcp dpt:32681
最后,在KUBE-NODEPORTS 链中,如果目标端口和 kubia service 的 nodePort 匹配上的话,就会打上 mark 并进去之前的 service 链 KUBE-SVC-L5EAUEZ74VZL5GSC, 也就是说无论你访问集群内的任意一台机器的 32681 端口,流量均会均匀地转发到对应的 pod 中。
接下来我们看一下前面打的 MARK or 0x4000 都用来干什么了。
#Chain POSTROUTING (policy ACCEPT)
#target prot opt source destination
#KUBE-POSTROUTING all -- 0.0.0.0/0 0.0.0.0/0 /* kubernetes postrouting rules */
#RETURN all -- 192.168.128.0/18 192.168.128.0/18
#MASQUERADE all -- 192.168.128.0/18 !224.0.0.0/4
#RETURN all -- !192.168.128.0/18 192.168.132.0/24
#MASQUERADE all -- !192.168.128.0/18 192.168.128.0/18
# 这里是在 POSTROUTING 中注入的 KUBE-POSTROUTING 链, 并使用 MASQUERADE 进行与 Pod subnet 相关的 SNAT,实际上这是将 service pod 的源 ip SNAT 为 service ip。后面会详细介绍这个过程。
#
#Chain KUBE-POSTROUTING (1 references)
#target prot opt source destination
#MASQUERADE all -- 0.0.0.0/0 0.0.0.0/0 /* kubernetes service traffic requiring SNAT */ mark match 0x4000/0x4000
KUBE-POSTROUTING 中匹配了所有打了 mark 0x4000 的报文,并通过 MASQUERADE 地址伪装,它是 SNAT 的一种特殊形式,可以自动化的进行源地址转换 SNAT,这个自动进行 SNAT 转换的过程实际是将报文的源地址转化为 Pod Subnet 的 ip 域。
回顾一下前面哪些情况下加了 mark:
- KUBE-SERVICES: source ip 不在 pob subnet 域并且 destination 是 service ip + service port 时,实际的访问者在哪个集群主机上,就会使用该主机被分配的 pod subnet ip,即便是在 pod 中访问也会使用该 pod 所处的 host 被分配的 pod subnet ip(即 flannel 网卡 ip),而不是使用 pod 的 ip;
- KUBE-SEP-DHEI6HLVRKMGWFA5: source ip 和 destination ip 都是目标服务 pod 的 ip 时,会使用该 pod 所处 host 的 pod subnet ip(即 flannel 网卡 ip),而不是 pod 内的 ip;
- KUBE-NODEPORTS: 如果访问了 Service 使用的 NodePort,比如 Worker1 的 32681 端口,那么 SNAT 将改为 Worker1 的 pod subnet ip(即 flannel 网卡 ip);
另一个 mark 0x4000 的用途在 iptables 的 filter 表中,该表决定了数据报文的丢弃与放行的决策。
iptables -t filter -nL
#Chain INPUT (policy ACCEPT)
#target prot opt source destination
#KUBE-SERVICES all -- 0.0.0.0/0 0.0.0.0/0 ctstate NEW /* kubernetes service portals */
#KUBE-EXTERNAL-SERVICES all -- 0.0.0.0/0 0.0.0.0/0 ctstate NEW /* kubernetes externally-visible service portals */
# 允许创建任意 ip 地址的连接,因为 service 网络是不存在与任何宿主机网络中的,但是我们需要允许以 service ip 建立连接
#
#Chain OUTPUT (policy ACCEPT)
#target prot opt source destination
#KUBE-SERVICES all -- 0.0.0.0/0 0.0.0.0/0 ctstate NEW /* kubernetes service portals */
# 同 INPUT,OUTPUT 链也放行以 service ip 相关的连接件建立
#Chain FORWARD (policy ACCEPT)
#ACCEPT all -- 192.168.128.0/18 0.0.0.0/0
#ACCEPT all -- 0.0.0.0/0 192.168.128.0/18
#KUBE-FORWARD all -- 0.0.0.0/0 0.0.0.0/0 /* kubernetes forwarding rules */
# 在 FORWARD 链中注入了 KUBE-FORWARD,放行源 ip 或者目的 ip 为 pod subnet 的包
#
#Chain KUBE-FORWARD (1 references)
#target prot opt source destination
#DROP all -- 0.0.0.0/0 0.0.0.0/0 ctstate INVALID
#ACCEPT all -- 0.0.0.0/0 0.0.0.0/0 /* kubernetes forwarding rules */ mark match 0x4000/0x4000
#ACCEPT all -- 192.168.128.0/18 0.0.0.0/0 /* kubernetes forwarding conntrack pod source rule */ ctstate RELATED,ESTABLISHED
#ACCEPT all -- 0.0.0.0/0 192.168.128.0/18 /* kubernetes forwarding conntrack pod destination rule */ ctstate RELATED,ESTABLISHED
ctstate description:
- NEW新建的连接的包
- ESTABLISHED已建立连接的后续包
- RELATED相关连接的包, 如FTP控制和数据
- INVALID没识别出来, 或者没有状态, 一般DROP此类的包
conntrack: 连接追踪表,记录了 SNAT 与 DNAT 的转换,可以通过
conntrack -L
查看
KUBE-FORWARD 中对打了 mark 0x4000 的包,源 ip 或者目的 ip 是 pod subnet 的包(状态:已建立连接或者存在相关连接)放行 Forward,对于其他非法状态的包则直接丢弃。
下图中所有黄色区域都为 Kubernetes 在 iptables 上做的改动。
从上述 iptables 的分析中,我们可以知道,service network 实际上只是存在于 iptables 的路由规则,里面记录了一个指定 service ip 应该将流量路由到哪个 Pod ip 地址,以及收到的包应该如何进行 SNAT。下面两个动图描述了,从 Pod 发包给 Service,以及 service pod 回包的情况:
我们可以看到,来自 pod1 的报文以某种形式流到了宿主机上(我们后面会详细介绍这部分的实现原理),然后要从宿主机的 eth0 设备发出,这时候触发了 iptables 的规则匹配,根据 iptables 中的规则,最终将请求的目标地址改成了 Pod ip 并发送出去。在这个过程中 Linux kernel 的 conntrack 会记住此时选择的 pod,并且在下次这个源 pod 要发送给该 service 时,仍路由到相同的 service pod。至此,iptables 就完成了集群内的负载均衡,剩下的传输过程就由 flannel network 负责了。
当 service 响应 pod 的请求后,数据包被 eth0 网卡接收到,并再次进入 iptables 的匹配过程,还记得前面 iptables 中的 POSTROUTING 吗?
# 从 service pod 发来的回包,将 SNAT 恢复为 service ip 或者虚拟机的 ip
#MASQUERADE all -- 192.168.128.0/18 !224.0.0.0/4
# 将对外部网络的访问恢复 SNAT
#MASQUERADE all -- !192.168.128.0/18 192.168.128.0/18
这里同样会使用到内核的 conntrack ,通过它可以将报文的源地址从 pod 的 ip 改为 service 的 ip,这下访问 service 的完整流程就走完了。如下是一个 conntrack 例子:
conntrack -L|grep 32681
#tcp 6 9 CLOSE src=<exeternal-ip> dst=<local-eth0-ip> sport=62142 dport=32681 src=192.168.132.73 dst=192.168.128.0 sport=8080 dport=62142 mark=0 use=1
#tcp 6 9 CLOSE src=<exeternal-ip> dst=<local-eth0-ip> sport=62790 dport=32681 src=192.168.132.72 dst=192.168.128.0 sport=8080 dport=62790 mark=0 use=1
conntrack -L|grep 192.168.192
#tcp 6 86377 ESTABLISHED src=192.168.128.2 dst=192.168.192.1 sport=38742 dport=443 src=<other-vm-ip> dst=<local-eth0-ip> sport=6443 dport=28222 [ASSURED] mark=0 use=1
#tcp 6 86394 ESTABLISHED src=<exeternal-ip> dst=192.168.192.1 sport=50800 dport=443 src=<local-eth0-ip> dst=<local-eth0-ip> sport=6443 dport=50800 [ASSURED] mark=0 use=1
文章说明
更多有价值的文章均收录于贝贝猫的文章目录
版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
创作声明: 本文基于下列所有参考内容进行创作,其中可能涉及复制、修改或者转换,图片均来自网络,如有侵权请联系我,我会第一时间进行删除。
参考内容
[1] kubernetes GitHub 仓库
[2] Kubernetes 官方主页
[3] Kubernetes 官方 Demo
[4] 《Kubernetes in Action》
[5] 理解Kubernetes网络之Flannel网络
[6] Kubernetes Handbook
[7] iptables概念介绍及相关操作
[8] iptables超全详解
[9] 理解Docker容器网络之Linux Network Namespace
[10] A Guide to the Kubernetes Networking Model
[11] Kubernetes with Flannel — Understanding the Networking
[12] 四层、七层负载均衡的区别