Redis 和 IO 多路複用

最近在看 UNIX 網絡編程並研究了一下 Redis 的實現,感受 Redis 的源代碼十分適合閱讀和分析,其中 I/O 多路複用(mutiplexing)部分的實現很是乾淨和優雅,在這裏想對這部分的內容進行簡單的整理。react

幾種 I/O 模型

爲何 Redis 中要使用 I/O 多路複用這種技術呢?web

首先,Redis 是跑在單線程中的,全部的操做都是按照順序線性執行的, 可是因爲讀寫操做等待用戶輸入或輸出都是阻塞的,因此 I/O 操做在通常狀況下每每不能直接返回,這會致使某一文件的 I/O 阻塞致使整個進程沒法對其它客戶提供服務,而 I/O 多路複用就是爲了解決這個問題而出現的。redis

Blocking I/O

先來看一下傳統的阻塞 I/O 模型究竟是如何工做的:當使用 read 或者 write 對某一個文件描述符(File Descriptor 如下簡稱 FD)進行讀寫時,若是當前 FD 不可讀或不可寫,整個 Redis 服務就不會對其它的操做做出響應,致使整個服務不可用。數據庫

這也就是傳統意義上的,也就是咱們在編程中使用最多的阻塞模型:
編程

阻塞模型雖然開發中很是常見也很是易於理解,可是因爲它會影響其餘 FD 對應的服務,因此在須要處理多個客戶端任務的時候,每每都不會使用阻塞模型。設計模式

I/O 多路複用

雖然還有不少其它的 I/O 模型,可是在這裏都不會具體介紹。服務器

阻塞式的 I/O 模型並不能知足這裏的需求,咱們須要一種效率更高的 I/O 模型來支撐 Redis 的多個客戶(redis-cli),這裏涉及的就是 I/O 多路複用模型了:網絡

在 I/O 多路複用模型中,最重要的函數調用就是 select,該方法的可以同時監控多個文件描述符的可讀可寫狀況,當其中的某些文件描述符可讀或者可寫時,select 方法就會返回可讀以及可寫的文件描述符個數。併發

Reactor 設計模式

Redis 服務採用 Reactor 的方式來實現文件事件處理器(每個網絡鏈接其實都對應一個文件描述符)
框架

文件事件處理器使用 I/O 多路複用模塊同時監聽多個 FD,當 accept、read、write 和 close 文件事件產生時,文件事件處理器就會回調 FD 綁定的事件處理器。

雖然整個文件事件處理器是在單線程上運行的,可是經過 I/O 多路複用模塊的引入,實現了同時對多個 FD 讀寫的監控,提升了網絡通訊模型的性能,同時也能夠保證整個 Redis 服務實現的簡單。

I/O 多路複用模塊

I/O 多路複用模塊封裝了底層的 select、epoll、avport 以及 kqueue 這些 I/O 多路複用函數,爲上層提供了相同的接口。

簡要了解該模塊的功能,整個 I/O 多路複用模塊抹平了不一樣平臺上 I/O 多路複用函數的差別性,提供了相同的接口.

子模塊的選擇

由於 Redis 須要在多個平臺上運行,同時爲了最大化執行的效率與性能,因此會根據編譯平臺的不一樣選擇不一樣的 I/O 多路複用函數做爲子模塊,提供給上層統一的接口;在 Redis 中,咱們經過宏定義的使用,合理的選擇不一樣的子模塊

由於 select 函數是做爲 POSIX 標準中的系統調用,在不一樣版本的操做系統上都會實現,因此將其做爲保底方案:

Redis 會優先選擇時間複雜度爲 O(1) 的 I/O 多路複用函數做爲底層實現,包括 Solaries 10 中的 evport、Linux 中的 epoll 和 macOS/FreeBSD 中的 kqueue,上述的這些函數都使用了內核內部的結構,而且可以服務幾十萬的文件描述符。

可是若是當前編譯環境沒有上述函數,就會選擇 select 做爲備選方案,因爲其在使用時會掃描所有監聽的描述符,因此其時間複雜度較差 O(n),而且只能同時服務 1024 個文件描述符,因此通常並不會以 select 做爲第一方案使用。

總結

Redis 對於 I/O 多路複用模塊的設計很是簡潔,經過宏保證了 I/O 多路複用模塊在不一樣平臺上都有着優異的性能,將不一樣的 I/O 多路複用函數封裝成相同的 API 提供給上層使用。

整個模塊使 Redis 能以單進程運行的同時服務成千上萬個文件描述符,避免了因爲多進程應用的引入致使代碼實現複雜度的提高,減小了出錯的可能性。

問答

  1. Redis爲何是單線程 ?
    由於CPU不是Redis的瓶頸。Redis的瓶頸最有多是機器內存或者網絡帶寬。(以上主要來自官方FAQ)既然單線程容易實現,並且CPU不會成爲瓶頸,那就瓜熟蒂落地採用單線程的方案了。關於redis的性能,官方網站也有,普通筆記本輕鬆處理每秒幾十萬的請求,參見:How fast is Redis?(https://link.zhihu.com/?target=https%3A//redis.io/topics/benchmarks)

  2. 若是萬一CPU成爲你的Redis瓶頸了,或者,你就是不想讓服務器其餘核閒置,那怎麼辦?
    那也很簡單,你多起幾個Redis進程就行了。Redis是keyvalue數據庫,又不是關係數據庫,數據之間沒有約束。只要客戶端分清哪些key放在哪一個Redis進程上就能夠了。redis-cluster能夠幫你作的更好。

  3. 單線程模型
    Redis客戶端對服務端的每次調用都經歷了發送命令,執行命令,返回結果三個過程。其中執行命令階段,因爲Redis是單線程來處理命令的,全部每一條到達服務端的命令不會馬上執行,全部的命令都會進入一個隊列中,而後逐個被執行。而且多個客戶端發送的命令的執行順序是不肯定的。可是能夠肯定的是不會有兩條命令被同時執行,不會產生併發問題,這就是Redis的單線程基本模型。

  4. 單線程模型每秒萬級別處理能力的緣由
    (1)純內存訪問。 數據存放在內存中,內存的響應時間大約是 100納秒 ,這是Redis每秒萬億級別訪問的重要基礎。
    (2)非阻塞I/ORedis採用epoll作爲I/O多路複用技術的實現 ,再加上Redis自身的事件處理模型將epoll中的鏈接,讀寫,關閉都轉換爲了時間,不在I/O上浪費過多的時間。
    (3)單線程 避免了線程切換和競態產生的消耗
    (4)Redis採用單線程模型,每條命令執行若是佔用大量時間, 會形成其餘線程阻塞,對於Redis這種高性能服務是致命的,因此Redis是面向高速執行的數據庫。

內部實現採用epoll,採用了epoll+本身實現的簡單的事件框架。 epoll中的讀、寫、關閉、鏈接都轉化成了事件,而後利用epoll的多路複用特性, 毫不在io上浪費一點時間

這3個條件不是相互獨立的,特別是第一條,若是請求都是耗時的,採用單線程吞吐量及性能可想而知了。應該說redis爲特殊的場景選擇了合適的技術方案。

參考:
https://draveness.me/redis-io-multiplexing
http://www.javashuo.com/article/p-vcsnfnsj-cv.html

相關文章
相關標籤/搜索