詳解redis網路IO模型

来源:https://www.cnblogs.com/jtea/archive/2022/12/09/16969386.html
-Advertisement-
Play Games

前言 "redis是單線程的" 這句話我們耳熟能詳。但它有一定的前提,redis整個服務不可能只用到一個線程完成所有工作,它還有持久化、key過期刪除、集群管理等其它模塊,redis會通過fork子進程或開啟額外的線程去處理。所謂的單線程是指從網路連接(accept) -> 讀取請求內容(read) ...


前言

"redis是單線程的" 這句話我們耳熟能詳。但它有一定的前提,redis整個服務不可能只用到一個線程完成所有工作,它還有持久化、key過期刪除、集群管理等其它模塊,redis會通過fork子進程或開啟額外的線程去處理。所謂的單線程是指從網路連接(accept) -> 讀取請求內容(read) -> 執行命令 -> 響應內容(write),這整個過程是由一個線程完成的,至於為什麼redis要設計為單線程,主要有以下原因:

  1. 基於記憶體。redis命令操作主要都是基於記憶體,這已經足夠快,不需要藉助多線程。
  2. 高效的數據結構。redis底層提供了動態簡單動態字元串(SDS)、跳錶(skiplist)、壓縮列表(ziplist)等數據結構來高效訪問數據。
  3. 保持簡單。引入多線程會使redis變得複雜,例如需要考慮多線程併發訪問資源競爭問題,數據結構也會變得複雜,hash就不能是單純的hash,需要像java一樣設計一個ConcurrentHashMap。還需要考慮線程切換帶來的性能損耗,基於第一點,當程式執行已經足夠快,多線程並不能帶來正面收益。

按照redis官方介紹,單個節點的redis qps可以達到10w+,已經非常優秀,如果有更高的要求,則可以通過部署主從、集群方式進一步提升。
單線程不是沒有缺點的,我們需要辯證的看待問題,不然所有的組件都可以使用redis替代了。首先是基於記憶體的操作有丟失數據的風險,儘管你可以配置appendfsync always每次將執行請求通過aof文件持久化,但這也會帶來性能的下降。另外單線程的執行意味著所有的請求都需要排隊執行,如果有一個命令阻塞了,其它命令也都執行不了,可以與之比較的是mysql,如果有一條sql語句執行比較慢,只要它不完全拖垮資料庫,其它請求的sql語句還是可以執行。最後,從上面可以看到從接收網路連接到寫迴響應內容,對於網路請求部分的處理其實是可以多線程執行來提升網路IO效率的。

redis 6.0
從redis 6.0開始,網路連接(accept) -> 讀取請求內容(read) -> 執行命令 -> 響應內容(write) 這個過程中的“執行命令”這個步驟依然保持單線程執行,而對於網路IO讀寫是多線程執行的了。原因是這部分是網路IO的解析、響應處理,已經不是單純的記憶體操作,可以充分利用多核CPU的優勢提升性能,對於這部分的性能需求其實一直都存在,社區也有KeyDB這樣的產品,其核心就是在redis的基礎上對多線程的支持,這多redis來說無疑是一種挑戰,所有redis6.0開始在網路IO處理支持多線程就顯得非常必要了。

我們知道redis客戶端連接是可以有很多個的,最多可以有maxclients參數配置的數量,預設是10000個,那麼redis是如何高效處理這麼多連接的呢?以及6.0和之前的版本是如何具體處理從接收連接到響應整個過程的,或者說redis線程模型是怎麼樣的,清楚的瞭解這些有助於我們更好的學習redis,其中的知識在以後學習其它中間件也可以很好的借鑒。

linux IO模型

在學習redis網路IO模型之前我們必須先瞭解一下linux的IO模型,以為redis也是基於操作系統去設計的。I/O是Input/Output的縮寫,是指操作系統與外部設備進行讀取、輸出的交互過程,外部設備可以是網卡、磁碟等。操作系統一般都分為內核和用戶空間兩部分,內核負責與底層硬體交互,用戶程式讀寫數據都需要經過內核空間,也就是數據會不斷的在內核-用戶空間進行複製,不同的IO模型在這個複製過程用戶線程有不同的表現,有的是阻塞,有的是非阻塞,有的是同步,有的是非同步。

以linux為例,常見的IO模型有阻塞IO、非阻塞IO、IO多路復用、信號驅動IO、非同步IO 5種,這次我們主要關註前3個,重點是IO多路復用,另外兩個在使用上有一些局限性,實際應用並不多。這5種IO模型我們在這一篇已經有詳細的介紹,這裡簡單再複習一遍。

以一個最簡單例子,現在有兩個客戶端需要連接、發送數據到我們的服務端,看下服務端在各種IO模型下是如何接收、讀取請求的。

