天天看點

Redis是單線程的,為什麼還這麼快?1、redis的單線程指的是什麼?2、redis是單線程為什麼還這麼快?3、Redis的IO多路複用原理4、select/poll/epoll的原理

Redis6之前所說的單線程指的是:隻有網絡請求子產品和資料操作子產品是單線程的。而其他的如持久化存儲子產品、叢集支撐子產品等是多線程的。

1、redis的單線程指的是什麼?

Redis在處理用戶端的請求時,包括擷取 (socket 讀)、解析、執行、内容傳回 (socket 寫) 等都由一個順序串行的主線程處理,這就是所謂的“單線程”。但如果嚴格來講從Redis4.0之後并不是單線程,除了主線程外,它也有背景線程在處理一些較為緩慢的操作,例如清理髒資料、無用連接配接的釋放、大 key 的删除等等。

2、redis是單線程為什麼還這麼快?

Redis這麼快的原因如下:

  • 完全基于記憶體的操作,時間為ns級别。
  • Redis的I/O多路複用,Redis采用epoll做為I/O多路複用技術的實作,再加上Redis自身的事件處理模型将epoll中的連接配接,讀寫,關閉都轉換為了時間,不在I/O上浪費過多的時間。
  • Redis基于C語言的,執行效率高。
  • Redis中的資料結構是專門進行設計的,資料結構簡單,對資料操作也簡單,類似于Hashmap。

3、Redis的IO多路複用原理

IO基本概念:

Linux的核心将所有外部裝置都可以看做一個檔案來操作,傳說中的linux下一切皆檔案。那麼我們對與外部裝置的操作都可以看做對檔案進行操作。我們對一個檔案的讀寫,都通過調用核心提供的系統調用;核心給我們傳回一個file descriptor(fd,檔案描述符)。對一個socket的讀寫也會有相應的描述符,稱為socketfd(socket描述符)。描述符就是一個數字(可以了解為一個索引),指向核心中一個結構體(檔案路徑,資料區,等一些屬性)。應用程式對檔案的讀寫就通過對描述符的讀寫完成。一個基本的IO,它會涉及到兩個系統對象,一個是調用這個IO的程序對象,另一個就是系統核心(kernel)。當一個read操作發生時,它會經曆兩個階段:

  1. 通過read系統調用向核心發起讀請求。
  2. 核心向硬體發送讀指令,并等待讀就緒。
  3. 核心把将要讀取的資料複制到描述符所指向的核心緩存區中。
  4. 将資料從核心緩存區拷貝到使用者程序空間中。

3.1 BIO(阻塞IO)

最常見的I/O模型是阻塞I/O模型,預設情形下,所有檔案操作都是阻塞的。我們以套接字為例來講解此模型。在程序空間中調用recvfrom,其系統調用直到資料報到達且被拷貝到應用程序的緩沖區中或者發生錯誤才傳回,期間一直在等待。我們就說程序在從調用recvfrom開始到它傳回的整段時間内是被阻塞的。

Redis是單線程的,為什麼還這麼快?1、redis的單線程指的是什麼?2、redis是單線程為什麼還這麼快?3、Redis的IO多路複用原理4、select/poll/epoll的原理
Redis是單線程的,為什麼還這麼快?1、redis的單線程指的是什麼?2、redis是單線程為什麼還這麼快?3、Redis的IO多路複用原理4、select/poll/epoll的原理

應用場景:BIO 方式适用于連接配接數目比較小且固定的架構, 這種方式對伺服器資源要求比較高, 但程式簡單易了解。

缺點:1、IO代碼裡read操作是阻塞操作,如果連接配接不做資料讀寫操作會導緻線程阻塞,浪費資源。

2、如果線程很多,會導緻伺服器線程太多,壓力太大。

3.2 NIO(非阻塞IO)

NIO:同步非阻塞,伺服器實作模式為一個線程可以處理多個請求(連接配接),用戶端發送的連接配接請求都會注冊到多路複用器selector上,多路複用器輪詢到連接配接有IO請求就進行處理,JDK1.4開始引入。

NIO調用流程:程序發起IO系統調用後,如果核心緩沖區沒有資料,需要到IO裝置中讀取,程序傳回一個錯誤而不會被阻塞;程序發起IO系統調用後,如果核心緩沖區有資料,核心就會把資料傳回程序。

