Redis基礎篇(二)高效能IO模型

大雜草發表於2020-12-25

我們經常聽到說Redis是單執行緒的,也會有疑問:為什麼單執行緒的Redis能那麼快?

這裡要明白一點:Redis是單執行緒,主要是指Redis的網路IO和鍵值對讀寫是由一個執行緒來完成的,這也是Redis對外提供鍵值儲存服務的主要流程。但Redis的其他功能,比如持久化、非同步刪除、叢集資料同步等,都是由額外的執行緒執行的。

我們知道多執行緒能夠提升併發效能,那為什麼Redis會採用單執行緒,而非多執行緒?為什麼單執行緒能那麼快?

下面我們就來學習一下Redis採用單執行緒的原因。

為什麼採用單執行緒?

使用多執行緒,雖然可以增加系統吞吐率,或是增加系統擴充套件性,但同樣會產生開銷。

Redis的資料是在記憶體裡的,是共享的,如果使用多執行緒就會引發共享資源的競爭,需要引入互斥鎖來解決,使得並行變序列。最終系統吞吐率並沒有隨著執行緒的增加而增加。

另外,多執行緒開發需要精細的設計,會增加系統的複雜度,降低程式碼的易除錯性和可維護性。為了避免這些問題,Redis採用單執行緒模式。

單執行緒Redis為什麼那麼快?

通常來說,單執行緒的處理能力比多執行緒要差很多,那Redis卻能使用單執行緒模型達到每秒數十萬級別的處理能力,這是為什麼呢?

一方面,Redis大多數操作是在記憶體上完成的,並且採用高效的資料結構,例如雜湊表和跳錶。另一方面,Redis採用了多路複用機制,使其在網路IO操作中能併發處理大量的客戶端請求,實現高吞吐率。

在學習多路複用機制前,我們要弄明白網路操作的基於IO模型和潛在的阻塞點。

基本IO模型與阻塞點

以Get請求為例,為了處理一個Get請求:

  1. 需要監聽客戶端請求(bind/listen)
  2. 和客戶端建立連線(accept)
  3. 從socket中讀取請求(recv)
  4. 解析客戶端傳送請求(parse)
  5. 根據請求型別讀取鍵值資料(get)
  6. 最後給客戶端返回結果,即向socket中寫回資料(send)。

下圖顯示了這一過程,其中,bind/listen、accept、recv、parse和send屬於網路IO處理,而get屬性鍵值資料操作。

image

但是在這裡的網路IO操作中,有潛在的阻塞點,分別是accept()和recv()。

  • 當Redis監聽到一個客戶端有連線請求,但一直未能成功建立起連線時,會阻塞在accept()
  • 當Redis通過recv()從一個客戶端讀取資料時,如果資料一直沒有到達,Redis也會一直阻塞在recv()

這就導致Redis整個執行緒阻塞,無法處理其他客戶端請求,效率很低。不過,幸運的是,socket網路模型本身支援非阻塞模式。

非阻塞模式

Socket網路模型可以設定非阻塞模式。

image

這樣能保證Redis執行緒既不會像基本IO模型中一直在阻塞點等待,也不會導致Redis無法處理實際到達的連線請求或資料。

下面就到多路複用機制登場了。

基於多路複用的高效能I/O模型

Linux的IO多路複用機制是指一個執行緒處理多個IO流,也就是select/epoll機制。

在Redis執行單執行緒下,該機制允許核心中,同時存在多個監聽套接字和已連線套接字。

基於多路複用的Redis IO模型

為了在請求到達時能通知到Redis執行緒,select/epoll提供了基於事件的回撥機制,即針對不同事件的發生,呼叫相應的處理函式

回撥機制的工作流程:

  1. select/epoll一旦臨聽到FD上有請求到達,就會觸發相應的事件,並放進一個事件佇列中。
  2. Redis單執行緒對事件佇列進行處理即可,無需一直輪詢是否有請求發生,避免CPU資源浪費。

因為Redis一直在對事件佇列進行處理,所以能及時響應客戶端請求,提升Redis的響應效能。

不過,需要注意的是,在不同的作業系統上,多路複用機制也是適用的。

擴充

在“Redis基本IO模型”圖中,有哪些潛在的效能瓶頸?

Redis單執行緒處理IO請求效能瓶頸主要包括2個方面:

1、任意一個請求在server中一旦發生耗時,都會影響整個server的效能 也就是說後面的請求都要等前面這個耗時請求處理完成,自己才能被處理到。

耗時的操作包括:

  • 操作bigkey:寫入一個bigkey在分配記憶體時需要消耗更多的時間,同樣,刪除bigkey釋放記憶體同樣會產生耗時
  • 使用複雜度過高的命令:例如SORT/SUNION/ZUNIONSTORE,或者O(N)命令,但是N很大,例如lrange key 0 -1一次查詢全量資料
  • 大量key集中過期:Redis的過期機制也是在主執行緒中執行的,大量key集中過期會導致處理一個請求時,耗時都在刪除過期key,耗時變長
  • 淘汰策略:溜達策略也是在主執行緒執行的,當記憶體超過Redis記憶體上限後,每次寫入都需要淘汰一些key,也會 造成耗時變長。
  • AOF刷盤開啟always機制:每次寫入都需要把這個操作刷到磁碟,寫磁碟的速度遠比寫記憶體慢,會拖慢Redis的效能
  • 主從全量同步生成RDB:雖然採用fork子程式生成資料快照,但fork這一瞬間也是會阻塞整個執行緒的,例項越大,阻塞時間越久

解決辦法:

  • 需要業務人員去規避
  • Redis在4.0推出了lazy-free機制,把bigkey釋放記憶體的耗時操作放在了非同步執行緒中執行,降低對主執行緒的影響

2、併發量非常大時,單執行緒讀寫客戶端IO資料存在效能瓶頸,雖然採用IO多路複用機制,但是讀寫客戶端資料依舊是同步IO,只能單執行緒依次讀取客戶端的資料,無法利用到CPU多核。

解決辦法:

  • Redis在6.0推出了多執行緒,可以在高併發場景下利用CPU多核多執行緒讀寫客戶端資料,進一步提升server效能
  • 當然,只針對客戶端的讀寫是並行的,每個命令的真正操作依舊是單執行緒的

參考資料

相關文章