紅聯Linux門戶
Linux幫助

并發服務器(五):Redis案例研究

發布時間:2018-03-08 09:32:15來源:linux.cn作者:qhwdw
這是我寫的并發網絡服務器系列文章的第五部分。在前四部分中我們討論了并發服務器的結構,這篇文章我們將去研究一個在生產系統中大量使用的服務器的案例—— Redis。
Redis 是一個非常有魅力的項目,我關注它很久了。它最讓我著迷的一點就是它的 C 源代碼非常清晰。它也是一個高性能、大并發的內存數據庫服務器的非常好的例子,它是研究網絡并發服務器的一個非常好的案例,因此,我們不能錯過這個好機會。
我們來看看前四部分討論的概念在真實世界中的應用程序。
 
本系列的所有文章有:
本系列的所有文章:
第一節 - 簡介(http://www.4179693.live/linux/32842.html)
第二節 - 線程(http://www.4179693.live/linux/32844.html)
第三節 - 事件驅動(http://www.4179693.live/linux/32997.html)
第四節 - libuv(http://www.4179693.live/linux/33257.html)
 
事件處理庫
Redis 最初發布于 2009 年,它最牛逼的一件事情大概就是它的速度 —— 它能夠處理大量的并發客戶端連接。需要特別指出的是,它是用一個單線程來完成的,而且還不對保存在內存中的數據使用任何復雜的鎖或者同步機制。
Redis 之所以如此牛逼是因為,它在給定的系統上使用了其可用的最快的事件循環,并將它們封裝成由它實現的事件循環庫(在 Linux 上是 epoll,在 BSD 上是 kqueue,等等)。這個庫的名字叫做 ae。ae 使得編寫一個快速服務器變得很容易,只要在它內部沒有阻塞即可,而 Redis 則保證 注1 了這一點。
在這里,我們的興趣點主要是它對文件事件的支持 —— 當文件描述符(如網絡套接字)有一些有趣的未決事情時將調用注冊的回調函數。與 libuv 類似,ae 支持多路事件循環(參閱本系列的第三節和第四節)和不應該感到意外的 aeCreateFileEvent 信號:
int aeCreateFileEvent(aeEventLoop *eventLoop, int fd, int mask,
aeFileProc *proc, void *clientData);
它在 fd 上使用一個給定的事件循環,為新的文件事件注冊一個回調(proc)函數。當使用的是 epoll 時,它將調用 epoll_ctl 在文件描述符上添加一個事件(可能是 EPOLLIN、EPOLLOUT、也或許兩者都有,取決于 mask 參數)。ae 的 aeProcessEvents 功能是 “運行事件循環和發送回調函數”,它在底層調用了 epoll_wait。
 
處理客戶端請求
我們通過跟蹤 Redis 服務器代碼來看一下,ae 如何為客戶端事件注冊回調函數的。initServer 啟動時,通過注冊一個回調函數來讀取正在監聽的套接字上的事件,通過使用回調函數 acceptTcpHandler 來調用 aeCreateFileEvent。當新的連接可用時,這個回調函數被調用。它調用 accept 注2 ,接下來是 acceptCommonHandler,它轉而去調用 createClient 以初始化新客戶端連接所需要的數據結構。
createClient 的工作是去監聽來自客戶端的入站數據。它將套接字設置為非阻塞模式(一個異步事件循環中的關鍵因素)并使用 aeCreateFileEvent 去注冊另外一個文件事件回調函數以讀取事件 —— readQueryFromClient。每當客戶端發送數據,這個函數將被事件循環調用。
readQueryFromClient 就讓我們期望的那樣 —— 解析客戶端命令和動作,并通過查詢和/或操作數據來回復。因為客戶端套接字是非阻塞的,所以這個函數必須能夠處理 EAGAIN,以及部分數據;從客戶端中讀取的數據是累積在客戶端專用的緩沖區中,而完整的查詢可能被分割在回調函數的多個調用當中。
 
將數據發送回客戶端
在前面的內容中,我說到了 readQueryFromClient 結束了發送給客戶端的回復。這在邏輯上是正確的,因為 readQueryFromClient 準備要發送回復,但它不真正去做實質的發送 —— 因為這里并不能保證客戶端套接字已經準備好寫入/發送數據。我們必須為此使用事件循環機制。
Redis 是這樣做的,它注冊一個 beforeSleep 函數,每次事件循環即將進入休眠時,調用它去等待套接字變得可以讀取/寫入。beforeSleep 做的其中一件事情就是調用 handleClientsWithPendingWrites。它的作用是通過調用 writeToClient 去嘗試立即發送所有可用的回復;如果一些套接字不可用時,那么當套接字可用時,它將注冊一個事件循環去調用 sendReplyToClient。這可以被看作為一種優化 —— 如果套接字可用于立即發送數據(一般是 TCP 套接字),這時并不需要注冊事件 ——直接發送數據。因為套接字是非阻塞的,它從不會去阻塞循環。
 
為什么 Redis 要實現它自己的事件庫?
在 第四節 中我們討論了使用 libuv 來構建一個異步并發服務器。需要注意的是,Redis 并沒有使用 libuv,或者任何類似的事件庫,而是它去實現自己的事件庫 —— ae,用 ae 來封裝 epoll、kqueue 和 select。事實上,Antirez(Redis 的創建者)恰好在 2011 年的一篇文章(http://oldblog.antirez.com/post/redis-win32-msft-patch.html) 中回答了這個問題。他的回答的要點是:ae 只有大約 770 行他理解的非常透徹的代碼;而 libuv 代碼量非常巨大,也沒有提供 Redis 所需的額外功能。
現在,ae 的代碼大約增長到 1300 多行,比起 libuv 的 26000 行(這是在沒有 Windows、測試、示例、文檔的情況下的數據)來說那是小巫見大巫了。libuv 是一個非常綜合的庫,這使它更復雜,并且很難去適應其它項目的特殊需求;另一方面,ae 是專門為 Redis 設計的,與 Redis 共同演進,只包含 Redis 所需要的東西。
這是我 前些年在一篇文章中(http://eli.thegreenplace.net/2017/benefits-of-dependencies-in-software-projects-as-a-function-of-effort/) 提到的軟件項目依賴關系的另一個很好的示例:
依賴的優勢與在軟件項目上花費的工作量成反比。
在某種程度上,Antirez 在他的文章中也提到了這一點。他提到,提供大量附加價值(在我的文章中的“基礎” 依賴)的依賴比像 libuv 這樣的依賴更有意義(它的例子是 jemalloc 和 Lua),對于 Redis 特定需求,其功能的實現相當容易。
 
Redis 中的多線程
在 Redis 的絕大多數歷史中,它都是一個不折不扣的單線程的東西。一些人覺得這太不可思議了,有這種想法完全可以理解。Redis 本質上是受網絡束縛的 —— 只要數據庫大小合理,對于任何給定的客戶端請求,其大部分延時都是浪費在網絡等待上,而不是在 Redis 的數據結構上。
然而,現在事情已經不再那么簡單了。Redis 現在有幾個新功能都用到了線程:
1.“惰性” 內存釋放。
2.在后臺線程中使用 fsync 調用寫一個 持久化日志。
3.運行需要執行一個長周期運行的操作的用戶定義模塊。
對于前兩個特性,Redis 使用它自己的一個簡單的 bio(它是 “Background I/O" 的首字母縮寫)庫。這個庫是根據 Redis 的需要進行了硬編碼,它不能用到其它的地方 —— 它運行預設數量的線程,每個 Redis 后臺作業類型需要一個線程。
而對于第三個特性,Redis 模塊 可以定義新的 Redis 命令,并且遵循與普通 Redis 命令相同的標準,包括不阻塞主線程。如果在模塊中自定義的一個 Redis 命令,希望去執行一個長周期運行的操作,這將創建一個線程在后臺去運行它。在 Redis 源碼樹中的 src/modules/helloblock.c 提供了這樣的一個示例。
有了這些特性,Redis 使用線程將一個事件循環結合起來,在一般的案例中,Redis 具有了更快的速度和彈性,這有點類似于在本系統文章中 第四節 討論的工作隊列。
 
注1:Redis 的一個核心部分是:它是一個 內存中 數據庫;因此,查詢從不會運行太長的時間。當然了,這將會帶來各種各樣的其它問題。在使用分區的情況下,服務器可能最終路由一個請求到另一個實例上;在這種情況下,將使用異步 I/O 來避免阻塞其它客戶端。
注2:使用 anetAccept;anet 是 Redis 對 TCP 套接字代碼的封裝。
 
如何讓網站不下線而從Redis 2遷移到Redis 3:http://www.4179693.live/linux/32802.html
ubuntu 17.04安裝最新版本redis:http://www.4179693.live/linux/32785.html
Linux Redis重啟數據丟失解決方案:http://www.4179693.live/linux/32076.html
Ubuntu16.04下安裝nginx+mysql+php+redis:http://www.4179693.live/linux/31935.html
在阿里云上開放Redis默認的6379端口:http://www.4179693.live/linux/31347.html
601268股票行情中心