1. 程式人生 > >TIME_WAIT引起Cannot assign requested address報錯

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;
	}

}

5.  瞭解redis效能相關指標

我們通過info命令輸出的資料可分為10個類別,分別是:

  • server
  • clients
  • memory
  • persistence:RDB 和 AOF 的相關資訊
  • stats:一般統計資訊
  • replication:主/從複製資訊
  • cpu:CPU 計算量統計資訊
  • commandstats:Redis 命令統計資訊
  • cluster: Redis 叢集資訊
  • keyspace:據庫相關的統計資訊

info命令可以新增引數來獲取單個分類下的資料。比如輸入info memory命令,會只返回與記憶體相關的資料。

1、記憶體指標

used_memory 欄位資料表示的是:由Redis分配器分配的記憶體總量,以位元組(byte)為單位。 其中used_memory_human上的資料和used_memory是一樣的值,它以M為單位顯示,僅為了方便閱讀。
# Memory
used_memory:1062992
used_memory_human:1.01M
used_memory_rss:6537216
used_memory_peak:1973720
used_memory_peak_human:1.88M
used_memory_lua:33792
mem_fragmentation_ratio:6.15
mem_allocator:jemalloc-3.6.0

used_memory:是Redis使用的記憶體總量,它包含了實際快取佔用的記憶體和Redis自身執行所佔用的記憶體(如元資料、lua)。它是由Redis使用記憶體分配器分配的記憶體,所以這個資料並沒有把記憶體碎片浪費掉的記憶體給統計進去。

其他欄位代表的含義,都以位元組為單位:

  • used_memory_rss:從作業系統上顯示已經分配的記憶體總量(俗稱常駐集大小)這個值和 top 、 ps 等命令的輸出一致。
  • used_memory_peak:Redis 的記憶體消耗峰值(以位元組為單位)
  • mem_fragmentation_ratio: 記憶體碎片率。used_memory_rss 和 used_memory 之間的比率
  • used_memory_lua: Lua指令碼引擎所使用的記憶體大小。
  • mem_allocator: 在編譯時指定的Redis使用的記憶體分配器,可以是libc、jemalloc、tcmalloc。
      在理想情況下, used_memory_rss 的值應該只比 used_memory 稍微高一點兒。       當 rss > used ,且兩者的值相差較大時,表示存在(內部或外部的)記憶體碎片。記憶體碎片的比率可以通過 mem_fragmentation_ratio 的值看出。       當 used > rss 時,表示 Redis 的部分記憶體被作業系統換出到交換空間了,在這種情況下,操作可能會產生明顯的延遲。

       Because Redis does not have control over how its allocations are mapped to memory pages, high used_memory_rss is often the result of a spike in memory usage.

     當 Redis 釋放記憶體時,分配器可能會,也可能不會,將記憶體返還給作業系統。      如果 Redis 釋放了記憶體,卻沒有將記憶體返還給作業系統,那麼 used_memory 的值可能和作業系統顯示的 Redis 記憶體佔用並不一致。 檢視 used_memory_peak 的值可以驗證這種情況是否發生。下面詳細說明:

2、記憶體碎片率

      info資訊中的mem_fragmentation_ratio給出了記憶體碎片率的資料指標,它是由操系統分配的記憶體除以Redis分配的記憶體得出:

   used_memory_rss 和 used_memory 之間的比率

    used_memory和used_memory_rss數字都包含的記憶體分配有:

  • 使用者定義的資料:記憶體被用來儲存key-value值。
  • 內部開銷: 儲存內部Redis資訊用來表示不同的資料型別。

      used_memory_rss的rss是Resident Set Size的縮寫,表示該程序所佔實體記憶體的大小,是作業系統分配給Redis例項的記憶體大小。除了使用者定義的資料和內部開銷以外,used_memory_rss指標還包含了記憶體碎片的開銷,記憶體碎片是由作業系統低效的分配/回收物理記憶體導致的。
    作業系統負責分配實體記憶體給各個應用程序,Redis使用的記憶體與實體記憶體的對映是由作業系統上虛擬記憶體管理分配器完成的。
   舉個例子來說,Redis需要分配連續記憶體塊來儲存1G的資料集,這樣的話更有利,但可能實體記憶體上沒有超過1G的連續記憶體塊,那作業系統就不得不      使用多個不連續的小記憶體塊來分配並存儲這1G資料,也就導致記憶體碎片的產生。
   記憶體分配器另一個複雜的層面是,它經常會預先分配一些記憶體塊給引用,這樣做會使加快應用程式的執行。

       跟蹤記憶體碎片率對理解Redis例項的資源效能是非常重要的。