阻塞IO(Blocking IO)

假設服務端只開啟一個線程處理請求,第一個請求到來,開始調用內核read函數,然後就會發生阻塞,第二個請求到來時服務端將無法處理,只能等第一個請求讀取完成。這種方式的缺點很明顯,每次只能處理一個請求,無法發揮cpu多核優勢,性能低下。

為瞭解決這個問題,我們可以引入多線程,這樣就可以同時處理多個請求了,但服務端可能同時有成千上萬的請求需要處理,隨之而來的是線程數膨脹,頻繁創建、銷毀線程帶來的性能影響,當然我們可以使用線程池,但服務能處理的總體數量就會受限於線程池線程數量。

非阻塞IO(NON-Blocking IO)

相比阻塞IO,非阻塞IO會立即返回,調用者不會阻塞,此時可以做一些其它事情,例如處理其它請求。但是非阻塞IO需要主動輪詢是否有數據需要處理,且這種輪詢需要從用戶態切換到內核態這,假如沒有數據產生就會有很多空輪詢,白白浪費cpu資源。

阻塞IO、非阻塞IO,要麼需要開啟更多線程去處理IO,要麼需要從用戶態切換到內核態輪詢IO事件,那麼有沒有一種機制,用戶程式只需要將請求提交給內核,由內核用少量的線程去監聽,有事件就通知用戶程式呢?這就是IO多路復用。

IO多路復用(IO Multiplexing)

IO多路復用機制是指一個線程處理多個IO流,多路是指網路連接,復用指的是同一個線程。
如果簡單從圖上看IO多路復用相比阻塞IO似乎並沒有什麼高明之處,假設服務只處理少量的連接,那麼相比阻塞IO確實沒有太大的提升,但如果連接數非常多,差距就會立竿見影。
首先IO多路復用會提交一批需要監聽的文件句柄(socket也是一種文件句柄)到內核,由內核開啟一個線程負責監聽,把輪詢工作交給內核,當有事件發生時,由內核通知用戶程式。這不需要用戶程式開啟更多的線程去處理連接,也不需要用戶程式切換到內核態去輪詢,用一個線程就能處理大量網路IO請求。
redis底層採用的就是IO多路復用模型,實際上基本所有中間件在處理網路IO這一塊都會使用到IO多路復用,如kafka,rocketmq等,所以本次學習之後對其它中間件的理解也是很有幫助的。

select/poll/epoll
這三個函數是實現linux io多路復用的內核函數,我們簡單瞭解下。

linux最開始提供的是select函數,方法如下:

select(int nfds, fd_set *r, fd_set *w, fd_set *e, struct timeval *timeout)

該方法需要傳遞3個集合,r,e,w分別表示讀、寫、異常事件集合。集合類型是bitmap,通過0/1表示該位置的fd(文件描述符,socket也是其中一種)是否關心對應讀、寫、異常事件。例如我們對fd為1和2的讀事件關心,r參數的第1,2個bit就設置為1。

用戶進程調用select函數將關心的事件傳遞給內核系統,然後就會阻塞,直到傳遞的事件至少有一個發生時,方法調用會返回。內核返回時,同樣把發生的事件用這3個參數返回回來,如r參數第1個bit為1表示fd為1的發生讀事件,第2個bit依然為0,表示fd為2的沒有發生讀事件。用戶進程調用時傳遞關心的事件,內核返回時返回發生的事件。

select存在的問題:

  1. 大小有限制。為1024,由於每次select函數調用都需要在用戶空間和內核空間傳遞這些參數,為了提升拷貝效率,linux限制最大為1024。
  2. 這3個集合有相應事件觸發時,會被內核修改,所以每次調用select方法都需要重新設置這3個集合的內容。
  3. 當有事件觸發select方法返回,需要遍歷集合才能找到就緒的文件描述符,例如傳1024個讀事件,只有一個讀事件發生,需要遍歷1024個才能找到這一個。
  4. 同樣在內核級別,每次需要遍歷集合查看有哪些事件發生,效率低下。

poll函數對select函數做了一些改進

poll(struct pollfd *fds, int nfds, int timeout)

struct pollfd {
	int fd;
	short events;
	short revents;
}

poll函數需要傳一個pollfd結構數組,其中fd表示文件描述符,events表示關心的事件,revents表示發生的事件,當有事件發生時,內核通過這個參數返回回來。

poll相比select的改進:

  1. 傳不固定大小的數組,沒有1024的限制了(問題1)
  2. 將關心的事件和實際發生的事件分開,不需要每次都重新設置參數(問題2)。例如poll數組傳1024個fd和事件,實際只有一個事件發生,那麼只需要重置一下這個fd的revent即可,而select需要重置1024個bit。

