多級緩衝的服務器數據服務機制實現(一)

很早就想寫一篇這樣的文章,可是第一工作較忙,第二,想用自己的開源服務器作爲藍本實現。由於自己前一段時間較忙,再加上自己也懶了一下,決定在這裏補上,提供給大家參考。作爲我將寫出的"網絡遊戲服務器核心服務開發"的一部分(等我慢慢原創出來),希望通過這些文章,你可以大概瞭解以及學會如何開發一個高效的遊戲服務器體系,併成組合在胸,其實遊戲服務器做到極致就是簡潔高效,減少複雜,這也是我開發的信條之一,如果你的服務器初級開發者都能看懂,那麼你出錯的機率就會很低,而且,更將帶給更多的朋友信心。其實在以前參與開發某知名網遊的時候,這個思想就存在了,一直在外面給別的朋友講課,也大多提及這套系統,至今覺得這樣的實現規則是我見過比較好的。但是自己重寫的時候,卻沒有那麼容易,很多地方想做的更通用一些,所以考慮的也就多了一些,好在是終於完成了。自己測試了一下,效能不錯。其實這套系統,不僅僅可以用在遊戲上,也可以用在很多應用方面,具有一定通用性。所以決定寫出來,和大家分享。
在網絡遊戲中,由於數據的交互,我們時常需要這樣的功能,就是從某一個介質(數據庫文件或者其他)中獲取玩家或者遊戲的數據,然後在遊戲運行期進行調用和修改。然後當玩家下線的時候,我需要把數據回寫到介質(數據庫,文件或者其他)中存儲。
(方案1)
最簡單的想到,我把玩家的數據放在數據庫中,當玩家登陸的時候,我從數據庫中將玩家信息載入內存,當玩家退出的時候,我再將內存寫入數據庫。OK,大功告成,一點也不復雜。恩,這個想法確實很好,好的是足夠簡單,代碼很容易理解,因爲簡單,出錯的機率也會很低。確實不失爲一種解決這類問題的好方法。不過,話說歸來,如果你的遊戲足夠的火爆,N多玩家興奮的等待你的服務器開服的瞬間,進入你的遊戲,體驗你的遊戲,當你開服的一剎那,3000-4000個玩家同時點擊登錄,會是一番怎樣的景象?你可能會很驕傲,但是據我所知,數據庫的訪問可能未必能達到你瞬間處理登錄3000人登錄,我們把3000人排排隊,初始化,登錄,數據加載。你可能會說,數據庫每條數據載入內存能達到1-2毫秒,以此計算,最慢我也可以用2秒多解決問題,實際真的如此嗎?要知道,服務器還要有資源處理別的事情,不可能專心致志只處理你的登錄。


好的,你說,既然如此,就讓我們把方案再進一步改一下,不就是登錄慢麼,那麼我可以在服務器啓動的時候,從數據庫把那些活躍的用戶預先載入內存,這樣服務器不就快了麼?是的,很不錯的方案,讓我們把方案改進一下。


(方案2)服務器啓動的時候,開始遍歷數據庫,一口氣把最後登錄過的5000個活躍用戶數據資料,加載到內存中。好了,讓我啓動一下服務器,還是這個場景,3000-4000個迫不及待的玩家在等你開服的一刻。當你開服的時候,4000人中的2500人的數據可能已經在你的內存中了,那麼,我只用查找剩下的1000人的數據就好了!服務器性能得到了大幅提升。沒錯,看上去很完美(其實也確實很完美),不過,當服務器運行一段時間過後,問題來了,現在同時在線的的玩家有4000人,如果我的服務器不穩定,突然在一個邏輯分支崩潰了,那麼這4000個玩家在這段時間玩遊戲的結果,豈不全付諸東流?
你肯定不會允許這樣的事情發生,聰明的你,開始思考,如果我有一個定時器,沒過一段時間,存儲一下這4000個玩家的數據,那麼就算服務器崩潰了,我也只會丟失距離上次存儲時間一小段的數據而已。恩,對,說幹就幹。


