zoukankan      html  css  js  c++  java
  • Socket connect error 99(Cannot assign requested address)

    转载请注明转自: 存储系统研究, 本文固定链接:socket connect error 99(Cannot assign request address) 


    这是近期使用libcurl写http服务的压力測试的时候遇到的一个问题。其直接表象是client在发送http请求时失败,终于原因是client的TIME_WAIT状态的socket进程过多。导致port被占满。以下看整个分析过程:

    (1) 首先看产生错误的源代码:

        /* get it! */
        res = curl_easy_perform(curl_handle);
    
    
        long http_code = 0;
        curl_easy_getinfo(curl_handle, CURLINFO_RESPONSE_CODE, &http_code);
    
    
        /* cleanup curl stuff */
        curl_easy_cleanup(curl_handle);
        if (res != CURLE_OK || http_code != 200) {
            cout << uri << ", res = " << res << ", http_code = " << http_code << endl;
        }
        return (res == CURLE_OK && http_code == 200);
    
    错误日志例如以下:
    http://10.237.92.30:8746/thumbnail/jpeg/l820/AppStore/b262b95f-95b8-4e0e-b4e0-edc3b76e3c81, 
    res = 7, http_code = 0
    http://10.237.92.30:8746/thumbnail/jpeg/l820/AppStore/a4c37951-d8b5-40ff-af27-4efcd1a58e71, 
    res = 7, http_code = 0
    http://10.237.92.30:8746/thumbnail/jpeg/l820/AppStore/abab08ff-75e1-40da-a113-053789e93686, 
    res = 7, http_code = 0
    
    查看curllib的错误代码。例如以下,错误代码为CURLE_COULDNT_CONNECT
      CURLE_OK = 0,
      CURLE_UNSUPPORTED_PROTOCOL,    /* 1 */
      CURLE_FAILED_INIT,             /* 2 */
      CURLE_URL_MALFORMAT,           /* 3 */
      CURLE_NOT_BUILT_IN,            /* 4 - [was obsoleted in August 2007 for
                                        7.17.0, reused in April 2011 for 7.21.5] */
      CURLE_COULDNT_RESOLVE_PROXY,   /* 5 */
      CURLE_COULDNT_RESOLVE_HOST,    /* 6 */
      CURLE_COULDNT_CONNECT,         /* 7 */
      CURLE_FTP_WEIRD_SERVER_REPLY,  /* 8 */
      CURLE_REMOTE_ACCESS_DENIED,    /* 9 a service was denied by the server
    
     

    (2) 分析curl_easy_perform返回错误的原因

    最直接的办法採用gdb跟踪client的执行情况,发现client在connect的时候返回错误,在源文件curl-7.28.1/lib/connect.c的singleipconnect函数中,于是增加日志在connect之后打印errno。代码例如以下:
     if(!isconnected && (conn->socktype == SOCK_STREAM)) {
        rc = connect(sockfd, &addr.sa_addr, addr.addrlen);
        if(-1 == rc) {
          error = SOCKERRNO;
          printf("connect failed with errno = %d", errno);
        }
        conn->connecttime = Curl_tvnow();
        if(conn->num_addr > 1)
          Curl_expire(data, conn->timeoutms_per_addr);
    再次执行測试程序,得到例如以下输出:
    connect failed with errno = 99 http://127.0.0.1:8902/thumbnail/jpeg/l820/AppStore/f8913ca1-
    ae5f-4fcc-abc5-cbe9ada1a67d, ret_code: 0, res: 7
    connect failed with errno = 99 http://127.0.0.1:8902/thumbnail/jpeg/l820/AppStore/3726a1e2-
    057e-402d-b347-61c5a5136cd9, ret_code: 0, res: 7
    connect failed with errno = 99 http://127.0.0.1:8902/thumbnail/jpeg/l820/AppStore/c19bad67-
    6b7d-4dc6-a17a-f74ea525c32a, ret_code: 0, res: 7
    connect failed with errno = 99 http://127.0.0.1:8902/thumbnail/jpeg/l820/AppStore/5d778568-
    d873-46a7-9651-ad8ac3810bf4, ret_code: 0, res: 7
    
     能够看到errno = 99,在内核的include/asm-generic/errno.h文件里能够查看errno = 99的解释为” Cannot assign requested address”。
    #define EAFNOSUPPORT    97  /* Address family not supported by protocol */
    #define EADDRINUSE  98  /* Address already in use */
    #define EADDRNOTAVAIL   99  /* Cannot assign requested address */
    #define ENETDOWN    100 /* Network is down */ 

    (3) errno = 99的原因;

    至于connect系统调用为什么返回失败,就仅仅能看系统调用的实现了。
    a) connect系统调用
    connect系统调用在net/socket.c中实现,Sys_connect系统调用的调用栈例如以下:
    Sys_connect--->
        sock->ops->connect                   // inet_stream_connect
            sk->sk_prot->connect               // tcp_v4_connect
    
    tcp_v4_connect的作用主要是完毕TCP连接三次握手中的第一个握手,即向服务端发送SYNC = 1和一个32位的序号的连接请求包。要发送SYNC请求包,依照TCP/IP协议,就必须有源IP地址和port。源IP地址的选择和路由相关,须要查询路由表。在ip_route_connect中实现,源port的选择在__inet_hash_connect中实现,并且假设找不到一个可用的port,这个函数会返回-EADDRNOTAVAIL,因此基本上能够确定是这个函数返回错误导致connect失败。
    b) __inet_hash_connect

    这个函数的主要作用是选择一个可用的port。其基本的实现过程例如以下:

    i. 调用inet_get_local_port_range(&low, &high);获取可用的端口链表;

    1. 调用read_seqbegin(&sysctl_local_ports.lock);得到顺序锁。
    2. 得到可用端口的low和high:

                    *low = sysctl_local_ports.range[0];

                    *high = sysctl_local_ports.range[1];

    ii. 对于每个port。进行以下的步骤:

    1. 在inet_hashinfo *hinfo中查找这个portinet_hashinfo用于保存已经使用的port信息,每个使用的port在这个hash表中有一个entry;
    2. 对port做hash得到链表头(使用链表解决hash冲突)
    3. 遍历链表中的每个entry:

                    a) 推断是否与这个要使用的port同样,假设同样转到步骤b,假设不同样则遍历下一个entry

                    b) 找到这个port,调用check_established(__inet_check_established)推断这个port能否够重用(TIME_WAIT状态下的port而且net.ipv4.tcp_tw_recycle = 1是port能够重用)

    1. 假设在链表中没有找到这个port,表示port没有被使用。调用inet_bind_bucket_create在hash表中插入一个entry;

    iii. 假设到最后都没有找到一个可用的port就返回EADDRNOTAVAIL。

    从这个函数的实现能够看出,主要是因为可用的port被占满了,所以找不到一个可用的port,导致连接失败。执行netstat能够发现确实存在非常多TIME_WAIT状态的socket,这些socket将可用port占满了。

    [root@test miuistorage-dev]# netstat -n | awk '/^tcp/ {++state[$NF]} END {for(key in state) 
    print key,"	",state[key]}'
    TIME_WAIT        26837
    ESTABLISHED      30 

    (4) 解决的方法:

    要解决port被TIME_WAIT状态的socket占满的问题。能够有下面的解决的方法:
    a) 改动可用port范围
    查看当前的port范围:
    root@guojun8-desktop:/linux-2.6.34# sysctl net.ipv4.ip_local_port_range
    net.ipv4.ip_local_port_range = 32768    61000
    
    改动port范围:
    root@guojun8-desktop:linux-2.6.34# sysctl net.ipv4.ip_local_port_range="32768    62000"
    net.ipv4.ip_local_port_range = 32768    62000
    
    这样的办法可能不能解决根本问题。由于假设使用短连接,即使添加可用port还是会被占满的。 
    b) 设置net.ipv4.tcp_tw_recycle = 1
    这个參数表示系统的TIME-WAIT sockets能否够高速回收
    root@guojun8-desktop:linux-2.6.34# sysctl net.ipv4.tcp_tw_recycle=1
    net.ipv4.tcp_tw_recycle = 1 
    c) 设置net.ipv4.tcp_tw_recycle = 1
    这个參数表示能否够重用TIME_WAIT状态的port;
    root@guojun8-desktop:linux-2.6.34# sysctl net.ipv4.tcp_tw_reuse=1
    net.ipv4.tcp_tw_reuse = 1 

    (5) 更深入的探讨:sysctl做了什么

    能够用strace跟踪一下sysctl的系统调用:
    root@guojun8-desktop:linux-2.6.34# strace sysctl net.ipv4.tcp_tw_recycle=1
    execve("/sbin/sysctl", ["sysctl", "net.ipv4.tcp_tw_recycle=1"], [/* 20 vars */]) = 0
    brk(0)                                  = 0x952f000
    …..
    open("/proc/sys/net/ipv4/tcp_tw_recycle", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3
    fstat64(3, {st_mode=S_IFREG|0644, st_size=0, ...}) = 0
    mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb788e000
    write(3, "1
    ", 2)                      = 2
    close(3)                                = 0
    munmap(0xb788e000, 4096)                = 0
    fstat64(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 8), ...}) = 0
    mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb788e000
    write(1, "net.ipv4.tcp_tw_recycle = 1
    ", 28net.ipv4.tcp_tw_recycle = 1
    ) = 28
    exit_group(0)                           = ?
    
    能够看到这个程序打开/proc/sys/net/ipv4/tcp_tw_recycle并向文件里写入1,可是这个设置时如何其作用的呢?在内核中对/proc/sys文件夹下的文件的i_fop做了特殊的处理。在proc_sys_make_inode 中设置:inode->i_fop = &proc_sys_file_operationsproc_sys_file_operations的定义例如以下:
    static const struct file_operations proc_sys_file_operations = {
    .read   = proc_sys_read,
    .write    = proc_sys_write,
    };
    proc_sys_write中会改动相应的文件,而且改动内存中的内容,不同的文件有不同的proc_handler,如tcp_tw_recycle相应的处理函数是proc_dointvec。这个函数会改动以下的变量:
    tcp_death_row.sysctl_tw_recycle
    
    这个变量在内核中表示TIME_WIAT状态的socket能否够被高速回收。 
  • 相关阅读:
    第三周psp
    作业2
    词频统计 SPEC 20170914 1 1 1 1 1
    psp1111
    第一周大作业1
    mongodb安装操作 (2018/11/12)
    中间件 express生成项目(2018/11/10)
    express环境搭建(2018/11/09)
    后端路由(2018/11/07)
    Node模块的基本使用(2018/11/06)
  • 原文地址:https://www.cnblogs.com/wgwyanfs/p/6800259.html
Copyright © 2011-2022 走看看