【網絡編程】說說Redis的服務端設計

引子

感覺這東西看過不記一下總會忘,所以手不能懶,及時總結一下。
本文主要針對Redis的服務端模型進行分析,力爭能有總體的思路和部分細緻的深入。源碼版本3.2.8.

正文

Redis服務端一個典型的單線程reactor模型,使用I/O多路複用來完成對文件描述符的監聽,然後主線程依次處理就緒的事件。

I/O多路複用

思路非常的簡單,首先我們知道I/O多路複用有好幾種方式,而這常常是和平臺相關的,所以爲了實現的簡潔,擴展性,跨平臺性,Redis在這裏進行了一層封裝,通過一套統一的API完成整個網絡通信部分。
以Linux下的epoll爲例,讓我們來看一下。

typedef struct aeApiState {
    int epfd;
    struct epoll_event *events;
} aeApiState;

static int aeApiCreate(aeEventLoop *eventLoop);

初始化,主要是分配內存和調用epoll_create生成epoll監聽fd,eventLoop結構體是服務端事件驅動的結構體。

static int aeApiResize(aeEventLoop *eventLoop, int setsize);
eventLoop記錄了能監控的事件最大數,這裏重新調整大小。

static void aeApiFree(aeEventLoop *eventLoop);
釋放直接分配給eventLoop的內存空間

static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask);
向eventLoop裏面添加要監控的事件,對於epoll來說就是epoll_ctl(EPOLL_CTL_ADD)

static void aeApiDelEvent(aeEventLoop *eventLoop, int fd, int delmask);
刪除監控的事件,對於epoll來說就是epoll_ctl(EPOLL_CTL_DEL)

static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp);
I/O多路複用的阻塞調用,對於epoll來說就是epoll_wait,第二個參數表示epoll要㩐待的最長時間。

static char *aeApiName(void);
返回封裝的I/O多路複用實現,對於epoll來說就是返回“epoll”

而在選擇I/O多路複用的實現時,是按照性能的從高到低。
在Redis的網絡庫ae.c最前面是這樣的

#ifdef HAVE_EVPORT
#include "ae_evport.c" // Solaris
#else
    #ifdef HAVE_EPOLL
    #include "ae_epoll.c" // Linux
    #else
        #ifdef HAVE_KQUEUE
        #include "ae_kqueue.c" // BSD
        #else
        #include "ae_select.c"
        #endif
    #endif
#endif

自然,跨平臺的select作爲保留選擇,但是由於其性能原因,放在最後了。

兩種事件

Redis的服務端通過事件驅動,I/O事件和定時事件,I/O事件我們都很熟悉,便是可讀/寫或者accept返回的fd,而定時事件的處理之前則沒怎麼接觸,最近也趁機好好學習了《高性能服務端》裏面的 定時器 一章。所以在這裏順便展開小說一下定時器。

定時器

定時器裏面有兩個最基本的屬性,即超時時間和處理函數(回調函數),當然可能還有其他的屬性,比如是否需要重啓等等,一個定時器就是一個定時事件,我們可以通過鏈表、時間輪、最小堆來組織定時事件。
而在監控定時事件時可以通過信號(定時發信號檢測是否到時),I/O多路複用(超時參數)來實現。

服務端事件驅動流程

在服務端啓動之後,首先會進行各種初始化工作,在初始化結束之後,便進入aeMain,開始執行服務端的事件循環,等待客戶端連接。

void aeMain(aeEventLoop *eventLoop) {
    eventLoop->stop = 0; // 終止flag
    while (!eventLoop->stop) {
        if (eventLoop->beforesleep != NULL)
            eventLoop->beforesleep(eventLoop);
        aeProcessEvents(eventLoop, AE_ALL_EVENTS);
    }
}

可以看到這裏便是不斷調用aeProcessEvents,並且監聽兩種種類事件(AE_ALL_EVENTS)

事件種類

#define AE_FILE_EVENTS 1 // I/O事件
#define AE_TIME_EVENTS 2 // 定時事件
#define AE_ALL_EVENTS (AE_FILE_EVENTS|AE_TIME_EVENTS)

每次aeProcessEvents都是一次I/O多路複用的輪詢,讓我們來看一下這個核心函數的細節
int aeProcessEvents(aeEventLoop *eventLoop, int flags)
eventLoop就是我們的事件驅動主結構體,flag則記錄了需要關心的事件(主循環似乎是會關心所有事件,這裏我認爲主要體現了Redis網網絡庫的拓展性,程序也許只需要關心一種事件)。

if (!(flags & AE_TIME_EVENTS) && !(flags & AE_FILE_EVENTS)) 
    return 0;
