|
| 作者 吳顯堅(jiān),騰訊云數(shù)據(jù)庫高級(jí)工程師,參與過360開源項(xiàng)目Pika的研發(fā)工作,現(xiàn)從事redis數(shù)據(jù)庫研發(fā)工作。 Redis服務(wù)器是一個(gè)事件驅(qū)動(dòng)程序, 事件是Redis服務(wù)器的核心, 它處理兩項(xiàng)重要的任務(wù), 一個(gè)是IO事件(文件事件), 另外一個(gè)是時(shí)間事件. Redis服務(wù)器通過套接字與客戶端進(jìn)行連接, 而文件事件可以理解為服務(wù)器對(duì)套接字操作的抽象. 服務(wù)器與客戶端的通信會(huì)產(chǎn)生相應(yīng)的文件事件, 而服務(wù)器則通過監(jiān)聽并處理這些事件來完成一系列網(wǎng)絡(luò)通信操作. 另外Redis內(nèi)部有一些操作(從Redis4.0的代碼分析目前時(shí)間事件只有serverCron)需要在給定的時(shí)間點(diǎn)執(zhí)行, 而時(shí)間事件就是Redis服務(wù)器對(duì)這類定時(shí)操作的抽象。 一、aeEventLoop 在分析具體代碼之前, 我們先了解一下在事件處理中處于核心部分的aeEventLoop到底是什么:
創(chuàng)建aeEventLoop只需要一個(gè)setsize參數(shù), 它標(biāo)識(shí)了當(dāng)前aeEventLoop最大可以監(jiān)聽的文件描述符數(shù)(通常redis傳入server.maxclients+CONFIG_FDSET_INCR,也就是在用戶指定的最大客戶端連接數(shù)的基礎(chǔ)上再額外增加128, 這128可以用于Redis內(nèi)部打開AOF,RDB文件以及主從, 集群互相通信所對(duì)應(yīng)的文件句柄), 創(chuàng)建aeEventLoop時(shí), aeFileEvent和aeFiredEvent數(shù)組的大小就由setsize確定。 1. aeFileEvent 內(nèi)部以掩碼的形式存儲(chǔ)了當(dāng)前套接字關(guān)心的事件(可讀/可寫事件), 內(nèi)部還有兩個(gè)函數(shù)指針指向可讀/可寫事件發(fā)生時(shí)應(yīng)該調(diào)用的函數(shù), 另外還有一個(gè)無類型的指針指向相關(guān)聯(lián)的數(shù)據(jù), 這里需要注意的是, events是一個(gè)數(shù)組, 而套接字就是作為下標(biāo)來進(jìn)行索引對(duì)應(yīng)aeFileEvent, 例如我當(dāng)前關(guān)心的套接字是9, 那么events[9]就是它對(duì)應(yīng)的文件事件數(shù)據(jù)結(jié)構(gòu)(csapp中提到過, 當(dāng)我們調(diào)用系統(tǒng)函數(shù)返回描述符數(shù)字時(shí), 返回的描述符總是在進(jìn)程中當(dāng)前沒有打開的最小描述符, 所以我們無需擔(dān)心文件描述符被反復(fù)的創(chuàng)建銷毀, 而越來越大的問題)。 2. aeFiredEvent 內(nèi)部以掩碼的形式存儲(chǔ)了當(dāng)前已經(jīng)觸發(fā)的事件和對(duì)應(yīng)的套接字, 實(shí)際上fired數(shù)組只有在調(diào)用aeApiPoll的時(shí)候才會(huì)被賦值, 例如當(dāng)前發(fā)現(xiàn)有套接字6, 8有可讀事件, 而套接字10有可寫事件, 那么fired數(shù)組的前三個(gè)元素會(huì)被賦值{fd = 6, mask =AE_READABLE}, {fd = 8, mask = AE_READABLE}, {fd = 10, mask = AE_WRITABLE}, 緊接著我們以6為索引, 找到文件事件數(shù)據(jù)結(jié)構(gòu)events[6],然后發(fā)現(xiàn)觸發(fā)的是可讀事件, 我們?cè)僬{(diào)用events[6]中rfileProc來處理可讀事件。
對(duì)于時(shí)間事件, aeEventLoop中有一個(gè)timeEventHead指針指向第一個(gè)時(shí)間事件, 由于aeEventLoop創(chuàng)建之初, 內(nèi)部沒有任何時(shí)間事件, 所以初始化時(shí)timeEventHead指向NULL, 每當(dāng)有新的時(shí)間事件時(shí), 總會(huì)被添加到timeEventHead頭部, 由于aeTimeEvent結(jié)構(gòu)體中有next指針可以指向下一個(gè)aeTimeEvent結(jié)構(gòu)體, 所以只要我們獲取timeEventHead就能遍歷當(dāng)前所有的時(shí)間事件了, 另外有一個(gè)細(xì)節(jié)需要注意, 最后一個(gè)aeTimeEvent結(jié)構(gòu)體中的next指針指向的是timeEventHead, 所以所有時(shí)間事件實(shí)際上就是由一個(gè)環(huán)形鏈表串連起來的。
二、文件事件 在介紹中有提到過文件事件實(shí)際上就是服務(wù)器對(duì)套接字操作的抽象, 當(dāng)套接字有可讀\寫事件觸發(fā)的時(shí)候, 我們需要調(diào)用相應(yīng)的處理函數(shù), 下面先看一下跟文件事件相關(guān)的結(jié)構(gòu)體:
在aeEventLoop初始化的時(shí)候會(huì)為aeFileEvent數(shù)組(events)分配空間, 數(shù)組的大小由參數(shù)setsize指定,表明了當(dāng)前Redis最大打開的套接字的大小, 套接字與aeFileEvent一一對(duì)應(yīng), 也就是說我們可以通過套接字?jǐn)?shù)值作為索引到events數(shù)組中找到他對(duì)應(yīng)的aeFileEvent對(duì)象。 當(dāng)我們?cè)赼eEventLoop中注冊(cè)一個(gè)文件事件時(shí), 首先我們判斷傳入的套接字對(duì)events數(shù)組是否有越界行為, 若沒有越界行為, 我們便可以獲取與當(dāng)前套接字對(duì)應(yīng)的aeFileEvent對(duì)象, 然后調(diào)用aeApiAddEvent將當(dāng)前的文件描述符以及監(jiān)聽的事件注冊(cè)到底層IO多路復(fù)用機(jī)制(epoll, select, evport, kqueue其中之一)中, 另外我們還需要指定當(dāng)可讀/可寫事件發(fā)生時(shí)需要調(diào)用的函數(shù),另外當(dāng)前文件事件的一些私有數(shù)據(jù)被存放在clientData指向的對(duì)象當(dāng)中。
三、時(shí)間事件 Redis內(nèi)部的時(shí)間事件實(shí)際可以分為兩類, 一類是定時(shí)事件, 也就是需要在未來某一個(gè)時(shí)間點(diǎn)觸發(fā)的事件(只觸發(fā)一次), 另外一類是周期性事件,和前面的定時(shí)事件只觸發(fā)一次不同, 周期性事件是每隔一段時(shí)間又會(huì)重新觸發(fā)一次。 Redis使用了timeProc指向函數(shù)的返回值來判斷當(dāng)前屬于哪類事件, 若函數(shù)返回AE_NOMORE(也就是-1),說明當(dāng)前事件無需再次觸發(fā)(將id置刪除標(biāo)記AE_DELETED_EVENT_ID), 若函數(shù)返回一個(gè)大于等于0的值n, 說明再等待n秒, 該事件需要再重新被觸發(fā)(根據(jù)返回值更新when_sec和when_ms),在博客開頭提到的serverCron時(shí)間事件實(shí)際上就是一個(gè)周期性事件, 函數(shù)末尾會(huì)返回1000/server.hz, server.hz默認(rèn)被設(shè)置為10, 也就是說serverCron平均每間隔100ms會(huì)被調(diào)用一次。
Redis調(diào)用aeCreateTimeEvent來創(chuàng)建一個(gè)時(shí)間任務(wù), 實(shí)現(xiàn)非常簡單, 傳參我們關(guān)注一下milliseconds和proc即可, 前者指定了時(shí)間事件距離當(dāng)前的觸發(fā)時(shí)間, 后者指定了時(shí)間事件觸發(fā)時(shí)應(yīng)調(diào)用的函數(shù), 內(nèi)部通過aeAddMillisecondsToNow將當(dāng)前定時(shí)任務(wù)觸發(fā)的時(shí)間戳計(jì)算出來賦值給when_sec和when_ms, 然后再將timeProc指向時(shí)間事件到達(dá)時(shí)應(yīng)該調(diào)用的函數(shù)。 在完成了aeTimeEvent結(jié)構(gòu)體內(nèi)部變量賦值之后, 最后將其添加到aeEventLoop內(nèi)部的存儲(chǔ)定時(shí)間事件的環(huán)形鏈表的頭部中(這里需要注意的是, 由于我們總是將新的時(shí)間事件加入環(huán)形鏈表的頭部, 所以時(shí)間事件觸發(fā)的時(shí)間先后并不是在環(huán)形鏈表中有序的, 我們需要將環(huán)形鏈表遍歷完畢才能保證當(dāng)前已經(jīng)到達(dá)的時(shí)間事件都已經(jīng)被處理完畢, 不過由于在開頭提到過, 目前Redis只存在serverCron一個(gè)時(shí)間事件, 所以我們無需擔(dān)心遍歷環(huán)形鏈表影響服務(wù)性能), 此時(shí)一個(gè)時(shí)間事件就算創(chuàng)建完成了。
Redis通過aeDeleteTimeEvent函數(shù)來刪除一個(gè)時(shí)間任務(wù), 傳參只有一個(gè)待刪除時(shí)間事件的id, 我們發(fā)現(xiàn)這里的刪除實(shí)際上是一種惰性刪除, 將aeTimeEvent中的id標(biāo)記為AE_DELETED_EVENT_ID, 而不是直接將aeTimeEvent對(duì)象從鏈表中刪除并且釋放, 個(gè)人認(rèn)為這么實(shí)現(xiàn)的原因更多是為了安全考慮以及代碼的簡潔性, 考慮在一個(gè)時(shí)間事件中本來想刪除另外一個(gè)時(shí)間事件, 但是由于id填錯(cuò), 誤刪成自己了, 此時(shí)如果釋放自身aeTimeEvent對(duì)象, 這是十分危險(xiǎn)的。
四、事件的調(diào)度與執(zhí)行 Redis是單線程的, 內(nèi)部是一直處于aeMain中的while循環(huán)中, 而循環(huán)內(nèi)部不斷調(diào)用aeProcessEvents函數(shù), 該函數(shù)會(huì)對(duì)上面提到的文件事件和時(shí)間事件進(jìn)行調(diào)度, 決定何時(shí)處理文件事件以及時(shí)間事件。
實(shí)際上aeProcessEvents函數(shù)內(nèi)部做的事情也非常簡單, 下面進(jìn)行了梳理: 1. 首先調(diào)用aeSearchNearestTimer獲取到達(dá)時(shí)間距離當(dāng)前最近的時(shí)間事件; 2. 計(jì)算上一步獲取到的時(shí)間事件還有多久才可以觸發(fā), 并且將結(jié)果記錄到一個(gè)struct timeval*指針指向的結(jié)構(gòu)體中(若在步驟一中沒有獲取到時(shí)間事件對(duì)象, 那么指針為NULL); 3. 阻塞并等待文件事件的產(chǎn)生, 最大的阻塞時(shí)間由步驟二決定(步驟二指針為NULL的場景表示當(dāng)前沒有時(shí)間事件, 我們可以永遠(yuǎn)阻塞, 直到有文件事件到達(dá)); 4. 如果在最大阻塞時(shí)間內(nèi)獲取到了文件事件, 則根據(jù)文件事件的類型調(diào)用對(duì)應(yīng)的讀事件處理函數(shù)或者寫事件處理函數(shù); 5. 遍歷時(shí)間事件鏈表, 在這個(gè)過程中可能會(huì)遇到id為AE_DELETED_EVENT_ID的代表已經(jīng)做了刪除標(biāo)記的時(shí)間事件, 需要將該時(shí)間事件從鏈表中移除, 并且進(jìn)行釋放, 如遇到已經(jīng)達(dá)到的時(shí)間事件, 則調(diào)用其綁定的處理函數(shù), 并且根據(jù)返回值來判斷該事件時(shí)間是否需要在給定的時(shí)間內(nèi)再重新觸發(fā)。 五、問題 Q1: 時(shí)間事件觸發(fā)的時(shí)間一定精準(zhǔn)么? A1: 時(shí)間事件的觸發(fā)并不能在指定的時(shí)間精準(zhǔn)觸發(fā), 一般都要比指定的時(shí)間稍晚一點(diǎn), 此外在Redis單線程模型下, 時(shí)間事件都是串行執(zhí)行的, 中間如果某個(gè)時(shí)間事件處理時(shí)間長, 更加影響了后面時(shí)間事件執(zhí)行時(shí)間的精準(zhǔn)性. 而且時(shí)間事件鏈表是無序的, 所以在極端場景下, 存在優(yōu)先級(jí)低的時(shí)間事件比優(yōu)先級(jí)高的事件先觸發(fā)的可能性, 不過好在目前Redis內(nèi)部只有一個(gè)時(shí)間事件, 所以影響不會(huì)太大. Q2: aeEventLoop在創(chuàng)建之初就指定了可監(jiān)聽文件描述符的數(shù)量, 之后又通過config set maxclients命令動(dòng)態(tài)調(diào)整客戶端最大連接數(shù)是怎么實(shí)現(xiàn)的? A2: 通過翻看源碼了解到, aeEventLoop提供了aeResizeSetSize函數(shù), 用戶重新分配events和fired數(shù)組的大小, 使aeEventLoop可監(jiān)聽的套接字?jǐn)?shù)量得以調(diào)整, 當(dāng)新的maxclients比原先要大的時(shí)候, 會(huì)調(diào)用該函數(shù), 擴(kuò)大aeEventLoop可監(jiān)聽文件描述符的數(shù)量, 以支持更多的客戶端連接.
六、總結(jié) Redis對(duì)事件的處理方式十分巧妙, 文件事件和時(shí)間事件之間相互配合, 充分的利用時(shí)間事件達(dá)到之前的這段時(shí)間等待和處理文件事件, 這樣既避免了CPU的空轉(zhuǎn)檢查, 也能及時(shí)的處理文件事件. 此外通過時(shí)間事件中timeProc函數(shù)的返回值, 將時(shí)間事件的移除和再次觸發(fā)權(quán)完全交給了用戶, 使用起來更加靈活.
|
|
|