问题背景
K8S集群内,PodA使用服务名称访问PodB,请求出现异常。其中,PodA在node1
节点上,PodB在node2
节点上。
原因分析
先上tcpdump
,观察请求是否有异常:
1 | [root@node1 ~]# tcpdump -n -i ens192 port 50300 |
从抓包数据可以看出,请求源地址端口号为177.177.176.150:50901
,目标地址端口号为10.96.22.136:50300
,其中10.96.22.136
是PodA使用server-svc
这个serviceName
请求得到的目的地址,也就是server-svc
对应的serviceIP
,那就确认一下这个地址有没有问题:
1 | [root@node1 ~]# kubectl get pod -A -owide|grep server |
1 | [root@node1 ~]# kubectl get svc -A -owide|grep server |
可以看出,源地址没有问题,但目标地址跟预期不符,实际查到的服务名server-svc
对应的地址为10.96.182.195
,这是怎么回事儿呢?我们知道,K8S从v1.13版本开始默认使用CoreDNS
作为服务发现,PodA使用服务名server-svc
发起请求时,需要经过CoreDNS
的解析,将服务名解析为serviceIP
,那就登录到PodA内,验证域名解析是不是有问题:
1 | [root@node1 ~]# kubectl exec -it -n ss server-xxx-xxx -- cat /etc/resolve.conf |
从查看结果看,域名解析没有问题,PodA内也可以正确解析出server-svc
对应的serviceIP
为10.96.182.195
,那最初使用tcpdump
命令抓到的serviceIP
为10.96.22.136
,难道这个地址是其他业务的服务,或者是残留的iptables规则,或者是有什么相关路由?分别查一下看看:
1 | [root@node1 ~]# kubectl get svc -A -owide|grep 10.96.22.136 |
结果是,集群上根本不存在10.96.22.136
这个地址,那PodA请求的目标地址为什么是它?既然主机上抓包时,目标地址已经是10.96.22.136
,那再确认下出PodA时目标地址是什么:
1 | [root@node1 ~]# ip route|grep 177.177.176.150 |
原来出PodA时,目标地址已经是错误的serviceIP
。而结合上面的域名解析的验证结果看,请求出PodA时的域名解析应该不存在问题。综合上面的定位情况,基本可以推测出,问题出在发送方。
为了进一步区分出,是PodA内的所有发送请求都存在问题,还是只有业务自身的发送请求存在问题,我们使用nc
命令在PodA内模拟发送一个UDP
数据包,然后在主机上抓包验证(PodA内恰巧有nc
命令,如果没有,感兴趣的同学可以使用/dev/{tcp|udp}模拟[1]):
1 | [root@node1 ~]# kubectl exec -it -n ss server-xxx-xxx -- echo “test” | nc -u server-svc 50300 -p 9999 |
可以看出,PodA内模拟发送的请求,目标地址是可以正确解析的,也就把问题限定在了业务自身的发送请求存在问题。因为问题是服务名没有解析为正确的IP地址,所以怀疑是业务使用了什么缓存,如果猜想正确,那么重启PodA,理论上可以解决。而考虑到业务是多副本的,我们重启其中一个,其他副本上的问题环境还可以保留,跟开发沟通后重启并验证业务的请求:
1 | [root@node1 ~]# docker ps |grep server-xxx-xxx | grep -v POD |awk '{print $1}' |xargs docker restart |
验证符合预期,进一步证明了业务可能是使用了什么缓存。与开发同学了解,业务的发送使用的是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缓存的设置。