最后更新于 .

一. 前言

2016年6月17日凌晨5點鐘,我們完成了服務器端V3版本的重構,切換的過程十分平滑且沒有對線上用戶產生任何影響。

這也正式標志著,我們的游戲服務器進入了一個全新的階段。

我們上一次的重構是在 2014年12月23日,現在看看,時間過的真快啊。

而熟悉我的人應該知道,我特意為上一次重構寫過一篇《游戲服務器端架構升級之路》,其中詳細的講述了我們游戲服務器從農業時代跨越到工業時代的歷程。

而這次V3版本的重構,我將其定義為第二次工業革命。也許它沒有那么的強大和完美,但是他切實的解決了現存的大部分問題。

二. 背景

之前的文章已經說過,V2版本的服務器的幾個優點:

  1. 支持服務器代碼熱更新而不影響外網服務
  2. 架構模式足夠簡單:push-pull

但是,其簡單的架構也存在一些缺點:

  1. 業務模塊之間容易互相影響

    比如兩個游戲玩法 A 和 B,內部使用的邏輯、存儲服務器都完全不同,但是在worker層卻是共用的。

    所以一旦玩法A的服務器出現問題導致處理變慢,那么worker就會被堵住,而玩法B也會跟著遭殃。

    同時,即時在一個業務模塊內,也存在請求處理優先級的問題,比如拉取牌局記錄和跟注,要盡量避免跟注這種核心邏輯受到影響。

  2. 限制了游戲邏輯的實現方式

    V2的多worker的模式,導致worker必須限制為無狀態的,因為worker可能處理任何一個請求,而一個請求也可能被分配到任何一個worker上。

    這一點是之前解決服務器熱重啟的關鍵,但同時也限制了我們代碼邏輯多樣性的實現。

    比如我們的游戲桌子數據是存儲在redis中,所有人對桌子的寫操作可能同時分配到多個worker上,而為了避免寫沖突,我們不得不通過redis來實現分布式鎖。

    但是這種分布式鎖的代價是很大的,一是增加了很多無用的查詢和修改請求,二是由于redis的分布式鎖并無法實現先請求先分配的邏輯,所以有可能導致有些請求會被鎖很久。

    而如果我們有辦法讓所以對于某個桌子的請求,都路由到某個進程的話,那么就不會再有鎖的問題了。

    但在V2的架構下,這個是完全沒法操作的。

三. V3實現

針對上面提到的問題,我可以說是冥思苦想了很久,問題的關鍵點在于:

  1. 要解決上面提到的幾個問題
  2. 要保持server支持熱重啟的特性
  3. 要架構足夠簡單,不要增加研發成本
  4. 要消息處理路徑不能太長,防止變慢

而最終的最終,我才確定下來如下的方案:

QQ20160621-1@2x

好吧,原諒我是用vim直接畫的。

這里詳細說明下:

broker

其實broker就是原來的worker,只是功能完全變成了路由的作用。負責將請求轉發到對應的處理server上面去。

broker與server之間使用tcp連接,broker不關心也不等待server的響應。

server

server則是真正進行真正的業務處理,比如我們現在線上業務大體分為: auth_server(登錄), play_server(打牌), hall_server(大廳), common_server(其他)。

這樣不同的業務邏輯在部署上就完全分離了,而由于broker本身是不阻塞的,所以即時auth_server變慢,也不會影響到play_server了。

而每一個server內部,是支持消息分組的,每個分組單獨一個消息隊列,只要將不同優先級的命令字分到不同的分組中去,就不用擔心阻塞了。

trigger

trigger在V2時代就存在,只是當時他的角色還沒有那么重要,因為worker本身是可以直接和gateway進行交互的,所以trigger當時只是在非worker中進行使用(比如web層)。

而在V3中,trigger變成了一個關鍵角色,因為broker(即原來的worker)不再處理業務邏輯,而server本身與gateway之間是沒有連接的,所以trigger就變成了server與gateway通信的唯一橋梁。

OK,現在我們看看到了這一步,我們已經解決了哪些問題。

  1. 業務模塊之間容易互相影響 YES
  2. 限制了游戲邏輯的實現方式 NO
  3. 要保持server支持熱重啟的特性 NO
  4. 要架構足夠簡單,不要增加研發成本 YES
  5. 要消息處理路徑不能太長,防止變慢 YES

我們來看看沒有完成的兩個問題:

要保持server支持熱重啟的特性

  1. server本身的實現是支持 kill -HUP 的,能夠優雅的重啟server內的worker進程。

    需要特殊說明一下的是,由于業務邏輯的復雜性,一個新worker啟動時需要導入的模塊太多,從而導致worker的啟動極慢,尤其當十幾個worker同時啟動時更是如此。

    所以我們做了一個優化,當worker啟動后,直到完全啟動成功前,不會替換現有的worker來分配任務。當所有模塊都導入結束之后,再進入替換worker的邏輯。

    這樣的邏輯是極有必要的,因為之前gateway對應的worker是無狀態無差別的,所以當一組worker啟動很慢時,可能還有另一個組worker可以幫助處理業務。

    而server內的worker是分組的,一旦worker啟動很慢導致消息被堵住,那么是沒有其他worker可以幫忙分擔的。

  2. 一旦需要重啟整個server而不只是server的worker進程時,對于不同的server我們可以在broker中配置不同的處理方案。

    比如play_server的重啟,我們可以要求broker必須等待到連接正常建立,并發送成功后再處理下一個消息;而auth_server的請求比較低,那么就可以直接重啟,broker發現連接斷開后嘗試連接一次,再失敗就直接報錯。

在保證了上面的兩種情況之后,我們基本可以保證業務的熱重啟了。

限制了游戲邏輯的實現方式

其實在引入了server的模式后,業務開發想使用單個進程做本地存儲已經不再受限制了。

我們重點來說一下給桌子廢棄掉鎖的問題。

  1. 我們會給play_server啟動多個分組,每個分組內一個worker。
  2. broker層在給play_server做轉發的時候,會將用戶所在桌子ID取出來放在包頭中
  3. server在收到請求后,會把消息通過取模或者其他方式路由到對應的worker上。由于一個桌子的所有消息都會被路由到一個固定的worker上,所以鎖就可以徹底廢棄了。

而進桌、換桌、挑戰等特殊邏輯的實現會比較復雜,需要將消息做內部的轉發處理。不過其實方式是一樣的。

四. 可能的問題

  1. 消息的路由可能不均勻,比如對于打牌消息的路由,在桌子數比較少的情況下,每個worker的繁忙程度很可能是不一樣的。

    如果要解決的話,要在路由函數那里記錄每個worker的繁忙程度。

    目前解決的優先級并不高

五. 總結

基本上V3相關的介紹就在這里了。

正如我文章開頭說的,也許V3的重構不是最強大和最完美的,但是他切實的解決了我們目前的問題。

我始終相信無論服務器端還是客戶端的架構,都是要不斷演進的。

而每一次在實踐中摸索出來的重構,都會是架構的一次升華。

Pingbacks

Pingbacks已打開。

Trackbacks

引用地址

評論

  1. 胡少

    胡少 on #

    不開源嗎

    Reply

    1. lizhaochao

      lizhaochao on #

      pypi上都開源了吧

      Reply

  2. 高一平

    高一平 on #

    期望貼上架構圖。

    Reply

發表評論