Redis核心技術筆記16-20

IT小馬發表於2023-01-12

16 阻塞式操作

影響 Redis 效能的 5 大方面的潛在因素:

  • Redis 內部的阻塞式操作;
  • CPU 核和 NUMA 架構的影響;
  • Redis 關鍵系統配置;
  • Redis 記憶體碎片;
  • Redis 緩衝區。

例項阻塞點

  • 客戶端:網路IO,鍵值對增刪改查,資料庫操作;
  • 磁碟:生成RDB快照,記錄AOF日誌,AOF日誌重寫;
  • 主從節點:主庫生成、傳輸RDB檔案,從庫接收RDB檔案、清空資料庫、載入RDB檔案;
  • 切片叢集例項:向其他例項傳輸雜湊槽資訊,資料遷移

客戶端阻塞點

  • 網路IO:Redis採用IO多路複用機制,網路IO不是阻塞點
  • 鍵值對操作:複雜度高O(N)的增刪改查操作肯定會阻塞redis。

    • 集合全量查詢和聚合操作
    • 集合自身刪除操作(釋放記憶體後系統會插入空閒記憶體連結串列用於管理,記憶體過大操作時間會增加)
  • 清空資料庫:刪除和釋放所有鍵值對

磁碟互動阻塞點

AOF日誌回寫:
Redis 直接記錄 AOF 日誌時,會根據不同的寫回策略對資料做落盤儲存。一個同步寫磁碟的操作的耗時大約是 1~2ms,如果有大量的寫操作需要記錄在 AOF 日誌中,並同步寫回的話,就會阻塞主執行緒了。

主從節點阻塞點

在主從叢集中,主庫需要生成 RDB 檔案,並傳輸給從庫。主庫在複製的過程中,建立和傳輸 RDB 檔案都是由子程式來完成的,不會阻塞主執行緒。

  1. 對於從庫來說,它在接收了 RDB 檔案後,需要使用 FLUSHDB 命令清空當前資料庫,造成阻塞
  2. 從庫在清空當前資料庫後,還需要把 RDB 檔案載入到記憶體,這個過程的快慢和 RDB 檔案的大小密切相關,RDB 檔案越大,載入過程越慢,造成阻塞

叢集互動阻塞

如果你使用了 Redis Cluster 方案,而且同時正好遷移的是 bigkey 的話,就會造成主執行緒的阻塞,因為 Redis Cluster 使用了同步遷移。

非同步執行

Redis 主執行緒啟動後,會使用作業系統提供的 pthread_create 函式建立 3 個子執行緒,分別由它們負責 AOF 日誌寫操作、鍵值對刪除以及檔案關閉的非同步執行。

主執行緒透過一個連結串列形式的任務佇列和子執行緒進行互動。

寫入 AOF 日誌

當 AOF 日誌配置成 everysec 選項後,主執行緒會把 AOF 寫日誌操作封裝成一個任務,也放到任務佇列中。後臺子執行緒讀取任務後,開始自行寫入 AOF 日誌,這樣主執行緒就不用一直等待 AOF 日誌寫完了。

惰性刪除lazy free

當收到鍵值對刪除和清空資料庫的操作時,主執行緒會把這個操作封裝成一個任務,放入到任務佇列中,然後給客戶端返回一個完成資訊,表明刪除已經完成。

但實際上,這個時候刪除還沒有執行,等到後臺子執行緒從任務佇列中讀取任務後,才開始實際刪除鍵值對,並釋放相應的記憶體空間。

此時,刪除或清空操作不會阻塞主執行緒,這就避免了對主執行緒的效能影響。

小結

會導致 Redis 效能受損的 5 大阻塞點,包括集合全量查詢和聚合操作、bigkey 刪除、清空資料庫、AOF 日誌同步寫,以及從庫載入 RDB 檔案。

關鍵路徑操作:不能被非同步執行的操作。

  • 讀操作是典型的關鍵路徑操作,包括集合全量查詢和聚合操作
  • 從庫載入RDB操作
  • 寫操作是否在關鍵路徑,需要看使用方是否需要確認寫入已經完成

