CMQ消费者报错,无法获取本机ip地址问题排查

2020-10-25 00:02:47 浏览数 (1)

背景

腾讯云消息队列(Cloud Message Queue,CMQ)是一种分布式消息队列服务,它能够提供可靠的基于消息的异步通信机制,能够将分布式部署的不同应用(或同一应用的不同组件)之间的收发消息,存储在可靠有效的 CMQ 队列中,防止消息丢失。CMQ 支持多进程同时读写,收发互不干扰,无需各应用或组件始终处于运行状态。

可是有一天遇到一个问题,一个客户使用同样的消费者代码在三台CVM上面部署应用,其中有一台无法消费任何消息,运行报错,对于java这种Write once,run anywhere的语言来说,是很奇怪的,根据以往经验,这往往是环境问题。那么就需要我们仔细分析一下问题的根本原因。

问题排查原因及解决方案

我们先来看看报错的截图:

初步看来是RequestIdHelper这个类初始化失败,这种问题往往是静态代码块或者实例变量初始化异常造成。接着仔细查看异常堆栈,从中发现了问题,根源就是消费者静态代码块中用于获取ip地址构造RequestId的代码抛了异常,这句代码就是InetAddress.getLocalHost(),一句简单的代码,造成了严重的问题,整个消费者无法正常消费消息。

原因分析

为什么一句简单的InetAddress.getLocalHost()会抛出异常呢,我们分析下JDK的源代码,我们在源码中加注释分析:

代码语言:java复制
 public static InetAddress getLocalHost() throws UnknownHostException {

        SecurityManager security = System.getSecurityManager();
        try {
            // 1.获取hotname,这是个native方法,hotspot中实现非常简单,
            直接系统调用gethostname,如果调用失败,那么获取硬编码值‘localhost’
            String local = impl.getLocalHostName();

            if (security != null) {
                security.checkConnect(local, -1);
            }

            if (local.equals("localhost")) {
                return impl.loopbackAddress();
            }

            InetAddress ret = null;
            synchronized (cacheLock) {
                long now = System.currentTimeMillis();
                // 2. 如果命中cachedLocalHost,直接用缓存值
                if (cachedLocalHost != null) {
                    if ((now - cacheTime) < maxCacheTime) // Less than 5s old?
                        ret = cachedLocalHost;
                    else
                        cachedLocalHost = null;
                }

                // we are calling getAddressesFromNameService directly
                // to avoid getting localHost from cache
                if (ret == null) {
                    InetAddress[] localAddrs;
                    try {
                        // 3. 下面的逻辑是去缓存或者系统调用获取地址
                        localAddrs =
                                InetAddress.getAddressesFromNameService(local, null);
                    } catch (UnknownHostException uhe) {
                        // Rethrow with a more informative error message.
                        UnknownHostException uhe2 =
                                new UnknownHostException(local   ": "  
                                        uhe.getMessage());
                        uhe2.initCause(uhe);
                        throw uhe2;
                    }
                    cachedLocalHost = localAddrs[0];
                    cacheTime = now;
                    ret = localAddrs[0];
                }
            }
            return ret;
        } catch (java.lang.SecurityException e) {
            return impl.loopbackAddress();
        }
    }

那么从上面代码来看,唯一出问题的可能就是在第三步InetAddress.getAddressesFromNameService,里面的逻辑无非就是从缓存中查找或者调用Inet4AddressImpl.lookupAllHostAddr。问题可能就出在这个lookupAllHostAddr方法。lookupAllHostAddr

代码如下:

代码语言:c复制
/*
 * Find an internet address for a given hostname.  Note that this
 * code only works for addresses of type INET. The translation
 * of %d.%d.%d.%d to an address (int) occurs in java now, so the
 * String "host" shouldn't *ever* be a %d.%d.%d.%d string
 *
 * Class:     java_net_Inet4AddressImpl
 * Method:    lookupAllHostAddr
 * Signature: (Ljava/lang/String;)[[B
 */

JNIEXPORT jobjectArray JNICALL
Java_java_net_Inet4AddressImpl_lookupAllHostAddr(JNIEnv *env, jobject this,
                                                jstring host) {
    ...
    ...
    // 系统调用
    error = getaddrinfo(hostname, NULL, &hints, &res);

    if (error) {
        /* report error */
        ThrowUnknownHostExceptionWithGaiError(env, hostname, error);
        JNU_ReleaseStringPlatformChars(env, host, hostname);
        return NULL;
    } else {
        ....
    }
    ...
}

