一、背景知识
本文主要介绍k8s网络中service 的两种模式(clusterIp、nodeport),数据是如何通过ipvs&iptables流转的。在学习上述知识的同时,还需要了解一下ipset、conntrack的相关知识。往期回顾文章
1.1、ipset
ipset是什么?ipset其实是iptables的扩展,可以定义一些列地址的集合。拿黑名单来举例,我想让黑名单里面的ip拒绝访问网站(黑名单有很多个),按照传统iptables做法,需要在filter表添加很多规则匹配时一条一条匹配效率很低(严重影响性能),而有了ipset,则只用添加一条规则即可,使用hash结构效率很高。
// 传统iptables方式不让这两个ip访问特定端口
iptables -I INPUT -p tcp -s 192.168.113.101 --dport 8410 -j DROP
iptables -I INPUT -p tcp -s 192.168.113.99 --dport 8410 -j DROP
而使用ipset命令如下
// 创建一个ipset规则,名字叫black,以hash 类型存储,ip + port 作为hash的key
ipset create black hash:ip,port
// 向black中添加具体 ip + port
ipset add black 192.168.113.100,8410
// 删除black中的某条规则
ipset del black 192.168.113.100,8410
// 添加到iptables规则上
iptables -t filter -I INPUT -m set --match-set black src,dst -j DROP
当然,ipset还支持 hash:ip,hash:ip,port,ip等多种hash key的组成,具体可以通过 ipset -h 查看。接下来说明一下 -m set 后面 src 和 dst 两个的含义。src 指来源,dst 指目标,此规则的意思是来自192.178.113.100 ip 访问本机8410端口的流量给DROP掉。
ipset使用hash结构,比iptables的链表遍历效率要高很多。ipset还有很多更加高级的玩法,本文就不在阐述了。
1.2、ipvs
lvs是什么?全称是Linux Virtual Server,是由章文嵩博士主导的开源负载均衡项目,目前已经集成到linux内核中。lvs提供了丰富的负载均衡能力,接收到用户请求后根据具体的负载均衡算法在内核态把请求转发到后端的某个server上,也就是说lvs不需要监听具体的端口。接下来我们看一下lvs的一些基本概念。
DS : director server 指负载均衡节点。
RS : real server 指后端提供服务的节点。
VIP : virtual ip ,client端直接访问的ip,称为虚拟ip。
DIP : director ip 指负载均衡节点ip。
RIP : real server ip 指后端节点ip。
CIP : client ip 指client端ip。
ipvs的原理如下。ipvs工作在iptables 的 input链上,VIP一般定义在DS节点上的一个虚拟ip,拿nat模式举例如下。
① : 当请求数据包到DS上最先经过iptables 的PREROUTING链,判断目标ip (VIP) 是本机的ip,于是把请求转发到INPUT链上。
② : 因为lvs工作在INPUT链上,数据到达INPUT链上后lvs会将用户请求和定义的后端服务做对比,如果是请求的后端服务,则使用某种负载均衡算法找到一个后端RIP,修改数据包的目的ip和端口为某个RIP的(DNAT转换)。
③ : 此时数据到达POSTROUTING链(不会做SNAT),数据包的源ip 为CIP,目的ip为RIP,数据包发往RIP上。
lvs提供了三种包转发模式,如下所示
NAT 模式,做DNAT 转换修改目的ip地址,需要DIP和RIP在一个网段内&RIP所在节点的网关指向DIP。(做了NAT转换,考验DS节点的性能)
DR 模式,DS修改数据包的mac地址,将源mac 地址修改为DIP的mac地址,目的mac地址为RIP的mac地址。同时RS的lo接口ip修改为VIP。
TUN模式,在原有的ip数据包上在封装一层ip包,最外层ip包 (源ip 为 DIP,目的ip为RIP),最内层ip包(源ip CIP,目的ip为VIP),同时RS上的lo接口的ip设置为VIP。
由于k8s使用的是NAT模式,接下来看下NAT模式下的数据包流向。如下图所示
①:请求数据包到达DS,数据包经过PREROUTING链,此时ip 包 src ip为CIP,dst ip 为VIP
②:由于请求的VIP是DS上的虚拟ip,数据包发往INPUT链。
③:数据包到INPUT链上后,ipvs发现数据包请求是定义的集群服务,于是使用定义好的负载均衡算法找到一个具体的RS节点,做DNAT,修改数据包dst ip为RIP,数据包到达POSTROUTING链,发送给RS。
④:RS收到数据包后对比dst ip 发现是自己,接收数据包做处理,处理完成后ip 数据包 src ip 为RIP,dst ip 为CIP,把数据包发给DS。
⑤:DS 接收到RS的响应包,修改src ip 为自身的VIP,dst ip 为CIP,把数据包发送给client端。
三种模式对比&优缺点
NAT模式下,进出流量都需要经过director server,并且需要做NAT转换,在高流量时对director server 的性能有一定影响。并且RS的网关要指向DIP。
DR模式下,只有入口流量要经过director server,由于只是修改了mac地址 这就要求RS和DS在一个二层网络内,效率会非常高。RS上的lo接口要配置vip的ip地址,并且RS的网关不能指向DIP。
TUN模式下,也是只有入口流量要经过director server,此种模式建立了ip隧道,RS的lo接口ip地址要设置为vip的ip地址,并且RS的网关不能设置为DIP。
接下来在简单聊一下ipvs的负载均衡策略,简单介绍下面四种。
rr 最简单的一种调度算法,轮训的把请求转发到后端server上。
wrr 加权轮训,可以为RS设置相应的权重,权重越高表示该RS的性能越高,接收到的请求也就越多。
lc 最少连接,根据RS请求的连接数,比如RS1的连接数比RS2的少,则把请求发给RS2
wlc 在最少连接的基础上添加了权重的概念。
上面介绍完了ipvs内核态的基本原理,接下来介绍一下如何使用 ipvsadm 用户态命令来操作ipvs。说明:此次试验是在四个虚拟机上,ipvs的模式使用的nat模式,RS的网关没有指向DS的ip(没办法做到)在DS节点上手动创建SNAT命令,下文有详细介绍。创建一个vip,在ip为192.168.113.101上
// -A 表示添加一个VIP,-t 表示tcp 协议,-s 负载均衡算法
ipvsadm -A -t 10.10.0.1:8410 -s rr
// -D 删除一个VIP
ipvsadm -D -t 10.10.0.1:8410
// 将vip地址添加到本地ens33网卡上
ip addr add 10.10.0.1/24 dev ens33
为vip添加RS
// -t 表示tcp,-m 表示使用nat
ipvsadm -a -t 10.10.0.1:8410 -r 192.168.113.99:8080 -m
ipvsadm -a -t 10.10.0.1:8410 -r 192.168.113.100:8080 -m
添加完成RS后,查看ipvs规则,如下图所示
client端的ip地址为192.168.113.102,client端要想直接访问vip的话,需要在client端添加静态路由,添加命令如下
route add -net 10.10.0.0/16 gw 192.168.113.101
添加完命令后,在client端curl 10.10.0.1:8410 发现不通,此时去某个RS上抓包如下
上图抓包显示,client 直接访问的vip,而数据包的目的ip 变为了rs的ip,因此可以看出ipvs做了DNAT转换。因为做了DNAT,RS发送响应数据直接发给client,client收到RS的数据包。client给vip发的包却收到了RS的响应包(client 想我从来没有给RS发过数据),因此client端会把此数据包丢弃。
因为ipvs没有做SNAT,接下来在DS上添加iptables规则自己实现SNAT的功能,添加完SNAT后,RS就看不到真实的CIP了。
iptables -t nat -A POSTROUTING -m ipvs --vaddr 10.10.0.1 --vport 8410 -j MASQUERADE
此时还是不通,查找资料后发现ipvs 的conntrack 没有开,手动打开,后续文章介绍conntrack是什么,设置完成后可以愉快的访问了。
sysctl net.ipv4.vs.conntrack=1
总结:通过ipvs提供的DNAT功能和负载均衡功能,很容易实现外部用户访问内网的需求。但是还要考虑高可用层面,比如主DS宕机VIP要漂移到备DS上,后端RS重启或宕机,ipvs负载均衡列表中要及时把有问题的RS剔除,这样才能真正的实现高可用。
1.3、conntrack
大家在家上网时用到的都是192.168.x.x的ip地址,这是私网ip地址。那么大家是如何能够成功的访问外网的呢?答案是路由器帮我们做了SNAT的功能,使我们发出的数据包的src ip变为路由器的公网ip,这样数据包就能在互联网上愉快的转发了。从而实现了对内网的保护。
那么问题来了,既然做了SNAT转换,那响应数据包回来以后路由器怎么知道转到哪台PC上呢?路由器可能链接了很多PC,不可能都给每一个PC转发吧。。。答案就是conntrack实现的。
接下来我拿上面ipvs的例子举例,我们手动实现了在DS上SNAT转换,在client上curl vip:8410,这时候查看DS上和client上的conntrack表如下
cat /proc/net/nf_conntrack
或者
conntrack -E
// DS 上的记录
ipv4 2 tcp 6 44 TIME_WAIT src=192.168.113.102 dst=10.10.0.1 sport=35562 dport=8410 src=192.168.113.99 dst=192.168.113.101 sport=8080 dport=35562 [ASSURED] mark=0 secctx=system_u:object_r:unlabeled_t:s0 zone=0 use=2
// client 上的记录
ipv4 2 tcp 6 38 TIME_WAIT src=192.168.113.102 dst=10.10.0.1 sport=35562 dport=8410 src=10.10.0.1 dst=192.168.113.102 sport=8410 dport=35562 [ASSURED] mark=0 secctx=system_u:object_r:unlabeled_t:s0 zone=0 use=2
先从client上的连接跟踪分析起:主要看 src、dst、sport、dport这几个字段。
client发送数据包
client端发出数据包的src ip 为192.168.113.102,dst ip 为10.10.0.1 (VIP), sport 为35562这个端口,dport为8410(VIP 定义端口)。
client端接收响应数据包
期望src ip 为vip(10.10.0.1),dst ip 为CIP(192.168.113.102),sport为8410,dport为35562
DS接收数据包
DS接收到src ip 为CIP(192.168.113.102),dst ip 为vip(10.10.0.1),sport为35562,dport为8410的数据包
DS接收响应数据包
由于在DS侧做了DNAT转换,根据负载均衡策略找到了一个RS(RIP 192.168.113.99),同时也做了SNAT转换(判断是否是VIP和端口),转换为DS的DIP。所以当DS收到src ip 为192.168.113.99(RIP),dst ip 为192.168.113.101(DIP),sport为8080,dport为35562,会根据连接跟踪表找到这个包是192.168.113.102这个client发过来的,因此把数据包在转发给192.168.113.102:35562 上。
conntrack各个字段的含义
第一列为网络协议名字,比如ipv4
第二列为网络协议号
第三列为传输协议名字,比如TCP
第四列为传输层协议号,TCP为6
第五列为该条记录剩余生存时间,当有新的数据包到来时更新此记录
第六列为连接状态,此处说明client端主动关闭连接
[ASSURED]说明:表示在请求和响应上都看到了流量。[UNREPLIED]说明未在响应上见到流量。
总结:
本文只是简单的说明了一下conntrack,并没有具体说明数据流经netfilter时何时创建记录,数据存储的数据结构啥样,底层比较复杂,感兴趣的大佬可以自行研究~
二、k8s网络通信
介绍完了ipset、ipvs、conntrack,接下来进入正题,看一下ipvs模式下k8s的网络通信。kube-proxy 的主要作用是watch apiserver,当监听到pod 或service变化时,修改本地的iptables规则或ipvs规则。
2.1、clusterIp模式
clusterIp模式为一个集群内部可访问的ip,集群外部没办法访问这个ip,试验环境如下:
// 首先创建一个deployment,kubectl apply -f nginx.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 1
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.14.2
ports:
- containerPort: 80
// 创建一个cluster ip 类型的service,kubectl apply -f nginx_service.yaml
apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
selector:
app: nginx
ports:
- protocol: TCP
port: 80
targetPort: 80
创建完deployment和service后,查看一下service的ip如下。
接下来看下宿主机网卡、ipvs规则、ipset规则有什么变化
// 可以看到,kube-ipvs0 上多了创建的service的clusterIp。
// 可以看到,kube-ipvs0 上多了创建的service的clusterIp。
kube-ipvs0: <BROADCAST,NOARP> mtu 1500 qdisc noop state DOWN group default
...
inet 10.108.113.237/32 brd 10.108.113.237 scope global kube-ipvs0
valid_lft forever preferred_lft forever
// 查看ipset规则,多了刚创建的clusterIp,k8s内部的serviceIp省略
Name: KUBE-CLUSTER-IP
Type: hash:ip,port
Revision: 5
Header: family inet hashsize 1024 maxelem 65536
Size in memory: 632
References: 2
Number of entries: 8
Members:
...
10.108.113.237,tcp:80
// 查看ipvs规则
TCP 10.108.113.237:80 rr
-> 10.244.1.143:80 Masq 1 0 0
查看iptables 的nat表和filter表,看一下k8s创建了哪些规则以及经过哪些链
接下来分析一下curl 10.108.113.237 数据是如何走的,只讨论在nat表和filter表的流向,因为在mangle和raw都没有规则。
1、nat表PREROUTING链
①:数据首先进入PREROUTING链,所有请求都会进入KUBE-SERVICES链。
②:进入KUBE-SERVICES后,查看对应在此链上的规则,发现请求的目的ip和port在KUBE-CLUSTER-IP 对应的ipset里面(上面已有展示),匹配上了则跳往KUBE-MARK-MASQ链。
-A KUBE-SERVICES ! -s 10.244.0.0/16 -m comment --comment "Kubernetes service cluster ip + port for masquerade purpose" -m set --match-set KUBE-CLUSTER-IP dst,dst -j KUBE-MARK-MASQ
③:数据流向KUBE-MARK-MASQ链,主要做了mark 打标记的功能,iptables命令如下
-A KUBE-MARK-MASQ -j MARK --set-xmark 0x4000/0x4000
④:之后走向KUBE-NODE-PORT链,因为没有定义nodepode 类型的service,此处先略过。2、filter表的INPUT链
⑤:首先进入INPUT链,所有数据转向KUBE-FIREWALL链。
⑥:进入KUBE-FIREWALL链,如果发现数据包打了0x8000/0x8000,DROP掉。因为ipvs工作在INPUT链上,做完DNAT之后直接转发到POSTROUTING链上。
3、nat表POSTROUTING链
⑦:进入POSTROUTING链,所有数据转向KUBE-POSTROUTING链
⑧:进入KUBE-POSTROUTING链,对有0x4000/0x4000标记的数据包做SNAT转换,因为ipvs只有DNAT功能。
-A KUBE-POSTROUTING -m comment --comment "kubernetes service traffic requiring SNAT" -m mark --mark 0x4000/0x4000 -j MASQUERADE
4、数据转发给flannel网卡,进行转发
⑨:flannel 根据具体的backend模式,对数据做封包等操作,然后发出去。flannel的网络模式比较复杂,之后会专门文章进行说明。
2.2、nodeport模式
要想把集群内部的服务可以让集群外部访问,可以使用nodeport模式在物理机上开一个端口,这样外部就能访问集群内部的服务了。说明:还是使用上面创建的deployment。
// 此处在每个node 上开了一个30080端口
apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
selector:
app: nginx
type: NodePort
ports:
- protocol: TCP
nodePort: 30080
port: 80
查看创建service的信息,发现也创建了集群内部的一个ip。
iptables规则如下
接下来看下ipset规则有什么变化,发现KUBE-NODE-PORT-TCP下的一个成员是刚才我们指定的那个nodePort的值。
Name: KUBE-NODE-PORT-TCP
Name: KUBE-NODE-PORT-TCP
Type: bitmap:port
Revision: 3
Header: range 0-65535
Size in memory: 8300
References: 1
Number of entries: 1
Members:
30080
接下来看一下iptables规则,nat表和filter表
1、nat表PREROUTING链
①:数据首先进入PREROUTING链,所有请求都会进入KUBE-SERVICES链。
②:ip和port匹配不上KUBE-CLUSTER-IP 的ipset,判断是访问的本地地址,进入KUBE-NODE-PORT链。
-A KUBE-SERVICES -m addrtype --dst-type LOCAL -j KUBE-NODE-PORT
③:进入KUBE-NODE-PORT链后,判断访问端口在 KUBE-NODE-PORT-TCP ipset规则中,因此进入KUBE-MARK-MASQ链。
-A KUBE-NODE-PORT -p tcp -m comment --comment "Kubernetes nodeport TCP port for masquerade purpose" -m set --match-set KUBE-NODE-PORT-TCP dst -j KUBE-MARK-MASQ
④:进入KUBE-MARK-MASQ链,对数据做mark标记
-A KUBE-MARK-MASQ -j MARK --set-xmark 0x4000/0x4000
后续流程跟clusterIp一样,此处就不在阐述。
2.3、dns相关
k8s中的dns默认使用的是coredns,通过以下命令查看。k8s中定义的service是有域名的,访问域名要通过dns解析,此时coredns就发挥它的作用了。
// 查看某个pod内dns server 的ip
kubectl exec nginx-deployment-7fd6966748-qxsrf -it -- cat /etc/resolv.conf
nameserver 10.96.0.10
search default.svc.cluster.local svc.cluster.local cluster.local
options ndots:5
上面的试验时我们创建了一个my-service 的nodePort的service,此时查看一下此域名对应的ip,如下图所示,域名解析出来的ip与service对应的ip相同,大功告成。
nslookup my-service.default.svc.cluster.local 10.96.0.10
参考:
以上相关内容介绍了k8s service ipvs的相关实现,如有错误欢迎指出~