33

kubernetes service模式分析 | 我爱西红柿

 4 years ago
source link: https://www.bladewan.com/2018/12/10/kubernetes_service_mode/?
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Kubernetes service是为POD提供统一访问入口的,实现主要依靠kube-proxy实现,kube-proxy有三种模式userspace、iptables,ipvs,同时我们也知道service有三种类型cluster_ip、nodeport,loadblance和三种端口类型port,targetport,nodeport。

环境信息:

OS:Ubuntu16.04
Kubernetes:v1.11.0
kubeadm:v1.11.0
docker:17.03
network:flannel

kube-proxy模式分析

userspace

userspace为kube-proxy为早期的模式,Kubernetes1.2版本之前主要使用这个模式,转发原理参考

https://kubernetes.io/docs/concepts/services-networking/service/

这个模式最大缺点就是,所以端口请求都需要先经过kube-proxy然后在通过iptables转发,这样带来一个问题就是需要在用户态和内核态不断进行切换,效率低。

iptables

Kubernetes在1.2版本开始将iptables做为kube-proxy的默认模式,iptables根之前userspace相比,完全工作在内核态而且不用在经过kube-proxy中转一次性能更强,下面介绍Kubernetes中iptables转发流程

iptables有链和表的概念,链就相当于一道道关卡,表就是这个关卡上对应的规则总共有四个表和五条链,kube-proxy在这里就使用了两个表分别是filter和nat表,也自定义了五个链KUBE-SERVICES,KUBE-NODE-PORTS,KUBE-POSTROUTING,KUBE-MARK-MASQ和KUBE-MARK-DROP五个链
目前kubernetes提供了两种负载分发策略:RoundRobin和SessionAffinity

RoundRobin:轮询模式,即轮询将请求转发到后端的各个Pod上。
SessionAffinity:基于客户端IP地址进行会话保持的模式,第一次客户端访问后端某个Pod,之后的请求都转发到这个Pod上
默认是RoundRobin模式。

service_mode_1.png

iptables数据包转发流程
1、首先一个数据包经过网卡进来,先经过PREROUTING链
2、判定目的地址是否为主机,为本机就通过INPUT链转发
3、若不为本机通过FORWARDl链转发到POSTROUTING出去

以下展示一个实例展示kube-proxy是如何根据service不同类型生成对应规则

我们创建名为test的deployment,镜像为nginx:latest,replicas为3个

kubectl run test --image=nginx --replicas=3



kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP
test-679b667858-9h9hr 1/1 Running 0 17h 10.244.0.16
test-679b667858-g827r 1/1 Running 0 17h 10.244.0.15
test-679b667858-nnr28 1/1 Running 0 6m 10.244.0.18

在给这个deployment创建一个ClusterIP类型的service

kubectl expose deployment/test --type=ClusterIP --port=80



kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
test ClusterIP 10.98.243.51 <none> 80/TCP 43s

接下来我们将iptables规则导出来观察,用iptables-save将iptables规则重定向到一个文件

iptables-save > /tmp/1
ClusterIP类型

查看规则
首先Kubernetes会指对每个service在创建一些名为KUBE-SEP-xxx,KUBE-SVC-xxx的链

以刚刚创建的类型为Cluster-ip名为test这个service为例,创建了以下规则

