檔案事件

TuxedoLinux發表於2018-06-01

Redis伺服器是一個事件驅動程式,伺服器需要處理以下兩類事件:

  • 檔案事件(file event):Redis伺服器通過套接字與客戶端(或者其他Redis伺服器)進行連線,而檔案事件就是伺服器對套接字操作的抽象。伺服器與客戶端(或者其他伺服器)的通訊會產生相應的檔案事件,而伺服器則通過監聽並處理這些事件來完成一系列網路通訊操作;
  • 時間事件(time event):Redis伺服器中的一些操作(比如serverCron函式)需要在給定的時間點執行,而時間事件就是伺服器對這類定時操作的抽象。

檔案事件

檔案事件處理器

  • 檔案事件處理器使用I/O多路複用(multiplexing)程式來同時監聽多個套接字,並根據套接字目前執行的任務來為套接字關聯不同的事件處理器;
  • 當被監聽的套接字準備好執行連線應答(accept)、讀取(read)、寫入(write)、關閉(close)等操作時,與操作相對應的檔案事件就會產生,這時檔案事件處理器就會呼叫套接字之前關聯好的事件處理器來處理這些事件。

組成

檔案事件處理器的四個組成部分,它們分別是套接字、I/O多路複用程式、檔案事件分派器(dispatcher),以及事件處理器。

檔案事件處理器的四個組成部分

檔案事件是對套接字操作的抽象,每當一個套接字準備好執行連線應答(accept)、寫入、讀取、關閉等操作時,就會產生一個檔案事件。因為一個伺服器通常會連線多個套接字,所以多個檔案事件有可能會併發地出現。

I/O多路複用程式負責監聽多個套接字,並向檔案事件分派器傳送那些產生了事件的套接字。

儘管多個檔案事件可能會併發地出現,但I/O多路複用程式總是會將所有產生事件的套接字都放到一個佇列裡面,然後通過這個佇列,以有序(sequentially)、同步(synchronously)、每次一個套接字的方式向檔案事件分派器傳送套接字。

檔案事件分派器接收I/O多路複用程式傳來的套接字,並根據套接字產生的事件的型別,呼叫相應的事件處理器;

事件處理器是一個個函式,它們定義了某個事件發生時,伺服器應該執行的動作。

I/O多路複用程式的實現

Redis的I/O多路複用程式的所有功能都是通過包裝常見的select、epoll、evport和kqueue這些I/O多路複用函式庫來實現的,每個I/O多路複用函式庫在Redis原始碼中都對應一個單獨的檔案,比如ae_select.c、ae_epoll.c、ae_kqueue.c,諸如此類。

因為Redis為每個I/O多路複用函式庫都實現了相同的API,所以I/O多路複用程式的底層實現是可以互換,如圖:

Redis在I/O多路複用程式的實現原始碼中用#include巨集定義了相應的規則,程式會在編譯時自動選擇系統中效能最高的I/O多路複用函式庫來作為Redis的I/O多路複用程式的底層實現:

複製程式碼
# 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
複製程式碼

事件的型別

I/O多路複用程式可以監聽多個套接字的ae.h/AE_READABLE事件和ae.h/AE_WRITABLE事件,這兩類事件和套接字操作之間的對應關係如下:

  • 當套接字變得可讀時(客戶端對套接字執行write操作,或者執行close操作),或者有新的可應答(acceptable)套接字出現時(客戶端對伺服器的監聽套接字執行connect操作),套接字產生AE_READABLE事件;
  • 當套接字變得可寫時(客戶端對套接字執行read操作),套接字產生AE_WRITABLE事件。

I/O多路複用程式允許伺服器同時監聽套接字的AE_READABLE事件和AE_WRITABLE事件,如果一個套接字同時產生了這兩種事件,那麼檔案事件分派器會優先處理AE_READABLE事件,等到AE_READABLE事件處理完之後,才處理AE_WRITABLE事件。