/*
既然只有兩種事件,那麼這兩種事件都不關心自然是直接結束了(return ASAP--as soon as possible)
*/
    if (eventLoop->maxfd != -1 ||
        ((flags & AE_TIME_EVENTS) && !(flags & AE_DONT_WAIT))) { // 需要關心時間事件,並且不是立即返回
        int j;
        aeTimeEvent *shortest = NULL;
        struct timeval tv, *tvp;

        if (flags & AE_TIME_EVENTS && !(flags & AE_DONT_WAIT))
            shortest = aeSearchNearestTimer(eventLoop);
            /*在最近記錄的定時事件中,查找還有最短時間的事件*/
        if (shortest) { //找到一個距離到時最近的定時事件
            long now_sec, now_ms;

            aeGetTime(&now_sec, &now_ms);//獲取現在的時間(秒和毫秒)
            tvp = &tv;

            long long ms =
                (shortest->when_sec - now_sec)*1000 +
                shortest->when_ms - now_ms;
            //計算距離到時還有多少時間
            if (ms > 0) { // 如果定時事件還沒到達時間,將剩餘時間記錄在tvp中
                tvp->tv_sec = ms/1000;
                tvp->tv_usec = (ms % 1000)*1000;
            } else {// 已經有定時事件到時了,那麼就不等待,將tvp設置爲0
                tvp->tv_sec = 0; 
                tvp->tv_usec = 0;
            }
        } else { //shortest爲NULL,目前沒有等待的定時事件
            if (flags & AE_DONT_WAIT) { /AE_DONT_WAIT被設置則不需要等待
                tv.tv_sec = tv.tv_usec = 0;
                tvp = &tv;
            } else {
                //永遠等待直到有事件發生
                tvp = NULL; 
            }
        }

經過這一步,我們已經計算出了多路複用需要的超時時間保存在tvp裏了。然後就是對監聽的fd進行輪詢了。

numevents = aeApiPoll(eventLoop, tvp);//返回發生的事件
        for (j = 0; j < numevents; j++) {//遍歷,處理事件
            aeFileEvent *fe = &eventLoop->events[eventLoop->fired[j].fd];
            int mask = eventLoop->fired[j].mask;
            int fd = eventLoop->fired[j].fd;
            int rfired = 0;

            if (fe->mask & mask & AE_READABLE) {// 可讀事件
                rfired = 1;
                fe->rfileProc(eventLoop,fd,fe->clientData,mask);//調用之前添加的處理函數
            }
            if (fe->mask & mask & AE_WRITABLE) {// 可寫事件
                if (!rfired || fe->wfileProc != fe->rfileProc)
                    fe->wfileProc(eventLoop,fd,fe->clientData,mask);// 同上
            }
            processed++;// 成功處理的事件數加1
        }
    }
    // 可以看到,當一個fd 上既可讀又可寫時是優先處理可讀事件的

因爲我們剛纔設置的傳給aeApiPoll的時間是定時事件剛好到時的剩餘時間,所以現在定時事件已經到時了,我們還需要再去處理定時事件。
當然,因爲我們是處理完所有非定時事件之後,才處理定時事件的,所以實際處理定時事件可能要比預定的時間慢一點。

    if (flags & AE_TIME_EVENTS)
        processed += processTimeEvents(eventLoop);
    return processed;

讓我們再來看processTimeEvents的細節。

首先是一個異常處理,如果系統時間發生錯亂被移到未來(moved to the future),這裏我們就不關心細節了。

    int processed = 0;
    aeTimeEvent *te, *prev;
    long long maxId;

    time_t now = time(NULL);   // 獲取現在的時間
    eventLoop->lastTime = now; // 設置時間

    prev = NULL;               // 前驅指針
    te = eventLoop->timeEventHead;// te就是定時事件鏈表頭
    maxId = eventLoop->timeEventNextId-1;//定時事件的最大ID
while(te) {// 主循環
        long now_sec, now_ms;
        long long id;

        if (te->id == AE_DELETED_EVENT_ID) { // 這個事件要被刪除
            aeTimeEvent *next = te->next;
            if (prev == NULL)
                eventLoop->timeEventHead = te->next;
            else
                prev->next = te->next;
            if (te->finalizerProc)// 清理資源
                te->finalizerProc(eventLoop, te->clientData);
            zfree(te);// 釋放資源
            te = next;
            continue;
        }


        if (te->id > maxId) {
            te = te->next;
            continue;
        }
        aeGetTime(&now_sec, &now_ms);
        if (now_sec > te->when_sec ||
            (now_sec == te->when_sec && now_ms >= te->when_ms)) //如果定時事件到時了(現在的時間大於設定的時間)
        {
            int retval;

            id = te->id;
            retval = te->timeProc(eventLoop, id, te->clientData); //調用處理函數處理
            processed++; //處理數+1
            if (retval != AE_NOMORE) { //如果這個事件之後還要執行,也就是隔一段時間再執行的事件
                aeAddMillisecondsToNow(retval,&te->when_sec,&te->when_ms); //添加時間
            } else {
                te->id = AE_DELETED_EVENT_ID;//否則設置刪除標誌
            }
        }
        prev = te;
        te = te->next;//處理下一個事件
    }
return processed;// 返回處理數

至此,一次對時間事件處理就結束了,aeProcessEvents也就結束了。
而服務端主要就是一個死循環aeProcessEvents來進行事件處理。

參考閱讀

《Redis設計與實現》中的 第12章事件,第14章服務器。
小夥伴Tanswer之前也分析了ae.c的源碼,並且還有之前學長利用ae寫的demo,請移步 Redis網絡庫源碼淺解

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章