一次网络连通性监控误报问题诊断

2022-07-01  本文已影响0人  艺超51iwowo

背景

业务存在一个监控系统,需要监控一些设备的网络是否正常。不过最近发现经常会发生断网误报情况,所以深入探究一下问题原因。

排查过程

阶段一:一直困扰在Ping的请求是否通的误区

开始的时候,粗略看了一下源代码,再加上平时一直使用Ping作为网络探测的首要方式,所以一直认为只有Ping通,才认为网络是通的。

if (!PingUtil.ping(ip)) {
    return DISCONNECT;
}

但是事实并非如此,发现有一些主机,从监控主机ping依然是不通的,但是监控平台显示网络状态是正常的。

阶段二:仔细阅读代码,判断问题

这就说明判断对方主机是否断网的策略应该不止是Ping方式。
进入PingUtil.cmdPing源码,可以看到包含两种策

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);
}

有几个点要注意

对于我们常规的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

image.png

仿佛发现了新大陆一样,发现即使是connection refused,也可以认为网络是可达的。
对connnection refused可以解释为

  1. 发送一个TCP SYN packet 给远端机器。
  2. 然后收到一个 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

说明:

可以看到通过tcp端口7发送SYN包,收到一个RST包。这样按照JDK说明,是可以判定网络可达的。

阶段四: 优化断网监控策略

Action 1:

从监控集群到目标主机的链路来看,依赖tcp和icmp两种协议的数据包,所以需要保障双向(从目标主机到监控集群、从监控集群到目标主机)的路由策略,不能拦截数据包。这一步需要网络运维同学配合完成。

Action 2:

原有的监控策略是依赖InetAddress.isReachable的,也就是从监控集群发出Ping请求或者是请求tcp 7端口。两种方式都属于监控集群主动监控。 在当前情况下,由于程序是非Root启动,所以会首先依赖tcp 7端口的响应。但是当偶尔发生丢包或者超时,就会被误认为断网。
所以增加一种校验策略,由于目标主机是会定期上传心跳,而上传心跳也就意味着设备可以通过网络访问到日志平台,可以基于心跳时间判断网络是否正常。

调整后的策略

  1. 判断最后一次心跳是否在允许的时间区间内。
  2. 如果心跳不正常,使用InetAddress.isReachable进行判断。
  3. 如果InetAddress.isReachable不通,尝试直接执行Ping命令。

只有当上述三种策略都不通的情况下,则认定目标主机已经断网。

总结

  1. 运维人员依赖监控告警,如果频繁的误报,会影响大家的判断,所以要尽可能减少误报,增加准确率。当然依赖心跳会引入告警及时性的问题。
  2. 网络是非常复杂的,在判定条件上,可以综合多种方式一起判断。同时要善于使用抓包工具进行验证。
  3. 对于源码问题,一定要找到问题根源,并加以验证。
上一篇下一篇

猜你喜欢

热点阅读