集合全量查詢和聚合操作、從庫載入 RDB 檔案是在關鍵路徑上,無法使用非同步操作來完成。對於這兩個阻塞點,我也給你兩個小建議。

  • 集合全量查詢和聚合操作:可以使用 SCAN 命令,分批讀取資料,再在客戶端進行聚合計算;
  • 從庫載入 RDB 檔案:把主庫的資料量大小控制在 2~4GB 左右,以保證 RDB 檔案能以較快的速度載入。

17 CPU結構

一個 CPU 處理器中一般有多個執行核心,我們把一個執行核心稱為一個物理核,每個物理核都可以執行應用程式。

主流架構

每個物理核都擁有私有的一級快取(Level 1 cache,簡稱 L1 cache),包括一級指令快取和一級資料快取,以及私有的二級快取(Level 2 cache,簡稱 L2 cache)。
L1 和 L2 快取的大小隻有KB級別。

不同的物理核還會共享一個共同的三級快取(Level 3 cache,簡稱為 L3 cache)。
L3有幾 MB 到幾十 MB。

另外,現在主流的 CPU 處理器中,每個物理核通常都會執行兩個超執行緒,也叫作邏輯核。同一個物理核的邏輯核會共享使用 L1、L2 快取。

伺服器上通常有多個 CPU 處理器(CPU Socket),每個處理器有自己的物理核(包括 L1、L2 快取),L3 快取,以及連線的記憶體,同時,不同處理器間透過匯流排連線。

遠端記憶體訪問:
應用在一個Socket上執行並把資料存入記憶體,當被排程到另一個 Socket 上執行再進行記憶體訪問時,就需要訪問之前 Socket 上連線的記憶體,稱為遠端記憶體訪問。
遠端記憶體訪問會增加應用程式的延遲。

在多 CPU 架構下,一個應用程式訪問所在 Socket 的本地記憶體和訪問遠端記憶體的延遲並不一致,所以,我們也把這個架構稱為非統一記憶體訪問架構(Non-Uniform Memory Access,NUMA 架構)。

多核影響

在 CPU 多核的環境下,透過繫結 Redis 例項和 CPU 核,可以有效降低 Redis 的尾延遲。

在一個 CPU 核上執行時,應用程式需要記錄自身使用的軟硬體資源資訊(例如棧指標、CPU 核的暫存器值等),我們把這些資訊稱為執行時資訊。

同時,應用程式訪問最頻繁的指令和資料還會被快取到 L1、L2 快取上,以便提升執行速度。

上下文切換 context switch:
在 CPU 多核的環境中,一個執行緒先在一個 CPU 核上執行,之後又切換到另一個 CPU 核上執行,這時就會發生 context switch。

Redis 主執行緒的執行時資訊需要被重新載入到另一個 CPU 核上,而且,此時,另一個 CPU 核上的 L1、L2 快取中,並沒有 Redis 例項之前執行時頻繁訪問的指令和資料,所以,這些指令和資料都需要重新從 L3 快取,甚至是記憶體中載入。

我們可以使用 taskset 命令把一個程式繫結在一個核上執行。

taskset -c 0 ./redis-server

把 Redis 例項綁在了 0 號核上,其中,“-c”選項用於設定要繫結的核編號。

我們最好把網路中斷程式和 Redis 例項綁在同一個 CPU Socket 上。

不過,需要注意的是,在 CPU 的 NUMA 架構下,對 CPU 核的編號規則,並不是先把一個 CPU Socket 中的所有邏輯核編完,再對下一個 CPU Socket 中的邏輯核編碼,而是先給每個 CPU Socket 中每個物理核的第一個邏輯核依次編號,再給每個 CPU Socket 中的物理核的第二個邏輯核依次編號。

lscpu

Architecture: x86_64
...
NUMA node0 CPU(s): 0-5,12-17
NUMA node1 CPU(s): 6-11,18-23
...

綁核風險:
當我們把 Redis 例項綁到一個 CPU 邏輯核上時,就會導致子程式、後臺執行緒和 Redis 主執行緒競爭 CPU 資源,一旦子程式或後臺執行緒佔用 CPU 時,主執行緒就會被阻塞,導致 Redis 請求延遲增加