1、-A KUBE-SERVICES ! -s 10.244.0.0/16 -d 10.98.243.51/32 -p tcp -m comment --comment "default/test: cluster IP" -m tcp --dport 80 -j KUBE-MARK-MASQ
2、-A KUBE-SERVICES -d 10.98.243.51/32 -p tcp -m comment --comment "default/test: cluster IP" -m tcp --dport 80 -j KUBE-SVC-IOIC7CRUMQYLZ32S
3、-A KUBE-SVC-IOIC7CRUMQYLZ32S -m comment --comment "default/test:" -m statistic --mode random --probability 0.33332999982 -j KUBE-SEP-DZSN6N54CDU2RTAQ
4、-A KUBE-SEP-DZSN6N54CDU2RTAQ -s 10.244.0.15/32 -m comment --comment "default/test:" -j KUBE-MARK-MASQ
5、-A KUBE-SEP-DZSN6N54CDU2RTAQ -p tcp -m comment --comment "default/test:" -m tcp -j DNAT --to-destination 10.244.0.15:80
6、-A KUBE-SVC-IOIC7CRUMQYLZ32S -m comment --comment "default/test:" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-XY6DO3BIJML2V7B5
7、-A KUBE-SEP-XY6DO3BIJML2V7B5 -s 10.244.0.16/32 -m comment --comment "default/test:" -j KUBE-MARK-MASQ
8、-A KUBE-SEP-XY6DO3BIJML2V7B5 -p tcp -m comment --comment "default/test:" -m tcp -j DNAT --to-destination 10.244.0.16:80
9、-A KUBE-SVC-IOIC7CRUMQYLZ32S -m comment --comment "default/test:" -j KUBE-SEP-SWCXAUIAGJXMWYFS
10、-A KUBE-SEP-SWCXAUIAGJXMWYFS -s 10.244.0.18/32 -m comment --comment "default/test:" -j KUBE-MARK-MASQ
11、-A KUBE-SEP-SWCXAUIAGJXMWYFS -p tcp -m comment --comment "default/test:" -m tcp -j DNAT --to-destination 10.244.0.18:80

1、对源IP非10.244.0.0/16访问目的地址10.98.243.51的80端口,执行KUBE-MARK-MASQ ,KUBE-MARK-MASQ会给这个包打上0x4000标签,后续KUBE-POSTROUTING链会根据这个标签做SNAT出去。
2、对于目的IP为10.98.243.51,目的端口为80,然后将请求丢给KUBE-SVC-IOIC7CRUMQYLZ32S链处理。
3、规则链KUBE-SVC-IOIC7CRUMQYLZ32S实现了将报文按33%的比例匹配,转给KUBE-SEP-DZSN6N54CDU2RTAQ链。
4、对源IP为10.244.0.24的包 丢给KUBE-MARK-MASQ链,就如我们上面说的这个链会给包打上tag然后做SNAT,让这个地址能访问外网。
5、KUBE-SEP-DZSN6N54CDU2RTAQ链直接进行DNAT操作将cluster-ip的80端口映射到pod的80。
6、规则链KUBE-SVC-IOIC7CRUMQYLZ32S实现了将报文按50%的比例匹配,转给KUBE-SEP-XY6DO3BIJML2V7B5链。
7、对源IP为10.244.0.25的包丢给KUBE-MARK-MASQ链,就如我们上面说的这个链会给包打上tag然后做SNAT,让这个地址能访问外网。
8、 KUBE-SEP-XY6DO3BIJML2V7B5链直接进行DNAT操作进行DNAT到10.244.0.16 80端口。
9、将KUBE-SVC-IOIC7CRUMQYLZ32S链剩余请求转发给KUBE-SEP-SWCXAUIAGJXMWYFS链。
10、对源ip为10.244.0.18的包镜像SNAT。
11、KUBE-SEP-SWCXAUIAGJXMWYFS 链直接进行DNAT操作进行DNAT到10.244.0.18 80端口。

NodePort类型

我们将service类型改为NodePort

kubectl edit service/test

将type改为NodePort

kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
test NodePort 10.98.243.51 <none> 80:32734/TCP 21m

重新保存iptables规则查看

iptables-save >/tmp/1

NodePort类型根ClusterIP 相比就多了两条规则,其他都一致。

1、-A KUBE-NODEPORTS -p tcp -m comment --comment "default/test:" -m tcp --dport 32734 -j KUBE-MARK-MASQ
2、-A KUBE-NODEPORTS -p tcp -m comment --comment "default/test:" -m tcp --dport 32734 -j KUBE-SVC-IOIC7CRUMQYLZ32S

两条规则,主要是允许数据包转发和对目的端口为32734的端口进行DNAT映射

sessionaffinity

对于一些特殊应用,我们需要做会话保持,让会话连接始终连接到上一次接收会话的POD上
编辑我们刚刚创建的service

kubectl edit service/test

将sessionaffinity参数改为 sessionAffinity: ClientIP,保存
再次保存iptables规则查看

