深入内核分析BindException异常原因

一、前言

前段时间公司内的站点发布时经常遇到Tomcat使用的8080端口被占用,导致启动报错BindException的情况。笔者参与了该问题的排查和修复,本文将深入Tomcat、OpenJDK、Linux内核等源码为大家讲解问题的原因以及排查过程。

报错信息

Caused by: java.net.BindException: Address already in use

二、Tomcat源码分析
起初我们通过Debug发现公司内使用的SpringBoot 1.5.x 版本的内置Tomcat没有默认设置reuse参数(如下图,soReuseAddress默认为null)。根据网上的相关资料,如果没有设置SO_REUSEADDR参数,就可能会与TIME_WAIT状态的Socket发生端口冲突。但服务端需要设置SO_REUSEADDR参数是网络编程的基础知识,Tomcat应该不会犯这么低级的错误,所以让我们带着这个疑问,逐步深入阅读源码进行求证。

三、OpenJDK源码分析

通过上述错误堆栈一路追查到了 bind0 方法,该方法是 native 的在Java层面看不到源码,我们使用的JDK版本也不是开源的OpenJDK,但我们可以阅读OpenJDK的源码作为参考:

3.1、bind0 函数源码

JNIEXPORT void JNICALL
Java_sun_nio_ch_Net_bind0(JNIEnv *env, jclass clazz, jobject fdo, jboolean preferIPv6,
          
               jboolean useExclBind, jobject iao, int port)
{
    SOCKADDR sa;
    int sa_len = SOCKADDR_LEN;
    int rv = 0;

    // 地址格式转换
    if (NET_InetAddressToSockaddr(env, iao, port, (struct sockaddr *)&sa, &sa_len, preferIPv6) != 0) {
      return;
   
}
    // 1. 调用NET_Bind函数进行bind
    rv = NET_Bind(fdval(env, fdo), (struct sockaddr *)&sa, sa_len);
    if (rv != 0) {
        // 2. 处理异常
        handleSocketError(env, errno);
   
}
}

该函数主要做了两件事:

调用NET_Bind函数执行bind操作。
调用handleSocketError函数处理bind操作返回的错误码。

3.2、NET_Bind函数源码

int
NET_Bind(int fd, struct sockaddr *him, int len)
{

if defined(__solaris__) && defined(AF_INET6)

    int level = -1;
    int exclbind = -1;

endif

    int rv;
    int arg, alen;

ifdef linux

    /*
     * ## get bugId
for this issue - goes back to 1.2.2 port ##
     * ## When IPv6
is enabled this will be an IPv4-mapped
     * ## with family
set to AF_INET6
     */
    if (him->sa_family == AF_INET) {
        struct sockaddr_in sa = (struct sockaddr_in )him;
        if ((ntohl(sa->sin_addr.s_addr) & 0x7f0000ff) == 0x7f0000ff) {
          
 errno = EADDRNOTAVAIL;
          
 return -1;
        }
   
}

endif

if defined(__solaris__)

    /*
     * Solaris has separate
IPv4 and IPv6 port spaces so we
     * use an
exclusive bind when SO_REUSEADDR is not used to
     * give the
illusion of a unified port space.
     * This also
avoids problems with IPv6 sockets connecting
     * to IPv4 mapped
addresses whereby the socket conversion
     * results in a
late bind that fails because the
     * corresponding
IPv4 port is in use.
     */
    alen = sizeof(arg);
    if (useExclBind || getsockopt(fd, SOL_SOCKET, SO_REUSEADDR,
          
        (char *)&arg, &alen) == 0) {
        if (useExclBind || arg == 0) {
          
 /*
          
  * SO_REUSEADDR is disabled or sun.net.useExclusiveBind
          
  * property is true so enable TCP_EXCLBIND or
          
  * UDP_EXCLBIND
          
  */
          
 alen = sizeof(arg);
          
 if (getsockopt(fd, SOL_SOCKET, SO_TYPE, (char *)&arg,
          
                &alen) == 0) {
          
     if (arg == SOCK_STREAM) {
          
         level = IPPROTO_TCP;
          
         exclbind = TCP_EXCLBIND;
          
     } else {
          
         level = IPPROTO_UDP;
          
         exclbind = UDP_EXCLBIND;
          
     }
            }

          
 arg = 1;
          
 // 调用setsockopt系统调用,设置参数
          
 setsockopt(fd, level, exclbind, (char *)&arg,
          
            sizeof(arg));
            }
   
}

endif

    // 调用bind系统调用
    rv = bind(fd, him, len);

if defined(__solaris__) && defined(AF_INET6)

    if (rv < 0) {
        int en = errno;
        / Restore _EXCLBIND if the bind fails */
        if (exclbind != -1) {
          
 int arg = 0;
          
 setsockopt(fd, level, exclbind, (char *)&arg,
          
            sizeof(arg));
        }
        errno = en;
   
}

endif


    return rv;
}

