k8s 查看pod流量_k8s网络诊断之我的流量去哪了-阿里云开发者社区
k8s网络诊断之我的流量去哪了背景信息:某客户反馈,生产业务的应用在做滚动更新时大量502报错,同时生产业务中pod设置了prestop来优雅下线(延迟关闭),但是在滚动更新时依然会有502的问题,由于生产环境流量比较高,决定搭建测试环境模拟复现该问题,实际复现过程中发现,pod切换完成后,客户端访问svc关联的SLB,应用会超时一段时间或者qps下降为0如图4.0.1所示,qps=0以及time
k8s网络诊断之我的流量去哪了
背景信息:
某客户反馈,生产业务的应用在做滚动更新时大量502报错,同时生产业务中pod设置了prestop来优雅下线(延迟关闭),但是在滚动更新时依然会有502的问题,由于生产环境流量比较高,决定搭建测试环境模拟复现该问题,实际复现过程中发现,pod切换完成后,客户端访问svc关联的SLB,应用会超时一段时间或者qps下降为0
如图4.0.1所示,qps=0以及timeout都是连接异常
图4.0.1
deplyment里面关于prestop的设置节选(具有 readiness健康检查)
spec:
containers:
- image: nginx:1.18
imagePullPolicy: IfNotPresent
lifecycle:
preStop:
exec:
command:
- /bin/bash
- -c
- sleep 20
readinessProbe:
failureThreshold: 5
httpGet:
path: /
port: 80
scheme: HTTP
initialDelaySeconds: 5
periodSeconds: 5
successThreshold: 1
timeoutSeconds: 1
复现架构(图4.0.2):
图4.0.2
这种问题我们在了解架构之后,看看抓包在哪个环节比较简便,先抓包看下是建联异常,还是返回数据异常呢?
抓eth0的对应svc的nodeport,看起来有重传,但是不知道重传给哪个pod,图4.1.1 图4.1.2
tcpdump -i eth0 port 31199 -s 0 -w nodeport.pcap
图4.1.1
图4.1.2
4.2 抓所有端口接着看,发现有异常的syn重传在包里有端口复用,如图4.2.1所示
图4.2.1
但是将端口过滤出来看,上一次复用该端口已经正常的fin掉了(证明不是长连接的问题),且间隔了一分钟左右才复用如图4.2.2,新pod没有重传报文,为什么还是将报文转发给了老的pod呢?
图4.2.2
4.3 是否会是ipvs规则更新比较慢导致转发给了老的pod?接着收集一下endpoint,pod ,ipvs等组件的变化看看是否存在未更新的情况,收集脚本如下(注意替换ip)
$ cat pod-ep-ipvs.sh
for i in {1..200}
do
echo $(date) >> pod.txt
kubectl get pods -n ratel -o wide |grep v***s-test >> pod.txt
kubectl describe ep -n ratel v***s-test-service >>pod.txt
sudo ipvsadm -Ln |egrep "10.27.255.42:80|10.20.81.251:80" -A 5 >>pod.txt(svc,slb的ip)
sudo ipvsadm -Ln -c |egrep "10.27.1.245|10.27.1.246"|egrep -vi "close|time_wait" >>pod.txt(pod的ip)
sudo egrep "10.27.1.245|10.27.1.246" /proc/net/nf_conntrack|egrep -vi "close|time_wait" >>pod.txt(pod的ip)
sleep 1s
done
从抓取的记录可以看到相关资源的ipvs规则,pod状态以及endpoint的设置都是新的podip,说明问题可能出在转发层面,而负载转发流量的恰恰就是ipvs本身
2020年 06月 08日 星期一 15:14:28 CST
====pod的采集====
v****s-test-85f56494bd-5swfv 1/1 Running 0 50s 10.27.1.249 cn-beijing.10.20.79.226
v***s-test-85f56494bd-lx59h 1/1 Running 0 57s 10.27.1.248 cn-beijing.10.20.79.226
====endpoint的采集====
Name: v***s-test-service
Namespace: r***l
Labels: app=v***s-test
team=default
Annotations:
Subsets:
Addresses: 10.27.1.248,10.27.1.249
NotReadyAddresses:
Ports:
Name Port Protocol
---- ---- --------
crontab 50091 TCP
main 80 TCP
Events:
====ipvs rule的采集====
TCP 10.20.81.251:80 rr
-> 10.27.1.248:80 Masq 1 0 0
-> 10.27.1.249:80 Masq 1 0 0
TCP 10.20.81.252:80 rr
-> 10.27.1.114:8080 Masq 1 0 0
-> 10.27.4.110:8080 Masq 1 0 0
--
TCP 10.27.255.42:80 rr
-> 10.27.1.248:80 Masq 1 0 0
-> 10.27.1.249:80 Masq 1 0 0
TCP 10.27.255.42:50091 rr
-> 10.27.1.248:50091 Masq 1 0 0
-> 10.27.1.249:50091 Masq 1 0 0
前面脚本抓取的日志中是有提取ipvs 的session以及nf_conn表的记录的,从日志里面可以看到表项里面都有syn建联的报文了,
====ipvs的session采集====
IPVS connection entries
pro expire state source virtual destination
TCP 00:09 SYN_RECV 100.97.130.6:27307 10.20.79.226:31199 10.27.1.245:80
TCP 00:59 SYN_RECV 10.23.31.87:59462 10.20.79.226:31199 10.27.1.246:80
TCP 00:09 SYN_RECV 100.97.130.130:26633 10.20.79.226:31199 10.27.1.245:80
====nf_conntrack表内session的采集====
ipv4 2 tcp 6 107 SYN_SENT src=10.27.2.92 dst=10.27.1.245 sport=37912 dport=18080 [UNREPLIED] src=10.27.1.245 dst=10.27.2.92 sport=18080 dport=37912 mark=0 zone=0 use=2
ipv4 2 tcp 6 119 SYN_SENT src=10.23.31.87 dst=10.20.79.226 sport=59462 dport=31199 [UNREPLIED] src=10.27.1.246 dst=10.23.31.87 sport=80 dport=59462 mark=0 zone=0 use=2
ipv4 2 tcp 6 103 SYN_SENT src=10.27.1.241 dst=10.27.1.246 sport=38702 dport=80 [UNREPLIED] src=10.27.1.246 dst=10.27.1.241 sport=80 dport=38702 mark=0 zone=0 use=2
ipv4 2 tcp 6 104 SYN_SENT src=10.27.4.30 dst=10.27.1.245 sport=36178 dport=18080 [UNREPLIED] src=10.27.1.245 dst=10.27.4.30 sport=18080 dport=36178 mark=0 zone=0 use=2
如下图所示图4.3.1,客户端的syn重传无法建联,我们知道tcp按次握手要 syn--synack--ack才能完成建联,为什么server端不返回synack呢?
图4.3.1
而在server端的抓包如图4.3.2,虽然有端口复用,但是上一次复用这个端口的时候,对应的连接已经被fin掉了,四次挥手fin已经完成的状态下,对应的连接进入time_Wait状态
图4.3.2
4.4 在k8s 集群里选择ipvs组件,那么负责转发流量给对应的endpoint就是ipvs,这种情况就要抓全量的session去看session状态变化,怀疑跟五元组相同复用有关系,通过抓ipvs以及/proc/net/nf_conntrack全量session对比发现上一秒time_Wait还在过期倒计时,下一秒复用端口变成了syn_recv,并转发给了老的ip
2020年 06月 08日 星期一 17:58:22 CST
TCP 01:00 TIME_WAIT 10.23.31.87:58578 10.20.79.226:31199 10.27.1.249:80
ipv4 2 tcp 6 60 TIME_WAIT src=10.23.31.87 dst=10.20.79.226 sport=58578 dport=31199 src=10.27.1.249 dst=10.23.31.87 sport=80 dport=58578 [ASSURED] mark=0 zone=0 use=2
2020年 06月 08日 星期一 17:58:24 CST
TCP 00:58 TIME_WAIT 10.23.31.87:58578 10.20.79.226:31199 10.27.1.249:80
ipv4 2 tcp 6 58 TIME_WAIT src=10.23.31.87 dst=10.20.79.226 sport=58578 dport=31199 src=10.27.1.249 dst=10.23.31.87 sport=80 dport=58578 [ASSURED] mark=0 zone=0 use=2
2020年 06月 08日 星期一 17:58:26 CST
TCP 00:59 SYN_RECV 10.23.31.87:58578 10.20.79.226:31199 10.27.1.249:80
ipv4 2 tcp 6 119 SYN_SENT src=10.23.31.87 dst=10.20.79.226 sport=58578 dport=31199 [UNREPLIED] src=10.27.1.249 dst=10.23.31.87 sport=80 dport=58578 mark=0 zone=0 use=2
2020年 06月 08日 星期一 17:58:27 CST
TCP 00:58 SYN_RECV 10.23.31.87:58578 10.20.79.226:31199 10.27.1.249:80
ipv4 2 tcp 6 117 SYN_SENT src=10.23.31.87 dst=10.20.79.226 sport=58578 dport=31199 [UNREPLIED] src=10.27.1.249 dst=10.23.31.87 sport=80 dport=58578 mark=0 zone=0 use=2
======start 这一段引用自网络资料========
原因
K8S 实现了 pod graceful termination,也就是在显式缩容或者部署新服务从而旧的 replica set 隐式缩容时,请求这个 pod 对应 cluster IP 的客户端无感。 其实现机制是这样的:
首先, kube-proxy 把 LVS 里某个 cluster IP 要删除的 pod IP 的 weight 设置成 0,但并不删除这个 pod IP,目的是让新的连接走剩下的活着的 pod IP,而老的没断开的连接继续传输数据;
然后, kube-proxy 不断检查 LVS 那个要删除的 pod IP 上的活跃连接和非活跃连接数,只有当两者都为零时,才会真的从 LVS 里删除这个 pod IP。
一般来说,K8S 里服务之间往往请求很短,会使用短连接或者 keepalive time 比较短的长连接,而 pod graceful termination 默认期限 30s,也就是 30s 内 pod 还没退出就会强行杀掉,所以「已有连接」有30s 的时间自然断开,不影响服务,而「新连接」有 weight 设置的缘故,不会走到这个要被删除的 pod IP,所以新连接畅通无阻, 总结来说,非常完美的 graceful termination。
然而细节是魔鬼,K8S 1.13 里为了提升性能,把 /proc/sys/net/ipv4/vs/conn_reuse_mode 从默认值1 改成 0 了, 这个值为 1 时,会导致某种情况下新建连接有 1s 的额外延迟,而改成 0 的效果是, 如果请求 cluster IP 的源端口被重用了,也就是在 conntrack table 里已经有了 条目了,那么 IPVS 会直接选择之前服务这个 的 pod,并不会真的按权重走负载均衡逻辑,导致新的连接去了那个要被删除的 pod,当那个 pod 30s 后被删除后,这些重用了源端口的流量就会连接失败了。在这个四元组里 src_ip 是请求服务的客户端 pod ip,对于这一个客户端 pod 来说是固定的,cluster 的 ip 和 port 也都是固定的,只有 src_port 可以在 net.ipv4.ip_local_port_range 规定的不到三万个端口里变化,在高并发短链接情况下非常容易发生源端口复用的情况。
======end 这一段引用自网络资料========
优化方案:
1,扩大客户端local_portrange
2,关闭ipvs所在的实例的net.ipv4.vs.conn_reuse_mode 设置为1,会导致性能下降,偶发1s的延迟(0复用会导致五元组相同转发给老的pod)
3,使用iptables的模式
总结:
在IPVS网络代理模式下的kubernetes集群中进行滚动更新,期间如果客户端在短时间(两分钟)内发送大量请求(短链接,需建联),且客户端端口触发复用(端口随机分配不是一直递增,但是本地端口依然是用的越少越不容易复现),这个时候由于客户端的请求报文网络五元组相同,且ipvs的复用连接参数net.ipv4.vs.conn_reuse_mode为0时,就会触发IPVS复用Connection(time_wait变SYN_RECV/syn_sent),会导致报文被转发到了一个已经“销毁”的Pod上,从而导致建联失败,引发业务异常
扩展:
为何ipvs是syn_recv,而nf_conntrack表是syn_sent?
我们依据tcp三次握手的状态变化来推断,client跟ipvs建连因此ipvs记录的是syn_recv,而ipvs实际将流量导向真实pod的时候,ipvs作为一个“中间商”,它发送syn给pod,因此nf_conntrack里面填充的是syn_sent
-------------------------专家服务 值得信赖------------------------------
-------------------------专家服务 不负所托------------------------------
======再扩展======
案例更新2:
后来遇到一个类似的case,如下这个截图,为什么tcp三次握手建联不成功呢?(端口复用,synack缘何没有,ack里面的seq是否正确?对端tcp连接的状态?)
案例更新3:
源pod访问目标地址为nodeip+nodeport,nodeip是terway-eniip的模式,eni网卡直通,但是会时通时不通
client重传报文如下:
server端接收报文如下:
server端的报文没有收到一开始的syn?那么可能在哪个环节被拒绝了呢?
conntrack表的采集日志如下:
更多推荐
所有评论(0)