-A KUBE-SEP-DZSN6N54CDU2RTAQ -p tcp -m comment --comment "default/test:" -m recent --set --name KUBE-SEP-DZSN6N54CDU2RTAQ --mask 255.255.255.255 --rsource -m tcp -j DNAT --to-destination 10.244.0.15:80
-A KUBE-SEP-SWCXAUIAGJXMWYFS -p tcp -m comment --comment "default/test:" -m recent --set --name KUBE-SEP-SWCXAUIAGJXMWYFS --mask 255.255.255.255 --rsource -m tcp -j DNAT --to-destination 10.244.0.18:80
-A KUBE-SEP-XY6DO3BIJML2V7B5 -p tcp -m comment --comment "default/test:" -m recent --set --name KUBE-SEP-XY6DO3BIJML2V7B5 --mask 255.255.255.255 --rsource -m tcp -j DNAT --to-destination 10.244.0.16:80

会多三条规则,用的iptables recent模块进行会话保持
总结一下
使用iptables模式后,所有的如端口转发,会话保持,负载均衡都是通过iptables对应的模块和对应的规则去实现的比如端口转发用的DNAT规则,会话保持用的recent模块,负载均衡用的statistic 模块。虽然iptables模式弥补了userspace模式的一些缺陷,但iptables模式本身也存在一些缺陷,主要是在存在大量service的场景下。问题如下

在大规模集群下,随着service的数量越来越多时iptables规则会成倍的增长,大量的规则同时也会产生一些问题:

  • iptables规则匹配延时:因为iptables采用的是线性匹配即一个数据包过来以线性的方式遍历整个规则集,直到找到匹配的否则退出,这种带来的问题,就是当iptables规则量很大时,性能会急剧下降,因为对应的匹配延时会增加
  • iptables规则更新延时:在实际使用过程中需要不断创建service,修改service,删除service,这其实也转换成了对iptables的不断修改,因为iptables是非增量式更新的,也就意味着,你上述所有操作它都是把全部归则拷贝出来,然后在修改,修改完在拷贝回去而且这个修改过程还会锁表。附上网易云的iptables测试的更新延时性能测试表
    https://zhuanlan.zhihu.com/p/39909011
service_mode_2.png
  • 负载均衡性能问题:前面我们也提过iptables并不是专业的负载均衡器,目前使用RoundRobin和sessionaffinity都是通过iptables内部模块statistic和recent实现的,性能根真正的负载均衡器相比肯定有差距。

  • QPS抖动问题:kube-proxy会周期性的更新iptables规则,大量iptables规则更新会花费很长时间,期间又会锁表所以会造成QPS抖动。

Kubernetes社区为了解决上述iptables问题,在1.8版本引入了ipvs模式,并在Kubernetes 1.11版本正式GA。
熟悉LVS的知道,LVS是一个工作在传输层的四层负载均衡器,是章文嵩博士开源贡献给社区的,后被并入linux内核,IPVS正是LVS的一部分。LVS根iptables一样也是工作在Netfilter之上。

IPVS主要有三种模式

DR模式:调度器LB直接修改报文的目的MAC地址为后端真实服务器地址,服务器响应处理后的报文无需经过调度器LB,直接返回给客户端。这种模式也是性能最好的。
TUN模式:LB接收到客户请求包,进行IP Tunnel封装。即在原有的包头加上IP Tunnel的包头。然后发给后端真实服务器,真实的服务器将响应处理后的数据直接返回给客户端。
NAT模式:LB将客户端发过来的请求报文修改目的IP地址为后端真实服务器IP另外也修改后端真实服务器发过来的响应的报文的源IP为LB上的IP。

kube-proxy的IPVS模式用的上NAT模式,因为DR,TUN模式都不支持端口映射。

ipvs也支持多种算法
rr:轮询
lc:最少连接
dh:目的地址哈希
sh:源地址哈希
sed:最少期望延迟
nq:永远不排队

通过kube-proxy的–ipvs-scheduler进行配置,目前这个配置是一个全局性的,无法针对单个service做单独配置,后续会支持单个service转发算法配置。
启用方法
以kubeadm为例
因为ipvs是需要使用ipvs内核模块,先保证有这些内核模块ip_vs_sh,ip_vs_wrr,ip_vs_rr,ip_vs,nf_conntrack
没有的话手动加载