API

  • ae.c/aeCreateFileEvent函式接受一個套接字描述符、一個事件型別,以及一個事件處理器作為引數,將給定套接字的給定事件加入到I/O多路複用程式的監聽範圍之內,並對事件和事件處理器進行關聯;
  • ae.c/aeDeleteFileEvent函式接受一個套接字描述符和一個監聽事件型別作為引數,讓I/O多路複用程式取消對給定套接字的給定事件的監聽,並取消事件和事件處理器之間的關聯;
  • ae.c/aeGetFileEvents函式接受一個套接字描述符,返回該套接字正在被監聽的事件型別:
    • 如果套接字沒有任何事件被監聽,那麼函式返回AE_NONE;
    • 如果套接字的讀事件正在被監聽,那麼函式返回AE_READABLE;
    • 如果套接字的寫事件正在被監聽,那麼函式返回AE_WRITABLE;
    • 如果套接字的讀事件和寫事件正在被監聽,那麼函式返回AE_READABLE|AE_WRITABLE。
  • ae.c/aeWait函式接受一個套接字描述符、一個事件型別和一個毫秒數為引數,在給定的時間內阻塞並等待套接字的給定型別事件產生,當事件成功產生,或者等待超時之後,函式返回
  • ae.c/aeApiPoll函式接受一個sys/time.h/struct timeval結構為引數,並在指定的時間內,阻塞並等待所有被aeCreateFileEvent函式設定為監聽狀態的套接字產生檔案事件,當有至少一個事件產生,或者等待超時後,函式返回;
  • ae.c/aeProcessEvents函式是檔案事件分派器,它先呼叫aeApiPoll函式來等待事件產生,然後遍歷所有已產生的事件,並呼叫相應的事件處理器來處理這些事件;
  • ae.c/aeGetApiName函式返回I/O多路複用程式底層所使用的I/O多路複用函式庫的名稱:返回"select"表示底層為select函式庫,諸如此類。

檔案事件的處理器

連線應答處理器

networking.c/acceptTcpHandler函式是Redis的連線應答處理器,這個處理器用於對連線伺服器監聽套接字的客戶端進行應答,其主要呼叫anet.c中anetTcpAccept函式實現,具體實現為sys/socket.h/accept函式的包裝。

當Redis伺服器進行初始化的時候,程式會將這個連線應答處理器和伺服器監聽套接字的AE_READABLE事件關聯起來,當有客戶端用sys/socket.h/connect函式連線伺服器監聽套接字的時候,套接字就會產生AE_READABLE事件,引發連線應答處理器執行,並執行相應的套接字應答操作。

命令請求處理器

networking.c/readQueryFromClient函式是Redis的命令請求處理器,這個處理器負責從套接字中讀入客戶端傳送的命令請求內容,具體實現為unistd.h/read函式的包裝。

當一個客戶端通過連線應答處理器成功連線到伺服器之後,伺服器會將客戶端套接字的AE_READABLE事件和命令請求處理器關聯起來,當客戶端向伺服器傳送命令請求的時候,套接字就會產生AE_READABLE事件,引發命令請求處理器執行,並執行相應的套接字讀入操作;

在客戶端連線伺服器的整個過程中,伺服器都會一直為客戶端套接字的AE_READABLE事件關聯命令請求處理器。

命令回覆處理器

networking.c/sendReplyToClient函式是Redis的命令回覆處理器,這個處理器負責將伺服器執行命令後得到的命令回覆通過套接字返回給客戶端,具體實現為unistd.h/write函式的包裝。

伺服器有命令回覆需要傳送給客戶端的時候伺服器會將客戶端套接字的AE_WRITABLE事件和命令回覆處理器關聯起來,當客戶端準備好接收伺服器傳回的命令回覆時,就會產生AE_WRITABLE事件,引發命令回覆處理器執行,並執行相應的套接字寫入操作

時間事件

時間事件分為以下兩類:

  • 定時事件:讓一段程式在指定的時間之後執行一次。比如說,讓程式X在當前時間的30毫秒之後執行一次;
  • 週期性事件:讓一段程式每隔指定時間就執行一次。比如說,讓程式Y每隔30毫秒就執行一次。

時間事件主要由以下三個屬性組成:

  • id:伺服器為時間事件建立的全域性唯一ID(標識號)。ID號按從小到大的順序遞增,新事件的ID號比舊事件的ID號要大;
  • when:毫秒精度的UNIX時間戳,記錄了時間事件的到達(arrive)時間;
  • timeProc:時間事件處理器,一個函式。當時間事件到達時,伺服器就會呼叫相應的處理器來處理事件。

