天天看點

Redis線程模型的前世今生

Redis線程模型為什麼要這麼設計,有什麼優點和缺點,有哪些思想是可以借鑒的...本文從網絡IO的曆史、Reactor模型的曆史、到Redis線程模型的設計由淺入深,慢慢道來。

衆所周知,Redis是一個高性能的資料存儲架構,在高并發的系統設計中,Redis也是一個比較關鍵的元件,是我們提升系統性能的一大利器。深入去了解Redis高性能的原理顯得越發重要,當然Redis的高性能設計是一個系統性的工程,涉及到很多内容,本文重點關注Redis的IO模型,以及基于IO模型的線程模型。

我們從IO的起源開始,講述了阻塞IO、非阻塞IO、多路複用IO。基于多路複用IO,我們也梳理了幾種不同的Reactor模型,并分析了幾種Reactor模型的優缺點。基于Reactor模型我們開始了Redis的IO模型和線程模型的分析,并總結出Redis線程模型的優點、缺點,以及後續的Redis多線程模型方案。本文的重點是對Redis線程模型設計思想的梳理,捋順了設計思想,就是一通百通的事了。

注:本文的代碼都是僞代碼,主要是為了示意,不可用于生産環境。

我們常說的網絡IO模型,主要包含阻塞IO、非阻塞IO、多路複用IO、信号驅動IO、異步IO,本文重點關注跟Redis相關的内容,是以我們重點分析阻塞IO、非阻塞IO、多路複用IO,幫助大家後續更好的了解Redis網絡模型。

我們先看下面這張圖;

Redis線程模型的前世今生

我們經常說的阻塞IO其實分為兩種,一種是單線程阻塞,一種是多線程阻塞。這裡面其實有兩個概念,阻塞和線程。

阻塞:指調用結果傳回之前,目前線程會被挂起,調用線程隻有在得到結果之後才會傳回; 線程:系統調用的線程個數。

像建立連接配接、讀、寫都涉及到系統調用,本身是一個阻塞的操作。

服務端單線程來處理,當用戶端請求來臨時,服務端用主線程來處理連接配接、讀取、寫入等操作。

以下用代碼模拟了單線程的阻塞模式;

我們準備用兩個用戶端同時發起連接配接請求、來模拟單線程阻塞模式的現象。同時發起連接配接,通過服務端日志,我們發現此時服務端隻接受了其中一個連接配接,主線程被阻塞在上一個連接配接的read方法上。

Redis線程模型的前世今生
Redis線程模型的前世今生

我們嘗試關閉第一個連接配接,看第二個連接配接的情況,我們希望看到的現象是,主線程傳回,新的用戶端連接配接被接受。

Redis線程模型的前世今生

從日志中發現,在第一個連接配接被關閉後,第二個連接配接的請求被處理了,也就是說第二個連接配接請求在排隊,直到主線程被喚醒,才能接收下一個請求,符合我們的預期。

此時不僅要問,為什麼呢?

主要原因在于accept、read、write三個函數都是阻塞的,主線程在系統調用的時候,線程是被阻塞的,其他用戶端的連接配接無法被響應。

通過以上流程,我們很容易發現這個過程的缺陷,伺服器每次隻能處理一個連接配接請求,CPU沒有得到充分利用,性能比較低。如何充分利用CPU的多核特性呢?自然而然的想到了——多線程邏輯。

對工程師而言,代碼解釋一切,直接上代碼。

BIO多線程

同樣,我們并行發起兩個請求;

Redis線程模型的前世今生

兩個請求,都被接受,服務端新增兩個線程來處理用戶端的連接配接和後續請求。

Redis線程模型的前世今生
Redis線程模型的前世今生

我們用多線程解決了,伺服器同時隻能處理一個請求的問題,但同時又帶來了一個問題,如果用戶端連接配接比較多時,服務端會建立大量的線程來處理請求,但線程本身是比較耗資源的,建立、上下文切換都比較耗資源,又如何去解決呢?

如果我們把所有的Socket(檔案句柄,後續用Socket來代替fd的概念,盡量減少概念,減輕閱讀負擔)都放到隊列裡,隻用一個線程來輪訓所有的Socket的狀态,如果準備好了就把它拿出來,是不是就減少了服務端的線程數呢?

一起看下代碼,單純非阻塞模式,我們基本上不用,為了示範邏輯,我們模拟了相關代碼如下;

系統初始化,等待連接配接;

Redis線程模型的前世今生

發起兩個用戶端連接配接,線程開始輪詢兩個連接配接中是否有資料。

Redis線程模型的前世今生

兩個連接配接分别輸入資料後,輪詢線程發現有資料準備好了,開始相關的邏輯處理(單線程、多線程都可)。

