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.www58058.com/2630