一個時間事件是定時事件還是週期性事件取決於時間事件處理器的返回值:

  • 如果事件處理器返回ae.h/AE_NOMORE,那麼這個事件為定時事件:該事件在達到一次之後就會被刪除,之後不再到達;
  • 如果事件處理器返回一個非AE_NOMORE的整數值,那麼這個事件為週期性時間:當一個時間事件到達之後,伺服器會根據事件處理器返回的值,對時間事件的when屬性進行更新,讓這個事件在一段時間之後再次到達,並以這種方式一直更新並執行下去。比如說,如果一個時間事件的處理器返回整數值30,那麼伺服器應該對這個時間事件進行更新,讓這個事件在30毫秒之後再次到達。(現在的Redis主要使用這個)

API

ae.c/aeCreateTimeEvent函式接受一個毫秒數milliseconds和一個時間事件處理器proc作為引數,將一個新的時間事件新增到伺服器,這個新的時間事件將在當前時間的milliseconds毫秒之後到達,而事件的處理器為proc。

ae.c/aeDeleteFileEvent函式接受一個時間事件ID作為引數,然後從伺服器中刪除該ID所對應的時間事件;

ae.c/aeSearchNearestTimer函式返回到達時間距離當前時間最接近的那個時間事件;

ae.c/processTimeEvents函式是時間事件的執行器,這個函式會遍歷所有已到達的時間事件,並呼叫這些事件的處理器。已到達指的是,時間事件的when屬性記錄的UNIX時間戳等於或小於當前時間的UNIX時間戳。

processTimeEvents函式的定義可以用以下虛擬碼來描述:

複製程式碼
def processTimeEvents():
    # 
遍歷伺服器中的所有時間事件
    for time_event in all_time_event():
        # 
檢查事件是否已經到達
        if time_event.when <= unix_ts_now():
            # 
事件已到達
            # 
執行事件處理器,並獲取返回值
            retval = time_event.timeProc()
            # 
如果這是一個定時事件
            if retval == AE_NOMORE:
                # 
那麼將該事件從伺服器中刪除
                delete_time_event_from_server(time_event)
        # 
如果這是一個週期性事件
        else:
            # 
那麼按照事件處理器的返回值更新時間事件的 when 
屬性
            # 
讓這個事件在指定的時間之後再次到達
            update_when(time_event, retval)
複製程式碼

 

時間事件應用例項:serverCron函式

持續執行的Redis伺服器需要定期對自身的資源和狀態進行檢查和調整,從而確保伺服器可以長期、穩定地執行,這些定期操作由redis.c/serverCron函式負責執行,它的主要工作包括:

  • 更新伺服器的各類統計資訊,比如時間、記憶體佔用、資料庫佔用情況等;
  • 清理資料庫中的過期鍵值對;
  • 關閉和清理連線失效的客戶端;
  • 嘗試進行AOF或RDB持久化操作;
  • 如果伺服器是主伺服器,那麼對從伺服器進行定期同步;
  • 如果處於叢集模式,對叢集進行定期同步和連線測試;

事件的排程與執行

事件的排程和執行由ae.c/aeProcessEvents函式負責。可以用一下原始碼完成:

複製程式碼
def aeProcessEvents():
    # 
獲取到達時間離當前時間最接近的時間事件
    time_event = aeSearchNearestTimer()
    # 
計算最接近的時間事件距離到達還有多少毫秒
    remaind_ms = time_event.when - unix_ts_now()
    # 
如果事件已到達,那麼remaind_ms
的值可能為負數,將它設定為0
    if remaind_ms < 0:
        remaind_ms 0
    # 
根據remaind_ms
的值,建立timeval
結構
    timeval = create_timeval_with_ms(remaind_ms)
    # 
阻塞並等待檔案事件產生,最大阻塞時間由傳入的timeval
結構決定
    # 
如果remaind_ms
的值為0
,那麼aeApiPoll
呼叫之後馬上返回,不阻塞
    aeApiPoll(timeval)
    # 
處理所有已產生的檔案事件(其實並沒有這個函式)
    processFileEvents()
    # 
處理所有已到達的時間事件
    processTimeEvents()

相關文章