記憶體碎片率稍大於1是合理的:這個值表示記憶體碎片率比較低,也說明redis沒有發生記憶體交換。

如果記憶體碎片率超過1.5:那就說明Redis消耗了實際需要實體記憶體的150%,其中50%是記憶體碎片率。

若是記憶體碎片率低於1的話:說明Redis記憶體分配超出了實體記憶體,作業系統正在進行記憶體交換。記憶體交換會引起非常明顯的響應延遲。

3、用記憶體碎片率超過1.5

      倘若記憶體碎片率超過了1.5,那可能是作業系統或Redis例項中記憶體管理變差的表現。下面的方法解決記憶體管理變差的問題,並提高Redis效能:

    1. 重啟Redis伺服器:如果記憶體碎片率超過1.5,重啟Redis伺服器可以讓額外產生的記憶體碎片失效並重新作為新記憶體來使用,使作業系統恢復高效的記憶體管理。額外碎片的產生是由於Redis釋放了記憶體塊,但記憶體分配器並沒有返回記憶體給作業系統,這個記憶體分配器是在編譯時指定的,可以是libc、jemalloc或者tcmalloc。 通過比較used_memory_peak, used_memory_rss和used_memory_metrics的資料指標值可以檢查額外記憶體碎片的佔用。從名字上可以看出,used_memory_peak是過去Redis記憶體使用的峰值,而不是當前使用記憶體的值。如果used_memory_peak和used_memory_rss的值大致上相等,而且二者明顯超過了used_memory值,這說明額外的記憶體碎片正在產生。 

      在重啟伺服器之前,需要在Redis-cli工具上輸入shutdown save命令,意思是強制讓Redis資料庫執行儲存操作並關閉Redis服務,這樣做能保證在執行Redis關閉時不丟失任何資料。 在重啟後,Redis會從硬碟上載入持久化的檔案,以確保資料集持續可用。

    2.修改記憶體分配器:
      Redis支援glibc’s malloc、jemalloc11、tcmalloc幾種不同的記憶體分配器,每個分配器在記憶體分配和碎片上都有不同的實現。不建議普通管理員修改Redis預設記憶體分配器,因為這需要完全理解這幾種記憶體分配器的差異,也要重新編譯Redis。這個方法更多的是讓其瞭解Redis記憶體分配器所做的工作,當然也是改善記憶體碎片問題的一種辦法。

3、用記憶體碎片率低於1:記憶體交換的效能問題

           如果一個Redis例項的記憶體使用率超過可用最大記憶體 (used_memory > used_memory_rss作業系統分配可用最大記憶體),那麼作業系統開始進行記憶體與swap空間交換,把記憶體中舊的或不再使用的內容寫入硬碟上(硬碟上的這塊空間叫Swap分割槽),以便騰出新的實體記憶體給新頁或活動頁(page)使用。 
           在硬碟上進行讀寫操作要比在記憶體上進行讀寫操作,時間上慢了近5個數量級,
記憶體是0.1μs單位、而硬碟是10ms。如果Redis程序上發生記憶體交換,那麼Redis和依賴Redis上資料的應用會受到嚴重的效能影響。

       若是在使用Redis期間沒有開啟rdb快照或aof持久化策略,那麼快取資料在Redis崩潰時就有丟失的危險。因為當Redis記憶體使用率超過可用記憶體的95%時,部分資料開始在記憶體與swap空間來回交換,這時就可能有丟失資料的危險。
          當開啟並觸發快照功能時,Redis會fork一個子程序把當前記憶體中的資料完全複製一份寫入到硬碟上。因此若是當前使用記憶體超過可用記憶體的45%時觸發快照功能,那麼此時進行的記憶體交換會變的非常危險(可能會丟失資料)。 倘若在這個時候例項上有大量頻繁的更新操作,問題會變得更加嚴重。