解決方案:

  1. 一個 Redis 例項對應綁一個物理核(把一個物理核的 2 個邏輯核都用上,緩解CPU競爭)
taskset -c 0,12 ./redis-server
  1. 最佳化Redis原始碼,把子程式和後臺執行緒綁到不同CPU上

18 響應延遲

基線效能:一個系統在低壓力、無干擾下的基本效能,這個效能只由當前的軟硬體配置決定。

Redis基線效能測試:

./redis-cli --intrinsic-latency 120

列印 120 秒內監測到的最大延遲
注意:為了避免網路對基線效能的影響,剛剛說的這個命令需要在伺服器端直接執行。

結論:如果 Redis 執行時延遲是其基線效能的 2 倍及以上,就可以認定 Redis 變慢了。

慢查詢命令

慢查詢命令,就是指在 Redis 中執行速度慢的命令,這會導致 Redis 延遲增加,和命令操作的複雜度有關。

排查方法:

  1. Redis 日誌
  2. latency monitor 工具

處理方法:

  1. 用其他高效命令代替。比如說,如果你需要返回一個 SET 中的所有成員時,不要使用 SMEMBERS 命令,而是要使用 SSCAN 多次迭代返回,避免一次返回大量資料,造成執行緒阻塞。
  2. 當你需要執行排序、交集、並集操作時,可以在客戶端完成,而不要用 SORT、SUNION、SINTER 這些命令,以免拖慢 Redis 例項。
  3. 注意生成環境不要用KEYS命令,它會遍歷儲存的鍵值對,延遲高。

過期KEY操作

過期 key 的自動刪除機制,是回收記憶體空間的常用機制,會引起 Redis 操作阻塞,導致效能變慢。

Redis預設每100毫秒刪除一些過期key:

  1. 取樣 ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP 個數(預設20)的 key,並將其中過期的 key 全部刪除;
  2. 如果超過 25% 的 key 過期了,則重複刪除的過程,直到過期 key 的比例降至 25% 以下。
    (觸發該條件後會一直執行刪除操作,導致Redis變慢)

演算法2觸發方式:
頻繁使用帶有相同時間引數的 EXPIREAT 命令設定過期 key,導致在同一時間大量key過期。

解決方案:加隨機數

小結

排查和解決 Redis 變慢問題的方法:

  1. 從慢查詢命令開始排查,並且根據業務需求替換慢查詢命令;
  2. 排查過期 key 的時間設定,並根據實際使用需求,設定不同的過期時間。

問題:有哪些其他命令可以代替 KEYS 命令,實現同樣的功能呢?
如果想要獲取整個例項的所有key,建議使用SCAN命令代替。客戶端透過執行SCAN $cursor COUNT $count可以得到一批key以及下一個遊標$cursor,然後把這個$cursor當作SCAN的引數,再次執行,以此往復,直到返回的$cursor為0時,就把整個例項中的所有key遍歷出來了。

但是SCAN可能會得到重複的key(Rehash時,舊錶已遍歷過的key會對映到新表沒有遍歷過的位置)。

19 檔案系統和作業系統

Redis 會持久化儲存資料到磁碟,這個過程要依賴檔案系統來完成,所以,檔案系統將資料寫回磁碟的機制,會直接影響到 Redis 持久化的效率。在持久化的過程中,Redis 也還在接收其他請求,持久化的效率高低又會影響到 Redis 處理請求的效能。

另一方面,Redis 是記憶體資料庫,記憶體操作非常頻繁,所以,作業系統的記憶體機制會直接影響到 Redis 的處理效率。比如Redis 的記憶體不夠用了,作業系統會啟動 swap 機制,這就會直接拖慢 Redis。

檔案系統:AOF模式

AOF 日誌提供了三種日誌寫回策略:no、everysec、always。

寫回策略依賴檔案系統的兩個系統呼叫完成,也就是 write 和 fsync:

  • write 只要把日誌記錄寫到核心緩衝區,就可以返回了,並不需要等待日誌實際寫回到磁碟;(no)
  • fsync 需要把日誌記錄寫回到磁碟後才能返回,時間較長。(everysec, always)

