最后更新于 .

其實第一次創業在技術上還是有不少歷史問題的,所以在第二次創業就趁機做了很多彌補,下面詳細說一下。

一.運行環境升級

直接列在這里

centos 6.5 x64  -> centos 7.5 x64
mysql 5.5       -> mysql 5.7
redis 2         -> redis 4
python 2.7      -> python 3.7
django 1.6      -> django 2.1
protobuf 2      -> protobuf 3

老版本有很多的問題,比如mysql5.5的性能問題,redis的dump性能問題,python2的中文處理問題,django1.x系列無法自動migrate,protobuf 2的default混亂問題。
所以當把所有這些升級結束后,真是說不出的酸爽啊。

當然,代價也蠻大,比如基本之前所有的python庫都做了一遍python3的兼容。

二.架構設計升級

I. 注冊登錄設計

以QQ登錄舉例,之前的方案是客戶端拿到用戶openid和token之后,直接連接gateway進行驗證。
這有幾個很大的缺點:

  1. 因為gateway是同步的(因為異步會增加復雜度),而第一次token驗證是要經過騰訊的webapi,時間不可控,所以不得不請求轉發出去或者通過celery做異步處理,導致代碼邏輯非常復雜。當被DDOS時,也極容易被針對。
  2. 沒有游戲自己的統一user token,外部驗證和內部邏輯沒有分離,每次接入新的賬戶SDK都很復雜

但其實這是完全可以解決的。
客戶端拿到用戶的openid和token之后,連接游戲的webserver進行驗證,我們web server可以通過gevent做成異步的,性能極高。
驗證通過之后,創建游戲內部賬號,并返回用戶的游戲內部賬號的userid和usertoken。
之后客戶端再使用獲得的userid和usertoken,連接gateway進行登錄,這里的登錄就一定是只和游戲內部相關的了,所以不用擔心同步的問題。

II. 進入游戲房間設計

在之前的棋牌游戲中,我當時為了簡單,采用了大廳和游戲服公用一個tcp連接的方案。
這個設計的好處是用戶在變更房間時十分迅速,因為不需要等待建立新的tcp連接。
但是缺點也比較明顯,就是分布式和負載均衡,容災等只能做在gateway的后面了。

當然,后來我們也確實做到了,但是總歸這并不是一個特別好的設計。
原因如下:

  1. 即使大廳和房間不共用一個tcp連接,也可以實現快速的房間變更。即只需要房間服務器本身的gateway是無狀態的即可。
  2. 大廳服務器和房間服務器的重要級別不一樣。
    DDOS在攻擊的時候,打死大廳服務器頂多影響新用戶注冊/登錄,而不會影響已經在游戲中的玩家。但是攻擊房間服務器會直接影響玩家體驗。
    所以一般黑客是更希望攻擊房間服務器的。而我們把大廳服務器和房間服務器合在一起,其實是變相方便了攻擊者。

當然,新方案也帶來一定的成本,即客戶端要比之前麻煩一點。
但這也是我現在越來越傾向的觀點:

必要的時候,可以要讓客戶端復雜一點。

舉個例子: 之前在處理換房的時候,我們的做法是,客戶端發送一個換房請求,服務器收到請求后,需要先查到玩家所屬的房間,之后執行退房,再執行進房。
但這導致了服務器的邏輯極其復雜,而且萬一中途出現bug,客戶端所處的狀態也要跟著變化。

而我現在的做法將會是:
客戶端發送一個退房請求,并且帶著自己所在的房間ID,服務器收到請求后,會以收到的房間ID去判斷房間與用戶的關系,如果存在綁定關系,則進行退房操作,并告知客戶端成功。
客戶端手動退房成功之后,向服務器發送進房請求,服務器處理即可。

可以看到,新的方案雖然客戶端稍微麻煩了一點,但是整個邏輯和容錯能力都大大增強。

III. 房間服務器的設計

因為之前的德州棋牌是由下籌碼的概念的,而對于籌碼是絕對不能丟失的。
所以這也是德州的房間和王者榮耀的房間最大的區別。
即,房間數據不可丟失。

所以當時我們的做法是,把整個房間的數據都放進來redis里面,每次有請求的時候就從redis讀取出來,處理完了之后再寫回去。
這樣確實極大的保證了數據的安全,從而確保我們每次發布服務器都可以直接重啟而不影響線上服務。

但這也不是沒有代價的:

  1. 多進程訪問統一redis的房間數據導致需要分布式鎖的問題。
    這個問題在訪問量小的時候可以忽略不記,但是訪問量大了之后會極大的降低性能。
    因為redis默認實現的分布式鎖不是排隊的,即很有可能一個進程申請了很久的鎖,但運氣不好就是拿不到。
    所以后來,我們特意為這種情況改了server架構,每個房間數據只會有一個進程進行修改。
    當然,讀取是可以不用鎖的,所以問題還算能OK。
    但是,即使如此,后來單個進程的處理能力還是成為了瓶頸,畢竟我們是用python做的,cpu密集型的處理本來就滿,而棋牌游戲又有很多需要計算概率的部分,所以后來問題還是蠻多。

  2. redis本身的使用錯誤
    redis的定位原本就是緩存服務器,但是我們這樣用就是假設了redis的數據不能丟失的。
    這導致我們付出了極大的代價,比如一開始我們做雙機熱備+冷備,而為了冷備的實時性,我們設置成60秒內有一次寫就要備份一次。導致機器的cpu和磁盤io常見飆高。
    后來我們實在對于redis的運維感覺力不從心,就切到了阿里云的托管redis上。但是托管redis也只是在熱備上做的比較好,冷備基本上一天才一次。
    真要是出現熱備也掛掉的情況下,冷備派不上一點用處。。

而這次我們的多人競技游戲更像王者榮耀的房間,數據丟了就丟了,大不了再來一局。

所以我們把所有的房間數據都放到了進程內存里,如果真的運氣很差,進程崩了,那也只能當作數據丟失處理了。

當然,這并不代表我們做不了服務器熱更新。
最簡單的做法就是部分切換:

  1. 臨時申請一批服務器,部署新代碼,并開始分配新版本的游戲任務。
  2. 已經在運行中的老房間服務器,不要停止。但是一旦游戲結束,不再分配新的游戲任務,進入空閑狀態。
  3. 等所有老房間服務器都進入空閑狀態后,統一升級新代碼重啟,開始分配游戲任務。
  4. 第一步里部署的房間服務器不再分配新的游戲任務,并在全部進入空閑狀態后,進行回收。

上面的方案基本是沒有問題的。

另外,說回到德州不能丟籌碼的問題,其實用新的方案也不是不能解決的,即進入房間時的兌換籌碼,并不是真的扣除了玩家身上的金幣,而僅僅只是鎖住了。
等到玩家從房間退出的時候,在籌碼兌換回金幣的同時,將鎖定的金幣才進行真實的扣除。
這樣,即使真的房間數據丟失,服務器在檢測到異常的鎖定金幣后,會回退給玩家,那么玩家也只是相當于沒贏沒輸。
也可以再安全一點,即每局結束后都進行一次牌桌數據的落地保存,這樣數據丟失也只會丟失一局的數據。 當然,部分的玩家依然會有意見,尤其是那些贏了大錢的人。
但是只要代碼足夠健壯的話,房間進程掛掉和redis掛掉的概率也沒什么太大區別吧。

好了,時間有限,先寫到這里。

Pingbacks

Pingbacks已打開。

Trackbacks

引用地址

評論

  1. lizhaochao

    lizhaochao on #

    上k8s哦

    Reply

發表評論