问题背景
K8S集群内,PodA使用服务名称访问PodB,请求出现异常。其中,PodA在 node1节点上,PodB在node2节点上。
原因分析
先上 tcpdump,观察请求是否有异常:
[root@node1~]#tcpdump-n-iens192port50300
...
13:48:17.630335IP177.177.176.150.distinct->10.96.22.136.50300:UDP,length214
13:48:17.630407IP192.168.7.21.distinct->10.96.22.136.50300:UDP,length214
...
从抓包数据可以看出,请求源地址端口号为 177.177.176.150:50901,目标地址端口号为10.96.22.136:50300,其中10.96.22.136是PodA使用server-svc这个serviceName请求得到的目的地址,也就是server-svc对应的serviceIP,那就确认一下这个地址有没有问题:
[root@node1~]#kubectlgetpod-A-owide|grepserver
ssserver-xxx-xxx1/1Running020h177.177.176.150node1
ssserver-xxx-xxx1/1Running020h177.177.254.245node2
ssserver-xxx-xxx1/1Running020h177.177.18.152node3
[root@node1~]#kubectlgetsvc-A-owide|grepserver
ssserver-svcClusterIP10.96.182.195<none>50300/UDP
可以看出,源地址没有问题,但目标地址跟预期不符,实际查到的服务名 server-svc对应的地址为10.96.182.195,这是怎么回事儿呢?我们知道,K8S从v1.13版本开始默认使用CoreDNS作为服务发现,PodA使用服务名server-svc发起请求时,需要经过CoreDNS的解析,将服务名解析为serviceIP,那就登录到PodA内,验证域名解析是不是有问题:
[root@node1~]#kubectlexec-it-nssserver-xxx-xxx--cat/etc/resolve.conf
nameserver10.96.0.10
searchss.svc.cluster.localsvc.cluster.localcluster.local
optionsndots:5
[root@node1~]#kubectlexec-it-nssserver-xxx-xxx--nslookupserver-svc
Server:10.96.0.10
Name:ss
Address:10.96.182.195
从查看结果看,域名解析没有问题,PodA内也可以正确解析出 server-svc对应的serviceIP为10.96.182.195,那最初使用tcpdump命令抓到的serviceIP 为10.96.22.136,难道这个地址是其他业务的服务,或者是残留的iptables规则,或者是有什么相关路由?分别查一下看看:
[root@node1~]#kubectlgetsvc-A-owide|grep10.96.22.136
[root@node1~]#iptables-save|grep10.96.22.136
[root@node1~]#iproute|grep10.96.22.136
结果是,集群上根本不存在 10.96.22.136这个地址,那PodA请求的目标地址为什么是它?既然主机上抓包时,目标地址已经是10.96.22.136,那再确认下出PodA时目标地址是什么:
[root@node1~]#iproute|grep177.177.176.150
177.177.176.150devcali9afa4438787scopelink
[root@node1~]#tcpdump-n-icali9afa4438787port50300
...
14:16:40.821511IP177.177.176.150.50902->10.96.22.136.50300:UDP,length214
...
原来出PodA时,目标地址已经是错误的 serviceIP。而结合上面的域名解析的验证结果看,请求出PodA时的域名解析应该不存在问题。综合上面的定位情况,基本可以推测出, 问题出在发送方 。
为了进一步区分出,是PodA内的所有发送请求都存在问题,还是只有业务自身的发送请求存在问题,我们使用 nc命令在PodA内模拟发送一个UDP数据包,然后在主机上抓包验证(PodA内恰巧有nc命令,如果没有,感兴趣的同学可以使用/dev/{tcp|udp}模拟[1]):
[root@node1~]#kubectlexec-it-nssserver-xxx-xxx--echo“test”|nc-userver-svc50300-p9999
[root@node1~]#tcpdump-n-icali9afa4438787port50300
...
15:46:45.871580IP177.177.176.150.50902->10.96.182.195.50300:UDP,length54
...
可以看出,PodA内模拟发送的请求,目标地址是可以正确解析的,也就把问题限定在了 业务自身的发送请求存在问题 。因为问题是服务名没有解析为正确的IP地址,所以怀疑是业务使用了什么缓存,如果猜想正确,那么重启PodA,理论上可以解决。而考虑到业务是多副本的,我们重启其中一个,其他副本上的问题环境还可以保留,跟开发沟通后重启并验证业务的请求:
[root@node1~]#dockerps|grepserver-xxx-xxx|grep-vPOD|awk'{print$1}'|xargsdockerrestart
[root@node1~]#tcpdump-n-iens192port50300
...
15:58:17.150535IP177.177.176.150.distinct->10.96.182.195.50300:UDP,length214
15:58:17.150607IP192.168.7.21.distinct->10.96.182.195.50300:UDP,length214
...
验证符合预期,进一步证明了业务可能是使用了什么缓存。与开发同学了解,业务的发送使用的是java原生的API发送 UDP数据,会不会是java在使用域名建立socket时默认会做缓存呢?
通过一番搜索,找了一篇相关博客[2],关键内容附上:
在通过DNS查找域名的过程中,可能会经过多台中间DNS服务器才能找到指定的域名,因此,在DNS服务器上查找域名是非常昂贵的操作。在Java中为了缓解这个问题,提供了DNS缓存。当InetAddress类第一次使用某个域名创建InetAddress对象后,JVM就会将这个域名和它从DNS上获得的信息(如IP地址)都保存在DNS缓存中。当下一次InetAddress类再使用这个域名时,就直接从DNS缓存里获得所需的信息,而无需再访问DNS服务器。
还真是,继续看怎么解决:
DNS缓存在默认时将永远保留曾经访问过的域名信息,但我们可以修改这个默认值。一般有两种方法可以修改这个默认值:
在程序中通过java.security.Security.setProperty方法设置安全属性networkaddress.cache.ttl的值(单位:秒)
设置java.security文件中的networkaddress.cache.negative.ttl属性。假设JDK的安装目录是C:/jdk1.6,那么java.security文件位于c:/jdk1.6/jre/lib/security目录中。打开这个文件,找到networkaddress.cache.ttl属性,并将这个属性值设为相应的缓存超时(单位:秒)
注:如果将networkaddress.cache.ttl属性值设为-1,那么DNS缓存数据将永远不会释放。
至此,问题定位结束。
解决方案
业务侧根据业务场景调整DNS缓存的设置。
参考资料
- https://blog.csdn.net/michaelwoshi/article/details/101107042
- https://blog.csdn.net/turkeyzhou/article/details/5510960