目录
[显示]

1.摘要

前一阵子发现服务会有偶发的服务不可用的情况,记录一下这个问题的排查过程。

现象是这样的:每天到了某个时间点,就会出现服务不稳定的情况,偶发接口调不通。

线上业务使用了lvs-nginx-tomcat三层结构,首先查看tomcat监控,没有什么特别异常的情况,响应时间和错误码没发现有什么异常,CPU、IO等等指标也都正常。

再查看nginx上的监控,发现在某个时刻这个服务的5xx报错突增,大概7、8秒之后又恢复了。

继续在nginx服务器上找线索,发现Nginx在那个时间点会出现报错:

线上nginx会每秒探测后端所有服务器的某个uri,如果返回的http状态码是200则认为正常,连续3次探测失败则摘除探测失败的服务器,直到探测成功再恢复。

从日志中可以发现nginx在出问题的时间点对于后端所有tomcat的探测请求都出现了问题,导致摘除了所有后端服务器,在这段时间里请求会报502异常。

从nginx上的日志可以看到探测请求没有返回,那么请求实际发到tomcat了没有?线上业务中的探测频率是1s/次,于是到tomcat的访问日志里查找线索,过滤一个nginx对tomcat的所有探测请求:

可用看出从7:00:10-7:00:40左右的探测请求是有丢失的。

前端机的负载并不高,于是我们第一时间认为这可能是nginx到tomcat服务器的网络有问题。统计了一下线上日志,出问题的机器集中在某个网段,并且集中在一天之内的某几个时间点,这似乎也进一步印证了我们的猜测。

但到此为止仅仅是怀疑,为了证明我们的猜测,我们尝试去复现问题。我们在nginx上部署了一个简单的脚本,用curl命令对同样的tomcat发起每秒一次的请求,但结果比较诡异:

监测方式 监测地址 http版本 频率 所在服务器 目的服务器 问题
nginx / 1.0 1s nginx tomcat
curl / 1.0 1s nginx tomcat

这跟我们之前的猜测不一致,没办法,尝试在两端抓包查看网络状况,

tomcat抓包:

nginx抓包:

tomcat服务器在7:00:10已经接收了请求并且回复了ACK,7:00:13 nginx超时主动断开连接,7:00:15时tomcat才返回数据,网络的问题被排除了。

那么接下来的重点就是tomcat本身,在接收问题请求的时候,tomcat服务究竟做了什么?

还是通过简单的脚本,在容易出问题的时间段连续使用jstack打印线程栈,查找出问题时处于RUNNABLE状态的catalina线程,发现这里有一句很可疑:

这个服务用的还是比较古老的tomcat6.0.32,查看源码,可以发现在tomcat对请求header做完解析之后会调用这个函数:

也就是说,如果request请求的header里没有设置host,那么tomcat会使用自己服务器的hostname作为request对象的host属性。

再对比线上nginx探测的请求和curl发出的请求,可以看出nginx的探测请求确实没有带任何header,而curl请求默认是带了3个header的:

curl:

nginx:

到这里可以确认,如果请求的header里没有带Host的话就有可能出现问题。找到了hang住的位置,那么接下来的问题就是,为什么这里会hang住?

第一个问题:这个getHostByAddr在做什么?翻出jvm源码,这个函数的定义在
jdk/src/share/classes/java/net/Inet4AddressImpl.java

继续研究getHostByAddr,对应的实现位于jdk/src/solaris/native/java/net/Inet6AddressImpl.c:

getnameinfo_ptr的定义位于jdk/src/solaris/native/java/net/net_util_md.c:

实际是调用了glibc库函数,man一下getnameinfo

结合man page说明和调用的上下文可以推测出这个函数可以通过ip查host,但是是怎么查的呢?继续查找代码,首先要确定操作系统用的glibc版本:

随便在机器上编译一个c程序,使用ldd命令查看它的依赖库路径:

去gnu官网下载对应版本的glibc源代码,查看源码,可以看出getnameinfo中调用了gethostbyaddr:

在gethostbyaddr函数中有这么一段:

这里把ip地址按8位翻转之后,加了一个“.in-addr.arpa”后缀,之后就通过通用的函数发出dns query请求,最终会调用res_mkquery,man一下这个函数(man 3 res_mkquery):

http://linux.die.net/man/3/res_mkquery

跟dns请求相关的实现略复杂,这里不再展开。

这里可以走一个小捷径,我们写一个最简单的c程序来查看getnameinfo都大致做了什么事情:

[root@localhost test]# gcc test.c

然后使用strace来跟踪系统调用:

可以看出,首次查询时会读取/etc/nsswitch.conf,/etc/resolv.conf,后面的请求先读了/etc/hosts,找不到就向dns服务器发送了一个udp查询请求(SOCK_DGRAM),之后使用了poll等待返回结果,有返回的话使用recvfrom接收结果。

为了验证看代码得到的结果再次抓包,不过这次只过滤53端口的数据包(dns服务的端口为53):

跟前面推测的一样,这里确实在向dns服务器发送这种后缀是.in-addr.arpa的请求。

可用在wiki上找到这类查询的详细描述:https://en.wikipedia.org/wiki/Reverse_DNS_lookup

最后一个问题是这个查询会超时吗?超时时间是多少?根据man page结果,dns查询的超时是5秒:

跟抓包结果一致,并且注意到strace中poll的最后一个参数是5000,和默认的超时时间一致。

我们可以在/etc/resolv.conf里增加关于超时时间的配置:

再用strace试一下,果然poll的参数变成1000了。

但是线上机器配置了dnsmasq缓存,为什么缓存没有生效?

配置了dnsmasq后再次使用tcpdump,可用看到lo网卡和eth1网卡都有查询请求,由于反向dns查询不到主机名,dnsmasq无法缓存结果,只能每次都把请求转发给实际dns。

线上除了这个网段的机器还有其他机器,为什么其他机器没有问题?

没出问题的机器里/etc/hosts配置了本机ip对应的hostname,在hosts文件中查询到了就不会再去搜索dns。

DNS解析在那个时间为什么会消耗5秒?

由于udp协议本身传输不可靠没有重发的机制,在网络异常的时候只能默默的等待超时,具体网络的问题这里就不展开了。

如何解决这个问题?

  1. 首先第一反应是想到升级tomcat版本,查看新版tomcat代码,有问题的代码果然没有了,线上服务升级到tomcat8后也恢复了正常。
  2. 如果不能升级tomcat,可以在nginx的探测增加host header,避免前端机反向查询请求。
  3. 如果两者都不能做,那么可以在本机hosts中配置对应本机ip的hostname,可以避免通过dns服务器查询。