no:呼叫write寫日誌檔案,由作業系統週期性的將日誌寫回磁碟
everysec:允許丟失一秒的操作記錄,使用後臺子執行緒完成fysnc操作
always:不使用後臺子執行緒執行

另外,AOF 重寫生成體量縮小的新的 AOF 日誌檔案,需要的時間很長,也容易阻塞 Redis 主執行緒,所以,Redis 使用子程式來進行 AOF 重寫。

風險點:

  • fsync需要等到資料寫到磁碟才能返回,AOF重寫會進行大量IO,可能阻塞fsync。
  • 主執行緒會監控fsync進度,如果發現上一次還沒執行完,主執行緒也會阻塞,導致Redis效能下降。

配置:
如果業務應用對延遲非常敏感,但同時允許一定量的資料丟失,那麼,可以把配置項 no-appendfsync-on-rewrite 設定為 yes

no-appendfsync-on-rewrite yes

建議:使用高速固態硬碟

作業系統:記憶體swap

記憶體 swap 是作業系統裡將記憶體資料在記憶體和磁碟間來回換入和換出的機制。
swap 觸發後影響的是 Redis 主 IO 執行緒,這會極大地增加 Redis 的響應時間。

觸發原因:物理機記憶體不足
1、Redis 例項自身使用了大量的記憶體,導致物理機器的可用記憶體不足;
2、和 Redis 例項在同一臺機器上執行的其他程式,在進行大量的檔案讀寫操作。檔案讀寫本身會佔用系統記憶體,這會導致分配給 Redis 例項的記憶體量變少,進而觸發 Redis 發生 swap。

解決思路:增加記憶體或使用叢集。

檢視swap情況:

# 1.檢視程式ID
redis-cli info | grep process_id
process_id: 5332

# 2.進入程式目錄
cd /proc/5332

# 3.檢視程式使用情況
cat smaps | egrep '^(Swap|Size)'

注意:當出現百 MB,甚至 GB 級別的 swap 大小時,就表明,此時,Redis 例項的記憶體壓力很大,很有可能會變慢。

作業系統:記憶體大頁

記憶體大頁機制(Transparent Huge Page, THP),也會影響 Redis 效能,該機制支援2MB大小的記憶體分配,常規記憶體分配是4KB。

缺點:RDB使用寫時複製機制,有資料要被修改時,會先複製一份再修改。當修改或新寫資料較多時,記憶體大頁將導致大量複製,影響Redis效能。

檢查記憶體大頁:

cat /sys/kernel/mm/transparent_hugepage/enabled

啟動:always 禁止:never

生產環境建議:關閉大頁機制

echo never > /sys/kernel/mm/transparent_hugepage/enabled

小結

檢查Redis效能9點:

  1. 獲取 Redis 例項在當前環境下的基線效能。
  2. 是否用了慢查詢命令?如果是的話,就使用其他命令替代慢查詢命令,或者把聚合計算命令放在客戶端做。
  3. 是否對過期 key 設定了相同的過期時間?對於批次刪除的 key,可以在每個 key 的過期時間上加一個隨機數,避免同時刪除。
  4. 是否存在 bigkey? 對於 bigkey 的刪除操作,如果你的 Redis 是 4.0 及以上的版本,可以直接利用非同步執行緒機制減少主執行緒阻塞;如果是 Redis 4.0 以前的版本,可以使用 SCAN 命令迭代刪除;對於 bigkey 的集合查詢和聚合操作,可以使用 SCAN 命令在客戶端完成。
  5. Redis AOF 配置級別是什麼?業務層面是否的確需要這一可靠性級別?如果我們需要高效能,同時也允許資料丟失,可以將配置項 no-appendfsync-on-rewrite 設定為 yes,避免 AOF 重寫和 fsync 競爭磁碟 IO 資源,導致 Redis 延遲增加。當然, 如果既需要高效能又需要高可靠性,最好使用高速固態盤作為 AOF 日誌的寫入盤。
  6. Redis 例項的記憶體使用是否過大?發生 swap 了嗎?如果是的話,就增加機器記憶體,或者是使用 Redis 叢集,分攤單機 Redis 的鍵值對數量和記憶體壓力。同時,要避免出現 Redis 和其他記憶體需求大的應用共享機器的情況。
  7. 在 Redis 例項的執行環境中,是否啟用了透明大頁機制?如果是的話,直接關閉記憶體大頁機制就行了。
  8. 是否執行了 Redis 主從叢集?如果是的話,把主庫例項的資料量大小控制在 2~4GB,以免主從複製時,從庫因載入大的 RDB 檔案而阻塞。
  9. 是否使用了多核 CPU 或 NUMA 架構的機器執行 Redis 例項?使用多核 CPU 時,可以給 Redis 例項繫結物理核;使用 NUMA 架構時,注意把 Redis 例項和網路中斷處理程式執行在同一個 CPU Socket 上。

