TIME_WAIT引起Cannot assign requested address报错

1.  问题描述

     有时候用redis客户端(php或者java客户端)连接Redis服务器,报错:“Cannot assign requested address。”

     原因是客户端频繁的连接服务器,由于每次连接都在很短时间内结束,导致很多的TIME_WAIT。所以新的连接没办法绑定端口,即“Cannot assign requested address”。

     我们可以通过netstat -nat | grep 127.0.0.1:6380 查看连接127.0.0.1:6380的状态。你会发现很多TIME_WAIT。

     很多人想到要用修改内核参数来解决:

     执行命令修改如下2个内核参数  
     sysctl -w net.ipv4.tcp_timestamps=1  开启对于TCP时间戳的支持,若该项设置为0,则下面一项设置不起作用
     sysctl -w net.ipv4.tcp_tw_recycle=1  表示开启TCP连接中TIME-WAIT sockets的快速回收

     其实不然,根本没有理解出现这个问题的本质原因。首先我们了解Redis处理客户端连接的机制和TCP的TIME_WAIT.

2.  Redis处理客户端连接机制

(参考:http://redis.io/topics/clients)

1、建立连接(TCP连接):

      Redis 通过监听一个 TCP 端口或者 Unix socket 的方式来接收来自客户端的连接,当一个连接建立后,Redis 内部会进行以下一些操作:

首先,客户端 socket 会被设置为非阻塞模式,因为 Redis 在网络事件处理上采用的是非阻塞多路复用模型。
     然后为这个socket 设置 TCP_NODELAY 属性,禁用 Nagle 算法
     然后创建一个 readable 的文件事件用于监听这个客户端 socket 的数据发送

     当客户端连接被初始化后,Redis 会查看目前的连接数,然后对比配置好的 maxclients 值,如果目前连接数已经达到最大连接数 maxclients 了,那么说明这个连接不能再接收,Redis 会直接返回客户端一个连接错误,并马上关闭掉这个连接。

2、服务器处理顺序

     如果有多个客户端连接上 Redis,并且都向 Redis 发送命令,那么 Redis 服务端会先处理哪个客户端的请求呢?答案其实并不确定,主要与两个因素有关,一是客户端对应的 socket 对应的数字的大小,二是 kernal 报告各个客户端事件的先后顺序。

Redis 处理一个客户端传来数据的步骤如下:

  它对触发事件的 socket 调用一次 read(),只读一次(而不是把这个 socket 上的消息读完为止),是为了防止由于某个别客户端持续发送太多命令,导致其它客户端的请求长时间得不到处理的情况。
当然,当这一次 read() 调用完成后,它里面无论包含多少个命令,都会被一次性顺序地执行。这样就保证了对各个客户端命令的公平对待。
3、关于最大连接数 maxclients

       在 Redis2.4 中,最大连接数是被直接硬编码在代码里面的,而在2.6版本中这个值变成可配置的。maxclients 的默认值是 10000,你也可以在 redis.conf 中对这个值进行修改。

      当然,这个值只是 Redis 一厢情愿的值,Redis 还会照顾到系统本身对进程使用的文件描述符数量的限制。在启动时 Redis 会检查系统的 soft limit,以查看打开文件描述符的个数上限。如果系统设置的数字,小于咱们希望的最大连接数加32,那么这个 maxclients 的设置将不起作用,Redis 会按系统要求的来设置这个值。(加32是因为 Redis 内部会使用最多32个文件描述符,所以连接能使用的相当于所有能用的描述符号减32)。

       当上面说的这种情况发生时(maxclients 设置后不起作用的情况),Redis 的启动过程中将会有相应的日志记录。比如下面命令希望设置最大客户端数量为100000,所以 Redis 需要 100000+32 个文件描述符,而系统的最大文件描述符号设置为10144,所以 Redis 只能将 maxclients 设置为 10144 – 32 = 10112。

$ ./redis-server –maxclients 100000
[41422] 23 Jan 11:28:33.179 # Unable to set the max number of files limit to 100032 (Invalid argument), setting the max clients configuration to 10112.

        所以说当你想设置 maxclients 值时,最好顺便修改一下你的系统设置,当然,养成看日志的好习惯也能发现这个问题。

具体的设置方法就看你个人的需求了,你可以只修改此次会话的限制,也可以直接通过sysctl 修改系统的默认设置。如:

ulimit -Sn 100000 # This will only work if hard limit is big enough.
sysctl -w fs.file-max=100000

4、输出缓冲区大小限制

       对于 Redis 的输出(也就是命令的返回值)来说,其大小经常是不可控的,可能是一个简单的命令,能够产生体积庞大的返回数据。另外也有可能因为执行命令太多,产生的返回数据的速率超过了往客户端发送的速率,这时也会产生消息堆积,从而造成输出缓冲区越来越大,占用过多内存,甚至导致系统崩溃。

      所以 Redis 设置了一些保护机制来避免这种情况的出现,这些机制作用于不同种类的客户端,有不同的输出缓冲区大小限制,限制方式有两种:

一种是大小限制,当某一个客户端的缓冲区超过某一大小时,直接关闭掉这个客户端连接
     另一种是当某一个客户端的缓冲区持续一段时间占用空间过大时,也直接关闭掉客户端连接

对于不同客户端的策略如下:

 对普通客户端来说,限制为0,也就是不限制,因为普通客户端通常采用阻塞式的消息应答模式,如:发送请求,等待返回,再发请求,再等待返回。这种模式通常不会导致输出缓冲区的堆积膨胀。
       对于 Pub/Sub 客户端来说,大小限制是32m,当输出缓冲区超过32m时,会关闭连接。持续性限制是,当客户端缓冲区大小持续60秒超过8m,也会导致连接关闭。
       而对于 Slave 客户端来说,大小限制是256m,持续性限制是当客户端缓冲区大小持续60秒超过64m时,关闭连接。

上面三种规则都是可配置的。可以通过 CONFIG SET 命令或者修改 redis.conf 文件来配置。

5、输入缓冲区大小限制

      Redis 对输入缓冲区大小的限制比较暴力,当客户端传输的请求大小超过1G时,服务端会直接关闭连接。这种方式可以有效防止一些客户端或服务端 bug 导致的输入缓冲区过大的问题。

6、Client超时

      对当前的 Redis 版本来说,服务端默认是不会关闭长期空闲的客户端的。但是你可以修改默认配置来设置你希望的超时时间。比如客户端超过多长时间无交互,就直接关闭。同理,这也可以通过 CONFIG SET 命令或者修改 redis.conf 文件来配置。

      值得注意的是,超时时间的设置,只对普通客户端起作用,对 Pub/Sub 客户端来说,长期空闲状态是正常的。

      另外,实际的超时时间可能不会像设定的那样精确,这是因为 Redis 并不会采用计时器或者轮训遍历的方法来检测客户端超时,而是通过一种渐近式的方式来完成,每次检查一部分。所以导致的结果就是,可能你设置的超时时间是10s,但是真实执行的时间是超时12s后客户端才被关闭。

3.  TCP的TIME_WAIT状态

    主动关闭的Socket端会进入TIME_WAIT状态,并且持续2MSL时间长度,MSL就是maximum segment lifetime(最大分节生命期),在windows下默认240秒,MSL是一个IP数据包能在互联网上生存的最长时间,超过这个时间将在网络中消失。MSL在RFC 1122上建议是2分钟,而源自berkeley的TCP实现传统上使用30秒,因而,TIME_WAIT状态一般维持在1-4分钟。

 

TIME_WAIT状态存在的理由:

1)可靠地实现TCP全双工连接的终止:(即在TIME_WAIT下等待2MSL,只是为了尽最大努力保证四次握手正常关闭)。

      TCP协议规定,对于已经建立的连接,网络双方要进行四次握手才能成功断开连接,如果缺少了其中某个步骤,将会使连接处于假死状态,连接本身占用的资源不会被释放。

    在进行关闭连接四路握手协议时,最后的ACK是由主动关闭端发出的,如果这个最终的ACK丢失,服务器将重发最终的FIN,因此客户端必须维护状态信息允许它重发最终的ACK。如果不维持这个状态信息,那么客户端将响应RST分节,因而,要实现TCP全双工连接的正常终止,必须处理终止序列四个分节中任何一个分节的丢失情况,主动关闭的客户端必须维持状态信息进入TIME_WAIT状态。

    我们看客户端主动关闭服务器被动关闭四次握手的流程:

1、 客户端发送FIN报文段,进入FIN_WAIT_1状态。

2、 服务器端收到FIN报文段,发送ACK表示确认,进入CLOSE_WAIT状态。

3、 客户端收到FIN的确认报文段,进入FIN_WAIT_2状态。

4、 服务器端发送FIN报文端,进入LAST_ACK状态。

5、 客户端收到FIN报文端,发送FIN的ACK,同时进入TIME_WAIT状态,启动TIME_WAIT定时器,超时时间设为2MSL。

6、 服务器端收到FIN的ACK,进入CLOSED状态。

7、 客户端在2MSL时间内没收到对端的任何响应,TIME_WAIT超时,进入CLOSED状态。

      如果不考虑报文延迟、丢失,确认延迟、丢失等情况,TIME_WAIT的确没有存在的必要。当网络在不理想的情况下通常会有报文的丢失延迟发生,让我们看下面的一个特例:

     客户端进入发送收到四次握手关闭的最后一个ACK后,进入TIME_WAIT同时发送ACK,如果其不停留2MSL时间,而是马上关闭连接,销毁连接上的资源,当发送如下情况时,将不能正常的完成四次握手关闭:

客户端发送的ACK在网路上丢失,这样服务器端收不到最后的ACK,重传定时器超时,将重传FIN到客户端,由于客户端关于该连接的所有资源都释放,收到重传的FIN后,它没有关于这个FIN的任何信息,所以向服务器端发送一个RST报文端,服务器端收到RST后,认为搞连接出现了异常(而非正常关闭)。