Redis是單線程的,為什麼還這麼快?1、redis的單線程指的是什麼?2、redis是單線程為什麼還這麼快?3、Redis的IO多路複用原理4、select/poll/epoll的原理
Redis是單線程的,為什麼還這麼快?1、redis的單線程指的是什麼?2、redis是單線程為什麼還這麼快?3、Redis的IO多路複用原理4、select/poll/epoll的原理

應用場景:NIO方式适用于連接配接數目多且連接配接比較短(輕操作) 的架構, 比如聊天伺服器, 彈幕系統, 伺服器間通訊,程式設計比較複雜。

缺點:NIO解決了BIO阻塞的問題,但是依然有問題,因為使用者态相當寫了一個while(true)的死循環頻繁調用核心查詢是否有網絡IO到達(我在自己應用裡面,我怎麼知道有沒有IO請求啊?,是以就要調用系統函數輪詢檔案描述符)。輪詢調用核心成本太高。

3.3 AIO(異步非阻塞)

Linux下AIO是個假貨。

3.4 IO多路複用

I/O是指網絡I/O,多路指多個TCP連接配接(即socket或者channel),複用指複用一個或幾個線程。意思說一個或一組線程處理多個TCP連接配接。最大優勢是減少系統開銷小,不必建立過多的程序/線程,也不必維護這些程序/線程。

Redis是單線程的,為什麼還這麼快?1、redis的單線程指的是什麼?2、redis是單線程為什麼還這麼快?3、Redis的IO多路複用原理4、select/poll/epoll的原理

IO多路複用使用兩個系統調用:(select/poll/epoll和recvfrom),blocking IO隻調用了recvfrom;select/poll/epoll 核心是可以同時處理多個connection,而不是更快,是以連接配接數不高的話,性能不一定比多線程+阻塞IO好,多路複用模型中,每一個socket,設定為non-blocking,阻塞是被select這個函數block,而不是被socket阻塞的。

4、select/poll/epoll的原理

4.1 select

Redis是單線程的,為什麼還這麼快?1、redis的單線程指的是什麼?2、redis是單線程為什麼還這麼快?3、Redis的IO多路複用原理4、select/poll/epoll的原理

基本原理:用戶端操作伺服器時就會産生這三種檔案描述符(簡稱fd):writefds(寫)、readfds(讀)、和exceptfds(異常)。select會阻塞住監視3類檔案描述符,等有資料、可讀、可寫、出異常或逾時、就會傳回;傳回後通過周遊fdset整個數組來找到就緒的描述符fd,然後進行對應的IO操作。

(1)使用copy_from_user從使用者空間拷貝fd_set到核心空間

(2)注冊回調函數__pollwait

(3)周遊所有fd,調用其對應的poll方法(對于socket,這個poll方法是sock_poll,sock_poll根據情況會調用到tcp_poll,udp_poll或者datagram_poll)

(4)以tcp_poll為例,其核心實作就是__pollwait,也就是上面注冊的回調函數。

(5)__pollwait的主要工作就是把current(目前程序)挂到裝置的等待隊列中,不同的裝置有不同的等待隊列,對于tcp_poll來說,其等待隊列是sk->sk_sleep(注意把程序挂到等待隊列中并不代表程序已經睡眠了)。在裝置收到一條消息(網絡裝置)或填寫完檔案資料(磁盤裝置)後,會喚醒裝置等待隊列上睡眠的程序,這時current便被喚醒了。

(6)poll方法傳回時會傳回一個描述讀寫操作是否就緒的mask掩碼,根據這個mask掩碼給fd_set指派。

(7)如果周遊完所有的fd,還沒有傳回一個可讀寫的mask掩碼,則會調用schedule_timeout是調用select的程序(也就是current)進入睡眠。當裝置驅動發生自身資源可讀寫後,會喚醒其等待隊列上睡眠的程序。如果超過一定的逾時時間(schedule_timeout指定),還是沒人喚醒,則調用select的程序會重新被喚醒獲得CPU,進而重新周遊fd,判斷有沒有就緒的fd。

(8)把fd_set從核心空間拷貝到使用者空間。

select模型是怎麼解決NIO頻繁調用核心的問題呢?select模型是這麼做的,使用者态每次把需要擷取的網絡IO資料的fd傳遞給select,然後select自己在核心監聽到有資料到達後,就會一次性告訴使用者态哪些fd有資料了,然後使用者态就會再循環調用核心态的read方法擷取對應fd的資料。