Redis線程模型的前世今生

再用一張流程圖輔助解釋下(系統實際采用檔案句柄,此時用Socket來代替,友善大家了解)。

Redis線程模型的前世今生

服務端專門有一個線程來負責輪詢所有的Socket,來确認作業系統是否完成了相關事件,如果有則傳回處理,如果無繼續輪詢,大家一起來思考下?此時又帶來了什麼問題呢。

CPU的空轉、系統調用(每次輪詢到涉及到一次系統調用,通過核心指令來确認資料是否準備好),造成資源的浪費,那有沒有一種機制,來解決這個問題呢?

server端有沒專門的線程來做輪詢操作(應用程式端非核心),而是由事件來觸發,當有相關讀、寫、連接配接事件到來時,主動喚起服務端線程來進行相關邏輯處理。模拟了相關代碼如下;

IO多路複用

同時建立兩個連接配接;

Redis線程模型的前世今生

兩個連接配接無阻塞的被建立;

Redis線程模型的前世今生

無阻塞的接收讀寫;

Redis線程模型的前世今生
Redis線程模型的前世今生

當然作業系統的多路複用有好幾種實作方式,我們經常使用的select(),epoll模式這裡不做過多的解釋,有興趣的可以檢視相關文檔,IO的發展後面還有異步、事件等模式,我們在這裡不過多的贅述,我們更多的是為了解釋Redis線程模式的發展。

我們一起來聊了阻塞、非阻塞、IO多路複用模式,那Redis采用的是哪種呢?

Redis采用的是IO多路複用模式,是以我們重點來了解下多路複用這種模式,如何在更好的落地到我們系統中,不可避免的我們要聊下Reactor模式。

首先我們做下相關的名詞解釋;

Reactor:類似NIO程式設計中的Selector,負責I/O事件的派發; Acceptor:NIO中接收到事件後,處理連接配接的那個分支邏輯; Handler:消息讀寫處理等操作類。
Redis線程模型的前世今生

處理流程

Reactor監聽連接配接事件、Socket事件,當有連接配接事件過來時交給Acceptor處理,當有Socket事件過來時交個對應的Handler處理。

優點

模型比較簡單,所有的處理過程都在一個連接配接裡;

實作上比較容易,子產品功能也比較解耦,Reactor負責多路複用和事件分發處理,Acceptor負責連接配接事件處理,Handler負責Scoket讀寫事件處理。

缺點

隻有一個線程,連接配接處理和業務處理共用一個線程,無法充分利用CPU多核的優勢。

在流量不是特别大、業務處理比較快的時候系統可以有很好的表現,當流量比較大、讀寫事件比較耗時情況下,容易導緻系統出現性能瓶頸。

怎麼去解決上述問題呢?既然業務處理邏輯可能會影響系統瓶頸,那我們是不是可以把業務處理邏輯單拎出來,交給線程池來處理,一方面減小對主線程的影響,另一方面利用CPU多核的優勢。這一點希望大家要了解透徹,友善我們後續了解Redis由單線程模型到多線程模型的設計的思路。

Redis線程模型的前世今生

這種模型相對單Reactor單線程模型,隻是将業務邏輯的處理邏輯交給了一個線程池來處理。

Handler完成讀事件後,包裝成一個任務對象,交給線程池來處理,把業務處理邏輯交給其他線程來處理。

讓主線程專注于通用事件的處理(連接配接、讀、寫),從設計上進一步解耦;

利用CPU多核的優勢。

貌似這種模型已經很完美了,我們再思考下,如果用戶端很多、流量特别大的時候,通用事件的處理(讀、寫)也可能會成為主線程的瓶頸,因為每次讀、寫操作都涉及系統調用。

有沒有什麼好的辦法來解決上述問題呢?通過以上的分析,大家有沒有發現一個現象,當某一個點成為系統瓶頸點時,想辦法把他拿出來,交個其他線程來處理,那這種場景是否适用呢?

Redis線程模型的前世今生

這種模型相對單Reactor多線程模型,隻是将Scoket的讀寫處理從mainReactor中拎出來,交給subReactor線程來處理。

mainReactor主線程負責連接配接事件的監聽和處理,當Acceptor處理完連接配接過程後,主線程将連接配接配置設定給subReactor;

subReactor負責mainReactor配置設定過來的Socket的監聽和處理,當有Socket事件過來時交個對應的Handler處理;

讓主線程專注于連接配接事件的處理,子線程專注于讀寫事件吹,從設計上進一步解耦;

實作上會比較複雜,在極度追求單機性能的場景中可以考慮使用。