所以,在TIME_WAIT状态下等待2MSL时间端,是为了能够正确处理第一个ACK(最长生存时间为MSL)丢失的情况下,能够收到对端重传的FIN(最长生存时间为MSL),然后重传ACK。

     是否只要主动关闭方在TIME_WAIT状态下停留2MSL,四次握手关闭就一定正常完成呢?

     答案是否定的?可以考虑如下的情况, 

     TIME_WAIT状态下发送的ACK丢失,LAST_ACK时刻设定的重传定时器超时,发送重传的FIN,很不幸,这个FIN也丢失,主动关闭方在TIME_WAIT状态等待2MSL没收到任何报文段,进入CLOSED状态,当此时被动关闭方并没有收到最后的ACK。所以即使要主动关闭方在TIME_WAIT状态下停留2MSL,也不一定表示四次握手关闭就一定正常完成。

2)确保老的报文段在网络中消失,不会影响新建立的连接 

        考虑如下的情况,主动关闭方在TIME_WAIT状态下发送的ACK由于网络延迟的原因没有按时到底(但并没有超过MSL的时间),导致被动关闭方重传FIN,在FIN重传后,延迟的ACK到达,被动关闭方进入CLOSED状态,如果主动关闭方在TIME_WAIT状态下发送ACK后马上进入CLOSED状态(也就是没有等待)2MSL时间,则上述的连接已不存在:

       现在考虑下面的情况,假设客户端(192.186.0.1:23) 到服务器192.168.1.1:6380)的TCP连接, 由于连接已关闭,我们可以马上建立一个相同的IP地址和端口之间的TCP连接,并且这个连接也是客户端(192.186.0.1:23) 到服务器192.168.1.1:6380),那么当上一个连接的重传FIN到达主动关闭方时,被新的连接所接受,这将导致新的连接被复位,很显然,这不是我们希望看到的事情。

       新的连接要建立,必须是在主动关闭方和被动关闭方都进入到CLOSED状态之后才有可能。所以,最有可能导致旧的报文段影响新的连接的情况是:

      在TIME_WAIT状态之前,主动关闭方发送的报文端在网络中延迟,但是TIME_WAIT设定为2MSL时,这些报文端必然会在网络中消失(最大生存时间为MSL)。被动关闭方最有可能影响新连接的报文段就是我们上面讨论的情况,对方ACK延迟到达,在此之前重传的FIN,这个报文端发送之后,TIME_WAIT的定时器超时时间肯定大于MSL,在1MSL时间内,这个FIN要么在网络中因为生成时间到达而消失,要么到达主动关闭方被这确的处理,不会影响新建立的连接。

    新的SCTP协议通过在消息头部添加验证标志避免了TIME_WAIT状态。