通过上述源码可以看出,JDK封装的bind函数并不是简单的对bind系统调用的封装,里面会根据各种判断逻辑在bind系统调用之前先执行setsockopt系统调用设置socket参数。所以Tomcat没有设置SO_REUSEADDR参数,是因为JDK默认设置了。

3.3、基于strace工具实验验证

由于我们使用的不是OpenJDK,而上述结论是基于OpenJDK的源码得出的,不够严谨。所以我们又进行了如下验证:

编写一个简单的SpringBoot程序并打成Jar包。
基于线上相同的JDK版本使用如下命令启动。

sudo strace -f -e socket,bind,setsockopt,getsockopt,listen,connect,accept -o strace.out java -jar test.jar

strace命令会打印这个Java程序运行时发起的系统调用的参数和返回值,输出如下:

...
16340 socket(AF_INET6, SOCK_STREAM, IPPROTO_IP) = 18
16340 setsockopt(18, SOL_IPV6, IPV6_V6ONLY, [0], 4) = 0
16340 setsockopt(18, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
16340 bind(18, {sa_family=AF_INET6, sin6_port=htons(8080), inet_pton(AF_INET6,
"::", &sin6_addr), sin6_flowinfo=htonl(0), sin6_scope_id=0}, 28)
= 0
16340 listen(18, 100)                
  = 0
16373 accept(18,  <unfinished ...>
...

可以看到在执行bind系统调用前,先执行了setsockopt系统调用,将SO_REUSEADDR设置为了true。所以端口冲突并不是与TIME_WAIT状态的连接发生的冲突。

排查到这我们有了新的思路:只要找到抛出BindException异常的代码逻辑,就可以找到端口冲突的原因。

3.4、handleSocketError函数源码

jint
handleSocketError(JNIEnv *env, jint errorValue)
{
    char *xn;
    switch (errorValue) {
        case EINPROGRESS:       / Non-blocking connect /
          
 return 0;

ifdef EPROTO

        case EPROTO:
          
 xn = JNU_JAVANETPKG "ProtocolException";
          
 break;

endif

        case ECONNREFUSED:
          
 xn = JNU_JAVANETPKG "ConnectException";
          
 break;
        case ETIMEDOUT:
          
 xn = JNU_JAVANETPKG "ConnectException";
          
 break;
        case EHOSTUNREACH:
          
 xn = JNU_JAVANETPKG "NoRouteToHostException";
          
 break;
        case EADDRINUSE:  /* Fall through
*/
        case EADDRNOTAVAIL:
          
 // 这里
          
 xn = JNU_JAVANETPKG "BindException";
          
 break;
        default:
          
 xn = JNU_JAVANETPKG "SocketException";
          
 break;
   
}
    errno = errorValue;
    JNU_ThrowByNameWithLastError(env, xn, "NioSocketError");
    return IOS_THROWN;
}

从handleSocketError函数源码可以看到只有当内核的bind系统调用返回的错误码等于EADDRINUSE或者EADDRNOTAVAIL时才会抛出BindException。那么接下来我们就去深入内核源码,找到bind系统调用返回这两个错误码的逻辑。

四、Kernel(6.3.9版本)源码分析

4.1、bind系统调用定义

// 定义了一个3个参数的系统调用 -- bind系统调用
SYSCALL_DEFINE3(bind, int, fd, struct sockaddr __user *, umyaddr, int, addrlen)
{
    return __sys_bind(fd, umyaddr, addrlen);
}

// bind系统调用的代码逻辑入口
int __sys_bind(int fd, struct sockaddr __user *umyaddr, int addrlen)
{
    struct socket *sock;
    struct sockaddr_storage address;
    int err, fput_needed;

    // 1. 通过文件描述符fd,找到对应的socket对象
    sock = sockfd_lookup_light(fd, &err, &fput_needed);
    if (sock) {
        // 2. 内存copy,将传入的address信息从用户态内存copy到内核态
        err = move_addr_to_kernel(umyaddr, addrlen, &address);
        if (!err) {
          
 err = security_socket_bind(sock,
          
                (struct sockaddr *)&address,
          
                addrlen);
          
 if (!err)
          
     // 3. socket可能是网络socket或者unix socket,这里要到具体的实现类中查看内部实现逻辑
          
     err = sock->ops->bind(sock,
          
                   (struct sockaddr *)
          
                   &address, addrlen);
        }
        fput_light(sock->file, fput_needed);
   
}
    return err;
}

通过文件描述符fd找到对应的socket对象。
将传入的address由用户态内存copy到内核态内存。
调用sock->ops->bind函数(inet_bind)。不同的协议有不同的bind实现。

4.2、inet_bind函数

int inet_bind(struct socket sock, struct sockaddr uaddr, int addr_len)
{
        struct sock *sk = sock->sk;
        u32 flags = BIND_WITH_LOCK;
        int err;

        / If the socket has its own bind function then use it. (RAW) /
        if (sk->sk_prot->bind) {
          
     return sk->sk_prot->bind(sk, uaddr, addr_len);
        }
        if (addr_len < sizeof(struct sockaddr_in))
          
     return -EINVAL;

        /* BPF prog is run before any checks are done so that if the prog
         * changes context in a wrong way it will be caught.
         */
        err = BPF_CGROUP_RUN_PROG_INET_BIND_LOCK(sk, uaddr,
          
                     
                CGROUP_INET4_BIND, &flags);
        if (err)
          
     return err;
        // 核心逻辑在__inet_bind函数中
        return __inet_bind(sk, uaddr, addr_len, flags);
}
EXPORT_SYMBOL(inet_bind);

int __inet_bind(struct sock sk, struct sockaddr uaddr, int addr_len,
          
     u32 flags)
{
        struct sockaddr_in addr = (struct sockaddr_in )uaddr;
        struct inet_sock *inet = inet_sk(sk);
        struct net *net = sock_net(sk);
        unsigned short snum;
        int chk_addr_ret;
        u32 tb_id = RT_TABLE_LOCAL;
        int err;

        if (addr->sin_family != AF_INET) {
          
     /* Compatibility games : accept AF_UNSPEC
(mapped to AF_INET)
          
      * only if s_addr is INADDR_ANY.
          
      */
          
     err = -EAFNOSUPPORT;
          
     if (addr->sin_family != AF_UNSPEC ||
          
         addr->sin_addr.s_addr != htonl(INADDR_ANY))
          
             goto out;
        }

        tb_id = l3mdev_fib_table_by_index(net, sk->sk_bound_dev_if) ? : tb_id;
        chk_addr_ret = inet_addr_type_table(net, addr->sin_addr.s_addr, tb_id);

        /* Not specified by any standard per-se, however it breaks too
         * many applications when removed. 
It is unfortunate since
         * allowing applications to make a non-local bind solves
         * several problems with systems using dynamic addressing.
         * (ie. your servers still start up even if your ISDN link
         *  is temporarily down)
         */
        // EADDRNOTAVAIL错误码,当内核返回这个错误码时JDK会抛出BindException。当inet_addr_valid_or_nonlocal函数返回false时则会直接返回这个错误码
        err = -EADDRNOTAVAIL;
        if (!inet_addr_valid_or_nonlocal(net, inet, addr->sin_addr.s_addr,
          
                     
        chk_addr_ret))
          
     goto out;
        // socket的源端口(8080)
        snum = ntohs(addr->sin_port);
        err = -EACCES;
        if (!(flags & BIND_NO_CAP_NET_BIND_SERVICE) &&
          
 snum && inet_port_requires_bind_service(net, snum) &&
          
 !ns_capable(net->user_ns, CAP_NET_BIND_SERVICE))
          
     goto out;

        /*      We keep a pair of
addresses. rcv_saddr is the one
         *      used by hash
lookups, and saddr is used for transmit.
         *
         *      In the BSD API
these are the same except where it
         *      would be illegal to
use them (multicast/broadcast) in
         *      which case the sending
device address is used.
         */
        if (flags & BIND_WITH_LOCK)
          
     lock_sock(sk);

        / Check these errors (active socket, double bind). /
        err = -EINVAL;
        if (sk->sk_state != TCP_CLOSE || inet->inet_num)
          
     goto out_release_sock;

        inet->inet_rcv_saddr = inet->inet_saddr = addr->sin_addr.s_addr;
        if (chk_addr_ret == RTN_MULTICAST || chk_addr_ret == RTN_BROADCAST)
          
     inet->inet_saddr = 0;  / Use device /

        // 服务端的源端口是主动设置上去的不是随机的所以这里源端口存在,所以会进入这个if
        / Make sure we are allowed to bind here. /
        if (snum || !(inet->bind_address_no_port ||
          
           (flags & BIND_FORCE_ADDRESS_NO_PORT))) {
          
     // 这里会校验源端口是否冲突,需要看这个方法内部会不会返回那两个特殊的错误码
          
     err = sk->sk_prot->get_port(sk, snum);
          
     if (err) {
          
             inet->inet_saddr = inet->inet_rcv_saddr = 0;
          
             goto out_release_sock;
          
     }
          
     if (!(flags & BIND_FROM_BPF)) {
          
             err = BPF_CGROUP_RUN_PROG_INET4_POST_BIND(sk);
          
             if (err) {
          
                     inet->inet_saddr = inet->inet_rcv_saddr = 0;
          
                     if (sk->sk_prot->put_port)
          
                     
       sk->sk_prot->put_port(sk);
          
                     goto out_release_sock;
          
            
}
          
     }
        }

        if (inet->inet_rcv_saddr)
          
     sk->sk_userlocks |= SOCK_BINDADDR_LOCK;
        if (snum)
          
     sk->sk_userlocks |= SOCK_BINDPORT_LOCK;
        inet->inet_sport = htons(inet->inet_num);
        inet->inet_daddr = 0;
        inet->inet_dport = 0;
        sk_dst_reset(sk);
        err = 0;
out_release_sock:
        if (flags & BIND_WITH_LOCK)
          
     release_sock(sk);
out:
        return err;
}

在__inet_bind函数中我们遇到了EADDRNOTAVAIL,当inet_addr_valid_or_nonlocal函数返回false时就会返回这个错误码。下面来看下这个函数的源码,看下在这个场景中他是否会返回false。除此之外,sk->sk_prot->get_port函数用于校验端口号是否可用,该函数也可能会返回EADDRINUSE、EADDRNOTAVAIL错误码,需要重点排查。

4.3、inet_addr_valid_or_nonlocal函数源码

static inline bool inet_addr_valid_or_nonlocal(struct
net *net,
                                              
struct inet_sock *inet,
                                              
__be32 addr,
                                              
int addr_type)
{
        return inet_can_nonlocal_bind(net,
inet) ||
                // 我们是服务端的socket,addr默认等于 INADDR_ANY,这条判断只会为true
                addr == htonl(INADDR_ANY)
||
                addr_type == RTN_LOCAL ||
                addr_type ==
RTN_MULTICAST ||
                addr_type ==
RTN_BROADCAST;
}

该函数是由5个条件的或运算表达式组成的,在我们的场景中addr ==
htonl(INADDR_ANY)必定为true,所以该函数在我们的场景中只会返回true不会返回false。如果后面没有其他地方返回EADDRNOTAVAIL错误码,那么可以判断BindException不是因为内核返回了EADDRNOTAVAIL错误码导致的。 接下来继续看sk->sk_prot->get_port函数(inet_csk_get_port)的逻辑。

4.4、inet_csk_get_port函数

int
inet_csk_get_port(struct sock *sk, unsigned short snum)
{
    struct inet_hashinfo *hinfo =
tcp_or_dccp_get_hashinfo(sk);
    // 由于我们之前设置过SO_REUSEADDR参数,并且此时还没有调用listen系统调用socket状态不等于TCP_LISTEN,所以reuse为true
    bool reuse = sk->sk_reuse
&& sk->sk_state != TCP_LISTEN;
    bool found_port = false,
check_bind_conflict = true;
    bool bhash_created = false,
bhash2_created = false;
    // 导致BindException的错误码出现了!
    int ret = -EADDRINUSE, port = snum,
l3mdev;
    struct inet_bind_hashbucket *head,
*head2;
    struct inet_bind2_bucket *tb2 = NULL;
    struct inet_bind_bucket *tb = NULL;
    bool head2_lock_acquired = false;
    struct net *net = sock_net(sk);

    l3mdev = inet_sk_bound_l3mdev(sk);
    // 如果没有指定端口,会进入这里随机端口。服务端socket不会进入这个if
    if (!port) {
        head =
inet_csk_find_open_port(sk, &tb, &tb2, &head2, &port);
        if (!head)
            return ret;

        head2_lock_acquired = true;

        if (tb && tb2)
            goto success;
        found_port = true;
    } else {
        head =
&hinfo->bhash[inet_bhashfn(net, port,
                         
hinfo->bhash_size)];
        spin_lock_bh(&head->lock);
        inet_bind_bucket_for_each(tb,
&head->chain)
            if
(inet_bind_bucket_match(tb, net, port, l3mdev))
                break;
    }

    if (!tb) {
        tb =
inet_bind_bucket_create(hinfo->bind_bucket_cachep, net,
                         head, port,
l3mdev);
        if (!tb)
            // ①当inet_bind_bucket_create函数返回为空时会返回EADDRINUSE错误码
            goto fail_unlock;
        bhash_created = true;
    }

    if (!found_port) {
        if
(!hlist_empty(&tb->owners)) {
            if (sk->sk_reuse ==
SK_FORCE_REUSE ||
                (tb->fastreuse > 0
&& reuse) ||
                sk_reuseport_match(tb,
sk))
                check_bind_conflict = false;
        }
        // ② 当inet_use_bhash2_on_bind函数和inet_bhash2_addr_any_conflict函数都返回true会返回EADDRINUSE错误码
        if (check_bind_conflict
&& inet_use_bhash2_on_bind(sk)) {
            if
(inet_bhash2_addr_any_conflict(sk, port, l3mdev, true, true))
                goto fail_unlock;
        }

        head2 =
inet_bhashfn_portaddr(hinfo, sk, net, port);
        spin_lock(&head2->lock);
        head2_lock_acquired = true;
        tb2 =
inet_bind2_bucket_find(head2, net, port, l3mdev, sk);
    }

    if (!tb2) {
        tb2 =
inet_bind2_bucket_create(hinfo->bind2_bucket_cachep,
                           net, head2,
port, l3mdev, sk);
        if (!tb2)
            // 与①相同,当inet_bind2_bucket_create返回空时返回EADDRINUSE错误码
            goto fail_unlock;
        bhash2_created = true;
    }

    if (!found_port &&
check_bind_conflict) {
        // ③当inet_csk_bind_conflict返回true时,返回EADDRINUSE错误码
        if (inet_csk_bind_conflict(sk,
tb, tb2, true, true))
            goto fail_unlock;
    }

success:
    inet_csk_update_fastreuse(tb, sk);

    if (!inet_csk(sk)->icsk_bind_hash)
        inet_bind_hash(sk, tb, tb2,
port);
   
WARN_ON(inet_csk(sk)->icsk_bind_hash != tb);
   
WARN_ON(inet_csk(sk)->icsk_bind2_hash != tb2);
    ret = 0;

fail_unlock:
    if (ret) {
        if (bhash_created)
           
inet_bind_bucket_destroy(hinfo->bind_bucket_cachep, tb);
        if (bhash2_created)
           
inet_bind2_bucket_destroy(hinfo->bind2_bucket_cachep,
                          tb2);
    }
    if (head2_lock_acquired)
        spin_unlock(&head2->lock);
    spin_unlock_bh(&head->lock);
    return ret;
}

在inet_csk_get_port函数源码中有3处可能会返回EADDRINUSE错误码的逻辑,下面会逐个分析。

当inet_bind_bucket_create函数返回为空时会返回EADDRINUSE错误码

inet_bind_bucket_create函数源码

struct
inet_bind_bucket inet_bind_bucket_create(struct kmem_cache cachep,
                         struct net *net,
                         struct
inet_bind_hashbucket *head,
                         const unsigned
short snum,
                         int l3mdev)
{
    // 分配内存
    struct inet_bind_bucket *tb =
kmem_cache_alloc(cachep, GFP_ATOMIC);
    // 如果内存分配成功则初始化,否则直接返回空
    if (tb) {
        write_pnet(&tb->ib_net,
net);
        tb->l3mdev    = l3mdev;
        tb->port      =
snum;
        tb->fastreuse = 0;
        tb->fastreuseport = 0;
       
INIT_HLIST_HEAD(&tb->owners);
        hlist_add_head(&tb->node,
&head->chain);
    }
    return tb;
}

从inet_bind_bucket_create函数源码中可以看出,只有当内存分配失败时才可能返回空,这种情况还是比较少见的基本可以排除这个原因。

当inet_use_bhash2_on_bind函数和inet_bhash2_addr_any_conflict函数都返回true会返回EADDRINUSE错误码

inet_use_bhash2_on_bind函数源码

static bool
inet_use_bhash2_on_bind(const struct sock *sk)
{

if IS_ENABLED(CONFIG_IPV6)

    if (sk->sk_family == AF_INET6) {
        int addr_type =
ipv6_addr_type(&sk->sk_v6_rcv_saddr);

        return addr_type != IPV6_ADDR_ANY
&&
            addr_type !=
IPV6_ADDR_MAPPED;
    }

endif

    // 服务端socket的源地址默认等于INADDR_ANY,所以这里只会返回false
    return sk->sk_rcv_saddr !=
htonl(INADDR_ANY);
}

inet_use_bhash2_on_bind函数在服务端socket的场景下只会返回false,所以排除这个可能性。

当inet_csk_bind_conflict返回true时,返回EADDRINUSE错误码

只剩这最后一种可能性了,从网上的资料可知inet_csk_bind_conflict函数是用来判断端口是否冲突的,当我们设置了SO_REUSEADDR参数之后,该函数还是返回端口冲突,那么就只能是在Tomcat启动之前有其他的Socket占用了8080端口导致的。 由于问题并不是必现的,所以一定不是其他的服务端socket也占用了8080端口导致的,只能是因为某些组件客户端在Tomcat启动之前进行初始化,发起了网络请求随机到了8080端口导致的(比如配置中心、MQ等都会在初始化时连接服务端)。在通过sysctl看了下系统配置,net.ipv4.ip_local_port_range的值被设置为了1024~65535,看到这里基本就可以确定是由于随机端口的范围设置不合理导致的,调整为10000~65535后问题得到解决。

五、总结

BindException是一个比较常见的异常,可能大多数程序员都遇到过,在遇到该问题时使用万能的重启大法基本都可以解决,所以也很少有人去排查定位该问题的根因。本文基于Tomcat、OpenJDK、Linux内核等源码由浅入深的分析了该异常可能出现的原因,希望对大家有所帮助。

作者简介

LGW 信也科技基础架构研发专家,主要负责分布式对象存储的研发工作。

 

 


信也科技布道师
12 声望10 粉丝