天天看點

為什麼 Redis 單線程能達到百萬+QPS?

作者:在江湖中coding

https://juejin.im/post/5e6097846fb9a07c9f3fe744

性能測試報告

檢視了下阿裡 Redis 的性能測試報告如下,能夠達到數十萬、百萬級别的 QPS(暫時忽略阿裡對 Redis 所做的優化),我們從 Redis 的設計和實作來分析一下 Redis 是怎麼做的。

為什麼 Redis 單線程能達到百萬+QPS?

Redis的設計與實作

其實 Redis 主要是通過三個方面來滿足這樣高效吞吐量的性能需求

高效的資料結構

多路複用 IO 模型

事件機制

1、高效的資料結構

Redis 支援的幾種高效的資料結構 string(字元串)、hash(哈希)、list(清單)、set(集合)、zset(有序集合)

以上幾種對外暴露的資料結構它們的底層編碼方式都是做了不同的優化的,不細說了,不是本文重點。

2、多路複用 IO 模型

假設某一時刻與 Redis 伺服器建立了 1 萬個長連接配接,對于阻塞式 IO 的做法就是,對每一條連接配接都建立一個線程來處理,那麼就需要 1萬個線程,同時根據我們的經驗對于 IO 密集型的操作我們一般設定,線程數 = 2 * CPU 數量 + 1,對于 CPU 密集型的操作一般設定線程 = CPU 數量 + 1。

當然各種書籍或者網上也有一個詳細的計算公式可以算出更加合适準确的線程數量,但是得到的結果往往是一個比較小的值,像阻塞式 IO 這也動則建立成千上萬的線程,系統是無法承載這樣的負荷的更加彈不上高效的吞吐量和服務了。

而多路複用 IO 模型的做法是,用一個線程将這一萬個建立成功的連結陸續的放入 event_poll,event_poll 會為這一萬個長連接配接注冊回調函數,當某一個長連接配接準備就緒後(建立建立成功、資料讀取完成等),就會通過回調函數寫入到 event_poll 的就緒隊列 rdlist 中,這樣這個單線程就可以通過讀取 rdlist 擷取到需要的資料。為什麼 Redis 是單線程,點選這裡看下這篇文章。

另外,大家可以關注微信公衆号:Java技術棧,在背景回複:redis,可以擷取我整理的 N 篇 Redis 教程,都是幹貨。

需要注意的是,除了異步 IO 外,其它的 I/O 模型其實都可以歸類為阻塞式 I/O 模型,不同的是像阻塞式 I/O 模型在第一階段讀取資料的時候,如果此時資料未準備就緒需要阻塞,在第二階段資料準備就緒後需要将資料從核心态複制到使用者态這一步也是阻塞的。而多路複用 IO 模型在第一階段是不阻塞的,隻會在第二階段阻塞。

通過這種方式,就可以用 1 個或者幾個線程來處理大量的連接配接了,極大的提升了吐吞量

為什麼 Redis 單線程能達到百萬+QPS?

3、事件機制

Redis 用戶端與 Redis 服務端建立連接配接,發送指令,Redis 伺服器響應指令都是需要通過事件機制來做的,如下圖

為什麼 Redis 單線程能達到百萬+QPS?

首先 redis 伺服器運作,監聽套接字的 AE_READABLE 事件處于監聽的狀态下,此時連接配接應答處理器工作

用戶端與 Redis 伺服器發起建立連接配接,監聽套接字産生 AE_READABLE 事件,當 IO 多路複用程式監聽到其準備就緒後,将該事件壓入隊列中,由檔案事件分派器擷取隊列中的事件交于連接配接應答處理器工作處理,應答用戶端建立連接配接成功,同時将用戶端 socket 的 AE_READABLE 事件壓入隊列由檔案事件分派器擷取隊列中的事件交指令請求處理器關聯

用戶端發送 set key value 請求,用戶端 socket 的 AE_READABLE 事件,當 IO 多路複用程式監聽到其準備就緒後,将該事件壓入隊列中,由檔案事件分派器擷取隊列中的事件交于指令請求處理器關聯處理

指令請求處理器關聯處理完成後,需要響應用戶端操作完成,此時将産生 socket 的 AE_WRITEABLE 事件壓入隊列,由檔案事件分派器擷取隊列中的事件交于指令恢複處理器處理,傳回操作結果,完成後将解除 AE_WRITEABLE 事件與指令恢複處理器的關聯

reactor模式

大體上可以說 Redis 的工作模式是,reactor 模式配合一個隊列,用一個 serverAccept 線程來處理建立請求的連結,并且通過 IO 多路複用模型,讓核心來監聽這些 socket,一旦某些 socket 的讀寫事件準備就緒後就對應的事件壓入隊列中,然後 worker 工作,由檔案事件分派器從中擷取事件交于對應的處理器去執行,當某個事件執行完成後檔案事件分派器才會從隊列中擷取下一個事件進行處理。

可以類比在 netty 中,我們一般會設定 bossGroup 和 workerGroup 預設情況下 bossGroup 為 1,workerGroup = 2 * cpu 數量,這樣可以由多個線程來處理讀寫就緒的事件,但是其中不能有比較耗時的操作如果有的話需要将其放入線程池中,不然會降低其吐吞量。在 Redis 中我們可以看做這二者的值都是 1。

為什麼說存儲的值不宜過大

比如一個 string key = a,存儲了 500MB,首先讀取事件壓入隊列中,檔案事件分派器從中擷取到後,交于指令請求處理器處理,此處就涉及到從磁盤中加載 500MB。

比如是普通的 SSD 硬碟,讀取速度 200MB/S,那麼需要 2.5S 的讀取時間,在記憶體中讀取資料比較快比如 DDR4 中 50G/秒,讀取 500MB 需要 100 毫秒左右。

線程的庫一般預設 10 毫秒就算慢查詢了,大部分的指令執行時間都是微秒級别,此時其它 socket 所有的請求都将處于等待過程中,就會導緻阻塞了 100 毫秒,同時又會占用較大的帶寬導緻吞吐量進一步下降。