影響redis性能主要有以下部分:
Redis 內部的阻塞式操作;
CPU核和NUMA架構
Redis關鍵系統配置
Redis內存碎片
Redis緩沖區
下面一個個來介紹這些地方
1.《redis 有哪些阻塞點?》
redis實例主要交互的對象有以下幾點,我們依據下面這些點看看redis有哪些阻塞操作:
客戶端交互:網絡IO,增刪改查,數據庫操作
磁盤交互: AOF 同步磁盤,AOF重寫,RDB模式持久化
從庫交互: 數據同步,RDB文件生成,RDB文件傳輸,清空數據庫, 從庫加載RDB文件
切片集群交互:向其他實例傳輸哈希槽信息,數據遷移。
1. reids阻塞點分析
1.客戶端交互
a. redis實例與客戶端網絡IO交互會阻塞redis嗎?
多個客戶端 可以同時與 redis 交互,競爭redis 資源, 由于redis 使用了IO多路復用的線程模式,使redis 不會等待阻塞在某一個客戶端,不響應與其他客戶端 的交互,所以 redis與客戶端的 IO 操作 不是 阻塞點;
b. 讀寫操作
集合元素全量查詢操作 HGETALL、SMEMBERS,以及集合的聚合統計操作,例如求交、并和差集。
這些操作往往時間復雜度是O(n),所以這是
另外,大量數據刪除操作,也是redis 的另一個阻塞點;
因為 redis刪除操作涉及 內存空間的釋放和管理,釋放內存只是redis的第一步,為了更好的管理規劃內存空間,在釋放內存時,操作系統會維護一個內存塊鏈表,把釋放的內存空間插入空閑的內存塊鏈表,以便后續管理使用;
當有大量內存被釋放時,空閑的內存塊鏈表操作時間就會增加,可能會造成redis主線程的阻塞;
所以,
2.磁盤交互
RDB快照和AOF重寫都 是 redis fork出來的子線程執行的,只會在fork子線程的時候阻塞主線程, RDB和AOF重寫都不會造成阻塞;
但是,redis AOF模式下 有三種同步落盤操作:NO,every seconds , 同步寫日志;
NO,every seconds 都是異步執行的 ,同步寫日志比較特殊,不靠異步子線程完成
一個同步寫磁盤的操作的耗時大約是 1~2ms,當有大量寫操作記錄在AOF日志時,并要求同步寫回的話,就會阻塞主線程;
所以,
3.主從交互
生成RDB文件,RDB文件傳輸都是 redis子線程來完成的,不阻塞;
對于從庫來說, 需要主線程 加載RDB文件后,才能執行以后的 數據同步操作,
所以,
4.切片集群
**數據遷移:**當我們部署 Redis 切片集群時,每個 Redis 實例上分配的哈希槽信息需要在不同實例間進行傳遞,同時,當需要進行負載均衡或者有實例增刪時,數據會在不同的實例間進行遷移。不過,哈希槽的信息量不大,而數據遷移是漸進式執行的,所以,一般來說,這兩類操作對 Redis 主線程的阻塞風險不大。
但是,如果你使用了 Redis Cluster 方案,而且同時正好遷移的是 bigkey 的話,就會造成主線程的阻塞,因為 Redis Cluster 使用了同步遷移,所以, Redis Cluster 中 大量bigkey數據遷移,可能導致 主線程;
2.關鍵路徑與非關鍵路徑
總結一下,有哪些阻塞點:
集合全量查詢和聚合操作;
bigkey 刪除和清空數據庫;
AOF 日志同步寫;
從庫加載 RDB 文件。
redis解決方案:
redis提供異步線程機制,那么以上所有阻塞點都可以用異步執行嗎?我們來分析下;
在這之前我們先來看看什么是 關鍵路徑 ,什么是非關鍵路徑;
關鍵路徑: 客戶端需要等待redis返回具體結果,并根據結果做出操作 的是關鍵路徑,如:讀操作;
非關鍵路徑:反之,客戶端不關心 返回結果,不用給客戶端返回具體數據的 操作就是非關鍵路徑,如:刪除操作;
集合全量查詢和聚合操作: 需要給客戶端返回具體結果, 關鍵路徑;
bigkey 刪除和清空數據庫: 非關鍵路徑
AOF 日志同步寫:為了保證數據可靠性,Redis 實例需要保證 AOF 日志中的操作記錄已經落盤,這個操作雖然需要實例等待,但它并不會返回具體的數據結果給實例。 所以,它是 非關鍵路徑。
從庫加載 RDB 文件: 庫要想對客戶端提供數據存取服務,就必須把 RDB 文件加載完成。 所以,它是關鍵路徑;
3.redis異步子線程機制
Redis 主線程啟動后,會使用操作系統提供的 pthread_create 函數創建 3 個子線程,分別由它們負責 AOF 日志寫操作、鍵值對刪除以及文件關閉的異步執行。
主線程通過一個鏈表形式的任務隊列和子線程進行交互。當收到鍵值對刪除和清空數據庫的操作時,主線程會把這個操作封裝成一個任務,放入到任務隊列中,然后給客戶端返回一個完成信息,表明刪除已經完成。
但實際上,這個時候刪除還沒有執行,等到后臺子線程從任務隊列中讀取任務后,才開始實際刪除鍵值對,并釋放相應的內存空間。因此,我們把這種異步刪除也稱為惰性刪除(lazy free)。此時,刪除或清空操作不會阻塞主線程,這就避免了對主線程的性能影響。
異步子線程機制:
注意點:異步的鍵值對刪除和數據庫清空操作是 Redis 4.0 后提供的功能,Redis 也提供了新的命令來執行這兩個操作:
鍵值對刪除:當你的集合類型中有大量元素(例如有百萬級別或千萬級別元素)需要刪除時,我建議你使用 UNLINK 命令。
清空數據庫:可以在 FLUSHDB 和 FLUSHALL 命令后加上 ASYNC 選項,這樣就可以讓后臺子線程異步地清空數據庫,如下所示:
FLUSHDB ASYNC
FLUSHALL AYSNC
1
2
4.建議與擴展
1.建議
集合全量查詢和聚合操作:可以使用 SCAN 命令,分批讀取數據,再在客戶端進行聚合計算;
從庫加載 RDB 文件:把主庫的數據量大小控制在 2~4GB 左右,以保證 RDB 文件能以較快的速度加載。
2.擴展
我們今天學習了關鍵路徑上的操作,你覺得,Redis 的寫操作(例如 SET、HSET、SADD 等)是在關鍵路徑上嗎?
需要客戶端根據業務需要來區分:
1、如果客戶端依賴操作返回值的不同,進而需要處理不同的業務邏輯,那么HSET和SADD操作算關鍵路徑,而SET操作不算關鍵路徑。因為HSET和SADD操作,如果field或member不存在時,Redis結果會返回1,否則返回0。而SET操作返回的結果都是OK,客戶端不需要關心結果有什么不同。
2、如果客戶端不關心返回值,只關心數據是否寫入成功,那么SET/HSET/SADD不算關鍵路徑,多次執行這些命令都是冪等的,這種情況下可以放到異步線程中執行。
3、但是有種例外情況,如果Redis設置了maxmemory,但是卻沒有設置淘汰策略,這三個操作也都算關鍵路徑。因為如果Redis內存超過了maxmemory,再寫入數據時,Redis返回的結果是OOM error,這種情況下,客戶端需要感知有錯誤發生才行。
注意:客戶端經常會阻塞等待發送的命令返回結果,在上一個命令還沒有返回結果前,客戶端會一直等待,直到返回結果后,才會發送下一個命令。此時,即使我們不關心返回結果,客戶端也要等到寫操作執行完成才行。所以,在不關心寫操作返回結果的場景下,可以對 Redis 客戶端做異步改造。具體點說,就是使用異步線程發送這些不關心返回結果的命令,而不是在 Redis 客戶端中等待這些命令的結果。
2.《為什么CPU結構也會影響Redis的性能?》
1.CPU多核架構
L1包括一級指令緩存和一級數據緩存;
**物理核的私有緩存,**它其實是指緩存空間只能被當前的這個物理核使用,其他的物理核無法對這個核的緩存空間進行數據存取。
一個CPU有多個物理核(運行核心),每個物理核有自己私有的的L1,12緩存 ,這些緩存一般只有幾kb,但是訪問效率確是 納秒級別,一般不超過 10納秒;
如果 L1、L2 緩存中沒有所需的數據,應用程序就需要訪問內存來獲取數據。
而應用程序的訪存延遲一般在 百納秒級別,是訪問 L1、L2 緩存的延遲的近 10 倍,不可避免地會對性能造成影響。
所以,不同的物理核還會共享一個共同的三級緩存(Level 3 cache,簡稱為 L3 cache)
為了平衡cpu和內存之間的差異,引入了 L3緩存(也就是CPU告訴緩存區),三級緩存不但平衡cpu L1,L2緩存與內內存訪問速度的差異,而且提升了 訪問容量,三級緩存容量可達到 幾MB,甚至 幾十MB ;
而且,CPU內 多個物理核 之間共享 L3三級緩存;
另外:現在主流的 CPU 處理器中, 每個物理核 內部有兩個邏輯核(超級線程),他們共享 物理核私有的L1,L2緩存;
下面看下 主流CPU架構圖:
2.CPU多核架構對Redis 性能的影響
在一個 CPU 核上運行時,應用程序需要記錄自身使用的軟硬件資源信息(例如棧指針、CPU 核的寄存器值等),我們把這些信息稱為運行時信息。
同時,應用程序訪問最頻繁的指令和數據還會被緩存到 L1、L2 緩存上,以便提升執行速度。
但是,在多核 CPU 的場景下,一旦應用程序需要在一個新的 CPU 核上運行,那么,運行時信息就需要重新加載到新的 CPU 核上。而且,新的 CPU 核的 L1、L2 緩存也需要重新加載數據和指令,這會導致程序的運行時間增加。
當 context switch 發生后,Redis 主線程的運行時信息需要被重新加載到另一個 CPU 核上,而且,此時,另一個 CPU 核上的 L1、L2 緩存中,并沒有 Redis 實例之前運行時頻繁訪問的指令和數據,所以,這些指令和數據都需要重新從 L3 緩存,甚至是內存中加載。這個重新加載的過程是需要花費一定時間的。而且,Redis 實例需要等待這個重新加載的過程完成后,才能開始處理請求,所以,這也會導致一些請求的處理時間增加。
在 CPU 多核場景下,Redis 實例被頻繁調度到不同 CPU 核上運行的話,那么,對 Redis 實例的請求處理時間影響就更大了。每調度一次,一些請求就會受到運行時信息、指令和數據重新加載過程的影響,這就會導致某些請求的延遲明顯高于其他請求。
所以,我們要避免 Redis 總是在不同 CPU 核上來回調度執行。于是,我們嘗試著**把 Redis 實例和 CPU 核綁定了,讓一個 Redis 實例固定運行在一個 CPU 核上。**我們可以使用 taskset 命令把一個程序綁定在一個核上運行。比如說,我們執行下面的命令,就把 Redis 實例綁在了 0 號核上,其中,“-c”選項用于設置要綁定的核編號。
taskset -c 0 ./redis-server
1
3.多CPU架構:NUMA
在主流的服務器上,一個 CPU 處理器會有 10 到 20 多個物理核。同時,為了提升服務器的處理能力,服務器上通常還會有多個 CPU 處理器(也稱為多 CPU Socket),每個處理器有自己的物理核(包括 L1、L2 緩存),L3 緩存,以及連接的內存,同時,不同處理器間通過總線連接。
**在多 CPU 架構上,應用程序可以在不同的處理器上運行。**在剛才的圖中,Redis 可以先在 Socket 1 上運行一段時間,然后再被調度到 Socket 2 上運行。
如果應用程序先在一個 Socket 上運行,并且把數據保存到了內存,然后被調度到另一個 Socket 上運行,此時,應用程序再進行內存訪問時,就需要訪問之前 Socket 上連接的內存,這種訪問屬于遠端內存訪問。和訪問 Socket 直接連接的內存相比,遠端內存訪問會增加應用程序的延遲。
4.NUMA架構對redis性能影響
在實際應用 Redis 時,我經常看到一種做法,為了提升 Redis 的網絡性能,把操作系統的網絡中斷處理程序和 CPU 核綁定。
這個做法可以避免網絡中斷處理程序在不同核上來回調度執行,的確能有效提升 Redis 的網絡處理性能。
但是,網絡中斷程序是要和 Redis 實例進行網絡數據交互的,一旦把網絡中斷程序綁核后,我們就需要注意 Redis 實例是綁在哪個核上了,這會關系到 Redis 訪問網絡數據的效率高低。
:網絡中斷處理程序從網卡硬件中讀取數據,并把數據寫入到操作系統內核維護的一塊內存緩沖區。內核會通過 epoll 機制觸發事件,通知 Redis 實例,Redis 實例再把數據從內核的內存緩沖區拷貝到自己的內存空間,如下圖所示:
**潛在的風險:**如果網絡中斷處理程序和 Redis 實例各自所綁的 CPU 核不在同一個 CPU Socket 上,那么,Redis 實例讀取網絡數據時,就需要跨 CPU Socket 訪問內存,這個過程會花費較多時間。
所以,為了避免 Redis 跨 CPU Socket 訪問網絡數據,我們最好把網絡中斷程序和 Redis 實例綁在同一個 CPU Socket 上
并不是先把一個 CPU Socket 中的所有邏輯核編完,再對下一個 CPU Socket 中的邏輯核編碼,而是先給每個 CPU Socket 中每個物理核的第一個邏輯核依次編號,再給每個 CPU Socket 中的物理核的第二個邏輯核依次編號。
假設有 2 個 CPU Socket,每個 Socket 上有 6 個物理核,每個物理核又有 2 個邏輯核,總共 24 個邏輯核。我們可以執行 *lscpu 命令,*查看到這些核的編號:
lscpu
Architecture: x86_64
...
NUMA node0 CPU(s): 0-5,12-17
NUMA node1 CPU(s): 6-11,18-23
...
1
2
3
4
5
6
7
可以看到,NUMA node0 的 CPU 核編號是 0 到 5、12 到 17。其中,0 到 5 是 node0 上的 6 個物理核中的第一個邏輯核的編號,12 到 17 是相應物理核中的第二個邏輯核編號。NUMA node1 的 CPU 核編號規則和 node0 一樣。
5.綁核的好處和壞處,以及解決方案
1.好處和壞處
好處:
在 CPU 多核的場景下,用 taskset 命令把 Redis 實例和一個核綁定,可以減少 Redis 實例在不同核上被來回調度執行的開銷,避免較高的尾延遲;
在多 CPU 的 NUMA 架構下,如果你對網絡中斷程序做了綁核操作,建議你同時把 Redis 實例和網絡中斷程序綁在同一個 CPU Socket 的不同核上,這樣可以避免 Redis 跨 Socket 訪問內存中的網絡數據的時間開銷。
壞處:
把 Redis 實例綁到一個 CPU 邏輯核上時,就會導致子進程、后臺線程和 Redis 主線程競爭 CPU 資源,一旦子進程或后臺線程占用 CPU 時,主線程就會被阻塞,導致 Redis 請求延遲增加。
2.解決方案
a.一個 Redis 實例對應綁一個物理核
按照上面的邏輯核編號,0,12 應該在同一個物理核內,我們可以把一個redis實例綁定一個物理核:
taskset ? -c 0,12 ./redis-server
1
把 Redis 實例和物理核綁定,可以讓主線程、子進程、后臺線程共享使用 2 個邏輯核,可以在一定程度上緩解 CPU 資源競爭。但是,因為只用了 2 個邏輯核,它們相互之間的 CPU 競爭仍然還會存在。如果你還想進一步減少 CPU 競爭,我再給你介紹一種方案。
b.修改redis源碼
通過編程實現綁核時,要用到操作系統提供的 1 個數據結構 cpu_set_t 和 3 個函數 CPU_ZERO、CPU_SET 和 sched_setaffinity,我先來解釋下它們。
cpu_set_t 數據結構:是一個位圖,每一位用來表示服務器上的一個 CPU 邏輯核。
CPU_ZERO 函數:以 cpu_set_t 結構的位圖為輸入參數,把位圖中所有的位設置為 0。
CPU_SET 函數:以 CPU 邏輯核編號和 cpu_set_t 位圖為參數,把位圖中和輸入的邏輯核編號對應的位設置為 1。
sched_setaffinity 函數:以進程 / 線程 ID 號和 cpu_set_t 為參數,檢查 cpu_set_t 中哪一位為 1,就把輸入的 ID 號所代表的進程 / 線程綁在對應的邏輯核上。
那么,怎么在編程時把這三個函數結合起來實現綁核呢?
很簡單,我們分四步走就行。
第一步:創建一個 cpu_set_t 結構的位圖變量;
第二步:使用 CPU_ZERO 函數,把 cpu_set_t 結構的位圖所有的位都設置為 0;
第三步:根據要綁定的邏輯核編號,使用 CPU_SET 函數,把 cpu_set_t 結構的位圖相應位設置為 1;
第四步:使用 sched_setaffinity 函數,把程序綁定在 cpu_set_t 結構位圖中為 1 的邏輯核上。
對于 Redis 來說,它是在 bio.c 文件中的 bioProcessBackgroundJobs 函數中創建了后臺線程。bioProcessBackgroundJobs 函數類似于剛剛的例子中的 worker 函數,在這個函數中實現綁核四步操作,就可以把后臺線程綁到和主線程不同的核上了。和給線程綁核類似,當我們使用 fork 創建子進程時,也可以把剛剛說的四步操作實現在 fork 后的子進程代碼中,示例代碼如下:
int main(){
? ?//用fork創建一個子進程
? ?pid_t p = fork();
? ?if(p < 0){
? ? ? printf(" fork error\n");
? ?}
? ?//子進程代碼部分
? ?else if(!p){
? ? ? cpu_set_t cpuset; ?//創建位圖變量
? ? ? CPU_ZERO(&cpu_set); //位圖變量所有位設置0
? ? ? CPU_SET(3, &cpuset); //把位圖的第3位設置為1
? ? ? sched_setaffinity(0, sizeof(cpuset), &cpuset); ?//把程序綁定在3號邏輯核
? ? ? //實際子進程工作
? ? ? exit(0);
? ?}
? ?...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
對于 Redis 來說,生成 RDB 和 AOF 日志重寫的子進程分別是下面兩個文件的函數中實現的。
rdb.c 文件:rdbSaveBackground 函數;
aof.c 文件:rewriteAppendOnlyFileBackground 函數。
這兩個函數中都調用了 fork 創建子進程,所以,我們可以在子進程代碼部分加上綁核的四步操作。
3.《響應波動延時:如何應對redis變慢?》
1.redis真的變慢了嗎?
redis是否變慢需要根據 Redis 的響應延遲 與 redis實例的 基線性能比較 來判斷;
大部分時候,Redis 延遲很低,但是在某些時刻,有些 Redis 實例會出現很高的響應延遲,甚至能達到幾秒到十幾秒,不過持續時間不長,這也叫延遲“毛刺”。當你發現 Redis 命令的執行時間突然就增長到了幾秒,基本就可以認定 Redis 變慢了。
1.怎么測試redis的基線性能呢?
所謂的基線性能呢,也就是一個系統在低壓力、無干擾下的基本性能,這個性能只由當前的軟硬件配置決定。
從 2.8.7 版本開始,<!–redis-cli 命令提供了–intrinsic-latency 選項,–>**可以用來監測和統計測試期間內的最大延遲,這個延遲可以作為 Redis 的基線性能。**一般情況下,運行 120 秒就足夠監測到最大延遲了,所以,我們可以把參數設置為 120。
./redis-cli --intrinsic-latency 120
Max latency so far: 17 microseconds.
Max latency so far: 44 microseconds.
Max latency so far: 94 microseconds.
Max latency so far: 110 microseconds.
Max latency so far: 119 microseconds.
36481658 total runs (avg latency: 3.2893 microseconds / 3289.32 nanoseconds per run).
Worst run took 36x longer than the average latency.
1
2
3
4
5
6
7
8
9
可以看出當前redis實例的最大延時是119 毫秒。
2. redis網絡延時 測試
如果你想了解網絡對 Redis 性能的影響,一個簡單的方法是用 iPerf 這樣的工具,測量從 Redis 客戶端到服務器端的網絡延遲。如果這個延遲有幾十毫秒甚至是幾百毫秒,就說明,Redis 運行的網絡環境中很可能有大流量的其他應用程序在運行,導致網絡擁塞了。這個時候,你就需要協調網絡運維,調整網絡的流量分配了。
2.如何應對redis變慢?
redis變慢排查我們可以從三個方面入手:
redis 自身特性
redis 文件系統
操作系統
1.redis 自身特性導致 變慢
1.慢查詢
Value 類型為 String 時,GET/SET 操作主要就是操作 Redis 的哈希表索引。這個操作復雜度基本是固定的,即 O(1)。但是,當 Value 類型為 Set 時,SORT、SUNION/SMEMBERS 操作復雜度分別為 O(N+M*log(M)) 和 O(N)。其中,N 為 Set 中的元素個數,M 為 SORT 操作返回的元素個數。這個復雜度就增加了很多。
可以通過 Redis 日志,或者是 latency monitor 工具,查詢變慢的請求;
解決慢查詢:
用其他高效命令代替。比如說,如果你需要返回一個 SET 中的所有成員時,不要使用 SMEMBERS 命令,而是要使用 SSCAN 多次迭代返回,避免一次返回大量數據,造成線程阻塞。
當你需要執行排序、交集、并集操作時,可以在客戶端完成,而不要用 SORT、SUNION、SINTER 這些命令,以免拖慢 Redis 實例。
KEYS 命令需要遍歷存儲的鍵值對,所以操作延時高,KEYS 命令一般不被建議用于生產環境中
2. 大量key 同時過期
過期 key 的自動刪除機制,默認情況下,Redis 每 100 毫秒會刪除一些過期 key:
采樣 ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP 個數的 key,并將其中過期的 key 全部刪除;
如果超過 25% 的 key 過期了,則重復刪除的過程,直到過期 key 的比例降至 25% 以下。
ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP 是 Redis 的一個參數,默認是 20;
刪除操作是阻塞的(Redis 4.0 后可以用異步線程機制來減少阻塞影響),頻繁使用帶有相同時間參數的 EXPIREAT 命令設置過期 key,這就會導致,在同一秒內有大量的 key 同時過期。那么就會導致一直執行 第二步,阻塞redis
盡量設置不同的過期時間,可以在設置時添加一個隨機數。
2.redis文件系統
Redis 會持久化保存數據到磁盤,這個過程要依賴文件系統來完成,所以,文件系統將數據寫回磁盤的機制,會直接影響到 Redis 持久化的效率。而且,在持久化的過程中,Redis 也還在接收其他請求,持久化的效率高低又會影響到 Redis 處理請求的性能。
1.AOF日志模式
AOF 日志提供了三種日志寫回策略:no、everysec、always。這三種寫回策略依賴文件系統的兩個系統調用完成,也就是 write 和 fsync。
write 只要把日志記錄寫到內核緩沖區,就可以返回了,并不需要等待日志實際寫回到磁盤;
而 fsync 需要把日志記錄寫回到磁盤后才能返回,時間較長。
當寫回策略配置為 everysec 時,Redis 會使用后臺的子線程異步完成 fsync 的操作。
而對于 always 策略來說,Redis 需要確保每個操作記錄日志都寫回磁盤,如果用后臺子線程異步完成,主線程就無法及時地知道每個操作是否已經完成了,這就不符合 always 策略的要求了。所以,always 策略并不使用后臺子線程來執行。
2.AOF重寫壓力過大導致fsync 阻塞
AOF重寫由子進程完成, 會有大量的IO操作;而fsync 雖然 是由后臺子線程完成寫入磁盤操作,但是,主線程在進行 寫操作時會監視 fsync的執行情況,如果子線程還未完成寫盤操作,主進程就會阻塞,不會返回給客戶端結果;
正是由于 主線程監視 上一次fsync操作執行情況,在寫磁盤壓力大時,可能導致 主線程阻塞;
例子:當主線程使用后臺子線程執行了一次 fsync,需要再次把新接收的操作記錄寫回磁盤時,如果主線程發現上一次的 fsync 還沒有執行完,那么它就會阻塞。所以,如果后臺子線程執行的 fsync 頻繁阻塞的話(比如 AOF 重寫占用了大量的磁盤 IO 帶寬),主線程也會阻塞,導致 Redis 性能變慢。
由于 fsync 后臺子線程和 AOF 重寫子進程的存在,主 IO 線程一般不會被阻塞。但是,如果在重寫日志時,AOF 重寫子進程的寫入量比較大,fsync 線程也會被阻塞,進而阻塞主線程,導致延遲增加。
如果業務應用對延遲非常敏感,但同時允許一定量的數據丟失,那么,可以把配置項 no-appendfsync-on-rewrite 設置為 yes,如下所示:
no-appendfsync-on-rewrite yes
1
no-appendfsync-on-rewrite 表示 AOF重寫時選擇不 進行 fsync刷盤操作,yes表示可以不執行fsync,no表示執行fsync
針對延遲非常敏感,但同時允許一定量的數據丟失的應用我們可以 設置no-appendfsync-on-rewrite yes
另外,我們可以采用高速的固態硬盤作為 AOF 日志的寫入設備。
3.操作系統:swap和內存大頁機制THP
1.Swap
內存 swap 是操作系統里將內存數據在內存和磁盤間來回換入和換出的機制,涉及到磁盤的讀寫,所以,一旦觸發 swap,無論是被換入數據的進程,還是被換出數據的進程,其性能都會受到慢速磁盤讀寫的影響。
一旦 swap 被觸發了,Redis 的請求操作需要等到磁盤數據讀寫完成才行,swap 觸發后影響的是 Redis 主 IO 線程,這會極大地增加 Redis 的響應時間。
Redis 實例自身使用了大量的內存,導致物理機器的可用內存不足;
和 Redis 實例在同一臺機器上運行的其他進程,在進行大量的文件讀寫操作。文件讀寫本身會占用系統內存,這會導致分配給 Redis 實例的內存量變少,進而觸發 Redis 發生 swap。
解決思路:
切片集群
加大內存
1.首先查看redis進程號,這里是5332
$ redis-cli info | grep process_id
process_id: 5332
1
2
2.Redis 所在機器的 /proc 目錄下的該進程目錄中:
Redis 所在機器的 /proc 目錄下的該進程目錄中:
1
3.查看該 Redis 進程的使用情況,這里截取部分結果
$cat smaps | egrep '^(Swap|Size)'
Size: 584 kB
Swap: 0 kB
Size: 4 kB
Swap: 4 kB
Size: 4 kB
Swap: 0 kB
Size: 462044 kB
Swap: 462008 kB
Size: 21392 kB
Swap: 0 kB
1
2
3
4
5
6
7
8
9
10
11
Size 代表當前redis所占內存,Swap表示這塊 Size 大小的內存區域有多少已經被換出到磁盤上了。如果這兩個值相等,就表示這塊內存區域已經完全被換出到磁盤了。
2. 內存大頁THP
該機制支持 2MB 大小的內存頁分配,而常規的內存頁分配是按 4KB 的粒度來執行的。
Redis 為了提供數據可靠性保證,需要將數據做持久化保存。這個寫入過程由額外的線程執行,所以,此時,Redis 主線程仍然可以接收客戶端寫請求。客戶端的寫請求可能會修改正在進行持久化的數據。在這一過程中,Redis 就會采用寫時復制機制,也就是說,一旦有數據要被修改,Redis 并不會直接修改內存中的數據,而是將這些數據拷貝一份,然后再進行修改。
如果采用了內存大頁,那么,即使客戶端請求只修改 100B 的數據,Redis 也需要拷貝 2MB 的大頁。相反,如果是常規內存頁機制,只用拷貝 4KB。兩者相比,你可以看到,當客戶端請求修改或新寫入數據較多時,內存大頁機制將導致大量的拷貝,這就會影響 Redis 正常的訪存操作,最終導致性能變慢。
查看內存大頁:
cat /sys/kernel/mm/transparent_hugepage/enabled
1
always代表使用了THP,never代表未使用
禁止使用THP:
echo never /sys/kernel/mm/transparent_hugepage/enabled
1
4.總結
梳理了一個包含 9 個檢查點的 Checklist,希望你在遇到 Redis 性能變慢時,按照這些步驟逐一檢查,高效地解決問題:
獲取 Redis 實例在當前環境下的基線性能。
是否用了慢查詢命令?如果是的話,就使用其他命令替代慢查詢命令,或者把聚合計算命令放在客戶端做。
是否對過期 key 設置了相同的過期時間?對于批量刪除的 key,可以在每個 key 的過期時間上加一個隨機數,避免同時刪除。
是否存在 bigkey? 對于 bigkey 的刪除操作,如果你的 Redis 是 4.0 及以上的版本,可以直接利用異步線程機制減少主線程阻塞;如果是 Redis 4.0 以前的版本,可以使用 SCAN 命令迭代刪除;對于 bigkey 的集合查詢和聚合操作,可以使用 SCAN 命令在客戶端完成。
Redis AOF 配置級別是什么?業務層面是否的確需要這一可靠性級別?如果我們需要高性能,同時也允許數據丟失,可以將配置項 no-appendfsync-on-rewrite 設置為 yes,避免 AOF 重寫和 fsync 競爭磁盤 IO 資源,導致 Redis 延遲增加。當然, 如果既需要高性能又需要高可靠性,最好使用高速固態盤作為 AOF 日志的寫入盤。
Redis 實例的內存使用是否過大?發生 swap 了嗎?如果是的話,就增加機器內存,或者是使用 Redis 集群,分攤單機 Redis 的鍵值對數量和內存壓力。同時,要避免出現 Redis 和其他內存需求大的應用共享機器的情況。
在 Redis 實例的運行環境中,是否啟用了透明大頁機制?如果是的話,直接關閉內存大頁機制就行了。
是否運行了 Redis 主從集群?如果是的話,把主庫實例的數據量大小控制在 2~4GB,以免主從復制時,從庫因加載大的 RDB 文件而阻塞。
是否使用了多核 CPU 或 NUMA 架構的機器運行 Redis 實例?使用多核 CPU 時,可以給 Redis 實例綁定物理核;使用 NUMA 架構時,注意把 Redis 實例和網絡中斷處理程序運行在同一個 CPU Socket 上。
4.redis 中 的內存碎片
1. 內存碎片帶來的影響?
question 1: 為什么redis已經刪除了數據,使用top命令還會顯示redis 內存占用較大呢?
那是因為redis雖然刪除了這些數據,回收了內存空間,但是 redis內存分配器不會立刻把 內存空間 返還給操作系統;
所以,出現redis已經刪除了數據,但是 任務管理器還會顯示redis占用大內存的情況;
question 2: redis 由內存分配器回收,分配內存,如果沒有內存自動整理功能(整理內存碎片),會有什么風險?
即使有大量的內存 ,但是空間碎片較多,內存利用率低, 當寫 bigkey 要求分配大量且連續的內存空間時,沒有較大的連續內存空間導致無法處理這個操作;
雖然有空閑空間,Redis 卻無法用來保存數據,不僅會減少 Redis 能夠實際保存的數據量,還會降低 Redis 運行機器的成本回報率。
2.內存碎片如何形成的?
主要有兩種方式:
內因:操作系統的內存分配機制
外因:Redis 的負載特征(鍵值大小不一致, 鍵值對修改,刪除等)
1. 內因:內存分配器的分配策略
Redis 可以使用 libc、jemalloc、tcmalloc 多種內存分配器來分配內存,默認使用 jemalloc。
jemalloc 的分配策略之一,是按照一系列固定的大小劃分內存空間。例如 8 字節、16 字節、32 字節、48 字節,…, 2KB、4KB、8KB 等。當程序申請的內存最接近某個固定值時,jemalloc 會給它分配相應大小的空間。
這樣的分配方式本身是為了減少分配次數。例如,Redis 申請一個 20 字節的空間保存數據,jemalloc 就會分配 32 字節,此時,如果應用還要寫入 10 字節的數據,Redis 就不用再向操作系統申請空間了,因為剛才分配的 32 字節已經夠用了,這就避免了一次分配操作。
2.外因:鍵值對大小不一樣和刪改操作
不同大小的鍵值對,Redis 申請內存空間分配時,本身就會有大小不一的空間需求。
鍵值對修改刪除帶來的內存空間變化,刪除和修改都會帶來空間碎片;
3.redis內存碎片處理
1.判斷是否有內存碎片
INFO memory
# Memory
used_memory:1073741736
used_memory_human:1024.00M
used_memory_rss:1997159792
used_memory_rss_human:1.86G
…
mem_fragmentation_ratio:1.86
1
2
3
4
5
6
7
8
mem_fragmentation_ratio: 表示Redis 當前的內存碎片率。它就是上面的命令中的兩個指標 used_memory_rss 和 used_memory 相除的結果。
used_memory_rss:操作系統實際分配給 Redis 的物理內存空間,里面就包含了碎片
used_memory :redis 申請的內存空間
問題:那么,該如何設置這個mem_fragmentation_ratio的值呢?這里有一些經驗設置閾值:
1 <= mem_fragmentation_ratio <= 1.5:這種情況是合理的。
mem_fragmentation_ratio > 1.5 :這表明內存碎片率已經超過了 50%。一般情況下,這個時候,我們就需要采取一些措施來降低內存碎片率了。
2.內存碎片的清理
a.內存清理
從 4.0-RC3 版本以后,Redis 自身提供了一種內存碎片自動清理的方法,我們先來看這個方法的基本機制。
內存碎片清理,簡單來說,就是**“搬家讓位,合并空間”**。
碎片清理是有代價的:
操作系統需要把多份數據拷貝到新位置,把原有空間釋放出來,這會帶來時間開銷。
因為 Redis 是單線程,在數據拷貝時,Redis 只能等著,這就導致 Redis 無法及時處理請求,性能就會降低。
而且,有的時候,數據拷貝還需要注意順序,就像剛剛說的清理內存碎片的例子,操作系統需要先拷貝 D,并釋放 D 的空間后,才能拷貝 B。這種對順序性的要求,會進一步增加 Redis 的等待時間,導致性能降低。
b.如何 降低內存清理時對redis性能的影響?
啟動內存碎片自動清理:
config set activedefrag yes
1
主要從兩個方面三個參數控制內存清理:
滿足兩個條件自動進行內存清理:
active-defrag-ignore-bytes 100mb:內存碎片字節數到達100MB
active-defrag-threshold-lower 10:表示內存碎片空間占操作系統分配給 Redis 的總空間比例達到 10% 時,開始清理。
注意:清理過程中,不滿足以上條件時立刻停止自動清理,滿足條件后會繼續自動清理
控制內存碎片CPU執行時間:
active-defrag-cycle-min 25: 表示自動清理過程所用 CPU 時間的比例不低于 25%,保證清理能正常開展;
active-defrag-cycle-max 75:表示自動清理過程所用 CPU 時間的比例不高于 75%,一旦超過,就停止清理,從而避免在清理時,大量的內存拷貝阻塞 Redis,導致響應延遲升高。
5.《redis 緩沖區》
1. 什么是緩沖區?
1.緩沖區解決什么問題?
redis緩沖區解決客戶端請求堆積或服務器處理數據速度過慢帶來的數據丟失以及性能問題。
2.緩沖區在redis 中有哪些應用場景?
cliet-server服務器模式高并發下暫存客戶端發送的命令數據,或者是服務器端返回給客戶端的數據結果
主從節點間進行數據同步時,用來暫存主節點接收的寫命令和數據。
注意:redis之所以適合做緩存是因為它有高性能的內存結構 ,以及完善的淘汰機制;
2.客戶端與服務器之間的緩沖區
cliet-server模式之間的緩沖區,分為輸入緩沖區的輸出緩沖區,服務器端給每個連接的客戶端都設置了一個輸入緩沖區和輸出緩沖區
client把請求命令和數據 放入 輸入緩沖區,server 逐步讀取命令把執行結果發送到輸出緩沖區,client去輸出緩沖區拿響應結果。
3.如何處理輸入緩沖區溢出問題
1.查看輸入緩沖區使用情況
CLIENT LIST
id=5 addr=127.0.0.1:50487 fd=9 name= age=4 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=26 qbuf-free=32742 obl=0 oll=0 omem=0 events=r cmd=client
1
2
client list 命令查看所有與server 相連的客戶端輸入緩沖區的信息
主要看兩類信息:
client 的信息,ip 地址,端口號
輸入緩沖區相關信息:
? cmd: 客戶端最新執行的命令(當前為client)
? qbuf: 當前客戶端已使用 緩沖區大小
? qbuf-free: 剩余 緩沖區大小
2.什么情況下出現輸入緩沖區溢出問題?以及解決方案
有批量bigkey請求
server性能低, 頻繁阻塞或阻塞時間教久
怎么解決呢?
由于輸入緩沖區 并不能設置緩沖區的大小,默認最多1G,所有只能避免bigkey ,避免server性能低
注意:當多個客戶端連接占用的內存總量,超過了 Redis 的 maxmemory 配置項時(例如 4GB),就會觸發 Redis 進行數據淘汰。
4.如何處理輸出緩存沖溢出問題?
1.什么情況下出現輸出緩沖區溢出問題?
輸出bigkey結果
執行MONITOR
輸出緩沖區設置過小
2.怎么設置輸出緩沖區的大小?
client-output-buffer-limit 配置項,來設置緩沖區的大小
例子:client-output-buffer-limit normal 0 0 0
四個參數代表什么?
one:類型,normal代表普通客戶端
two: 緩沖區最大限制
three : 持續輸出最大的數據量
four:持續輸出最大時間
3.不同的客戶端不同分配模式
普通客戶端
?client-output-buffer-limit normal 0 0 0
1
普通客戶端來說,它每發送完一個請求,會等到請求結果返回后,再發送下一個請求,這種發送方式稱為阻塞式發送。在這種情況下,如果不是讀取體量特別大的 bigkey,服務器端的輸出緩沖區一般不會被阻塞的。
所以,我們通常把普通客戶端的緩沖區大小限制,以及持續寫入量限制、持續寫入時間限制都設置為 0,也就是不做限制。
訂閱客戶端
一旦訂閱的 Redis 頻道有消息了,服務器端都會通過輸出緩沖區把消息發給客戶端。所以,訂閱客戶端和服務器間的消息發送方式,不屬于阻塞式發送。
因此,我們會給訂閱客戶端設置緩沖區大小限制、緩沖區持續寫入量限制,以及持續寫入時間限制,可以在 Redis 配置文件中這樣設置:
client-output-buffer-limit pubsub 8mb 2mb 60
1
5.主從集群中的緩沖區
1.復制緩沖區(replication_buffer)
replication_buffer的大小不算入 maxmemory
在全量復制過程中,主節點在向從節點傳輸 RDB 文件的同時,會繼續接收客戶端發送的寫命令請求。這些寫命令就會先保存在復制緩沖區中,主節點上會為每個從節點都維護一個復制緩沖區,來保證主從節點間的數據同步。
按通常的使用經驗,我們會**把主節點的數據量控制在 2~4GB,**這樣可以讓全量同步執行得更快些,避免復制緩沖區累積過多命令。
config set client-output-buffer-limit slave 512mb 128mb 60
1
2.復制積壓緩沖區(repl_backlog_buffer)
repl_backlog_buffer算入maxmemeory
增量復制時使用的緩沖區,這個緩沖區稱為復制積壓緩沖區,為了應對復制積壓緩沖區的溢出問題,我們可以調整復制積壓緩沖區的大小,也就是設置 repl_backlog_size 這個參數的值.
6. 總結
針對命令數據發送過快過大的問題,對于普通客戶端來說可以避免 bigkey,而對于復制緩沖區來說,就是避免過大的 RDB 文件。
針對命令數據處理較慢的問題,解決方案就是減少 Redis 主線程上的阻塞操作,例如使用異步的刪除操作。
針對緩沖區空間過小的問題,解決方案就是使用 client-output-buffer-limit 配置項設置合理的輸出緩沖區、復制緩沖區和復制積壓緩沖區大小。當然,我們不要忘了,輸入緩沖區的大小默認是固定的,我們無法通過配置來修改它,除非直接去修改 Redis 源碼。
?