redis源碼分析 -- cs結構之服務器

服務器與客戶端是如何交互的

redis客戶端向服務器發送命令請求,服務器接收到客戶端發送的命令請求之後,讀取解析命令,並執行命令,同時將命令執行結果返回給客戶端。

客戶端與服務器交互的代碼流程如下圖所示:
這裏寫圖片描述

Redis 服務器負責與多個客戶端建立網絡連接,處理客戶端發送的命令請求,在數據庫中保存客戶端執行的命令產生的數據,並通過資源管理器來維護服務器自身的運轉。

redis服務器是一個事件驅動程序,主要爲文件事件(File Event)和時間事件(Time Event)。當啓動服務器時,服務器在初始化的過程中,會創建時間事件和文件事件,並將對應的事件與事件處理函數綁定,當客戶端請求服務器連接或者發送命令請求時,服務器端會觸發相應的事件,通過事件處理函數處理完畢後,由服務器通過應答處理事件返回給客戶端。時間事件有定時事件和週期性事件兩種。

服務器中的事件驅動

redis服務器主要處理兩種事件:

  • 文件事件:這是服務器對套接字操作的抽象,服務器與客戶端的通信會產生相應的文件事件,服務器通過監聽並處理這些事件來完成一系列的網絡通信操作。
  • 時間事件:redis中的一些操作需要在指定的時間點執行,時間事件就是服務器對這些定點操作的抽象。

文件事件

redis 基於 Reactor 模式開發了自己的網絡事件處理器,稱爲文件事件處理器。

文件事件處理器的構成

redis文件事件處理器分爲四個組成部分,套接字、IO多路複用程序、文件事件分派器和時間處理函數。

文件事件是對套接字的抽象,當一個套接字準備好執行連接應答(accept)、寫入(write)、讀取(read)、關閉(close)操作時,就會產生一個文件事件,服務器通過IO多路複用同時監聽多個套接字,當這些監聽的套接字產生文件事件時,通過輪詢的方式,文件事件分派器會對這些文件事件啓動相應的事件處理函數。在aeProcessEvents函數中,通過循環的方式,對每一個文件事件進行處理。IO多路複用總是將所有產生事件的套接字都放在一個隊列裏面,然後按照順序每次一個套接字的方式向文件事件分派器傳送套接字,當上一個套接字產生的事件處理完畢之後,纔會處理下一個套接字的文件事件。

IO多路複用程序

redis中的IO多路複用程序的功能是通過包裝 select、epoll、evpoll 和 kqueue 這些IO多路複用庫函數來實現的,在源碼中對應的文件名爲 ae_select.cae_epoll.cae_evpoll.cae_kqueue.c。redis在封裝這些庫函數時,都使用了相同的API,類似於C++的多態實現,這樣,IO多路複用程序的底層實現就能夠互換。代碼如下所示

/* Include the best multiplexing layer supported by this system.
 * The following should be ordered by performances, descending. */
#ifdef HAVE_EVPORT
#include "ae_evport.c"
#else
    #ifdef HAVE_EPOLL
    #include "ae_epoll.c"
    #else
        #ifdef HAVE_KQUEUE
        #include "ae_kqueue.c"
        #else
        #include "ae_select.c"
        #endif
    #endif
#endif

通過宏定義規則,在編譯時自動選擇系統中性能最高的IO多路複用庫函數作爲redis底層IO多路複用程序的實現,這種方法很巧妙。

文件事件處理器的實現

問題:redis文件事件處理器是由套接字、IO多路複用程序、文件事件分派器和事件處理函數組成,那麼套接字能夠產生哪些事件,事件處理函數又有哪些操作呢?

事件的類型

在redis中,文件事件創建函數爲 aeCreateFileEvent,其函數如下

/* 
fd : socket file descriptor
mask : type of event, READABLE or WRITABLE
proc : handler of file event
 */
int aeCreateFileEvent(aeEventLoop *eventLoop, int fd, int mask,
        aeFileProc *proc, void *clientData)
{
    if (fd >= eventLoop->setsize) {
        errno = ERANGE;
        return AE_ERR;
    }
    aeFileEvent *fe = &eventLoop->events[fd];

    /* add file event, attach fd to event 
       if type is READABLE, add fd to fd_set rfds, else if type is WRITABLE, add to wfds
     */
    if (aeApiAddEvent(eventLoop, fd, mask) == -1)   
        return AE_ERR;
    fe->mask |= mask;
    if (mask & AE_READABLE) fe->rfileProc = proc;   //now fd attach to file event
    if (mask & AE_WRITABLE) fe->wfileProc = proc;
    fe->clientData = clientData;
    if (fd > eventLoop->maxfd)
        eventLoop->maxfd = fd;
    return AE_OK;
}

mask 爲事件的類型,爲 AE_WRITABLEAE_READABLE 兩種,分別爲可寫和可讀兩種類型。proc 爲文件事件處理函數,fd 爲套接字的文件描述符,而 clientData 則是客戶端在服務器端的狀態信息,這個後面會重點講述。

也就是說,文件事件的類型分爲可讀和可寫兩種類型,當然,同一個套接字是允許同時產生這兩種類型的事件的。

問題:那麼,什麼時候,套接字產生的文件是可讀的,什麼時候是可寫的呢?

  1. 當客戶端對服務器發起連接請求(即客戶端對服務器監聽的套接字執行connect操作),或者客戶端對套接字執行 write 或 close 操作時,套接字變的可讀,此時產生可讀事件 (AE_READABLE)。
  2. 當客戶端對套接字執行 read 操作時,套接字變得可寫,此時產生可寫事件 (AE_WRITABLE)。

通過查看 ae_select.c/aeApiPoll 函數理解服務器是如何監聽套接字的文件事件的。

static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {
    aeApiState *state = eventLoop->apidata;
    int retval, j, numevents = 0;

    memcpy(&state->_rfds,&state->rfds,sizeof(fd_set));
    memcpy(&state->_wfds,&state->wfds,sizeof(fd_set));

    /* allow a program to monitor multiple file descriptors, waiting until 
       one or more of the file descriptors become "ready" */
    retval = select(eventLoop->maxfd+1,
                &state->_rfds,&state->_wfds,NULL,tvp);
    if (retval > 0) {
        for (j = 0; j <= eventLoop->maxfd; j++) {
            int mask = 0;
            aeFileEvent *fe = &eventLoop->events[j];

            if (fe->mask == AE_NONE) continue;
            if (fe->mask & AE_READABLE && FD_ISSET(j,&state->_rfds))
                mask |= AE_READABLE;
            if (fe->mask & AE_WRITABLE && FD_ISSET(j,&state->_wfds))
                mask |= AE_WRITABLE;
            eventLoop->fired[numevents].fd = j;
            eventLoop->fired[numevents].mask = mask;
            numevents++;
        }
    }
    return numevents;
}

返回值 numevents,爲產生的文件事件的個數。通過多路複用IO庫函數 select,監聽多個套接字,當套接字符合上述要求時,會變得可讀或者可寫,可讀的套接字保存在套接字集合 state->_rfds 中,可寫的保存在 state->_wfds 中,異常情況的套接字集合設置爲 NULL,這裏不關心。然後根據套接字的可讀或者可寫狀態,預設文件事件,將他們的文件描述符fd 和 事件類型 mask 保存在 fired 數組中,這個數組中保存的都是產生事件的套接字,然後通過掃描 fired 數組,對產生的文件事件一個一個的進行處理。

