Redis:事件驱动(IO多路复用)
目錄
§??從Redis的工作模式談起
§??Reactor模式
·????????C10K問題
·????????I/O多路復(fù)用技術(shù)
·????????Reactor的定義
·????????Java中的NIO與Netty
§??Redis與Reactor
§??總結(jié)
§??參考資料
從Redis的工作模式談起
我們在使用Redis的時候,通常是多個客戶端連接Redis服務(wù)器,然后各自發(fā)送命令請求(例如GetSet)到Redis服務(wù)器,最后Redis處理這些請求返回結(jié)果
那Redis服務(wù)端是使用單進程還是多進程,單線程還是多線程來處理客戶端請求的呢?
答案是單進程單線程
當(dāng)然,Redis除了處理客戶端的命令請求還有諸如RDB持久化AOF重寫這樣的事情要做,而在做這些事情的時候,Redis會fork(分叉出)子進程去完成但對于accept客戶端連接處理客戶端請求返回命令結(jié)果等等這些,Redis是使用主進程及主線程來完成的。我們可能會驚訝Redis在使用單進程及單線程來處理請求為什么會如此高效?在回答這個問題之前,
我們先來討論一個I/O多路復(fù)用的模式--Reactor
Reactor模式
C10K問題
考慮這樣一個問題:有10000個客戶端需要連上一個服務(wù)器并保持TCP連接,客戶端會不定時的發(fā)送請求給服務(wù)器,服務(wù)器收到請求后需及時處理并返回結(jié)果我們應(yīng)該怎么解決?
方案一:我們使用一個線程來監(jiān)聽,當(dāng)一個新的客戶端發(fā)起連接時,建立連接并new一個線程來處理這個新連接
缺點:當(dāng)客戶端數(shù)量很多時,服務(wù)端線程數(shù)過多,即便不壓垮服務(wù)器,由于CPU有限其性能也極其不理想因此此方案不可用
方案二:我們使用一個線程監(jiān)聽,當(dāng)一個新的客戶端發(fā)起連接時,建立連接并使用線程池處理該連接
優(yōu)點:客戶端連接數(shù)量不會壓垮服務(wù)端
缺點:服務(wù)端處理能力受限于線程池的線程數(shù),而且如果客戶端連接中大部分處于空閑狀態(tài)的話服務(wù)端的線程資源被浪費
因此,一個線程僅僅處理一個客戶端連接無論如何都是不可接受的,那能不能一個線程處理多個連接呢?該線程輪詢每個連接,如果某個連接有請求則處理請求,沒有請求則處理下一個連接,這樣可以實現(xiàn)嗎?
答案是肯定的,而且不必輪詢我們可以通過I/O多路復(fù)用技術(shù)來解決這個問題
I/O多路復(fù)用技術(shù)(三種里最佳)
現(xiàn)代的UNIX操作系統(tǒng)提供了select/poll/kqueue/epoll這樣的系統(tǒng)調(diào)用,這些系統(tǒng)調(diào)用的功能是:你告知我一批套接字(socket),當(dāng)這些套接字的可讀或可寫事件發(fā)生時,我通知你這些事件信息。(IO中講到的,里面的事件分離者。在我的理解有點像中介的味道,在socket和事件處理者中充當(dāng)傳話的角色)、
I/O 多路復(fù)用模塊(整個 I/O 多路復(fù)用模塊在事件循環(huán)看來就是一個輸入事件、輸出 aeFiredEvent 數(shù)組的一個黑箱)
I/O 多路復(fù)用模塊封裝了底層的 select、epoll、avport 以及 kqueue這些 I/O 多路復(fù)用函數(shù)(實現(xiàn)了handle找實現(xiàn)的handler過程),為上層提供了相同的接口。
?
當(dāng)如下任一情況發(fā)生時,會產(chǎn)生套接字的可讀事件:
§??該套接字的接收緩沖區(qū)中的數(shù)據(jù)字節(jié)數(shù)大于等于套接字接收緩沖區(qū)低水位標記的大小;
§??該套接字的讀半部關(guān)閉(也就是收到了FIN),對這樣的套接字的讀操作將返回0(也就是返回EOF);
§??該套接字是一個監(jiān)聽套接字且已完成的連接數(shù)不為0;
§??該套接字有錯誤待處理,對這樣的套接字的讀操作將返回-1
當(dāng)如下任一情況發(fā)生時,會產(chǎn)生套接字的可寫事件:
§??該套接字的發(fā)送緩沖區(qū)中的可用空間字節(jié)數(shù)大于等于套接字發(fā)送緩沖區(qū)低水位標記的大小;
§??該套接字的寫半部關(guān)閉,繼續(xù)寫會產(chǎn)生SIGPIPE信號;
§??非阻塞模式下,connect返回之后,該套接字連接成功或失敗;
§??該套接字有錯誤待處理,對這樣的套接字的寫操作將返回-1
此外,在UNIX系統(tǒng)上,一切皆文件套接字也不例外,每一個套接字都有對應(yīng)的fd(即文件描述符)我們簡單看看這幾個系統(tǒng)調(diào)用的原型
select(int nfds, fd_set *r, fd_set *w,fd_set *e, struct timeval *timeout)
對于select(),我們需要傳3個集合,r(讀),w(寫)和e其中,r表示我們對哪些fd的可讀事件感興趣,w表示我們對哪些fd的可寫事件感興趣每個集合其實是一個bitmap,通過0/1表示我們感興趣的fd例如,
如:我們對于fd為6的可讀事件感興趣,那么r集合的第6個bit需要被設(shè)置為1這個系統(tǒng)調(diào)用會阻塞,直到我們感興趣的事件(至少一個)發(fā)生調(diào)用返回時,內(nèi)核同樣使用這3個集合來存放fd實際發(fā)生的事件信息也就是說,調(diào)用前這3個集合表示我們感興趣的事件,調(diào)用后這3個集合表示實際發(fā)生的事件
select為最早期的UNIX系統(tǒng)調(diào)用,它存在4個問題:
1)這3個bitmap有大小限制(FD_SETSIZE,通常為1024);
2)由于這3個集合在返回時會被內(nèi)核修改,因此我們每次調(diào)用時都需要重新設(shè)置;
3)我們在調(diào)用完成后需要掃描這3個集合才能知道哪些fd的讀/寫事件發(fā)生了,一般情況下全量集合比較大而實際發(fā)生讀/寫事件的fd比較少,效率比較低下;
4)內(nèi)核在每次調(diào)用都需要掃描這3個fd集合,然后查看哪些fd的事件實際發(fā)生,在讀/寫比較稀疏的情況下同樣存在效率問題
由于存在這些問題,于是人們對select進行了改進,從而有了poll
poll(struct pollfd *fds, int nfds, inttimeout)
?
struct pollfd {
int fd;
short events;
short revents;
}
?
poll調(diào)用需要傳遞的是一個pollfd結(jié)構(gòu)的數(shù)組,調(diào)用返回時結(jié)果信息也存放在這個數(shù)組里面pollfd的結(jié)構(gòu)中存放著fd我們對該fd感興趣的事件(events)以及該fd實際發(fā)生的事件(revents)poll傳遞的不是固定大小的bitmap,因此
select的問題1解決了;poll將感興趣事件和實際發(fā)生事件分開了,因此
select的問題2也解決了但
select的問題3和問題4仍然沒有解決
select問題3比較容易解決,只要系統(tǒng)調(diào)用返回的是實際發(fā)生相應(yīng)事件的fd集合,我們便不需要掃描全量的fd集合
對于select的問題4,我們?yōu)槭裁葱枰看握{(diào)用都傳遞全量的fd呢?
內(nèi)核可不可以在第一次調(diào)用的時候記錄這些fd,然后我們在以后的調(diào)用中不需要再傳這些fd呢?
問題的關(guān)鍵在于無狀態(tài)對于每一次系統(tǒng)調(diào)用,內(nèi)核不會記錄下任何信息,所以每次調(diào)用都需要重復(fù)傳遞相同信息
上帝說要有狀態(tài),所以我們有了epoll和kqueue
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的作用是創(chuàng)建一個context,這個context相當(dāng)于狀態(tài)保存者的概念
epoll_ctl的作用是,當(dāng)你對一個新的fd的讀/寫事件感興趣時,通過該調(diào)用將fd與相應(yīng)的感興趣事件更新到context中
epoll_wait的作用是,等待context中fd的事件發(fā)生
就是這么簡單
epoll是Linux中的實現(xiàn),kqueue則是在FreeBSD的實現(xiàn)
int kqueue(void);
int kevent(int kq, const struct kevent*changelist, int nchanges, struct kevent *eventlist, int nevents, const structtimespec *timeout);
與epoll相同的是,kqueue創(chuàng)建一個context;與epoll不同的是,kqueue用kevent代替了epoll_ctl和epoll_wait
epoll和kqueue解決了select存在的問題通過它們,我們可以高效的通過系統(tǒng)調(diào)用來獲取多個套接字的讀/寫事件,從而解決一個線程處理多個連接的問題
Reactor的定義
通過select/poll/epoll/kqueue這些I/O多路復(fù)用函數(shù)庫,我們解決了一個線程處理多個連接的問題,但整個Reactor模式的完整框架是怎樣的呢?參考這篇paper,我們可以對Reactor模式有個完整的描述
?
Handles:表示操作系統(tǒng)管理的資源,我們可以理解為fd
Synchronous Event Demultiplexer:同步事件分離器,阻塞等待Handles中的事件發(fā)生
Initiation Dispatcher:初始分派器,作用為添加Event handler(事件處理器)刪除Event handler以及分派事件給Event handler也就是說,SynchronousEvent Demultiplexer負責(zé)等待新事件發(fā)生,事件發(fā)生時通知InitiationDispatcher,然后Initiation Dispatcher調(diào)用event handler處理事件
Event Handler:事件處理器的接口
Concrete Event Handler:事件處理器的實際實現(xiàn),而且綁定了一個Handle因為在實際情況中,我們往往不止一種事件處理器,因此這里將事件處理器接口和實現(xiàn)分開,與C++Java這些高級語言中的多態(tài)類似
以上各子模塊間協(xié)作的步驟描述如下:(其實就是項目中所做的基于redis 的異步框架差不多)
1.????我們注冊Concrete Event Handler到InitiationDispatcher中
2.???Initiation Dispatcher調(diào)用每個Event Handler的get_handle接口獲取其綁定的Handle
3.???Initiation Dispatcher調(diào)用handle_events開始事件處理循環(huán)在這里,InitiationDispatcher會將步驟2獲取的所有Handle都收集起來,使用Synchronous Event Demultiplexer來等待這些Handle的事件發(fā)生
4.????當(dāng)某個(或某幾個)Handle的事件發(fā)生時,Synchronous Event Demultiplexer通知InitiationDispatcher
5.???Initiation Dispatcher根據(jù)發(fā)生事件的Handle找出所對應(yīng)的Handler
6.????InitiationDispatcher調(diào)用Handler的handle_event方法處理事件
時序圖如下:
另外,該文章舉了一個分布式日志處理的例子,感興趣的同學(xué)可以看下
通過以上的敘述,我們清楚了Reactor的大概框架以及涉及到的底層I/O多路復(fù)用技術(shù)
Java中的NIO與Netty
談到Reactor模式,在這里奉上Java大神Doug Lea的Scalable IO in Java,里面提到了Java網(wǎng)絡(luò)編程中的經(jīng)典模式NIO(非堵塞)以及Reactor,并且有相關(guān)代碼幫助理解,看完后獲益良多
另外,Java的NIO是比較底層的,我們實際在網(wǎng)絡(luò)編程中還需要自己處理很多問題(譬如socket的讀半包),稍不注意就會掉進坑里幸好,我們有了Netty這么一個網(wǎng)絡(luò)處理框架,免去了很多麻煩
Redis與Reactor
在上面的討論中,我們了解了Reactor模式,那么Redis中又是怎么使用Reactor模式的呢?
首先,Redis服務(wù)器中有兩類事件,文件事件和時間事件
§??文件事件(file event):Redis客戶端通過socket與Redis服務(wù)器連接,而文件事件就是服務(wù)器對套接字操作的抽象例如,客戶端發(fā)了一個GET命令請求,對于Redis服務(wù)器來說就是一個文件事件
§??時間事件(time event):服務(wù)器定時或周期性執(zhí)行的事件例如,定期執(zhí)行RDB持久化
在這里我們主要關(guān)注Redis處理文件事件的模型參考Redis的設(shè)計與實現(xiàn),Redis的文件事件處理模型是這樣的:
在這個模型中,Redis服務(wù)器用主線程執(zhí)行I/O多路復(fù)用程序文件事件分派器以及事件處理器而且,盡管多個文件事件可能會并發(fā)出現(xiàn),Redis服務(wù)器是順序處理各個文件事件的
Redis服務(wù)器主線程的執(zhí)行流程在Redis.c的main函數(shù)中體現(xiàn),而關(guān)于處理文件事件的主要的有這幾行:
int main(int argc, char **argv) {
...
initServer();
...
aeMain();
...
aeDeleteEventLoop(server.el);
return 0;
}
在initServer()中,建立各個事件處理器;在aeMain()中,執(zhí)行事件處理循環(huán);在aeDeleteEventLoop(server.el)中關(guān)閉停止事件處理循環(huán);最后退出
總結(jié)
多路 I/O 復(fù)用模型是利用select、poll、epoll可以同時監(jiān)察多個流的 I/O 事件的能力,在空閑的時候,會把當(dāng)前線程阻塞掉,當(dāng)有一個或多個流有I/O事件時,就從阻塞態(tài)中喚醒,于是程序就會輪詢一遍所有的流(epoll是只輪詢那些真正發(fā)出了事件的流),并且只依次順序的處理就緒的流,這種做法就避免了大量的無用操作。這里“多路”指的是多個網(wǎng)絡(luò)連接,“復(fù)用”指的是復(fù)用同一個線程。采用多路 I/O 復(fù)用技術(shù)可以讓單個線程高效的處理多個連接請求(盡量減少網(wǎng)絡(luò)IO的時間消耗),且Redis在內(nèi)存中操作數(shù)據(jù)的速度非常快(內(nèi)存內(nèi)的操作不會成為這里的性能瓶頸),主要以上兩點造就了Redis具有很高的吞吐量。
在這篇文章中,我們從Redis的工作模型開始,討論了C10K問題、I/O多路復(fù)用技術(shù)、Java的NIO,最后回歸到Redis的Reactor模式中。如有紕漏,懇請大家指出,我會一一加以勘正。謝謝!
?
總結(jié)
以上是生活随笔為你收集整理的Redis:事件驱动(IO多路复用)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Redis:事务、管道、Lua脚本
- 下一篇: MYSQL:约束?添加约束?删除约束?以