不足:由于是采用輪詢方式全盤掃描,會随着檔案描述符FD數量增多而性能下降。每次調用 select(),需要把 fd 集合從使用者态拷貝到核心态,并進行周遊(消息傳遞都是從核心到使用者空間),預設單個程序打開的FD有限制是1024個,可修改宏定義,但是效率仍然慢。

4.2 epoll

select/poll的幾大缺點:

(1)每次調用select,都需要把fd集合從使用者态拷貝到核心态,這個開銷在fd很多時會很大

(2)同時每次調用select都需要在核心周遊傳遞進來的所有fd,這個開銷在fd很多時也很大

(3)select支援的檔案描述符數量太小了,預設是1024

epoll既然是對select和poll的改進,就應該能避免上述的三個缺點。那epoll都是怎麼解決的呢?在此之前,我們先看一下epoll和select和poll的調用接口上的不同,select和poll都隻提供了一個函數——select或者poll函數。而epoll提供了三個函數,epoll_create,epoll_ctl和epoll_wait,epoll_create是建立一個epoll句柄;epoll_ctl是注冊要監聽的事件類型;epoll_wait則是等待事件的産生。

1) epoll_create()系統啟動時,在Linux核心裡面申請一個B+樹結構檔案系統,傳回epoll對象,也是一個fd。

2) epoll_ctl() 每建立一個連接配接,都通過該函數操作epoll對象,在這個對象裡面修改添加删除對應的連結fd, 綁定一個callback函數

3) epoll_wait() 輪訓所有的callback集合,并完成對應的IO操作

Redis是單線程的,為什麼還這麼快?1、redis的單線程指的是什麼?2、redis是單線程為什麼還這麼快?3、Redis的IO多路複用原理4、select/poll/epoll的原理

對于第一個缺點,epoll的解決方案在epoll_ctl函數中。每次注冊新的事件到epoll句柄中時(在epoll_ctl中指定EPOLL_CTL_ADD),會把所有的fd拷貝進核心,而不是在epoll_wait的時候重複拷貝。epoll保證了每個fd在整個過程中隻會拷貝一次。

對于第二個缺點,epoll的解決方案不像select或poll一樣每次都把current輪流加入fd對應的裝置等待隊列中,而隻在epoll_ctl時把current挂一遍(這一遍必不可少)并為每個fd指定一個回調函數,當裝置就緒,喚醒等待隊列上的等待者時,就會調用這個回調函數,而這個回調函數會把就緒的fd加入一個就緒連結清單)。epoll_wait的工作實際上就是在這個就緒連結清單中檢視有沒有就緒的fd(利用schedule_timeout()實作睡一會,判斷一會的效果,和select實作中的第7步是類似的)。

對于第三個缺點,epoll沒有這個限制,它所支援的FD上限是最大可以打開檔案的數目,這個數字一般遠大于2048,舉個例子,在1GB記憶體的機器上大約是10萬左右,具體數目可以cat /proc/sys/fs/file-max察看,一般來說這個數目和系統記憶體關系很大。

 

select/poll的幾大缺點:

 1、每次調用select/poll,都需要把fd集合從使用者态拷貝到核心态,這個開銷在fd很多時會很大

 2、同時每次調用select/poll都需要在核心周遊傳遞進來的所有fd,這個開銷在fd很多時也很大

 3、針對select支援的檔案描述符數量太小了,預設是1024

 4、select傳回的是含有整個句柄的數組,應用程式需要周遊整個數組才能發現哪些句柄發生了事件;

 5、select的觸發方式是水準觸發,應用程式如果沒有完成對一個已經就緒的檔案描述符進行IO操作,那麼之後每次select調用還是會将這些檔案描述符通知程序。

而epoll隻管你“活躍”的連接配接  ,而跟連接配接總數無關,是以在實際的網絡環境中, epoll 的效率就會遠遠高于 select 和 poll 。

select poll epoll(jdk 1.5及以上)
操作方式 周遊 周遊 回調
底層實作 數組 連結清單 哈希表
IO效率 每次調用都進行線性周遊,時間複雜度為O(n) 每次調用都進行線性周遊,時間複雜度為O(n) 事件通知方式,每當有IO事件就緒,系統注冊的回調函數就會被調用,時間複雜度O(1)
最大連接配接 有上限 無上限 無上限

推薦語雀筆記軟體 https://www.yuque.com/michael-qambz/igeais/qqxr0q

繼續閱讀