通過減少Redis的記憶體佔用率,來避免這樣的問題,或者使用下面的技巧來避免記憶體交換髮生:

  1. 假如快取資料小於4GB,就使用32位的Redis例項。因為32位例項上的指標大小隻有64位的一半,它的記憶體空間佔用空間會更少些。 這有一個壞處就是,假設實體記憶體超過4GB,那麼32位例項能使用的記憶體仍然會被限制在4GB以下。 要是例項同時也共享給其他一些應用使用的話,那可能需要更高效的64位Redis例項,這種情況下切換到32位是不可取的。 不管使用哪種方式,Redis的dump檔案在32位和64位之間是互相相容的, 因此倘若有減少佔用記憶體空間的需求,可以嘗試先使用32位,後面再切換到64位上。

  2. 儘可能的使用Hash資料結構。因為Redis在儲存小於100個欄位的Hash結構上,其儲存效率是非常高的。所以在不需要集合(set)操作或list的push/pop操作的時候,儘可能的使用Hash結構。比如,在一個web應用程式中,需要儲存一個物件表示使用者資訊,使用單個key表示一個使用者,其每個屬性儲存在Hash的欄位裡,這樣要比給每個屬性單獨設定一個key-value要高效的多。 通常情況下倘若有資料使用string結構,用多個key儲存時,那麼應該轉換成單key多欄位的Hash結構。 如上述例子中介紹的Hash結構應包含,單個物件的屬性或者單個使用者各種各樣的資料。Hash結構的操作命令是HSET(key, fields, value)和HGET(key, field),使用它可以儲存或從Hash中取出指定的欄位。

  3. 設定key的過期時間。一個減少記憶體使用率的簡單方法就是,每當儲存物件時確保設定key的過期時間。倘若key在明確的時間週期內使用或者舊key不大可能被使用時,就可以用Redis過期時間命令(expire,expireat, pexpire, pexpireat)去設定過期時間,這樣Redis會在key過期時自動刪除key。 假如你知道每秒鐘有多少個新key-value被建立,那可以調整key的存活時間,並指定閥值去限制Redis使用的最大記憶體。

  4. 回收key。在Redis配置檔案中(一般叫Redis.conf),通過設定“maxmemory”屬性的值可以限制Redis最大使用的記憶體,修改後重啟例項生效。 也可以使用客戶端命令config set maxmemory 去修改值,這個命令是立即生效的,但會在重啟後會失效,需要使用config rewrite命令去重新整理配置檔案。 若是啟用了Redis快照功能,應該設定“maxmemory”值為系統可使用記憶體的45%,因為快照時需要一倍的記憶體來複制整個資料集,也就是說如果當前已使用45%,在快照期間會變成95%(45%+45%+5%),其中5%是預留給其他的開銷。 如果沒開啟快照功能,maxmemory最高能設定為系統可用記憶體的95%。

        當記憶體使用達到設定的最大閥值時,需要選擇一種key的回收策略,可在Redis.conf配置檔案中修改“maxmemory-policy”屬性值。 若是Redis資料集中的key都設定了過期時間,那麼“volatile-ttl”策略是比較好的選擇。但如果key在達到最大記憶體限制時沒能夠迅速過期,或者根本沒有設定過期時間。那麼設定為“allkeys-lru”值比較合適,它允許Redis從整個資料集中挑選最近最少使用的key進行刪除(LRU淘汰演算法)。Redis還提供了一些其他淘汰策略,如下:

  • volatile-lru:使用LRU演算法從已設定過期時間的資料集合中淘汰資料。
  • volatile-ttl:從已設定過期時間的資料集合中挑選即將過期的資料淘汰。
  • volatile-random:從已設定過期時間的資料集合中隨機挑選資料淘汰。
  • allkeys-lru:使用LRU演算法從所有資料集合中淘汰資料。
  • allkeys-random:從資料集合中任意選擇資料淘汰
  • no-enviction:禁止淘汰資料。

          通過設定maxmemory為系統可用記憶體的45%或95%(取決於持久化策略)和設定“maxmemory-policy”為“volatile-ttl”或“allkeys-lru”(取決於過期設定),可以比較準確的限制Redis最大記憶體使用率,在絕大多數場景下使用這2種方式可確保Redis不會進行記憶體交換。倘若你擔心由於限制了記憶體使用率導致丟失資料的話,可以設定noneviction值禁止淘汰資料。

    2.限制記憶體交換: 如果記憶體碎片率低於1,Redis例項可能會把部分資料交換到硬碟上。記憶體交換會嚴重影響Redis的效能,所以應該增加可用實體記憶體或減少實Redis記憶體佔用。