poll沒有解決select的問題3和4。另外,雖然poll沒有1024個大小的限制,但每次依然需要在用戶和內核空間傳輸這些內容,數量大時效率依然較低。

這幾個問題的根本實際很簡單,核心問題是select/poll方法對於內核來說是無狀態的,內核不會保存用戶調用傳遞的數據,所以每次都是全量在用戶和內核空間來回拷貝,如果調用時傳給內核就保存起來,有新增文件描述符需要關註就再次調用增量添加,有事件觸發時就只返回對應的文件描述符,那麼問題就迎刃而解了,這就是epoll做的事情。

epoll對應3個方法

int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

epoll_create負責創建一個上下文,用於存儲數據,底層是用紅黑樹,以後的操作就都在這個上下文上進行。
epoll_ctl負責將文件描述和所關心的事件註冊到上下文。
epoll_wait用於等待事件的發生,當有有事件觸發,就只返回對應的文件描述符了。

reactor模式

前面我們介紹的IO多路復用是操作系統的底層實現,藉助IO多路復用我們實現了一個線程就可以處理大量網路IO請求,那麼接收到這些請求後該如何高效的響應,這就是reactor要關註的事情,reactor模式是基於事件的一種設計模式。在reactor中分為3中角色:
Reactor:負責監聽和分發事件
Acceptor:負責處理連接事件
Handler:負責處理請求,讀取數據,寫回數據

從線程角度出發,reactor又可以分為單reactor單線程,單reactor多線程,多reactor多線程3種。

單reactor單線程

處理過程:reactor負責監聽連接事件,當有連接到來時,通過acceptor處理連接,得到建立好的socket對象,reactor監聽scoket對象的讀寫事件,讀寫事件觸發時,交由handler處理,handler負責讀取請求內容,處理請求內容,響應數據。
可以看到這種模式比較簡單,讀取請求數據,處理請求內容,響應數據都是在一個線程內完成的,如果整個過程響應都比較快,可以獲得比較好的結果。缺點是請求都在一個線程內完成,無法發揮多核cpu的優勢,如果處理請求內容這一塊比較慢,就會影響整體性能。

單reactor多線程

既然處理請求這裡可能由性能問題,那麼這裡可以開啟一個線程池來處理,這就是單reactor多線程模式,請求連接、讀寫還是由主線程負責,處理請求內容交由線程池處理,相比之下,多線程模式可以利用cpu多核的優勢。單仔細思考這裡依然有性能優化的點,就是對於請求的讀寫這裡依然是在主線程完成的,如果這裡也可以多線程,那效率就可以進一步提升。

多reactor多線程

多reactor多線程下,mainReactor接收到請求交由acceptor處理後,mainReactor不再讀取、寫回網路數據,直接將請求交給subReactor線程池處理,這樣讀取、寫回數據多個請求之間也可以併發執行了。

redis網路IO模型

redis網路IO模型底層使用IO多路復用,通過reactor模式實現的,在redis 6.0以前屬於單reactor單線程模式。如圖:

在linux下,IO多路復用程式使用epoll實現,負責監聽服務端連接、socket的讀取、寫入事件,然後將事件丟到事件隊列,由事件分發器對事件進行分發,事件分發器會根據事件類型,分發給對應的事件處理器進行處理。我們以一個get key簡單命令為例,一次完整的請求如下:


請求首先要建立TCP連接(TCP3次握手),過程如下:
redis服務啟動,主線程運行,監聽指定的埠,將連接事件綁定命令應答處理器。
客戶端請求建立連接,連接事件觸發,IO多路復用程式將連接事件丟入事件隊列,事件分發器將連接事件交由命令應答處理器處理。
命令應答處理器創建socket對象,將ae_readable事件和命令請求處理器關聯,交由IO多路復用程式監聽。

連接建立後,就開始執行get key請求了。如下:

客戶端發送get key命令,socket接收到數據變成可讀,IO多路復用程式監聽到可讀事件,將讀事件丟到事件隊列,由事件分發器分發給上一步綁定的命令請求處理器執行。
命令請求處理器接收到數據後,對數據進行解析,執行get命令,從記憶體查詢到key對應的數據,並將ae_writeable寫事件和響應處理器關聯起來,交由IO多路復用程式監聽。
客戶端準備好接收數據,命令請求處理器產生ae_writeable事件,IO多路復用程式監聽到寫事件,將寫事件丟到事件隊列,由事件分發器發給命令響應處理器進行處理。
命令響應處理器將數據寫回socket返回給客戶端。