20 記憶體分配

問題:做了資料刪除,使用 top 命令檢視時,為什麼 Redis 還是佔用了很多記憶體呢?
原因:資料刪除後,Redis 釋放的記憶體空間會由記憶體分配器管理,並不會立即返回給作業系統。所以,作業系統仍然會記錄著給 Redis 分配了大量記憶體。

記憶體碎片

內因:記憶體分配策略
外因:鍵值對大小不一樣;刪改操作;

記憶體分配策略

Redis 可以使用 libc、jemalloc、tcmalloc 多種記憶體分配器來分配記憶體,預設使用 jemalloc。

jemalloc 的分配策略之一,是按照一系列固定的大小劃分記憶體空間。當程式申請的記憶體最接近某個固定值時,jemalloc 會給它分配相應大小的空間。

如果 Redis 每次向分配器申請的記憶體空間大小不一樣,這種分配方式就會有形成碎片的風險。

鍵值對大小

記憶體分配器只能按固定大小分配記憶體,所以,分配的記憶體空間一般都會比申請的空間大一些,不會完全一致,這本身就會造成一定的碎片,降低記憶體空間儲存效率。

修改刪除操作

鍵值對被修改和刪除,會導致空間的擴容和釋放。

  • 如果修改後的鍵值對變大或變小了,就需要佔用額外的空間或者釋放不用的空間
  • 刪除的鍵值對不再需要記憶體空間了,會把空間釋放出來,形成空閒空間

判斷記憶體碎片大小

使用INFO命令:

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

mem_fragmentation_ratio 的指標,它表示的就是 Redis 當前的記憶體碎片率。

mem_fragmentation_ratio = used_memory_rss / used_memory
used_memory_rss 是作業系統實際分配給 Redis 的實體記憶體空間,裡面就包含了碎片;
used_memory 是 Redis 為了儲存資料實際申請使用的空間。

經驗:

  1. mem_fragmentation_ratio 大於 1.5 。這表明記憶體碎片率已經超過了 50%。一般情況下,這個時候,我們就需要採取一些措施來降低記憶體碎片率了。
  2. 小於1,說明沒有足夠的實體記憶體,發生swap了。

清理記憶體碎片

從 4.0-RC3 版本以後,Redis 自身提供了一種記憶體碎片自動清理的方法。

代價:
碎片清理是有代價的,作業系統需要把多份資料複製到新位置,把原有空間釋放出來,這會帶來時間開銷。因為 Redis 是單執行緒,在資料複製時,Redis 只能等著,這就導致 Redis 無法及時處理請求,效能就會降低。

解決方案:
可以透過設定引數,來控制碎片清理的開始和結束時機,以及佔用的 CPU 比例,從而減少碎片清理對 Redis 本身請求處理的效能影響。

Redis 需要啟用自動記憶體碎片清理,可以把 activedefrag 配置項設定為 yes:

config set activedefrag yes

配置自動清理條件(同時滿足):

  • 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,導致響應延遲升高。

小結

info memory:檢視碎片率的情況;
碎片率閾值:判斷是否要進行碎片清理了;
記憶體碎片自動清理:提高記憶體實際利用率。

相關文章