3)有关内核级别的keepalive和time_wait的优化调整

有关内核级别的keepalive和time_wait的优化调整
vi /etc/sysctl
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_tw_recycle = 1
net.ipv4.tcp_keepalive_time = 1800
net.ipv4.tcp_fin_timeout = 30
net.core.netdev_max_backlog =8096

修改完记的使用sysctl -p 让它生效
以上参数的注解
/proc/sys/net/ipv4/tcp_tw_reuse
该文件表示是否允许重新应用处于TIME-WAIT状态的socket用于新的TCP连接。

/proc/sys/net/ipv4/tcp_tw_recycle
recyse是加速TIME-WAIT sockets回收

对tcp_tw_reuse和tcp_tw_recycle的修改,可能会出现.warning, got duplicate tcp line warning, got BOGUS tcp line.上面这二个参数指的是存在这两个完全一样的TCP连接,这会发生在一个连接被迅速的断开并且重新连接的情况,而且使用的端口和地址相同。但基本 上这样的事情不会发生,无论如何,使能上述设置会增加重现机会。这个提示不会有人和危害,而且也不会降低系统性能,目前正在进行工作

/proc/sys/net/ipv4/tcp_keepalive_time
表示当keepalive起用的时候,TCP发送keepalive消息的频度。缺省是2小时

/proc/sys/net/ipv4/tcp_fin_timeout 最佳值和BSD一样为30
fin_wait1状态是在发起端主动要求关闭tcp连接,并且主动发送fin以后,等待接收端回复ack时候的状态。对于本端断开的socket连接,TCP保持在FIN-WAIT-2状态的时间。对方可能会断开连接或一直不结束连接或不可预料的进程死亡。

/proc/sys/net/core/netdev_max_backlog
该文件指定了,在接口接收数据包的速率比内核处理这些包的速率快时,允许送到队列的数据包的最大数目 

4)time_wait的优化处理

Linux系统中TCP是面向连接的,在实际应用中通常都需要检测连接是否还可用.如果不可用,可分为:

a. 连接的对端正常关闭.

b. 连接的对端非正常关闭,这包括对端设备掉电,程序崩溃,网络被中断等.这种情况是不能也无法通知对端的,所以连接会一直存在,浪费国家的资源.

TCP协议栈有个keepalive的属性,可以主动探测socket是否可用,不过这个属性的默认值很大.

全局设置可更改/etc/sysctl.conf,加上:

net.ipv4.tcp_keepalive_intvl = 20
net.ipv4.tcp_keepalive_probes = 3
net.ipv4.tcp_keepalive_time = 60

在程序中设置如下:

int keepAlive = 1; // 开启keepalive属性
int keepIdle = 60; // 如该连接在60秒内没有任何数据往来,则进行探测
int keepInterval = 5; // 探测时发包的时间间隔为5 秒
int keepCount = 3; // 探测尝试的次数.如果第1次探测包就收到响应了,则后2次的不再发.

setsockopt(rs, SOL_SOCKET, SO_KEEPALIVE, (void *)&keepAlive, sizeof(keepAlive));
setsockopt(rs, SOL_TCP, TCP_KEEPIDLE, (void*)&keepIdle, sizeof(keepIdle));
setsockopt(rs, SOL_TCP, TCP_KEEPINTVL, (void *)&keepInterval, sizeof(keepInterval));
setsockopt(rs, SOL_TCP, TCP_KEEPCNT, (void *)&keepCount, sizeof(keepCount));

4.  解决问题

    我们了解Redis处理客户端连接的机制和TCP的TIME_WAIT.我们可以重现上述问题,我们快速建立2000个连接,