reids 6.0以前網路IO的讀寫和請求的處理都在一個線程完成,儘管redis在請求處理基於記憶體處理很快,不會稱為系統瓶頸,但隨著請求數的增加,網路讀寫這一塊存在優化空間,所以redis 6.0開始對網路IO讀寫提供多線程支持。需要知道的是,redis 6.0對多線程的預設是不開啟的,可以通過 io-threads 4 參數開啟對網路寫數據多線程支持,如果對於讀也要開啟多線程需要額外設置 io-threads-do-reads yes 參數,該參數預設是no,因為redis認為對於讀開啟多線程幫助不大,但如果你通過壓測後發現有明顯幫助,則可以開啟。

redis 6.0多線程模型思想上類似單reactor多線程和多reactor多線程,但不完全一樣,這兩者handler對於邏輯處理這一塊都是使用線程池,而redis命令執行依舊保持單線程。如下:

可以看到對於網路的讀寫都是提交給線程池去執行,充分利用了cpu多核優勢,這樣主線程可以繼續處理其它請求了。
開啟多線程後多redis進行壓測結果可以參考這裡,如下圖可以看到,對於簡單命令qps可以達到20w左右,相比單線程有一倍的提升,性能提升效果明顯,對於生產環境如果大家使用了新版本的redis,現在7.0也出來了,建議開啟多線程。

總結

本篇我們學習redis單線程具體是如何單線程以及在不同版本的區別,通過網路IO模型知道IO多路復用如何用一個線程處理監聽多個網路請求,並詳細瞭解3種reactor模型,這是在IO多路復用基礎上的一種設計模式。最後學習了redis單線程、多線程版本是如何基於reactor模型處理請求。其中IO多路復用和reactor模型在許多中間件都有使用到,後續再接觸到就不陌生了。

歡迎關註我的github:https://github.com/jmilktea/jtea


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • Listener記憶體馬 0x01Lintener機制分析 Java Web 開發中的監聽器(Listener)就是 Application、Session 和 Request 三大對象創建、銷毀或者往其中添加、修改、刪除屬性時自動執行代碼的功能組件。 Listener 三個域對象 ServletCo ...
  • 就像黑火藥時代里突然誕生的核彈一樣,OpenAI的ChatGPT語言模型的橫空出世,是人工智慧技術發展史上的一個重要里程碑。這是一款無與倫比、超凡絕倫的模型,能夠進行自然語言推理和對話,並且具有出色的語言生成能力。 ...
  • 前言 之所以會搞這個手勢識別分類,其實是為了滿足之前群友提的需求,就是針對稚暉君的ElectronBot機器人的上位機軟體的功能豐富,因為本來擅長的技術棧都是.NET,也剛好試試全能的.NET是不是真的全能就想著做下試試了,MediaPipe作為谷歌開源的機器視覺庫,功能很豐富了,而且也支持c++, ...
  • 一:背景 1. 簡介 .NET 高級調試要想玩的好,看懂彙編是基本功,但看懂彙編和能寫點彙編又完全是兩回事,所以有時候看的多,總手癢癢想寫一點,在 Windows 平臺上搭建彙編環境不是那麼容易,大多還是用微軟的 MASM + DosBox 搭一個 8086 的環境,這玩意距今快 50 年了。 在以 ...
  • 原文鏈接 [https://www.cnblogs.com/densen2014/p/16964858.html] 在Blazor項目嵌入 pdf.js 時不能正確顯示中文,瀏覽器F12顯示如下錯誤 錯誤 l10n.js /web/locale/locale.properties not found ...
  • 大家好,我是痞子衡,是正經搞技術的痞子。今天痞子衡給大家講的是存儲器大廠Micron的NOR Flash晶元特殊絲印設計(FBGA代碼)。 痞子衡之前寫過一篇文章 《J-Flash在Micron Flash固定區域下載校驗失敗的故事》,這篇文章里提及了 Micron 家的串列 NOR Flash 與 ...
  • 大家好,我是痞子衡,是正經搞技術的痞子。今天痞子衡給大家講的是國內外串列NOR Flash廠商官網Cross Reference功能。 串列 NOR Flash 是一個相對發展穩定的市場,目前全球市場約 90% 的份額被中國的三家廠商(Winbond華邦/MXIC旺巨集/GigaDevice兆易創新) ...
  • 我們前面採集的日誌數據已經保存到 Kafka 中,作為日誌數據的 ODS 層,從 Kafka 的ODS 層讀取的日誌數據分為 3 類, 頁面日誌、啟動日誌和曝光日誌。這三類數據雖然都是用戶行為數據,但是有著完全不一樣的數據結構,所以要拆分處理。將拆分後的不同的日誌寫回 Kafka 不同主題中,作為日 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...