for i in {ip_vs_sh,ip_vs_wrr,ip_vs_rr,ip_vs,nf_conntrack};do modprobe $i;done

记得设置开机自动加载。
安装ipset和ipvsadm管理工具

apt-get install ipset ipvsadm

创建kubeadm部署配置文件,文件内容如下

apiVersion: kubeadm.k8s.io/v1alpha2
kind: MasterConfiguration
kubernetesVersion: v1.11.0
networking:
podSubnet: 10.244.0.0/16
kubeProxy:
config:
mode: ipvs

执行kubeadm init –config xxxx 部署Kubernetes集群,然后就像之前方法一样通过kubeadm join加节点。
部署完执行ipvsadm可以看见创建的一些ipvs规则

service_mode_3.png

kube-proxy也会在集群每个节点创建一个kube-ipvs0的网卡,将集群的cluster-ip挂在上面。

service_mode_4.png

service_mode_5.png
service_mode_6.png
ClusterIP
kubectl run test --image=nginx --replicas=3

创建一个clusterip类型的service

kubectl expose deployment/test --port=80 --type=ClusterIP

查看service的cluster-ip

kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 13h
test ClusterIP 10.105.170.66 <none> 80/TCP 12m

查看pod的ip

kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE
test-679b667858-kz4wd 1/1 Running 0 18m 10.244.1.8 wan-node2
test-679b667858-npdlp 1/1 Running 0 18m 10.244.1.7 wan-node2
test-679b667858-qgwnj 1/1 Running 0 18m 10.244.0.21 wan-node1

可以看见kube-proxy将刚刚创建的testservice的cluster-ip 10.105.170.66挂载到kube-ipvs0虚拟网卡上了。

root@wan-node1:~# ip a
kube-ipvs0: <BROADCAST,NOARP> mtu 1500 qdisc noop state DOWN group default
link/ether 4a:ec:28:5d:a0:24 brd ff:ff:ff:ff:ff:ff
inet 10.96.0.10/32 brd 10.96.0.10 scope global kube-ipvs0
valid_lft forever preferred_lft forever
inet 10.96.0.1/32 brd 10.96.0.1 scope global kube-ipvs0
valid_lft forever preferred_lft forever
inet 10.105.170.66/32 brd 10.105.170.66 scope global kube-ipvs0
valid_lft forever preferred_lft forever

查看ipvs规则

ipvsadm -ln
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
-> RemoteAddress:Port Forward Weight ActiveConn InActConn
TCP 10.105.170.66:80 rr
-> 10.244.0.21:80 Masq 1 0 0
-> 10.244.1.7:80 Masq 1 0 0
-> 10.244.1.8:80 Masq 1 0 0

可以看见ipvs生成了对应的规则,VIP为10.105.170.66端口为80端口转发模式为rr,后端服务器IP为10.244.0.21,10.244.1.7,10.244.1.8正是我们的POD的IP.

NodePort

修改service类型为NodePort
kubectl edit svc/test将type修改为NodePort

kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
test NodePort 10.105.170.66 <none> 80:31389/TCP 21m

在次查看ipvs规则

ipvsadm -ln
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
-> RemoteAddress:Port Forward Weight ActiveConn InActConn
TCP 192.168.250.200:31389 rr
-> 10.244.0.21:80 Masq 1 0 0
-> 10.244.1.7:80 Masq 1 0 0
-> 10.244.1.8:80 Masq 1 0 0
TCP 10.244.0.0:31389 rr
-> 10.244.0.21:80 Masq 1 0 0
-> 10.244.1.7:80 Masq 1 0 0
-> 10.244.1.8:80 Masq 1 0 0
TCP 172.17.0.1:31389 rr
-> 10.244.0.21:80 Masq 1 0 0
-> 10.244.1.7:80 Masq 1 0 0
-> 10.244.1.8:80 Masq 1 0 0
TCP 10.244.0.1:31389 rr
-> 10.244.0.21:80 Masq 1 0 0
-> 10.244.1.7:80 Masq 1 0 0
-> 10.244.1.8:80 Masq 1 0 0
TCP 127.0.0.1:31389 rr
-> 10.244.0.21:80 Masq 1 0 0
-> 10.244.1.7:80 Masq 1 0 0
-> 10.244.1.8:80 Masq 1 0 0

