併發伺服器(五):Redis 案例研究
這是我寫的併發網路伺服器系列文章的第五部分。在前四部分中我們討論了併發伺服器的結構,這篇文章我們將去研究一個在生產系統中大量使用的伺服器的案例—— Redis。
Redis 是一個非常有魅力的專案,我關注它很久了。它最讓我著迷的一點就是它的 C 原始碼非常清晰。它也是一個高效能、大併發的記憶體資料庫伺服器的非常好的例子,它是研究網路併發伺服器的一個非常好的案例,因此,我們不能錯過這個好機會。
我們來看看前四部分討論的概念在真實世界中的應用程式。
本系列的所有文章有:
事件處理庫
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 年的一篇文章 中回答了這個問題。他的回答的要點是:ae 只有大約 770 行他理解的非常透徹的程式碼;而 libuv 程式碼量非常巨大,也沒有提供 Redis 所需的額外功能。
現在,ae 的程式碼大約增長到 1300 多行,比起 libuv 的 26000 行(這是在沒有 Windows、測試、示例、文件的情況下的資料)來說那是小巫見大巫了。libuv 是一個非常綜合的庫,這使它更復雜,並且很難去適應其它專案的特殊需求;另一方面,ae 是專門為 Redis 設計的,與 Redis 共同演進,只包含 Redis 所需要的東西。
這是我 前些年在一篇文章中 提到的軟體專案依賴關係的另一個很好的示例:
依賴的優勢與在軟體專案上花費的工作量成反比。
在某種程度上,Antirez 在他的文章中也提到了這一點。他提到,提供大量附加價值(在我的文章中的“基礎” 依賴)的依賴比像 libuv 這樣的依賴更有意義(它的例子是 jemalloc 和 Lua),對於 Redis 特定需求,其功能的實現相當容易。
Redis 中的多執行緒
在 Redis 的絕大多數歷史中,它都是一個不折不扣的單執行緒的東西。一些人覺得這太不可思議了,有這種想法完全可以理解。Redis 本質上是受網路束縛的 —— 只要資料庫大小合理,對於任何給定的客戶端請求,其大部分延時都是浪費在網路等待上,而不是在 Redis 的資料結構上。
然而,現在事情已經不再那麼簡單了。Redis 現在有幾個新功能都用到了執行緒:
對於前兩個特性,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 套接字程式碼的封裝。
via: https://eli.thegreenplace.net/2017/concurrent-servers-part-5-redis-case-study/
作者:Eli Bendersky 譯者:qhwdw 校對:wxy
相關文章
- 關於redis的幾件小事(五)redis保證高併發以及高可用Redis
- Redis-高併發篇Redis
- Redis如何防止高併發?Redis
- JDK併發AQS系列(五)JDKAQS
- 併發工具類(五) Phaser類
- Redis在.net中的使用(6)Redis併發鎖Redis
- go 併發程式設計案例二 常見併發模型介紹Go程式設計模型
- Redis實現併發阻塞鎖方案Redis
- 聊聊併發(五)——執行緒池執行緒
- Redis快取穿透、快取雪崩、redis併發問題分析Redis快取穿透
- [分散式]高併發案例---庫存超發問題分散式
- 函式儲存過程併發控制-案例函式儲存過程
- 案例研究:Healthcheck
- WirelessCar 案例研究
- 利用Redis實現高併發計數器Redis
- PHP利用Redis鎖解決併發訪問PHPRedis
- 百萬級併發 之 MQTT 伺服器MQQT伺服器
- 五、併發控制(1):執行緒的互斥執行緒
- Jmeter5.0 搶紅包併發操作案例JMeter
- Golang 併發,有快取通道,通道同步案例演示Golang快取
- 「分散式技術專題」併發系列三:樂觀併發控制之理論研究分散式
- 案例研究 之一
- AWS 案例研究:Momenta
- 實時資料併發寫入 Redis 優化Redis優化
- 線上Redis高併發效能調優實踐Redis
- InnoDB學習(五)之MVCC多版本併發控制MVC
- 用PHP實現高併發伺服器PHP伺服器
- 如何提升伺服器的高併發能力伺服器
- Python web伺服器3: 靜態伺服器&併發web伺服器PythonWeb伺服器
- go 併發程式設計案例一 課程介紹Go程式設計
- 【高併發】Redis如何助力高併發秒殺系統,看完這篇我徹底懂了!!Redis
- Java併發相關知識點梳理和研究Java
- IM伺服器:開發一個高併發的IM伺服器難在哪伺服器
- Redis效能篇(五)Redis緩衝區Redis
- Java併發:分散式應用限流 Redis + Lua 實踐Java分散式Redis
- 超高併發下,Redis熱點資料風險破解Redis
- 一次併發處理過程, 基於 RedisRedis
- 使用redis中setnx防止併發二次寫入Redis