如果對 select 函數不瞭解,可查看 select函數詳解及實例解析[http://blog.csdn.net/leo115/article/details/8097143]

aeProcessEvents 函數中,通過調用上述的 aeApiPoll 函數,等待和分配文件事件,然後調用對應的事件處理函數進行處理。

/* Process every pending time event, then every pending file event
 * (that may be registered by time event callbacks just processed).
 * Without special flags the function sleeps until some file event
 * fires, or when the next time event occurs (if any).
 *
 * If flags is 0, the function does nothing and returns.
 * if flags has AE_ALL_EVENTS set, all the kind of events are processed.
 * if flags has AE_FILE_EVENTS set, file events are processed.
 * if flags has AE_TIME_EVENTS set, time events are processed.
 * if flags has AE_DONT_WAIT set the function returns ASAP until all
 * the events that's possible to process without to wait are processed.
 *
 * The function returns the number of events processed. */
 /* the event dispatcher, make a choice to select an event handler */
int aeProcessEvents(aeEventLoop *eventLoop, int flags)
{
    int processed = 0, numevents;

    /* Nothing to do? return ASAP */
    if (!(flags & AE_TIME_EVENTS) && !(flags & AE_FILE_EVENTS)) return 0;

    /* Note that we want call select() even if there are no
     * file events to process as long as we want to process time
     * events, in order to sleep until the next time event is ready
     * to fire. */
    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;

            /* Calculate the time missing for the nearest
             * timer to fire. */
            aeGetTime(&now_sec, &now_ms);
            tvp = &tv;
            tvp->tv_sec = shortest->when_sec - now_sec;
            if (shortest->when_ms < now_ms) {
                tvp->tv_usec = ((shortest->when_ms+1000) - now_ms)*1000;
                tvp->tv_sec --;
            } else {
                tvp->tv_usec = (shortest->when_ms - now_ms)*1000;
            }
            if (tvp->tv_sec < 0) tvp->tv_sec = 0;
            if (tvp->tv_usec < 0) tvp->tv_usec = 0;
        } else {
            /* If we have to check for events but need to return
             * ASAP because of AE_DONT_WAIT we need to set the timeout
             * to zero */
            if (flags & AE_DONT_WAIT) {
                tv.tv_sec = tv.tv_usec = 0;
                tvp = &tv;
            } else {
                /* Otherwise we can block */
                tvp = NULL; /* wait forever */
            }
        }

        numevents = aeApiPoll(eventLoop, tvp);  //how many events fired
        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;

        /* note the fe->mask & mask & ... code: maybe an already processed
             * event removed an element that fired and we still didn't
             * processed, so we check if the event is still valid. */
            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++;
        }
    }
    /* Check time events */
    if (flags & AE_TIME_EVENTS)
        processed += processTimeEvents(eventLoop);

    return processed; /* return the number of processed file/time events */
}

aeProcessEvents 函數,先處理文件事件,如果此時時間事件觸發,在處理時間事件。aeProcessEvents 就是時間分派器,將產生的文件事件分派給對應的事件處理函數進行處理。

事件處理函數

現在再來回顧一下,redis文件事件處理器的構成,套接字、IO多路複用程序、事件分派器和事件處理函數。如下圖所示:
這裏寫圖片描述

redis 服務器中,事件處理函數,主要由上圖中列出的三種,連接應答處理器(acceptTcpHandler)、命令請求處理器(readQueryFromClient)和命令回覆處理器(sendReplyToClient)。這裏所說的都是文件事件處理函數。

連接應答處理器 acceptTcpHandler
用於對服務器監聽的套接字請求連接的客戶端進行應答(即客戶端執行connect),具體實現爲 accept() 函數的封裝。

void initServer (void)
{
    ...
    /* Create an event handler for accepting new connections in TCP and Unix
     * domain sockets. */
    for (j = 0; j < server.ipfd_count; j++) {
        if (aeCreateFileEvent(server.el, server.ipfd[j],    AE_READABLE,
            acceptTcpHandler,NULL) == AE_ERR) {
                redisPanic(
                    "Unrecoverable error creating server.ipfd file event.");
            }
    }
    ...
} 

redis 在初始化時,會創建文件事件,將連接應答處理器與服務器監聽的套接字的 AE_READABLE 類型的事件關聯起來(或者說是綁定),當客戶端連接服務器(connect)時,該被服務器監聽的套接字會會變成 AE_READABLE ,IO多路複用程序將該套接字保存在可讀的套接字集合中,引發連接應答處理器執行相應的操作。

void acceptTcpHandler(aeEventLoop *el, int fd, void *privdata, int mask) {
    int cport, cfd, max = MAX_ACCEPTS_PER_CALL;
    char cip[REDIS_IP_STR_LEN];
    REDIS_NOTUSED(el);
    REDIS_NOTUSED(mask);
    REDIS_NOTUSED(privdata);

    while(max--) {
        cfd = anetTcpAccept(server.neterr, fd, cip, sizeof(cip), &cport);
        if (cfd == ANET_ERR) {
            if (errno != EWOULDBLOCK)
                redisLog(REDIS_WARNING,
                    "Accepting client connection: %s", server.neterr);
            return;
        }
        redisLog(REDIS_VERBOSE,"Accepted %s:%d", cip, cport);
        acceptCommonHandler(cfd,0);
    }
}

命令請求處理器 readQueryFromClient
負責讀取客戶端發送的命令請求內容,底層實現爲 read 函數的封裝。當客戶端通過連接應答處理器成功連接服務器之後,服務器會將命令請求處理器與套接字的 AE_READABLE 關聯起來,當客戶端向服務器發送命令請求的時候,套接字就產生了 AE_READABLE 類型的文件事件,觸發命令請求處理器,由該處理器對套接字執行相應的操作。

在服務器端,會有一個 redisClient 結構,用於保存客戶端的狀態信息。

static void acceptCommonHandler(int fd, int flags) {
    redisClient *c;
    if ((c = createClient(fd)) == NULL) {
        redisLog(REDIS_WARNING,
            "Error registering fd event for the new client: %s (fd=%d)",
            strerror(errno),fd);
        close(fd); /* May be already closed, just ignore errors */
        return;
    }
    /* If maxclient directive is set and this is one client more... close the
     * connection. Note that we create the client instead to check before
     * for this condition, since now the socket is already set in non-blocking
     * mode and we can send an error for free using the Kernel I/O */
    if (listLength(server.clients) > server.maxclients) {
        char *err = "-ERR max number of clients reached\r\n";

        /* That's a best effort error message, don't check write errors */
        if (write(c->fd,err,strlen(err)) == -1) {
            /* Nothing to do, Just to avoid the warning... */
        }
        server.stat_rejected_conn++;
        freeClient(c);
        return;
    }
    server.stat_numconnections++;
    c->flags |= flags;
}

連接應答處理器連接成功時,會處理上述函數,函數的主要功能,是當客戶端成功連接服務器時,就創建一個新的客戶端類型的對象(redisClient)用於保存客戶端的信息,同時,將該客戶端加入到服務器的客戶端鏈表中。

redisClient *createClient(int fd) {
    redisClient *c = zmalloc(sizeof(redisClient));

    /* passing -1 as fd it is possible to create a non connected client.
     * This is useful since all the Redis commands needs to be executed
     * in the context of a client. When commands are executed in other
     * contexts (for instance a Lua script) we need a non connected client. */
    if (fd != -1) {
        anetNonBlock(NULL,fd);
        anetEnableTcpNoDelay(NULL,fd);
        if (server.tcpkeepalive)
            anetKeepAlive(NULL,fd,server.tcpkeepalive);
        if (aeCreateFileEvent(server.el,fd,AE_READABLE,
            readQueryFromClient, c) == AE_ERR)
        {
            close(fd);
            zfree(c);
            return NULL;
        }
    }
    /* 對客戶端其他信息的初始化 */
    ...
}

而在創建客戶端時,就會創建文件事件,將套接字的 AE_READABLE 與命令請求處理器關聯。如上述函數所示。

void readQueryFromClient(aeEventLoop *el, int fd, void *privdata, int mask) {
    redisClient *c = (redisClient*) privdata;
    int nread, readlen;
    size_t qblen;
    REDIS_NOTUSED(el);
    REDIS_NOTUSED(mask);

    server.current_client = c;
    readlen = REDIS_IOBUF_LEN;
    /* If this is a multi bulk request, and we are processing a bulk reply
     * that is large enough, try to maximize the probability that the query
     * buffer contains exactly the SDS string representing the object, even
     * at the risk of requiring more read(2) calls. This way the function
     * processMultiBulkBuffer() can avoid copying buffers to create the
     * Redis Object representing the argument. */
    if (c->reqtype == REDIS_REQ_MULTIBULK && c->multibulklen && c->bulklen != -1
        && c->bulklen >= REDIS_MBULK_BIG_ARG)
    {
        int remaining = (unsigned)(c->bulklen+2)-sdslen(c->querybuf);

        if (remaining < readlen) readlen = remaining;
    }

    qblen = sdslen(c->querybuf);
    if (c->querybuf_peak < qblen) c->querybuf_peak = qblen;
    c->querybuf = sdsMakeRoomFor(c->querybuf, readlen);
    nread = read(fd, c->querybuf+qblen, readlen);
    if (nread == -1) {
        if (errno == EAGAIN) {
            nread = 0;
        } else {
            redisLog(REDIS_VERBOSE, "Reading from client: %s",strerror(errno));
            freeClient(c);
            return;
        }
    } else if (nread == 0) {
        redisLog(REDIS_VERBOSE, "Client closed connection");
        freeClient(c);
        return;
    }
    if (nread) {
        sdsIncrLen(c->querybuf,nread);
        c->lastinteraction = server.unixtime;
        if (c->flags & REDIS_MASTER) c->reploff += nread;
        server.stat_net_input_bytes += nread;
    } else {
        server.current_client = NULL;
        return;
    }
    /* 如果緩衝區超出最大範圍,關閉該客戶端 */
    if (sdslen(c->querybuf) > server.client_max_querybuf_len) { //max query buf is 1GB
        sds ci = catClientInfoString(sdsempty(),c), bytes = sdsempty();

        bytes = sdscatrepr(bytes,c->querybuf,64);
        redisLog(REDIS_WARNING,"Closing client that reached max query buffer length: %s (qbuf initial bytes: %s)", ci, bytes);
        sdsfree(ci);
        sdsfree(bytes);
        freeClient(c);
        return;
    }
    processInputBuffer(c);  //解析c->querybuf 中的參數,以redisStringObject的方式放入 c->argv 數組中
    server.current_client = NULL;
}

readQueryFromClient 函數,讀取客戶端發送的命令請求,存放在 c->querybuf 中,這是客戶端緩衝區,最大限制爲 REDIS_MAX_QUERYBUF_LEN,這個宏定義爲 redis.h

#define REDIS_MAX_QUERYBUF_LEN  (1024*1024*1024) /* 1GB max query buffer. */

也就是說客戶端緩衝區最大爲 1GB,如果超過這個大小,服務器將會關閉這個客戶端。processInputBuffer 函數是對客戶端緩衝區中的命令請求進行解析。

命令回覆處理器
負責將服務器執行命令後得到的結果通過套接字返回給客戶端。底層實現爲 write 函數的封裝。當服務器執行命令結果需要返回給客戶端的時候,服務器就會創建文件事件,將命令回覆處理器和套接字的 AE_WRITABLE 類型的時間關聯起來。當客戶端需要接受服務器傳回的結果時,就會產生 AE_WRITABLE 類型的文件事件,引發命令回覆處理器執行,對套接字進行操作。

/* This function is called every time we are going to transmit new data
 * to the client. The behavior is the following:
 *
 * If the client should receive new data (normal clients will) the function
 * returns REDIS_OK, and make sure to install the write handler in our event
 * loop so that when the socket is writable new data gets written.
 *
 * If the client should not receive new data, because it is a fake client
 * (used to load AOF in memory), a master or because the setup of the write
 * handler failed, the function returns REDIS_ERR.
 *
 * The function may return REDIS_OK without actually installing the write
 * event handler in the following cases:
 *
 * 1) The event handler should already be installed since the output buffer
 *    already contained something.
 * 2) The client is a slave but not yet online, so we want to just accumulate
 *    writes in the buffer but not actually sending them yet.
 *
 * Typically gets called every time a reply is built, before adding more
 * data to the clients output buffers. If the function returns REDIS_ERR no
 * data should be appended to the output buffers. */
int prepareClientToWrite(redisClient *c) {
    /* If it's the Lua client we always return ok without installing any
     * handler since there is no socket at all. */
    if (c->flags & REDIS_LUA_CLIENT) return REDIS_OK;

    /* Masters don't receive replies, unless REDIS_MASTER_FORCE_REPLY flag
     * is set. */
    if ((c->flags & REDIS_MASTER) &&
        !(c->flags & REDIS_MASTER_FORCE_REPLY)) return REDIS_ERR;

    if (c->fd <= 0) return REDIS_ERR; /* Fake client for AOF loading. */

    /* Only install the handler if not already installed and, in case of
     * slaves, if the client can actually receive writes. */
    if (c->bufpos == 0 && listLength(c->reply) == 0 &&
        (c->replstate == REDIS_REPL_NONE ||
         (c->replstate == REDIS_REPL_ONLINE && !c->repl_put_online_on_ack)))
    {
        /* Try to install the write handler. */
        if (aeCreateFileEvent(server.el, c->fd, AE_WRITABLE,
                sendReplyToClient, c) == AE_ERR)
        {
            freeClientAsync(c);
            return REDIS_ERR;
        }
    }

    /* Authorize the caller to queue in the output buffer of this client. */
    return REDIS_OK;
}

sendReplyToClient 函數就是將命令結果返回到客戶端

void sendReplyToClient(aeEventLoop *el, int fd, void *privdata, int mask) {
    redisClient *c = privdata;
    int nwritten = 0, totwritten = 0, objlen;
    size_t objmem;
    robj *o;
    REDIS_NOTUSED(el);
    REDIS_NOTUSED(mask);

    while(c->bufpos > 0 || listLength(c->reply)) {
        if (c->bufpos > 0) {
            nwritten = write(fd,c->buf+c->sentlen,c->bufpos-c->sentlen);
            if (nwritten <= 0) break;
            c->sentlen += nwritten;
            totwritten += nwritten;

            /* If the buffer was sent, set bufpos to zero to continue with
             * the remainder of the reply. */
            if (c->sentlen == c->bufpos) {
                c->bufpos = 0;
                c->sentlen = 0;
            }
        } else {
            o = listNodeValue(listFirst(c->reply));
            objlen = sdslen(o->ptr);
            objmem = getStringObjectSdsUsedMemory(o);

            if (objlen == 0) {
                listDelNode(c->reply,listFirst(c->reply));
                c->reply_bytes -= objmem;
                continue;
            }

            nwritten = write(fd, ((char*)o->ptr)+c->sentlen,objlen-c->sentlen);
            if (nwritten <= 0) break;
            c->sentlen += nwritten;
            totwritten += nwritten;

            /* If we fully sent the object on head go to the next one */
            if (c->sentlen == objlen) {
                listDelNode(c->reply,listFirst(c->reply));
                c->sentlen = 0;
                c->reply_bytes -= objmem;
            }
        }
        /* Note that we avoid to send more than REDIS_MAX_WRITE_PER_EVENT
         * bytes, in a single threaded server it's a good idea to serve
         * other clients as well, even if a very large request comes from
         * super fast link that is always able to accept data (in real world
         * scenario think about 'KEYS *' against the loopback interface).
         *
         * However if we are over the maxmemory limit we ignore that and
         * just deliver as much data as it is possible to deliver. */
        server.stat_net_output_bytes += totwritten;
        if (totwritten > REDIS_MAX_WRITE_PER_EVENT &&
            (server.maxmemory == 0 ||
             zmalloc_used_memory() < server.maxmemory)) break;
    }
    if (nwritten == -1) {
        if (errno == EAGAIN) {
            nwritten = 0;
        } else {
            redisLog(REDIS_VERBOSE,
                "Error writing to client: %s", strerror(errno));
            freeClient(c);
            return;
        }
    }
    if (totwritten > 0) {
        /* For clients representing masters we don't count sending data
         * as an interaction, since we always send REPLCONF ACK commands
         * that take some time to just fill the socket output buffer.
         * We just rely on data / pings received for timeout detection. */
        if (!(c->flags & REDIS_MASTER)) c->lastinteraction = server.unixtime;
    }
    if (c->bufpos == 0 && listLength(c->reply) == 0) {
        c->sentlen = 0;
        aeDeleteFileEvent(server.el,c->fd,AE_WRITABLE);

        /* Close connection after entire reply has been sent. */
        if (c->flags & REDIS_CLOSE_AFTER_REPLY) freeClient(c);
    }
}

時間事件

/* Time event structure */
typedef struct aeTimeEvent {
    long long id; /* time event identifier. , 值是遞增的 */
    long when_sec; /* seconds , 時間事件達到時間,秒精度 */
    long when_ms; /* milliseconds, 毫秒精度 */
    aeTimeProc *timeProc;   /* 時間事件處理函數 */
    aeEventFinalizerProc *finalizerProc;
    void *clientData;
    struct aeTimeEvent *next;   /* 時間事件,以鏈表的形式連接 */
} aeTimeEvent;

時間事件分爲兩種,一個是定時事件,一個是週期性事件。
定時事件:讓一段程序在指定一段時間之後執行
週期性事件:讓一段程序每隔指定時間執行一次。

創建時間事件

long long aeCreateTimeEvent(aeEventLoop *eventLoop, long long milliseconds,
        aeTimeProc *proc, void *clientData,
        aeEventFinalizerProc *finalizerProc)
{
    long long id = eventLoop->timeEventNextId++;
    aeTimeEvent *te;

    te = zmalloc(sizeof(*te));
    if (te == NULL) return AE_ERR;
    te->id = id;
    aeAddMillisecondsToNow(milliseconds,&te->when_sec,&te->when_ms);
    te->timeProc = proc;
    te->finalizerProc = finalizerProc;
    te->clientData = clientData;
    te->next = eventLoop->timeEventHead;    //strat from zero
    eventLoop->timeEventHead = te;
    return id;
}

milliseconds:是多久之後執行時間事件的參數
id:是時間事件的唯一 id 標識,從 0 開始計數

aeAddMillisecondsToNow 函數用於更新時間事件的 when_sec 和 when_ms 變量,即用當前時間加上 milliseconds 轉換的時間,表示 milliseconds 時間之後將會執行該時間事件。

刪除時間事件

int aeDeleteTimeEvent(aeEventLoop *eventLoop, long long id)
{
    aeTimeEvent *te, *prev = NULL;

    te = eventLoop->timeEventHead;
    while(te) {
        if (te->id == id) {
            if (prev == NULL)
                eventLoop->timeEventHead = te->next;
            else
                prev->next = te->next;
            if (te->finalizerProc)
                te->finalizerProc(eventLoop, te->clientData);
            zfree(te);
            return AE_OK;
        }
        prev = te;
        te = te->next;
    }
    return AE_ERR; /* NO event with the specified ID found */
}

在 redis 中,多個時間事件是通過單鏈表連接起來的,鏈表頭結點爲 eventLoop->timeEventHead,刪除時間事件時,先通過 id 找到時間事件,然後在單鏈表中刪除該節點。

查找當前時間最近的時間事件

/* Search the first timer to fire.
 * This operation is useful to know how many time the select can be
 * put in sleep without to delay any event.
 * If there are no timers NULL is returned.
 *
 * Note that's O(N) since time events are unsorted.
 * Possible optimizations (not needed by Redis so far, but...):
 * 1) Insert the event in order, so that the nearest is just the head.
 *    Much better but still insertion or deletion of timers is O(N).
 * 2) Use a skiplist to have this operation as O(1) and insertion as O(log(N)).
 */
static aeTimeEvent *aeSearchNearestTimer(aeEventLoop *eventLoop)
{
    aeTimeEvent *te = eventLoop->timeEventHead;
    aeTimeEvent *nearest = NULL;

    while(te) {
        if (!nearest || te->when_sec < nearest->when_sec ||
                (te->when_sec == nearest->when_sec &&
                 te->when_ms < nearest->when_ms))
            nearest = te;
        te = te->next;
    }
    return nearest;
}

當創建一個時間事件時,將該事件加入到時間事件單鏈表中,查找鏈表中離當前時間最近的事件,需要掃描整個鏈表,類似於一次冒泡排序。

時間事件的調度

/* Process time events */
static int processTimeEvents(aeEventLoop *eventLoop) {
    int processed = 0;
    aeTimeEvent *te;
    long long maxId;
    time_t now = time(NULL);

    /* If the system clock is moved to the future, and then set back to the
     * right value, time events may be delayed in a random way. Often this
     * means that scheduled operations will not be performed soon enough.
     *
     * Here we try to detect system clock skews, and force all the time
     * events to be processed ASAP when this happens: the idea is that
     * processing events earlier is less dangerous than delaying them
     * indefinitely, and practice suggests it is. */
    if (now < eventLoop->lastTime) {
        te = eventLoop->timeEventHead;
        while(te) {
            te->when_sec = 0;
            te = te->next;
        }
    }
    eventLoop->lastTime = now;

    te = eventLoop->timeEventHead;
    maxId = eventLoop->timeEventNextId-1;
    while(te) {
        long now_sec, now_ms;
        long long id;

        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++;
            /* After an event is processed our time event list may
             * no longer be the same, so we restart from head.
             * Still we make sure to don't process events registered
             * by event handlers itself in order to don't loop forever.
             * To do so we saved the max ID we want to handle.
             *
             * FUTURE OPTIMIZATIONS:
             * Note that this is NOT great algorithmically. Redis uses
             * a single time event so it's not a problem but the right
             * way to do this is to add the new elements on head, and
             * to flag deleted elements in a special way for later
             * deletion (putting references to the nodes to delete into
             * another linked list). */
            if (retval != AE_NOMORE) {
                aeAddMillisecondsToNow(retval,&te->when_sec,&te->when_ms);
            } else {
                aeDeleteTimeEvent(eventLoop, id);
            }
            te = eventLoop->timeEventHead;
        } else {
            te = te->next;
        }
    }
    return processed;
}

如果當前時間 now 小於 eventloop->lastTime,那麼

if (now < eventLoop->lastTime) {
    te = eventLoop->timeEventHead;
    while(te) {
        te->when_sec = 0;
        te = te->next;
    }
}

redis 會處理整個時間鏈表中的所有時間事件。

一個時間事件時定時事件還是週期性事件時根據時間處理函數的返回值來判斷的:

  • 如果返回值爲 AE_NOMORE,改時間爲定時事件,該事件在達到處理之後,將會被從時間事件鏈表中刪除,不在執行
  • 如果返回值是非 AE_NOMORE 的值,那麼該事件是週期性事件,更新時間的 when_sec 和 when_ms 的值,等到下一次事件到達時繼續執行。

目前版本的 redis 只是用週期性事件,還沒有使用定時事件。

時間事件的使用 servCron 事件

在redis 服務器初始化時,會創建時間事件

/* Create the serverCron() time event, that's our main way to process
 * background operations. */
if(aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL) == AE_ERR) {
    redisPanic("Can't create the serverCron time event.");
    exit(1);
}

該時間事件的處理函數爲 serverCron

服務器中的 client 狀態

Redis 服務器負責與多個客戶端建立網絡連接,處理客戶端發送的命令請求,在數據庫中保存客戶端執行命令所產生的數據,並通過資源管理來維持服務器自身的運轉。
對每個與服務器連接的客戶端,服務器都爲這些客戶端建立了相應的結構,用於保存客戶端的狀態信息,以及執行相關功能時需要用到的數據結構, redis.h/redisClient

/* With multiplexing we need to take per-client state.
 * Clients are taken in a linked list. */
typedef struct redisClient {
    uint64_t id;            /* Client incremental unique ID. */
    int fd;                 /* socket file descriptor */
    redisDb *db;
    int dictid;
    robj *name;             /* As set by CLIENT SETNAME */
    sds querybuf;
    size_t querybuf_peak;   /* Recent (100ms or more) peak of querybuf size */
    int argc;
    robj **argv;
    struct redisCommand *cmd, *lastcmd;
    int reqtype;
    int multibulklen;       /* number of multi bulk arguments left to read */
    long bulklen;           /* length of bulk argument in multi bulk request */
    list *reply;
    unsigned long reply_bytes; /* Tot bytes of objects in reply list */
    int sentlen;            /* Amount of bytes already sent in the current
                               buffer or object being sent. */
    time_t ctime;           /* Client creation time */
    time_t lastinteraction; /* time of the last interaction, used for timeout */
    time_t obuf_soft_limit_reached_time;
    int flags;              /* REDIS_SLAVE | REDIS_MONITOR | REDIS_MULTI ... */
    int authenticated;      /* when requirepass is non-NULL */
    int replstate;          /* replication state if this is a slave */
    int repl_put_online_on_ack; /* Install slave write handler on ACK. */
    int repldbfd;           /* replication DB file descriptor */
    off_t repldboff;        /* replication DB file offset */
    off_t repldbsize;       /* replication DB file size */
    sds replpreamble;       /* replication DB preamble. */
    long long reploff;      /* replication offset if this is our master */
    long long repl_ack_off; /* replication ack offset, if this is a slave */
    long long repl_ack_time;/* replication ack time, if this is a slave */
    long long psync_initial_offset; /* FULLRESYNC reply offset other slaves
                                       copying this slave output buffer
                                       should use. */
    char replrunid[REDIS_RUN_ID_SIZE+1]; /* master run id if this is a master */
    int slave_listening_port; /* As configured with: SLAVECONF listening-port */
    int slave_capa;         /* Slave capabilities: SLAVE_CAPA_* bitwise OR. */
    multiState mstate;      /* MULTI/EXEC state */
    int btype;              /* Type of blocking op if REDIS_BLOCKED. */
    blockingState bpop;     /* blocking state */
    long long woff;         /* Last write global replication offset. */
    list *watched_keys;     /* Keys WATCHED for MULTI/EXEC CAS */
    dict *pubsub_channels;  /* channels a client is interested in (SUBSCRIBE) */
    list *pubsub_patterns;  /* patterns a client is interested in (SUBSCRIBE) */
    sds peerid;             /* Cached peer ID. */

    /* Response buffer */
    int bufpos;
    char buf[REDIS_REPLY_CHUNK_BYTES];  /* 16K output buffer, soft limit */
} redisClient;

1. fd : 套接字文件描述符
2. name : 客戶端的名字,是一個 redisObject 對象,redisStringObject 對象
3. db : 客戶端使用的數據庫的指針
4. argc, argv, cmd, lastcmd : 客戶端命令參數及指向執行命令的函數指針
5. flags : 客戶端的標識,記錄了客戶端的角色以及目前客戶端的狀態
6. querybuf, buf : 輸入和輸出緩衝區
7. ctime : 客戶端創建時間
8. lastinteraction : 客戶端與服務器最後一次通信時間
9. obuf_soft_limit_reached_time : 客戶端輸出緩衝區大小超出軟性限制的時間
10. ……

客戶端中的幾個重要屬性

標誌 flags

flags 屬性的值可以是單個標誌:

flags = <flag>

也可以是多個標誌的二進制:

flags = <flag1> | <flag2> | ...

redis 中客戶端標誌的宏定義如下所示

/* Client flags */
#define REDIS_SLAVE (1<<0)   /* This client is a slave server */
#define REDIS_MASTER (1<<1)  /* This client is a master server */
#define REDIS_MONITOR (1<<2) /* This client is a slave monitor, see MONITOR */
#define REDIS_MULTI (1<<3)   /* This client is in a MULTI context */
#define REDIS_BLOCKED (1<<4) /* The client is waiting in a blocking operation */
#define REDIS_DIRTY_CAS (1<<5) /* Watched keys modified. EXEC will fail. */
#define REDIS_CLOSE_AFTER_REPLY (1<<6) /* Close after writing entire reply. */
#define REDIS_UNBLOCKED (1<<7) /* This client was unblocked and is stored in
                                  server.unblocked_clients */
#define REDIS_LUA_CLIENT (1<<8) /* This is a non connected client used by Lua ,表示客戶端是專門用於處理lua腳本中包含 redis 命令的僞客戶端 */
#define REDIS_ASKING (1<<9)     /* Client issued the ASKING command */
#define REDIS_CLOSE_ASAP (1<<10)/* Close this client ASAP */
#define REDIS_UNIX_SOCKET (1<<11) /* Client connected via Unix domain socket */
#define REDIS_DIRTY_EXEC (1<<12)  /* EXEC will fail for errors while queueing */
#define REDIS_MASTER_FORCE_REPLY (1<<13)  /* Queue replies even if is master */
#define REDIS_FORCE_AOF (1<<14)   /* Force AOF propagation of current cmd. */
#define REDIS_FORCE_REPL (1<<15)  /* Force replication of current cmd. */
#define REDIS_PRE_PSYNC (1<<16)   /* Instance don't understand PSYNC. */
#define REDIS_READONLY (1<<17)    /* Cluster client is in read-only state. */
#define REDIS_PUBSUB (1<<18)      /* Client is in Pub/Sub mode. */

輸入緩衝區 querybuf

redis 客戶端狀態信息中的輸入緩衝區 querybuf 用於保存客戶端發送的命令請求, readQueryFromClient 這個函數就是讀取客戶端發送的命令請求並保存在 querybuf 中,該緩衝區的最大大小爲 1GB,當超出這個值時,服務器將關閉這個客戶端。

命令與命令參數 argc, argv

argc 表示客戶端發送的命令參數的個數, argv 是一個 redisObject 結構體的數組,每一個參數就是一個 redisObject 類型的變量。當服務器讀取完客戶端發送的命令請求之後,通過 Networking.c/processInlineBufferNetworking.c/processMultiBulkBuffer 這兩個函數,將 querybuf 中的內容解析後,存放在 argv 中,argc 保存的是參數的個數。

命令實現函數 cmd, lastcmd

當參數解析存放在 argv 中後,redis服務器會通過 argv[0] 查找命令處理函數,在 redis.c/processCommand

c->cmd = c->lastcmd = lookupCommand(c->argv[0]->ptr);

redis.c/initServerConfig 中調用 populateCommandTable 函數,初始化 server.commands 字典,通過命令名稱,在字典中查找對應的命令實現函數。

struct redisCommand redisCommandTable[] = {
    {"get",getCommand,2,"rF",0,NULL,1,1,1,0,0},
    {"set",setCommand,-3,"wm",0,NULL,1,1,1,0,0},
    {"setnx",setnxCommand,3,"wmF",0,NULL,1,1,1,0,0},
    {"setex",setexCommand,4,"wm",0,NULL,1,1,1,0,0},
    {"psetex",psetexCommand,4,"wm",0,NULL,1,1,1,0,0},
    {"append",appendCommand,3,"wm",0,NULL,1,1,1,0,0},
    {"strlen",strlenCommand,2,"rF",0,NULL,1,1,1,0,0},
    {"del",delCommand,-2,"w",0,NULL,1,-1,1,0,0},
    {"exists",existsCommand,-2,"rF",0,NULL,1,-1,1,0,0},
    {"setbit",setbitCommand,4,"wm",0,NULL,1,1,1,0,0},
    {"getbit",getbitCommand,3,"rF",0,NULL,1,1,1,0,0},
    {"setrange",setrangeCommand,4,"wm",0,NULL,1,1,1,0,0},
    ...
};

輸出緩衝區

/* Response buffer */
    int bufpos;
    char buf[REDIS_REPLY_CHUNK_BYTES];  /* 16K output buffer */
    ...
    list *reply;

服務器執行命令結果會保存在輸出緩衝區,每一個客戶端都會有兩個緩衝區,一個固定大小的緩衝區和一個可變大小的緩衝區。

  • 固定大小的緩衝區用於保存長度較小的結果,比如 OK、整數值、錯誤回覆、簡短的字符串值等。
  • 可變大小的緩衝區用於保存那些長度比較大的結果,比如包含了很多元素的集合或者一個非常大的字符串值等。

在固定大小的緩衝區中,buf 長度最大爲 16K,bufpos 爲實際使用的字節數。

可變大小的緩衝區由 reply 鏈表組成,這是一個雙向鏈表。鏈表長度不受 16KB 的限制。

驗證 authenticated

客戶端的 authenticated 屬性,用於記錄客戶端是否通過驗證。如果值爲0,表示未通過驗證;如果爲1,表示通過。

authenticated 的值爲0時,客戶端發送的命令除了 AUTH 之外,其餘的所有命令將都會被服務器拒絕執行。

authenticated 屬性只有在服務器啓用了身份驗證功能時使用,在 redis.config 配置文件中通過設置 requirepass 選項可以設置該功能。如果沒有啓動身份驗證功能,及時 authenticated 的值爲0,服務器也不會拒絕客戶端的命令請求。

服務器實現的細節 ( redis.c/main )

redis 服務器啓動時,需要做很多準備工作

  1. 設置編碼  setlocale(LC_COLLATE,"");
  2. 設置線程安全模式 zmalloc_enable_thread_safeness();
  3. 設置 OOM 異常處理方法 zmalloc_set_oom_handler(redisOutOfMemoryHandler);
  4. 設置哈希種子 dictSetHashFunctionSeed(tv.tv_sec^tv.tv_usec^getpid())
  5. 檢查服務器是否是以 Sentinel Mode 的方式啓動 checkForSentinelMode(argc,argv)
  6. 讀取 redis.config 配置文件,初始化服務器配置 initServerConfig()
  7. 初始化服務器參數 initServer()
  8. 啓動服務器守護進程模式 daemonize()
  9. 創建 pid 文件 createPidFile()
  10. 進入主循環 aeMain()

初始化服務器

server 是一個全局變量,在 redis.c 中定義

/* Global vars */
struct redisServer server; /* server global state */

初始化服務器狀態結構

redis.c/initServerConfig() 函數中,對 server 變量進行了初始化

void initServerConfig(void) {
    int j;

    getRandomHexChars(server.runid,REDIS_RUN_ID_SIZE);  //get redis "Run ID" by SHA algorithm, to keep every redis "Run ID" are different
    server.configfile = NULL;   //配置文件
    server.hz = REDIS_DEFAULT_HZ;   //服務器頻率
    server.runid[REDIS_RUN_ID_SIZE] = '\0';
    server.arch_bits = (sizeof(long) == 8) ? 64 : 32;   //服務器運行架構
    server.port = REDIS_SERVERPORT;     //默認端口,一般是6379
    server.tcp_backlog = REDIS_TCP_BACKLOG; //默認監聽隊列長度
    ...
    server.lruclock = getLRUClock();    //初始化LRU時鐘
    ...
    populateCommandTable();     //創建命令表
    ..
}

getRandomHexChars 函數是通過 SHA1 算法獲取 server 的 runid,擺正 runid 的唯一性,在 redis 註釋中也有如下說明

/* Generate the Redis "Run ID", a SHA1-sized random number that identifies a
 * given execution of Redis, so that if you are talking with an instance
 * having run_id == A, and you reconnect and it has run_id == B, you can be
 * sure that it is either a different instance or it was restarted. */

initServerConfig 函數只創建了服務器狀態的一些基本屬性參數,比如整數、浮點數和字符串屬性,但是對數據庫、Lua環境、共享對象、慢查詢日誌這些數據結構的初始化並沒有創建,這些將在後悔實現。

載入配置選項

redis 服務器啓動時,一般會指定配置文件,如果沒有指定配置文件參數,系統誰使用默認的配置文件,比如 redis.config

服務器通過 loadServerConfig 函數加載配置文件

/* Load the server configuration from the specified filename.
 * The function appends the additional configuration directives stored
 * in the 'options' string to the config file before loading.
 *
 * Both filename and options can be NULL, in such a case are considered
 * empty. This way loadServerConfig can be used to just load a file or
 * just load a string. */
void loadServerConfig(char *filename, char *options) {
    sds config = sdsempty();
    char buf[REDIS_CONFIGLINE_MAX+1];

    /* Load the file content */
    if (filename) {
        FILE *fp;

        if (filename[0] == '-' && filename[1] == '\0') {
            fp = stdin;
        } else {
            if ((fp = fopen(filename,"r")) == NULL) {
                redisLog(REDIS_WARNING,
                    "Fatal error, can't open config file '%s'", filename);
                exit(1);
            }
        }
        while(fgets(buf,REDIS_CONFIGLINE_MAX+1,fp) != NULL)
            config = sdscat(config,buf);
        if (fp != stdin) fclose(fp);
    }
    /* Append the additional options */
    if (options) {
        config = sdscat(config,"\n");
        config = sdscat(config,options);
    }
    loadServerConfigFromString(config);
    sdsfree(config);
}

loadServerConfig 函數將配置文件全部加載到 config 變量中,整個文件的參數都加載到 config 字符串變量中,此時,config 是一個很長很長的字符串變量,然後通過 loadServerConfigFromSrting 函數,將 config 進行分割,並對 server 中的相關參數進行賦值。

void loadServerConfigFromString(char *config) {
    char *err = NULL;
    int linenum = 0, totlines, i;
    int slaveof_linenum = 0;
    sds *lines;

    lines = sdssplitlen(config,strlen(config),"\n",1,&totlines);

    for (i = 0; i < totlines; i++) {
        sds *argv;
        int argc;

        linenum = i+1;
        lines[i] = sdstrim(lines[i]," \t\r\n");

        /* Skip comments and blank lines */
        if (lines[i][0] == '#' || lines[i][0] == '\0') continue;

        /* Split into arguments */
        argv = sdssplitargs(lines[i],&argc);
        if (argv == NULL) {
            err = "Unbalanced quotes in configuration line";
            goto loaderr;
        }

        /* Skip this line if the resulting command vector is empty. */
        if (argc == 0) {
            sdsfreesplitres(argv,argc);
            continue;
        }
        sdstolower(argv[0]);

        /* Execute config directives */
        if (!strcasecmp(argv[0],"timeout") && argc == 2) {
            server.maxidletime = atoi(argv[1]);
            if (server.maxidletime < 0) {
                err = "Invalid timeout value"; goto loaderr;
            }
        } else if (!strcasecmp(argv[0],"tcp-keepalive") && argc == 2) {
            server.tcpkeepalive = atoi(argv[1]);
            if (server.tcpkeepalive < 0) {
                err = "Invalid tcp-keepalive value"; goto loaderr;
            }
        ...
}

服務器在載入用戶指定的配置選項,並對 server 狀態進行更新之後,服務器就進入初始化第三個階段 – 初始化服務器數據結構。

服務器的守護進程實現

大家都知道,如何實現一個守護進程,首先需要了解守護進程的特徵。

  1. 大多數守護進程都是以 root 超級用戶權限運行。
  2. 所有的守護進程都沒有控制終端,ps 查看的結果中終端名設置爲 ?
  3. 內核守護進程以無控制終端方式運行,而用戶層守護進程無控制終端可能是調用 setsid 的結果。
  4. 大多數用戶層進程都是進程組的組長進程以及會話的首進程,同時也是這些進程組和會話中的唯一進程。
  5. 用戶層守護進程的父進程是 init 進程。

那麼,根據以上特徵,按照一定的規則就能創建守護進程,這裏所說的一般都是用戶層的守護進程。

(1) 首先需要做的就是 fork 創建一個進程,然後使父進程退出(子進程成爲孤兒進程),此時,如果是在 terminal 上啓動的,子進程繼承父進程的屬性,會繼承父進程的 umask 掩碼、進程組、控制終端屬性等。

(2) 父進程退出之後,使用 setsid ,新創建一個會話 session。
  一個會話可以包含一個或多個進程組,一個進程組可以包含一個或多個進程。這些進程組可共享一個控制終端,所以該會話與控制終端相聯繫。控制終端與會話是一一對應的。因爲父進程創建子進程,所以該子進程不可能是父進程所在進程組的組長和會話組長,使用 setsid 創建一個新的會話,此時,該進程成爲這個會話的唯一進程,也是這個會話中進程組組長。
  setsid 函數在進程時進程組組長時會執行失敗。如果執行成功,那麼,因爲會話與控制終端是一一對應的,此時,該進程將擺脫父進程的影響,存在一個新的進程組和會話中,並且與控制終端不相關。

(3) 使用 umask (0),將文件掩碼清除,繼承自父進程的掩碼,可能會被設置爲拒絕某些權限。

(4) 將當前工作目錄更改爲根目錄。從父進程繼承來的工作目錄可能掛載某一個文件系統中。因爲守護進程通常是在系統引導之前一直存在的,所以如果守護進程的工作目錄掛載在某一個文件系統中,該文件系統將不能被卸載。

(5) 關閉不再需要的文件描述符。一般將 STDIN、STDOUT、STDERR都重定向到 /dev/null 空洞文件中,然後在關閉 0,1,2 文件描述符。因爲守護進程不與終端設備相關聯,所以輸出無處顯示,也無法從交互式用戶那裏接收輸入。

struct rlimit rl;
getrlimit (RLIMIT_NOFILE, &rl);

int j;
for (j=0; i<rl.rlim_max; i++) {
    close (i);
}

int fd;
if ((fd = open("/dev/null", O_RDWR, 0)) != -1) {
    dup2(fd, STDIN_FILENO);
    dup2(fd, STDOUT_FILENO);
    dup2(fd, STDERR_FILENO);
    if (fd > STDERR_FILENO) close(fd);
}

redis 的 daemonize() 函數的實現如下所示,實現 redis 的守護進程

void daemonize(void) {
    int fd;

    if (fork() != 0) exit(0); /* parent exits */
    setsid(); /* create a new session */

    /* Every output goes to /dev/null. If Redis is daemonized but
     * the 'logfile' is set to 'stdout' in the configuration file
     * it will not log at all. */
    if ((fd = open("/dev/null", O_RDWR, 0)) != -1) {
        dup2(fd, STDIN_FILENO);
        dup2(fd, STDOUT_FILENO);
        dup2(fd, STDERR_FILENO);
        if (fd > STDERR_FILENO) close(fd);
    }
}

參考:
《Unix 高級環境編程(第3版)》,第13章,守護進程。

初始化服務器數據結構

initServerConfig 函數中,程序只初始化了服務器命令表這一個數據結構,其他的數據結構在 initServer 函數中進行初始化。比如:

  • server.clients 鏈表,這是一個服務器端維護客戶端狀態的鏈表,記錄了客戶端的參數、命令執行函數、與服務器最近交互的時間等信息。
  • server.db 數組,包含了服務器中的所有數據庫,一般默認是 16 個數據庫。
  • server.lua 用戶執行 Lua 腳本的 Lua 環境
  • server.slowlog 用於保存慢查詢日誌

問題:服務器爲什麼在 initServerConfig 初始化狀態結構,並加載完配置文件後才初始化這些數據結構呢?

這是因爲,用戶可以在配置文件中制定相關的配置選項參數,服務器必須先載入用戶指定的配置選項,否則,當用戶修改配置文件參數時,服務器就需要重新調整和修改已經創建好的數據結構。

當然,initServer 函數還做了一些其他的操作:

  • 爲服務器設置進程信號處理器 setupSignalHandlers()
  • 創建共享對象 createSharedObjects(),共享對象是一個全局變量,在 redis.c 中申明

    struct sharedObjectsStruct shared;
    大部分都是一些能夠共享的字符串類型的對象,比如錯誤消息等。

  • 打開服務器的監聽端口 Listen(),並創建文件事件,爲套接字關聯連接應答處理器,等待服務器正式運行時接收客戶端的連接。

  • 創建時間事件,關聯 serverCron 函數
  • 如果AOF持久化功能打開,那麼打開現有的AOF文件,如果AOF文件不存在,那麼創建並打開一個新的AOF文件,爲AOF寫入做好準備。
    /* Open the AOF file if needed. */
    if (server.aof_state == REDIS_AOF_ON) {
        server.aof_fd = open(server.aof_filename,
                               O_WRONLY|O_APPEND|O_CREAT,0644);
        if (server.aof_fd == -1) {
            redisLog(REDIS_WARNING, "Can't open the append-only file: %s",
                strerror(errno));
            exit(1);
        }
    }
  • 初始化服務器後臺 I/O 模塊(bio),爲 I/O 操作做好準備。 bioInit()

還原數據庫狀態

在完成了 server 的一系列初始化之後,服務器需要載入 AOF 文件或者 RDB 文件來還原數據庫的狀態。但是,在載入這些文件之前,服務器還需要檢查一下系統參數是否正常。

檢查系統允許的套接字監聽隊列長度的最大值

/* Check that server.tcp_backlog can be actually enforced in Linux according
 * to the value of /proc/sys/net/core/somaxconn, or warn about it. */
void checkTcpBacklogSettings(void) {
#ifdef HAVE_PROC_SOMAXCONN
    FILE *fp = fopen("/proc/sys/net/core/somaxconn","r");
    char buf[1024];
    if (!fp) return;
    if (fgets(buf,sizeof(buf),fp) != NULL) {
        int somaxconn = atoi(buf);
        if (somaxconn > 0 && somaxconn < server.tcp_backlog) {
            redisLog(REDIS_WARNING,"WARNING: The TCP backlog setting of %d cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of %d.", server.tcp_backlog, somaxconn);
        }
    }
    fclose(fp);
#endif
}

對於一個TCP連接,Server 與 Client 需要通過三次握手來建立網絡連接。當三次握手成功後,我們可以看到端口的狀態由 LISTEN 轉變爲 ESTABLISHED。接着這條鏈路上就可以開始傳送數據了。

每一個處於監聽(Listen)狀態的端口,都有自己的監聽隊列.監聽隊列的長度,與如下兩方面有關:

  • somaxconn參數,在 rhel 中,/proc/sys/net/core/somaxconn
  • 使用該端口的程序中 listen(int sockfd, int backlog) 函數.

檢查內存狀態

#ifdef __linux__
int linuxOvercommitMemoryValue(void) {
    FILE *fp = fopen("/proc/sys/vm/overcommit_memory","r");
    char buf[64];

    if (!fp) return -1;
    if (fgets(buf,64,fp) == NULL) {
        fclose(fp);
        return -1;
    }
    fclose(fp);

    return atoi(buf);
}

overcommit_memory 文件指定了內核針對內存分配的策略,其值可以是0、1、2。

  • 0, 表示內核將檢查是否有足夠的可用內存供應用進程使用;如果有足夠的可用內存,內存申請允許;否則,內存申請失敗,並把錯誤返回給應用進程。
  • 1, 表示內核允許分配所有的物理內存,而不管當前的內存狀態如何。
  • 2, 表示內核允許分配超過所有物理內存和交換空間總和的內存

什麼是Overcommit和OOM
  Linux對大部分申請內存的請求都回復”yes”,以便能跑更多更大的程序。因爲申請內存後,並不會馬上使用內存。這種技術叫做Overcommit。當linux發現內存不足時,會發生OOM killer(OOM=out-of-memory)。它會選擇殺死一些進程(用戶態進程,不是內核線程),以便釋放內存。
  當 oom-killer發生時,linux會選擇殺死哪些進程?選擇進程的函數是oom_badness 函數(在mm/oom_kill.c中),該函數會計算每個進程的點數(0~1000)。點數越高,這個進程越有可能被殺死。每個進程的點數跟oom_score_adj 有關,而且 oom_score_adj 可以被設置(-1000最低,1000最高)。

當 redis 中因爲 overcommit_memory 系統參數出現問題時,會出現如下的日誌信息

17 Mar 13:18:02.207 # WARNING overcommit_memory is set to 0! Background save may fail under low memory condition. To fix this issue add 'vm.overcommit_memory = 1' to /etc/sysctl.conf and then reboot or run the command 'sysctl vm.overcommit_memory=1' for this to take effect.

解決辦法
在 root 權限下,修改內核參數

  • 編輯 /etc/sysctl.conf ,改 vm.overcommit_memory=1 ,然後 sysctl -p 使配置文件生效
  • sysctl vm.overcommit_memory=1
  • echo 1 > /proc/sys/vm/overcommit_memory

在 redis 中,需要查看系統是否支持 THP,即 Transparent Huge Page(透明巨頁)

#ifdef __linux__
/* Returns 1 if Transparent Huge Pages support is enabled in the kernel.
 * Otherwise (or if we are unable to check) 0 is returned. */
/* my /sys/kernel/mm/transparent_hugepage/enabled is "[always] never", so THP is set on, if file content is [never], THP is set off */
int THPIsEnabled(void) {
    char buf[1024];

    FILE *fp = fopen("/sys/kernel/mm/transparent_hugepage/enabled","r");
    if (!fp) return 0;
    if (fgets(buf,sizeof(buf),fp) == NULL) {
        fclose(fp);
        return 0;
    }
    fclose(fp);
    return (strstr(buf,"[never]") == NULL) ? 1 : 0;
}
#endif

  一般而言,內存管理的最小塊級單位叫做 page ,一個 page 是 4096 bytes,1M 的內存會有256個 page,1GB的話就會有256,000個 page。CPU 通過內置的內存管理單元維護着 page 表記錄。
  現代的硬件內存管理單元最多隻支持數百到上千的 page 表記錄,並且,對於數百萬 page 表記錄的維護算法必將與目前的數百條記錄的維護算法大不相同才能保證性能,目前的解決辦法是,如果一個程序所需內存page數量超過了內存管理單元的處理大小,操作系統會採用軟件管理的內存管理單元,但這會使程序運行的速度變慢。
  從redhat 6(centos,sl,ol)開始,操作系統開始支持 Huge Pages,也就是大頁。
  簡單來說, Huge Pages就是大小爲 2M 到 1GB 的內存 page,主要用於管理數千兆的內存,比如 1GB 的 page 對於 1TB 的內存來說是相對比較合適的。
  THP(Transparent Huge Pages)是一個使管理 Huge Pages 自動化的抽象層。使用透明巨頁內存的好處:

  1. 可以使用swap,內存頁默認是2M大小,需要使用swap的時候,內存被分割爲4k大小
  2. 對用戶透明,不需要用戶做特殊配置
  3. 不需要root權限
  4. 不需要依某種庫文件

參考
1. 有關 linux 下 redis overcommit_memory 的問題 [http://blog.csdn.net/whycold/article/details/21388455]
2. Transparent Huge Pages 相關概念及對 mysql 的影響 [https://my.oschina.net/llzx373/blog/226446]
3. 透明大頁介紹 [http://www.cnblogs.com/kerrycode/archive/2015/07/23/4670931.html]

加載 AOF 或者 RDB 文件

/* Function called at startup to load RDB or AOF file in memory. */
void loadDataFromDisk(void) {
    long long start = ustime(); //get current time as seconds
    if (server.aof_state == REDIS_AOF_ON) {
        if (loadAppendOnlyFile(server.aof_filename) == REDIS_OK)
            redisLog(REDIS_NOTICE,"DB loaded from append only file: %.3f seconds",(float)(ustime()-start)/1000000);
    } else {
        if (rdbLoad(server.rdb_filename) == REDIS_OK) {
            redisLog(REDIS_NOTICE,"DB loaded from disk: %.3f seconds",
                (float)(ustime()-start)/1000000);
        } else if (errno != ENOENT) {
            redisLog(REDIS_WARNING,"Fatal error loading the DB: %s. Exiting.",strerror(errno));
            exit(1);
        }
    }
}

如果服務器啓用了 AOF 持久化功能,server.aof_state == REDIS_AOF_ON,服務器使用 AOF 文件來還原數據庫狀態;否則,服務器使用 RDB 文件來還原數據庫狀態。

執行事件循環

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

事件循環,處理文件事件和時間事件。

服務器接收回復客戶端的詳細經過

一個命令從客戶端發送到服務器,再由服務器接收執行和回覆的經過,需要客戶端和服務器完成一系列的操作。

命令請求的執行過程

這裏寫圖片描述
加入客戶端發送 SET KEY REDIS 命令給服務器到獲得回覆 OK 期間,需要共同完成以下操作:
1) 客戶端向服務器發送命令請求
2) 服務器接收到客戶端發送的命令請求,執行操作,並在數據庫中設置,操作成功後產生命令回覆OK
3) 服務器將命令結果OK發送給客戶端
4) 客戶端接收到命令回覆OK,打印給用戶

發送命令請求

在前面的章節《redis源碼分析 – cs結構分析之客戶端》[http://blog.csdn.net/honglicu123/article/details/53169843]中已經介紹了客戶端發送命令到服務器的細節,用戶在客戶端鍵入命令,發送到服務器時,是按照 redis 協議格式發送的。

讀取命令請求

當服務器初始化成功後,創建文件事件,將套接字與連接請求處理器關聯,當客戶端與服務器連接之後,就會創建文件事件,將套接字與命令請求處理器連接,客戶端向服務器發送命令請求,觸發該事件,引發命令請求處理器處理,接收客戶端的命令。

1) 讀取套接字中協議格式的命令請求,並保存到客戶端狀態的輸入緩衝區中 c->querybuf

void readQueryFromClient(aeEventLoop *el, int fd, void *privdata, int mask) {
    ...
    qblen = sdslen(c->querybuf);
    if (c->querybuf_peak < qblen) c->querybuf_peak = qblen;
    c->querybuf = sdsMakeRoomFor(c->querybuf, readlen);
    nread = read(fd, c->querybuf+qblen, readlen);
    ...
    if (nread) {
        sdsIncrLen(c->querybuf,nread);
        c->lastinteraction = server.unixtime;
        if (c->flags & REDIS_MASTER) c->reploff += nread;
        server.stat_net_input_bytes += nread;
    }
    ...
}

2) 對輸入緩衝區中的命令進行解析,將參數和參數個數保存在客戶端狀態的 argc 和 argv 中,networking.c/processInlineBuffernetworking.c/processMultibulkBuffer 就是完成這個操作。將redis協議格式的命令請求解析之後,每一個命令參數都生成一個 redisStringObject 類型的結構,保存在 argv 數組中。比如 SET NAME REDIS ,在客戶端狀態結構中將如下所示的形式存儲
這裏寫圖片描述
3) 調用命令執行函數,執行命令。

問題:命令時如何執行的呢

命令執行過程

一、 查找命令實現函數
在服務器初始化 initServerConfig 函數中,

/* Command table -- we initiialize it here as it is part of the
     * initial configuration, since command names may be changed via
     * redis.conf using the rename-command directive. */
    server.commands = dictCreate(&commandTableDictType,NULL);
    server.orig_commands = dictCreate(&commandTableDictType,NULL);
    populateCommandTable();
    server.delCommand = lookupCommandByCString("del");
    server.multiCommand = lookupCommandByCString("multi");
    server.lpushCommand = lookupCommandByCString("lpush");
    server.lpopCommand = lookupCommandByCString("lpop");
    server.rpopCommand = lookupCommandByCString("rpop");

對命令表做了初始化,創建了命令表字典,在上面 服務器中的客戶端狀態 小節中有所描述。

當需要執行命令時,首先根據客戶端狀態中解析出的命令參數 argv[0] 在命令表字典中查找命令實現函數

c->cmd = c->lastcmd = lookupCommand(c->argv[0]->ptr);

c->cmd 和 c->lastcmd 是 redisCommand 結構的指針

struct redisCommand {
    char *name;
    redisCommandProc *proc;
    int arity;
    char *sflags; /* Flags as string representation, one char per flag. */
    int flags;    /* The actual flags, obtained from the 'sflags' field. */
    /* Use a function to determine keys arguments in a command line.
     * Used for Redis Cluster redirect. */
    redisGetKeysProc *getkeys_proc;
    /* What keys should be loaded in background when calling this command? */
    int firstkey; /* The first argument that's a key (0 = no keys) */
    int lastkey;  /* The last argument that's a key */
    int keystep;  /* The step between first and last key */
    long long microseconds, calls;
};

name :是命令的名稱,比如 “SET”
proc :是命令實現函數指針,命令SET的命令實現函數爲 setCommand
arity:命令參數的個數,用於檢查命令請求的格式是否正確。如果是負值 -N,表明這個命令的參數個數大於等於N,如果是正數,就表明參數個數爲N
sflags:字符串形式的標識,比如 “wrm”,這個在初始化命令字典表示,有定義

/* Populates the Redis Command Table starting from the hard coded list
 * we have on top of redis.c file. */
void populateCommandTable(void) {
    int j;
    int numcommands = sizeof(redisCommandTable)/sizeof(struct redisCommand);

    for (j = 0; j < numcommands; j++) {
        struct redisCommand *c = redisCommandTable+j;
        char *f = c->sflags;
        int retval1, retval2;

        while(*f != '\0') {
            switch(*f) {
            case 'w': c->flags |= REDIS_CMD_WRITE; break;
            case 'r': c->flags |= REDIS_CMD_READONLY; break;
            case 'm': c->flags |= REDIS_CMD_DENYOOM; break;
            case 'a': c->flags |= REDIS_CMD_ADMIN; break;
            case 'p': c->flags |= REDIS_CMD_PUBSUB; break;
            case 's': c->flags |= REDIS_CMD_NOSCRIPT; break;
            case 'R': c->flags |= REDIS_CMD_RANDOM; break;
            case 'S': c->flags |= REDIS_CMD_SORT_FOR_SCRIPT; break;
            case 'l': c->flags |= REDIS_CMD_LOADING; break;
            case 't': c->flags |= REDIS_CMD_STALE; break;
            case 'M': c->flags |= REDIS_CMD_SKIP_MONITOR; break;
            case 'k': c->flags |= REDIS_CMD_ASKING; break;
            case 'F': c->flags |= REDIS_CMD_FAST; break;
            default: redisPanic("Unsupported command flag"); break;
            }
            f++;
        }

        retval1 = dictAdd(server.commands, sdsnew(c->name), c);
        /* Populate an additional dictionary that will be unaffected
         * by rename-command statements in redis.conf. */
        retval2 = dictAdd(server.orig_commands, sdsnew(c->name), c);
        redisAssert(retval1 == DICT_OK && retval2 == DICT_OK);
    }
}

flags:是對 sflags 分析得出的二進制標識
calls:記錄服務器執行該命令的次數
milliseconds:記錄服務器執行該命令所耗費總時長

二、命令執行前的檢查工作
1. 檢查命令實現函數是否查找成功,如果 cmd 爲NULL,說明沒有找到該命令的實現函數,返回客戶端一個錯誤 “unknown command”
2. 根據 cmd 的 arity 屬性,檢查命令的參數格式是否正確,如果不正確,返回客戶端錯誤信息 “wrong number of arguments for XX command”
3. 檢查服務器是否啓用 requirepass,如果啓用檢查客戶端是否通過身份驗證,未通過驗證的客戶端只能執行 AUTH 命令,其他命令,服務器將返回客戶端一個錯誤信息 “-NOAUTH Authentication required.\r\n”
4. 如果服務器打開了 maxmemory 功能,在執行命令之前,先檢查內存佔用情況,在需要的情況下,會回收一部分內存。如果執行失敗,將返回錯誤 “-OOM command not allowed when used memory > ‘maxmemory’.\r\n”

服務器執行命令前需要做若干項檢查,具體可通過閱讀源碼或者查看《redis 設計與實現》中的服務器章節。

調用命令實現

前面的操作,服務器已經將命令參數和命令實現函數都保存在了客戶端狀態結構中,服務器只需要執行相應的語句即可

void call(redisClient *c, int flags) {
    ...
    c->cmd->proc(c);
    ...
    /* Log the command into the Slow log if needed, and populate the
     * per-command statistics that we show in INFO commandstats. */
    if (flags & REDIS_CALL_SLOWLOG && c->cmd->proc != execCommand) {
        char *latency_event = (c->cmd->flags & REDIS_CMD_FAST) ?
                              "fast-command" : "command";
        latencyAddSampleIfNeeded(latency_event,duration/1000);
        slowlogPushEntryIfNeeded(c->argv,c->argc,duration);
    }
    if (flags & REDIS_CALL_STATS) {
        c->cmd->microseconds += duration;
        c->cmd->calls++;
    }

    /* Propagate the command into the AOF and replication link */
    if (flags & REDIS_CALL_PROPAGATE) {
        int flags = REDIS_PROPAGATE_NONE;

        if (c->flags & REDIS_FORCE_REPL) flags |= REDIS_PROPAGATE_REPL;
        if (c->flags & REDIS_FORCE_AOF) flags |= REDIS_PROPAGATE_AOF;
        if (dirty)
            flags |= (REDIS_PROPAGATE_REPL | REDIS_PROPAGATE_AOF);
        if (flags != REDIS_PROPAGATE_NONE)
            propagate(c->cmd,c->db->id,c->argv,c->argc,flags);
    }
    ...
}

命令執行完之後,還需要做一些其他操作:
如果服務器開啓了慢查詢日誌功能,服務器會檢查是否需要爲剛執行的命令添加一條慢查詢日誌;
更新客戶端狀態屬性 milliseconds 和 calls 屬性;
如果服務器開啓了AOF,那麼剛纔執行的命令會被寫入到AOF緩衝區;
如果其他服務器正在複製當前服務器,那麼剛執行的命令會被廣播給所有從服務器。

回覆命令給客戶端

當命令執行完之後,如果是 set、hset類的命令,直接 addReply(“+OK\r\n”),回覆客戶端

如果是 get、hget 類的命令,需要將結果保存在客戶端狀態結構的輸出緩衝區中,然後通過 sendReplyToClient 函數返回給客戶端。

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