(方案3)
服務器啓動的時候,開始遍歷數據庫,一口氣把最後登錄過的5000個活躍用戶數據資料,加載到內存中。然後我創建一個定時器,比如每5分鐘運行一次,遍歷一下我服務器上的所有活躍玩家,並存入數據庫。很好,當你打開服務器的,有3000-4000個如狼似虎的玩家在涌入你的服務器開始享受遊戲。大家很高興在其中,但是玩家們發現,每到5分鐘的時候,服務器就會變慢一段時間,過一會次恢復。滴答滴答,類似鐘錶的走動的聲音,在深夜中格外的讓人心煩。如果這時候你在看你的服務器,會發現每到5分鐘的時候,CPU和內存就上去了,然後慢慢下來。有點類似心跳。
想做到更優秀的你,開始想,我有沒有辦法解決這個問題呢?畢竟,我的遊戲服務器在到這個時間點,必須要做這個動作,這一點是沒法省去的。就算優化代碼,這部分成本依舊會拖累服務器,畢竟運算量在那裏擺着。怎麼辦?你會想到,我的遊戲服務器進程是一個,能不能我把這個動作,放在另一個進程去做呢?就比如你在飯館吃飯,你點了你喜歡的菜點,服務器找到廚師下單,這時候服務員可以繼續服務別的顧客,不用等菜品做完,只要廚師做完了,放在出餐口,按一下電鈴,"叮咚",服務員去取,然後根據菜品的座位號送過去就行了,是不是很酷?我一直覺得,生活實際是我們最好的老師,在我面對有些問題百思不得其解的時候,生活中的點滴也許早就告訴了你最好的解決方法,所以我認爲,程序員第一要具備的,就是一顆觀察的心。呵呵,說多了,繼續我們的話題。如果你有眼前一亮的感覺,那麼祝賀你,你的思維進階了!
好的,讓我討論一下,如何把這部分工作交給另一個進程,讓它全權負責存儲(廚師),而對於服務器(服務員),就不用管數據什麼時候存儲的,只要讀取和修改就行了。所有遊戲服務資源,專心提供給玩家服務。那麼,我怎麼做到,兩個進程共同使用一個內存呢?這個。。。其實我一開始也不會,讓我們谷歌一下,看看有沒有前輩這麼做過?當我點擊搜索的時候,居然有10多萬個符合條件的結果???什麼?共享內存?這個東東可以跨進程訪問內存?這不就是我要的東東嗎?哈哈,看來,操作系統開發者早就替我們想到了這一點,嘿嘿,讓我們學着站在巨人的肩膀吧。來,繼續完善我們的方案。
(如果想了解共享內存的特性,請讀我以前寫的文章,http://www.acejoy.com/bbs/viewthread.php?tid=2139&extra=page%3D2
(方案4)
服務器啓動的時候,開始遍歷數據庫,一口氣把最後登錄過的5000個活躍用戶數據資料,加載到共享內存中,然後遊戲服務器從中讀取,如果沒有,那麼從介質(數據庫或者文件)中加載進來,放入共享內存,然後我們啓動一個程序,這個程序代碼很簡單,定時將共享內存的數據刷入介質(數據庫或者文件)中好,依舊有3000-4000個如狼似虎的玩家等待進入你的遊戲,當你服務器啓動的時候,大家開始享受遊戲,這時候你可以笑了,因爲你發現,玩家再也不會卡了。共享內存還有一個很好的特性,就是你的進程如果崩潰了,只要不是所有的共享內存引用都沒有了,下次啓動內存還在,不用重新加載。玩家的數據得到了最大的保護。(windows的特性,Linux就算都崩了,只要你不清除,就一直在),哇塞,這個功能太有用了!我興奮了,你呢?還有就是,就算數據存儲失敗了,遊戲服務器不受影響。數據庫崩潰了,只是此後不再緩衝中的玩家不能登錄了,其他玩家可以繼續遊戲。好了,我的遊戲服務器很強大了,不過問題又來了,當我的遊戲服務器運行一段時間後,隨着登錄和離開的玩家越來越多,我的共享內存越來越大,這可不行!聰明的你肯定會想,我的遊戲服務器有一個最大允許玩家在遊戲中的數量,那麼其他的數據是不是對我無用呢?比如你的服務器允許3000個玩家在線,可否把共享內存上限控制在最多加載10000個玩家數據(多餘的7000個是活躍但沒有登錄的用戶,用於增加登錄玩家命中率),很好,想到這個,當然可以,爲什麼不可以?如果共享內存達到了10000個玩家數據,我只需要刪除一個最不常用的玩家,替換成新玩家即可。好的,那麼網上有沒有這樣的算法呢?我們繼續谷歌一下,好傢伙,依舊有十幾萬的索索結果,看來遇到我想到的問題的人還真不是少數。什麼?LRU算法?這個貌似被經常提及。我可以使用它,不過我想更完美一些,我的刪除算法是,最後最不常訪問的玩家,而不是簡單最不常登錄的玩家,因爲有時候,我需要查詢別的不在線的玩家信息。我要最大的保持共享內存的命中率。MRU算法?呵呵,太好了,這個正好對我的胃口。
(如果想了解MRU算法,請讀我的文章,http://www.acejoy.com/bbs/viewthread.php?tid=2971&extra=page%3D2)


(方案5)
服務器啓動的時候,開始遍歷數據庫,一口氣把最後登錄過的5000個活躍用戶數據資料,加載到共享內存中,然後遊戲服務器從中讀取,如果沒有,那麼從介質(數據庫或者文件)中加載進來,放入共享內存,然後我們啓動一個程序,這個程序代碼很簡單,定時將共享內存的數據刷入介質(數據庫或者文件)中,在服務器對共享內存訪問上,添加一個層,負責MRU算法哇塞,你真是太偉大了,你的系統越來越健壯了,連單一服務器崩潰都無法阻擋你的前進了,而且你的共享內存大小是恆定的,連內存碎片都只有對你嘆息的份了,它們也奈何不了你了。真棒!那麼,聰明的你,可否讓我們更完美一些呢?答案當然是可能的。那麼,這個思路看上去哪裏還有瑕疵呢?對了,就是共享內存本身,因爲我的遊戲服務器需要修改玩家數據,那樣肯定是會修改共享內存的數據。而我的定時進程,也會寫我的共享內存(比如標記存儲成功的時間戳)。好傢伙,我有不好的預感。多線程讀寫共享內存,如果兩個進程同一時刻一起寫一個位置,程序必崩無疑。這個傢伙太討厭了,我不能讓他阻擋我。我要解決你!讓我再去谷歌一下,我現在覺得谷歌上肯定有很多朋友遇到和我一樣的問題。恩,什麼,共享內存鎖?信號量?這個真給我雪中送炭啊。不過,等等,再讓我看看,什麼,這個鎖會降低系統的性能?也是,多線程共享資源,哪個鎖能不降低性能呢?怎麼辦,我要完美,完美!不用鎖,把服務器的性能體現到最強!有辦法嗎?有辦法嗎?在無數次糾結之後,我忽然想到,如果我把共享內存一份玩家數據,分爲頭+體。頭由定時進程進行修改,體由遊戲服務器本身修改。那麼,會有改善嗎?對於遊戲服務器,我對數據頭只讀不寫,對體只寫。定時服務器我對頭只寫,對體只讀。只要指針點不同,那麼是否就能繞開鎖呢?我要保證同一時刻,就算都寫,只寫一個地方。OK。如果你也想到了,證明你比我聰明多了,因爲我想這個用了2天時間,呵呵。


(方案6)
服務器啓動的時候,開始遍歷數據庫,一口氣把最後登錄過的5000個活躍用戶數據資料,加載到共享內存中,然後遊戲服務器從中讀取,如果沒有,那麼從介質(數據庫或者文件)中加載進來,放入共享內存,然後我們啓動一個程序,這個程序代碼很簡單,定時將共享內存的數據刷入介質(數據庫或者文件)中,在服務器對共享內存訪問上,添加一個層,負責MRU算法。共享內存一個用戶數據單元分爲頭和體,對於遊戲服務器,只關心"體"就行了。對於存儲服務器,它關心的是"頭"。雙寫入錯誤,你拿我也沒辦法了吧。哈哈。哦,對了,可能機智的你會問,你這個想法依舊不完美,如果我的遊戲服務器正在寫還沒寫完,這時候你的存儲進程恰好在存儲,豈不會存儲一個"半截"的髒數據?是的,說的好,關於這個問題,我的想法是,我的存儲服務,每次都採用memcpy的方法把數據取出來。那麼你肯定會辯駁,別蒙我!memcpy會被線程打斷,OK,沒錯,不過,要符合玩家數據正在修改+存儲服務正在存這個數據+memcpy正好拷貝到這個點。機率並不大,那麼,就算髮生了,你的數據5分鐘後還會恰好在這裏嗎?如果次次都遇到,說明你要去買彩票了。髒數據會存在,那麼我能控制在一段時間後會被健康的數據覆蓋,就行了。


寫到這裏,大家肯定知道了一個思路,下面,就讓我用代碼來給大家說明一下,畢竟,百說不如一練,寫出來纔算本事!
好,下一章,我來講講怎麼實現,當然,個人偏好,我用我的開源服務框架實現這個體系吧
發佈了4 篇原創文章 · 獲贊 13 · 訪問量 17萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章