上面去除一些无关逻辑,可以看到,查询hostname的ip地址的是一个系统调用方法getaddrinfo。如果查不到,那么就可能抛出异常。那么接着分析下这个getaddrinfo是如何执行的。下面写一段代码,准备使用strace分析分析。测试代码如下:

代码语言:javascript复制
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <string.h>
#include <stdio.h>
#include <arpa/inet.h>

int main(int argc, char **argv)
{
    int ret = -1;
    struct addrinfo *res;
    struct addrinfo hint;
    struct addrinfo *curr;
    char ipstr[16];   

    if (argc != 2) {
        printf("parameter errorn");
        return -1;
    }

    bzero(&hint, sizeof(hint));
    hint.ai_family = AF_INET;
    hint.ai_socktype = SOCK_STREAM;

    ret = getaddrinfo(argv[1], NULL, &hint, &res);
     printf("getaddrinfo finishn");
    if (ret != 0) 
    {
        printf("getaddrinfo errorn");
        return -1;
    }

    for (curr = res; curr != NULL; curr = curr->ai_next) 
    {
        inet_ntop(AF_INET,&(((struct sockaddr_in *)(curr->ai_addr))->sin_addr), ipstr, 16);
        printf("%sn", ipstr);
    }

    freeaddrinfo(res);

    return 0;
}

测试代码中简单调用getaddrinfo方法,代码编写后,在腾讯云上申请一台CVM,系统环境为:Linux version 3.10.0-1062.18.1.el7.x86_64 (mockbuild@kbuilder.bsys.centos.org) (gcc version 4.8.5 20150623 (Red Hat 4.8.5-39) (GCC) ) #1 SMP Tue Mar 17 23:49:17 UTC 2020。然后执行 gcc testGetAddr.c -o testGetAddr,编译得到可执行程序。再执行strace ./testGetAddr {hostname},看到系统调用过程, 省略无关语句整理如下:

代码语言:javascript复制
...
...
open("/lib64/libresolv.so.2", O_RDONLY|O_CLOEXEC) = 3
open("/etc/host.conf", O_RDONLY|O_CLOEXEC) = 3
open("/etc/resolv.conf", O_RDONLY|O_CLOEXEC) = 3
open("/etc/nsswitch.conf", O_RDONLY|O_CLOEXEC) = 3
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
open("/lib64/libnss_files.so.2", O_RDONLY|O_CLOEXEC) = 3
open("/etc/hosts", O_RDONLY|O_CLOEXEC)  = 3
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
...
open("/lib64/libnss_dns.so.2", O_RDONLY|O_CLOEXEC) = 3
...
uname({sysname="Linux", nodename="efg", ...}) = 0
socket(AF_NETLINK, SOCK_RAW|SOCK_CLOEXEC|SOCK_NONBLOCK, NETLINK_ROUTE) = 3
setsockopt(3, SOL_SOCKET, SO_PASSCRED, [1], 4) = 0
setsockopt(3, SOL_NETLINK, 3, [1], 4)   = 0
bind(3, {sa_family=AF_NETLINK, pid=0, groups=00000000}, 16) = 0
getsockname(3, {sa_family=AF_NETLINK, pid=8933, groups=00000000}, [12]) = 0
sendto(3, "x18x00x00x00x16x00x05x03x01x00x00x00x00x00x00x00x02x20x00x00x00x00x00x00", 24, 0, {sa_family=AF_NETLINK, pid=0, groups=00000000}, 16) = 24
clock_gettime(CLOCK_MONOTONIC, {63428, 128822240}) = 0
recvmsg(3, {msg_name(0)=NULL, msg_iov(1)=[{NULL, 0}], msg_controllen=56, [{cmsg_len=20, cmsg_level=SOL_NETLINK, cmsg_type=3}, {cmsg_len=28, cmsg_level=SOL_SOCKET, cmsg_type=SCM_CREDENTIALS, {pid=0, uid=0, gid=0}}], msg_flags=MSG_TRUNC}, MSG_PEEK|MSG_TRUNC) = 164
recvmsg(3, {msg_name(0)=NULL, msg_iov(1)=[{"x4cx00x00x00x14x00x02x00x01x00x00x00xe5x22x00x00x02x08x80xfex01x00x00x00x08x00x01x00x7fx00x00x01"..., 328}], msg_controllen=56, [{cmsg_len=20, cmsg_level=SOL_NETLINK, cmsg_type=3}, {cmsg_len=28, cmsg_level=SOL_SOCKET, cmsg_type=SCM_CREDENTIALS, {pid=0, uid=0, gid=0}}], msg_flags=0}, MSG_TRUNC) = 164
clock_gettime(CLOCK_MONOTONIC, {63428, 128967588}) = 0
...

