loki的基礎服務——timer

這一次我們來說timer服務。

無論從什麼角度上來說,timer服務都是一個很特殊的服務。在skynet中,timer服務提供了三個函數,但主要是定時器,並不支持重複的定時服務,而定時器觸發的時候,也是給服務發送一個之前設置的時候指定了session的消息,在概念上很漂亮,然而並不好用。

loki的timer服務則看上去更像一個真正的定時器,loki提供了五個函數,其中lk_time()獲取當前的時間,lk_newtimer()/lk_deltimer()負責創建計時器對象,注意創建對象的時候,需要指定計時器觸發時調用的函數,然後是lk_starttimer()和lk_canceltimer()負責啟動計時器和取消計時器。

和skynet不一樣,skynet的skynet_timeout()通過消息的方式發送將定時器請求發送給了timer服務。然而上面的幾個函數都是直接加鎖訪問了計時器服務的內部狀態,這樣更加直接。但這樣也同樣打破了服務的「獨立性」,理論上,服務需要是獨立的,任何人向服務發送消息,然後服務回消息以通知完成,但這裡就沒有這麼做。能不能這麼做呢?其實也可以,但是用戶就拿不到計時器對象了,用起來顯然不如現在這麼方便,這就是為什麼loki這麼設計的原因。在使用方便的前提下,兼顧概念的完整性,這是loki的設計方式。

但是,這樣是不是就是說,timer服務無法跨進程使用了呢?其實loki本身框架上並不是跨進程的,但是,可以在loki的基礎上製作跨進程的服務容器,從這個角度上來說,timer更像是一個「工具箱函數」,而並不是一個普通的獨立服務。

事實上,所有的基礎服務,除了log,都是通過這種方式來完成任務的,既然每個loki容器都有基礎服務,那麼為什麼需要捨近求遠向對方的基礎服務發送消息呢?這些基礎服務,其實本質上做的是本地的信息維護的工作,因此介面上限制在本地使用,也是順理成章的,就算是skynet,也不得不有些服務只能通過介面服務(比如socket服務),也是這個道理。

skynet的timer的實現方式是時間輪(time wheel),也就是將相近時間的timer放在一個桶里,然後用輪轉的方式減少處理的複雜度。這種處理方法十分的優雅而快速,然而loki並沒有選擇這種方法。loki只是簡單地使用了二叉堆來存儲計時器,經過profile,二叉堆的方法的速度至少比時間輪快一倍以上,因此最終我就選擇了這種O(lgn)插入,O(lgn)刪除的方式,每次執行的複雜度是O(nlgn),但實際上因為只需要判斷首位,所以速度是非常快的。

如果時間到了,skynet的做法是發送一個消息給服務就撒手不管了,而loki的處理方法其實差不多,也是給目標發送一個消息,然而,loki有一個很有特色的功能叫做「重構函數」,timer就註冊了重構函數。當消息被送達的時候,timer的重構函數被執行,「劫下」了這個包,然後就在服務的上下文,直接執行服務提供的回調函數,並且根據回調函數的返回值,自動註冊下一個計時器的觸發時間!通過這個方式,loki的計時器非常好用,而且支持反覆觸發。因為是在目標服務的上下文執行的這個函數,這個函數對於目標服務而言天生就是線程安全的,並不需要任何加鎖的措施,也不會有同一個服務的函數並行地執行。

那麼,最後一個問題,如果我就是想讓其他進程的loki容器執行一個計時操作,又該怎麼辦呢?雖然timer的確是通過發送消息來工作的,但是註冊timer和重構函數都無法跨進程使用。那麼最簡單的方法是提供一個可以跨進程執行的服務,由這個服務來「轉發」這個操作。這樣的服務被叫做「proxy」服務。然而,要實現一個proxy服務其實很複雜,我們來想想proxy服務需要哪些服務的支撐。

顯而易見地,proxy需要跨進程通訊,因此socket服務是需要的;其次,proxy需要知道其他進程的所有服務,那麼monitor和loader是需要的。monitor服務是用來監視當前進程的所有服務的狀態的,當一個服務啟動、被依賴、被銷毀的時候,註冊到monitor服務的鉤子函數就會被執行,類似這裡提到的timer服務。而loader服務負責直接根據一個服務的名字和參數來載入對應的服務,服務可以在dll里,也可以是其他語言的文件。loader服務的工作方式類似Lua語言的require函數,用戶可以向loader服務註冊新的loader函數,loader提供一個lk_require()介面,當require新的服務的時候,loader順次詢問所有的loader函數如何載入這個模塊,如果所有的載入失敗,那麼require載入失敗,寫日誌之後返回NULL,否則,只要任何一個loader知道怎麼載入這個服務,那麼服務就會被載入進來。loader服務本身不負責載入服務,因此我們還需要很多具體的loader負責服務的載入,比如載入dll中的服務,或者載入Lua文件作為服務等等,這些都是之後的開發目標。

而harbor服務則是在loader,monitor和socket的基礎上實現的服務,proxy服務本身就是依賴harbor的,harbor這個詞來自skynet,是海岸的意思,我猜這裡是把消息類比成了漂流瓶。loki也會有harbor服務,但loki的harbor服務是中心協調的風格,並不是和skynet一樣的所有容器互聯。具體的作法是,每一個loki容器啟動的時候,都會嘗試監聽一個特殊的IP和埠號(可以在配置里設定),如果監聽成功,那麼這個loki就成為了root服務,它的harbor就是中心伺服器,如果監聽失敗,則代表已經有了一個中心伺服器,harbor就是客戶端了。當需要發送一個消息給其他的容器的時候,消息先被發送給了harbor,然後harbor直接將消息發送給root中心伺服器,而中心的harbor則負責消息的轉發。如果中心伺服器退出或者崩潰(其他所有伺服器都會得到socket斷線的消息,或者通過超時知道這一點),那麼所有其他的伺服器就會搶奪中心伺服器的許可權,第一個成功監聽埠的loki容器成為新的中心伺服器。

目前的開發狀態是monitor,socket,loader框架(默認的loader還未開發)已經完成,而接下來需要完成的是設想中的harbor和proxy,一切完成之後,一個Lua的loki綁定會優先進行開發,在這完成之後,loki.c會開始開發,這就會是一個開箱即用的服務容器了。到了這一步,loki才具有跟skynet一樣的功能,現在skynet1.0已經發布,我希望能夠趕在skynet2.0之前完成loki的開發,我相信loki一定比skynet更加精鍊、優雅和高效。

接下來的文章,我會繼續講述loki的基礎服務,這裡只是稍微提到了一些設計的想法,而接下來會詳細說明monitor,socket和loader的實現細節與使用方式。

我們下回見 :)

推薦閱讀:

伺服器端編程心得(二)—— Reactor模式
伺服器端編程心得(七)——開源一款即時通訊軟體的源碼
深挖NUMA
伺服器端編程心得(一)—— 主線程與工作線程的分工

TAG:遊戲伺服器 | 伺服器架構 | 手機遊戲開發 |