一次网络连通性监控误报问题诊断
背景
业务存在一个监控系统,需要监控一些设备的网络是否正常。不过最近发现经常会发生断网误报情况,所以深入探究一下问题原因。
排查过程
阶段一:一直困扰在Ping的请求是否通的误区
开始的时候,粗略看了一下源代码,再加上平时一直使用Ping作为网络探测的首要方式,所以一直认为只有Ping通,才认为网络是通的。
if (!PingUtil.ping(ip)) {
return DISCONNECT;
}
但是事实并非如此,发现有一些主机,从监控主机ping依然是不通的,但是监控平台显示网络状态是正常的。
阶段二:仔细阅读代码,判断问题
这就说明判断对方主机是否断网的策略应该不止是Ping方式。
进入PingUtil.cmdPing源码,可以看到包含两种策
- 策略一:使用JDK InetAddress的isReachable探测。
- 策略二:如果策略一失败,则尝试使用Ping方法探测。
public static boolean ping(String ipAddress) {
try {
InetAddress inet = InetAddress.getByName(ipAddress);
boolean isReachable = inet.isReachable(1000);
if (isReachable) {
return true;
}
} catch (IOException e) {
logger.error("ping reachable error,ip:" + ipAddress, e);
}
String osName = System.getProperty("os.name");
String cmd = null;
int pingTimes = 4;
if (osName.contains(WINDOWS)) {
cmd = "ping -n " + pingTimes + " " + ipAddress;
} else {
cmd = "ping -c " + pingTimes + " " + ipAddress;
}
Process process = null;
try {
process = Runtime.getRuntime().exec(cmd);
} catch (IOException e) {
logger.error("ping exec error,ip:" + ipAddress, e);
}
if (process == null) {
return false;
}
int connected = 0;
StringBuilder content=new StringBuilder();
try (BufferedReader in = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
String line = null;
while ((line = in.readLine()) != null) {
content.append(line).append(";");
if (line.contains("ttl=") || line.contains("TTL=")) {
connected++;
}
}
} catch (IOException e) {
logger.error("ping read error,ip:" + ipAddress, e);
}
return connected > 0;
}
回到源码,只能说明策略一在起作用,也就是InetAddress.isReachable
看一下代码的JDK注释
/**
* Test whether that address is reachable. Best effort is made by the
* implementation to try to reach the host, but firewalls and server
* configuration may block requests resulting in a unreachable status
* while some specific ports may be accessible.
* A typical implementation will use ICMP ECHO REQUESTs if the
* privilege can be obtained, otherwise it will try to establish
* a TCP connection on port 7 (Echo) of the destination host.
* <p>
* The timeout value, in milliseconds, indicates the maximum amount of time
* the try should take. If the operation times out before getting an
* answer, the host is deemed unreachable. A negative value will result
* in an IllegalArgumentException being thrown.
*
* @param timeout the time, in milliseconds, before the call aborts
* @return a {@code boolean} indicating if the address is reachable.
* @throws IOException if a network error occurs
* @throws IllegalArgumentException if {@code timeout} is negative.
* @since 1.5
*/
public boolean isReachable(int timeout) throws IOException {
return isReachable(null, 0 , timeout);
}
有几个点要注意
- 典型的实现是使用ICMP协议,不过这要求有Root权限,有资料显示是因为ICMP需要使用Raw Socket。
- 如果没有权限,那么就会尝试使用TCP 7端口同远程host建立连接。
对于我们常规的java应用,都不是以Root权限运行的,所以依赖于同远程host建立TCP连接的方式。到这里,立马去机器上执行
telnet xx.xx.xx.xx 7
很遗憾,直接得到了Conenction Refused。
这里也补充一下端口7的说明,端口7是echo服务的,通常就是发起服务器什么,就会返回什么。但是由于存在安全隐患,该端口通常都是关闭的。
image.png
既然TCP端口7不通,而且Ping也不通,那究竟如何判断网络是正常的呢? 还是要求助源码。
github上找了一下InetAddress的实现(https://github.com/frohoff/jdk8u-dev-jdk/blob/master/src/solaris/native/java/net/Inet4AddressImpl.c)
仿佛发现了新大陆一样,发现即使是connection refused,也可以认为网络是可达的。
对connnection refused可以解释为
- 发送一个TCP SYN packet 给远端机器。
- 然后收到一个 TCP RST packet 响应。
也就是说请求可以到达远端机器,至于远端给的是可以连接的响应,还是拒绝的响应,至少可以证明链路是通的。
这里有一点需要注意,有一些特殊情况下,链路中间的防火墙会拦截TCP SYN packet,然后给一个TCP RST响应。所以InetAddress.isReachable通常是局域网内的判断。
这里以百度的一个ip为例,发现的确向对方TCP端口7发送了请求,但是并未收到响应,所以此时根据InetAddress.isReachable得到的结果就是false了。
image.png
阶段三:问题验证,事实说话
关于InetAddress.isReachable方法,还是需要验证一下。
本地验证
就将源码单独运行一下,本地开启wireshark,抓包验证。
public static void main(String[] args) throws IOException, InterruptedException {
while (true) {
InetAddress inet = InetAddress.getByName("192.168.2.116");
boolean isReachable = inet.isReachable(1000);
System.out.println(isReachable);
Thread.sleep(3000);
}
}
验证1:非root账号运行
直接使用Idea Run程序运行,
image.png
注意右下角,可以看到如前面阶段二所述,是给远端 TCP 7发送建连接请求,并且也收到了一个RST响应,而且判断结果是true。
验证2:使用Root账户运行
image.png在这种情况下,使用Mac Root账号执行,然后抓包后,发现是使用的ICMP协议了。
验证3:使用非Root账户运行,并且模拟端口7不可达。
linux环境下,可以使用iptables做一些路由。如果是Mac电脑,需要使用pfctl工具(Mac下使用pfctl)。
修改pfctl,将发送给远端端口7的包drop掉。
block drop out proto tcp from any to 192.168.2.116 port 7
image.png
可以看到此时的InetAddress.isReachable返回了false。
监控服务器验证
linux服务器上可以借助tcpdump进行抓包。
抓取icmp包
命令
sudo tcpdump -c 5 -nn -i 网卡 icmp
可以Ping通的情况下,应该包含request请求和reply响应。
image.png
如果无法ping通的话,只包含了request请求。
抓取tcp端口7的包
命令
sudo tcpdump -nn tcp port 7 |grep '目标ip'
执行结果(右上角是tcpdump的抓包结果,右下角是wireshark的结果)
image.png
说明:
- Flag说明
| 值 | 标志类型 | 描述 |
| --- | --- | --- |
| S | SYN | Connection Start |
| F | FIN | Connection Finish |
| P | PUSH | Data push |
| R | RST | Connection reset |
| . | ACK | Acknowledgment |
可以看到通过tcp端口7发送SYN包,收到一个RST包。这样按照JDK说明,是可以判定网络可达的。
阶段四: 优化断网监控策略
Action 1:
从监控集群到目标主机的链路来看,依赖tcp和icmp两种协议的数据包,所以需要保障双向(从目标主机到监控集群、从监控集群到目标主机)的路由策略,不能拦截数据包。这一步需要网络运维同学配合完成。
Action 2:
原有的监控策略是依赖InetAddress.isReachable的,也就是从监控集群发出Ping请求或者是请求tcp 7端口。两种方式都属于监控集群主动监控。 在当前情况下,由于程序是非Root启动,所以会首先依赖tcp 7端口的响应。但是当偶尔发生丢包或者超时,就会被误认为断网。
所以增加一种校验策略,由于目标主机是会定期上传心跳,而上传心跳也就意味着设备可以通过网络访问到日志平台,可以基于心跳时间判断网络是否正常。
调整后的策略
- 判断最后一次心跳是否在允许的时间区间内。
- 如果心跳不正常,使用InetAddress.isReachable进行判断。
- 如果InetAddress.isReachable不通,尝试直接执行Ping命令。
只有当上述三种策略都不通的情况下,则认定目标主机已经断网。
总结
- 运维人员依赖监控告警,如果频繁的误报,会影响大家的判断,所以要尽可能减少误报,增加准确率。当然依赖心跳会引入告警及时性的问题。
- 网络是非常复杂的,在判定条件上,可以综合多种方式一起判断。同时要善于使用抓包工具进行验证。
- 对于源码问题,一定要找到问题根源,并加以验证。