以上我們聊了,IO網路模型的發展曆史,也聊了IO多路複用的reactor模式。那Redis采用的是哪種reactor模式呢?在回答這個問題前,我們先梳理幾個概念性的問題。

Redis伺服器中有兩類事件,檔案事件和時間事件。

檔案事件:在這裡可以把檔案了解為Socket相關的事件,比如連接配接、讀、寫等; 時間時間:可以了解為定時任務事件,比如一些定期的RDB持久化操作。

本文重點聊下Socket相關的事件。

首先我們來看下Redis服務的線程模型圖;

Redis線程模型的前世今生

IO多路複用負責各事件的監聽(連接配接、讀、寫等),當有事件發生時,将對應事件放入隊列中,由事件分發器根據事件類型來進行分發;

如果是連接配接事件,則分發至連接配接應答處理器;GET、SET等redis指令分發至指令請求處理器。

指令處理完後産生指令回複事件,再由事件隊列,到事件分發器,到指令回複處理器,回複用戶端響應。

Redis線程模型的前世今生

連接配接過程

Redis服務端主線程監聽固定端口,并将連接配接事件綁定連接配接應答處理器。

用戶端發起連接配接後,連接配接事件被觸發,IO多路複用程式将連接配接事件包裝好後丢人事件隊列,然後由事件分發處理器分發給連接配接應答處理器。

連接配接應答處理器建立client對象以及Socket對象,我們這裡關注Socket對象,并産生ae_readable事件,和指令處理器關聯,辨別後續該Socket對可讀事件感興趣,也就是開始接收用戶端的指令操作。

目前過程都是由一個主線程負責處理。

Redis線程模型的前世今生

SET指令執行過程

用戶端發起SET指令,IO多路複用程式監聽到該事件後(讀事件),将資料包裝成事件丢到事件隊列中(事件在上個流程中綁定了指令請求處理器);

事件分發處理器根據事件類型,将事件分發給對應的指令請求處理器;

指令請求處理器,讀取Socket中的資料,執行指令,然後産生ae_writable事件,并綁定指令回複處理器;

IO多路複用程式監聽到寫事件後,将資料包裝成事件丢到事件隊列中,事件分發處理器根據事件類型分發至指令回複處理器;

指令回複處理器,将資料寫入Socket中傳回給用戶端。

以上流程分析我們可以看出Redis采用的是單線程Reactor模型,我們也分析了這種模式的優缺點,那Redis為什麼還要采用這種模式呢?

Redis本身的特性

指令執行基于記憶體操作,業務處理邏輯比較快,是以指令處理這一塊單線程來做也能維持一個很高的性能。

Reactor單線程模型的優點,參考上文。

Reactor單線程模型的缺點也同樣在Redis中來展現,唯一不同的地方就在于業務邏輯處理(指令執行)這塊不是系統瓶頸點。

随着流量的上漲,IO操作的的耗時會越來越明顯(read操作,核心中讀資料到應用程式。write操作,應用程式中的資料到核心),當達到一定閥值時系統的瓶頸就展現出來了。

Redis又是如何去解的呢?

哈哈~将耗時的點從主線程拎出來呗?那Redis的新版本是這麼做的嗎?我們一起來看下。

Redis線程模型的前世今生

Redis的多線程模型跟”多Reactor多線程模型“、“單Reactor多線程模型有點差別”,但同時用了兩種Reactor模型的思想,具體如下;

Redis的多線程模型是将IO操作多線程化,本身邏輯處理過程(指令執行過程)依舊是單線程,借助了單Reactor思想,實作上又有所區分。 将IO操作多線程化,又跟單Reactor衍生出多Reactor的思想一緻,都是将IO操作從主線程中拎出來。

指令執行大緻流程

用戶端發送請求指令,觸發讀就緒事件,服務端主線程将Socket(為了簡化了解成本,統一用Socket來代表連接配接)放入一個隊列,主線程不負責讀;

IO 線程通過Socket讀取用戶端的請求指令,主線程忙輪詢,等待所有 I/O 線程完成讀取任務,IO線程隻負責讀不負責執行指令;

主線程一次性執行所有指令,執行過程和單線程一樣,然後需要傳回的連接配接放入另外一個隊列中,有IO線程來負責寫出(主線程也會寫);

主線程忙輪詢,等待所有 I/O 線程完成寫出任務。

了解一個元件,更多的是要去了解他的設計思路,要去思考為什麼要這麼設計,做這種技術選型的背景是啥,對後續做系統架構設計有什麼參考意義等等。一通百通,希望對大家有參考意義。

作者:vivo網際網路伺服器團隊-Wang Shaodong

分享 vivo 網際網路技術幹貨與沙龍活動,推薦最新行業動态與熱門會議。