上面系统调用主要保留了打开的文件,可以看到先调用libresolv.so.2动态链接库,再打开文件open("/etc/nsswitch.conf", O_RDONLY|O_CLOEXEC) = 3,用于判断从host文件还是从dns server获取地址。由于本机修改了hostname为"efg",以及没有在hosts文件中设置hostname的ip,通过host文件肯定是获取不到地址信息的。那么程序接下来就通过NETLINK的单播形式,给内核发送消息,并尝试得到地址。如果还是得不到的话,那么就会查看/etc/host.conf的内容,本机的是multi on,表示libresolv.so.2需要获取所有ip地址,解析器就会根据/etc/resolv.conf里面指定的所有nameserver获取地址,大概过程就是这样。

所以,InetAddress.getLocalHost实现的步骤总结如下:

* 是否设置好了hostname,如果没有设置,返回hostname为localhost,ip地址硬编码写死127.0.0.1的地址信息实体。

* 如果设置了hostname,那么看看java本地缓存有没有地址信息,如果有,返回地址信息,如果没有,则执行一些列检查逻辑,纠正错误逻辑(如特殊的hostname有它特定的处理),如果是个特殊的hostname,直接返回。

* 执行系统调用getaddrinfo,打开/etc/nsswitch.conf,判断先查host还是先从dns server查,本例子中是则根据名字在hosts文件中查找,找不到,先和内核空间进程通过单播形式通信,尝试获取,如果失败,则使用DNS客户端进行域名解析处理 * 打开文件/etc/services,查找服务 * 打开etc/host.conf 该配置文件为域名解析顺序配置文件,设定解析顺序方式 * 打开/etc/resolv.conf配置文件,该文件用于指定解析的DNS服务器,得dns server * 打开/etc/hosts 文件,查询主机名 * hosts中找不到记录,从nameserver进行主机名称解析。

那么一台机器找不到ip地址,就有可能是上面步骤出了问题。

解决方案/最佳实践

如何解决呢,最简单的办法就是在hosts文件做兜底方案,设置hostname的ip地址即可,或者干脆删除hostname也行,但是这种方案有三个问题,一个是ip地址可能会被瞎写,一个是不同系统设置不同,可能会导致像这次故障一样应用起不来,最后一种就是查询过程中有getnameinfo走dns解析,假如这里网络异常不但可能起不来,还可能使得应用启动缓慢(特别是云函数这种场景影响会非常大)

所以,正常情况下,我们一般不会直接使用InetAddress.getLocalHost,而是通过NetworkInterface

的方式获取ip,下面代码复制自 https://cloud.tencent.com/developer/article/1610919:

代码语言:javascript复制

public static InetAddress getLocalHostExactAddress() {
    try {
        InetAddress candidateAddress = null;

        Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces();
        while (networkInterfaces.hasMoreElements()) {
            NetworkInterface iface = networkInterfaces.nextElement();
            // 该网卡接口下的ip会有多个,也需要一个个的遍历,找到自己所需要的
            for (Enumeration<InetAddress> inetAddrs = iface.getInetAddresses(); inetAddrs.hasMoreElements(); ) {
                InetAddress inetAddr = inetAddrs.nextElement();
                // 排除loopback回环类型地址(不管是IPv4还是IPv6 只要是回环地址都会返回true)
                if (!inetAddr.isLoopbackAddress()) {
                    if (inetAddr.isSiteLocalAddress()) {
                        // 如果是site-local地址,就是它了 就是我们要找的
                        // ~~~~~~~~~~~~~绝大部分情况下都会在此处返回你的ip地址值~~~~~~~~~~~~~
                        return inetAddr;
                    }

                    // 若不是site-local地址 那就记录下该地址当作候选
                    if (candidateAddress == null) {
                        candidateAddress = inetAddr;
                    }

                }
            }
        }

        // 如果出去loopback回环地之外无其它地址了,那就回退到原始方案吧
        return candidateAddress == null ? InetAddress.getLocalHost() : candidateAddress;
    } catch (Exception e) {
        e.printStackTrace();
    }
    return null;
}

0 人点赞