<?php  
$num = 2000;  
for($i=0; $i<$num; $i++) {  
    $redis = new Redis();  
    $redis->connect('127.0.0.1',6379);  
    //sleep(1);  
}  
sleep(10);

然后查看状态:netstat -nat | grep 127.0.0.1:6379你会发现很多TIME_WAIT。

如果$num加大到40000或者,报错:Cannot assign requested address。

    因此如果客户端(php)连接redis出现这个问题,说明你程序出现bug了。你某个循环里面实例化Redis了(即每次都new Redis),造成每一次循环都建立一个连接。

   解决这个问题不是修改内核参数,而是把连接redis封装成单实例,确保在同一进程内,连接redis是唯一实例。

class Class_Redis {  
  
    private $_redis;  
    private static $_instance = null;  
      
    private  function __construct() {  
        $this->_redis = new Redis();  
        $this->_redis->connect('127.0.0.1',6379);  
  
    }  
      
    public static function getInstance() {  
        if(self::$_instance === null) {  
            self::$_instance = new self();  
        }  
        return self::$_instance;  
      
    }  
  
      
    public  function getRedis() {  
        return $this->_redis;  
    }  
  
}

转自:http://blog.csdn.net/hguisu/article/details/10241519

原创文章,作者:s19930811,如若转载,请注明出处:http://www.178linux.com/2630

(0)
s19930811s19930811
上一篇 2016-03-09
下一篇 2016-03-09

相关推荐

  • 走进linux的世界

    1、描述计算机的组成及其功能。  所谓的计算机就是一种计算器,而计算器其实是:接受用户输入指令与数据,经由中央处理器的数学与逻辑单元运算处理后,以产生或储存成有用的信息。 计算机系统由硬件系统和软件系统两个部分组成。 硬件系统由运算器、控制器、存储器、输入设备、输出设备组成。 运算器 是计算机中进行算术运算和逻辑运算的主要部件是计算机的主体。 控…

    Linux干货 2017-01-14
  • Linux中实现java和c语言打印Hello world小程序

    1、C语言实现 先确认linux系统中是否gcc编译器,可以通过rpm -q gcc查询系统已经安装gcc软件包,如果没有安装可以到系统自带光盘里面安装相应的安装包软件。 使用rpm -ivh gcc-4.8.5-4.el7.x86_64.rpm安装编译器,安装后再查询确认已经成功 环境准备好了,我们就可以编写C语言的代码了,我们可以使用vim编辑器编写第一…

    Linux干货 2016-08-26
  • 第5周作业

    1、显示当前系统上root、fedora或user1用户的默认shell     ~]# egrep "^(root|fedora|user1)" /etc/passwd 2、找出/etc/rc.d/init.d/functions文件中某单词后面跟一组小括号的行,如:hello();  …

    Linux干货 2017-01-07
  • linux密码的破解与保护

    找回root口令:   遇到密码丢失的情况,如果只是普通用户的密码丢失,那么可以让管理员重新设定密码。但是如果管理员用户的密码丢失,那该如何解决?   在 Linux环境中root 密码忘记还是可以救回来的!只要能够进入并且挂载 / , 然后重新设定一下root的密码,就救回来啦!这是因为开机流程中,若强制进入runlevel 1 时, …

    Linux干货 2016-09-22
  • 第二天作业

    1、echo 查闪烁,下划,颜色    echo -e "\033[31;5;1m*******\033[0m\n \033[32;5;1m*****\033[0m\n  \033[33;5;1m***\033[0m\n   \033[34;5;1m*\033[0m" 2、scr…

    Linux干货 2016-07-26
  • wk_02 作业

    Linux 文件管理命令 cp命令 功能 Linux 系统中cp命令是用来复制目录/文件的。 概要 单源复制 cp [OPTION]… [-T] SOURCE DEST DEST不存在则事先创建此文件,并复制源文件的数据流至DEST中; DEST存在 DEST是非目录文件:则覆盖目标文件; DEST是目录文件:则先…

    Linux干货 2016-12-11