5.  回收key

      info資訊中的evicted_keys欄位顯示的是,因為maxmemory限制導致key被回收刪除的數量。關於maxmemory的介紹見前面章節,回收key的情況只會發生在設定maxmemory值後,不設定會發生記憶體交換。 當Redis由於記憶體壓力需要回收一個key時,Redis首先考慮的不是回收最舊的資料,而是在最近最少使用的key或即將過期的key中隨機選擇一個key,從資料集中刪除。

      這可以在配置檔案中設定maxmemory-policy值為“volatile-lru”或“volatile-ttl”,來確定Redis是使用lru策略還是過期時間策略。 倘若所有的key都有明確的過期時間,那過期時間回收策略是比較合適的。若是沒有設定key的過期時間或者說沒有足夠的過期key,那設定lru策略是比較合理的,這可以回收key而不用考慮其過期狀態。

根據key回收定位效能問題

跟蹤key回收是非常重要的,因為通過回收key,可以保證合理分配Redis有限的記憶體資源。如果evicted_keys值經常超過0,那應該會看到客戶端命令響應延遲時間增加,因為Redis不但要處理客戶端過來的命令請求,還要頻繁的回收滿足條件的key。
需要注意的是,回收key對效能的影響遠沒有記憶體交換嚴重,若是在強制記憶體交換和設定回收策略做一個選擇的話,選擇設定回收策略是比較合理的,因為把記憶體資料交換到硬碟上對效能影響非常大(見前面章節)。

減少回收key以提升效能

減少回收key的數量是提升Redis效能的直接辦法,下面有2種方法可以減少回收key的數量:

1.增加記憶體限制:倘若開啟快照功能,maxmemory需要設定成實體記憶體的45%,這幾乎不會有引發記憶體交換的危險。若是沒有開啟快照功能,設定系統可用記憶體的95%是比較合理的,具體參考前面的快照和maxmemory限制章節。如果maxmemory的設定是低於45%或95%(視持久化策略),通過增加maxmemory的值能讓Redis在記憶體中儲存更多的key,這能顯著減少回收key的數量。 若是maxmemory已經設定為推薦的閥值後,增加maxmemory限制不但無法提升效能,反而會引發記憶體交換,導致延遲增加、效能降低。 maxmemory的值可以在Redis-cli工具上輸入config set maxmemory命令來設定。
需要注意的是,這個設定是立即生效的,但重啟後丟失,需要永久化儲存的話,再輸入config rewrite命令會把記憶體中的新配置重新整理到配置檔案中。

2.對例項進行分片:分片是把資料分割成合適大小,分別存放在不同的Redis例項上,每一個例項都包含整個資料集的一部分。通過分片可以把很多伺服器聯合起來儲存資料,相當於增加總的實體記憶體,使其在沒有記憶體交換和回收key的策略下也能儲存更多的key。假如有一個非常大的資料集,maxmemory已經設定,實際記憶體使用也已經超過了推薦設定的閥值,那通過資料分片能明顯減少key的回收,從而提高Redis的效能。 分片的實現有很多種方法,下面是Redis實現分片的幾種常見方式:

  • a. Hash分片:一個比較簡單的方法實現,通過Hash函式計算出key的Hash值,然後值所在範圍對應特定的Redis例項。
  • b. 代理分片:客戶端把請求傳送到代理上,代理通過分片配置表選擇對應的Redis例項。 如Twitter的Twemproxy,豌豆莢的codis。
  • c. 一致性Hash分片: 參見前面部落格《一致性Hash分片詳解
  • d. 虛擬桶分片:參見前面部落格《虛擬桶分詳解