当service为NodePort,ipvs会以宿主机上所有网卡的ip为vip生成对应的转发规则,端口为nodeport端口

SessionAffinity

编辑我们刚刚创建的service

kubectl edit service/test

将sessionaffinity参数改为 sessionAffinity: ClientIP,保存
在此查看ipvs规则

root@wan-node1:~# ipvsadm -ln
-> RemoteAddress:Port Forward Weight ActiveConn InActConn
TCP 192.168.250.200:31389 rr persistent 10800
-> 10.244.0.21:80 Masq 1 0 0
-> 10.244.1.7:80 Masq 1 0 0
-> 10.244.1.8:80 Masq 1 0 0

ipvs在虚拟服务器中设置了会话超时时间,默认为10800秒(180分钟)

总结:可以看见ipvs模式根之前iptables有很大区别,之前iptables都是通过生成对应的iptables规则来实现端口映射,负载均衡,会话保持,但ipvs模式是通过将cluster-ip绑在kube-ipvs0虚拟网卡上,然后通过创建对应的ipvs规则来实现端口映射,负载均衡,会话保持。

ipvs依赖iptables

因为ipvs只能实现端口映射,负载均衡,会话保存,但像包过滤、SNAT、hairpin-masquerade tricks(地址伪装)这些还是需要通过iptables实现,但也并不是直接调用iptables生成规则实现的而是通过ipset。
ipset是什么?
ipset是iptables的扩展,它可以创建一个集合,这个集合内容可以是ip地址,ip网段,端口等,然后iptables可以直接添加规则对这个集合进行操作。这样的好处在于不用针对每个ip或每个端口添加单独的规则,可以减少大量iptables规则添加,减少性能损耗。比如我们要禁止上万个IP访问我们的服务器,用iptables的话,你需要添加一条条规则,这样会在iptables中生成大量规则造成性能损耗,但通过ipset,可以将地址直接加入到ipset集合中,然后iptables可以添加规则对这个ipset进行操作。
为什么用ipset?
因为单独操作iptables就回到iptables模式的问题了,一但Kubernetes集群中service过多,会产生大量iptables规则,造成性能损耗,但用ipset可以配置集合将对象添加进去,这样可以保证即使我有在多的service和pod,但iptables规则是固定不变的。

查看ipset集合

ipset list



ipset save 集合名 -f /tmp/1

kube-proxy使用的ipset集合

service_mode_7.png

Kubernetes哪些场景会用到ipset
kube-proxy配置–masquerade-all = true参数
在kube-proxy启动中指定集群CIDR
使用Loadbalancer类型的service
使用NodePort类型的service

注意点:
在用户环境中使用发现一些需要长连接的应用使用ipvs模式经常出现”Connection reset by peer”的错误,后经过抓包分析发现链接是被IPVS清理掉了,随后通过以下命令发现IPVS默认tcp连接超时时间为900s(15分钟)

ipvsadm -l --timeout
Timeout (tcp tcpfin udp): 900 120 300

而操作系统默认是7200s(2小时),这就产生了一个问题如果client的tcp链接空闲时间超过900s后会首先被IPVS强制断开,但操作系统认为该链接还没有超时会继续保活,所以就产生了上述问题。

sysctl -a|grep net.ipv4.tcp_keepalive_time
net.ipv4.tcp_keepalive_time = 7200

解决办法
将net.ipv4.tcp_keepalive_time = 7200设置为小于ipvs的900s即可,比如设置为600s

https://berlinsaint.github.io/blog/2018/11/01/Mysql_On_Kubernetes%E5%BC%95%E5%8F%91%E7%9A%84TCP%E8%B6%85%E6%97%B6%E9%97%AE%E9%A2%98%E5%AE%9A%E4%BD%8D/
https://kubernetes.io/blog/2018/07/09/ipvs-based-in-cluster-load-balancing-deep-dive/
https://github.com/projectcalico/calico/issues/2165
https://fixatom.com/block-ip-with-ipset/
https://github.com/kubernetes/kubernetes/blob/master/pkg/proxy/ipvs/README.md


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK