http://blog.sina.com.cn/s/blog_7e0be1580101r09c.html
引言
“哦,怎么又是一門編程語言,夠了,我不需要了!”
也許你看到本文題目首先想到的就是這個。我第一次看到Erlang語言時也是這么想的。
曾幾何時,面向對象的程序設計語言(C++、JAVA、C#等)一直是我心目中無所不能的“大神”,但是在計算機多核化發展日新月異、“云計算”“云存儲”火爆異常的今天,這些主流語言對并發和分布式程序的支持似乎不是那么得心應手。越來越多的鎖、信號量、線程、同步、互斥之類的概念總是讓程序員頭痛不已。
為了適應并行計算和分布式應用的大趨勢,Java5.0引入了concurrency庫,C++有了標準化的OpenMP和MPI,微軟發布了Parrallel FX和MPI.NET等一系列產品。然而,這些亡羊補牢的手段卻不能彌補它們的“先天不足”——即這些語言在創作時就沒有把并行化的問題放到優先的位置上去考慮。
也許是時候讓天生面向并發的Erlang語言大顯身手了。
Erlang最初應用于電信領域,但是現在也遠不限于此。Facebook(也許中國人不大熟悉)用它重寫了超大型聊天系統,Amazon用它開發了SimpleDB云存儲系統,Yahoo!用它開發了Deliciou2.0,類似的成功項目比比皆是。
那么,Erlang語言到底是何方神圣呢?且聽下文分解。
1 Erlang語言的由來
有人說Erlang語言是“因并發而生,因云計算而熱”。與主流語言不同,它是一種“函數式語言(Functional programming,FP)”,使用的是面向并發編程(COP,Concurrency Oriented Programming)的方法。Erlang并不是一門新的語言,它已經有二十多年的歷史了。只是由于近些年對高并發、分布部署、持續服務的需求日漸增加,以及多核CPU的全面普及,Erlang才又引起人們的關注。
Erlang得名于丹麥數學家及統計學家Agner Krarup Erlang,同時Erlang還可以表示Ericsson Language。1987年,Erlang語言創始人Joe Armstrong當年在愛立信做電話網絡方面的開發,他使用Smalltalk,可惜那個時候Smalltalk太慢,不能滿足電話網絡的高性能要求。但Joe實在喜歡Smalltalk,于是定購了一臺Tektronix Smalltak機器。但機器要兩個月時間才到,Joe在等待中百無聊賴,就開始使用Prolog,結果等Tektronix到來的時候,他已經對Prolog更感興趣,Joe當然不滿足于精通Prolog,經過一段時間的試驗,Joe給Prolog加上了并發處理和錯誤恢復,于是Erlang就誕生了。
2 Erlang語言的特性
Erlang語言非常有特色,并發、分布、容錯貫穿于程序的始終。在它的語法里,相等其實不是相等,變量實際上不能改變,實在是非常古怪。但是為了不使本文過于冗長,這里不介紹它的語法,只
2.1 跨平臺性
Erlang運行時環境是一個虛擬機,有點像Java虛擬機,這樣的代碼一經編譯,同樣可以隨處運行。當然,如果你需要更高效的話,字節代碼也可以編譯成本地代碼運行。
2.2并發性
現實世界是并發的。我們很多人可以共同協作完成一項工作。人與人之間不存在任何“Shared Memory(共享的記憶)”,通過傳遞消息(如語言、文字、手勢、表情等)來進行交互。這是現實世界的存在方式,也是Erlang進程的工作方式。
Erlang的并發特性源自語言本身而非操作系統。它創建了一種新的做法,使進程可以非常快地創建和銷毀。因此,Erlang程序可以由多達幾百萬個超輕量級的進程組成,這些進程可以運行于單處理器、多核處理器或者處理器網絡上。Erlang進程之間高度隔離,沒有共享任何數據,只能通過消息傳遞來進行交互,并且這種消息傳遞也是非常迅捷的。
Erlang程序的消息傳遞是通過進程的消息郵箱來實現的。每個進程都有一個用于接受別的進程發來的消息的郵箱。進程可以在任何時候檢查自己的郵箱有沒有新消息抵達,如果有,進程可以按照抵達的順序依次讀取。
消除了共享內存和進程之間的消息通信是Erlang最明顯的優勢之一,這一特點為進行并行與分布式應用的開發提供了非常好的基礎。
2.3分布式
Erlang被設計用于運行在分布式環境下。一個Erlang虛擬機作為一個Erlang節點。一個分布式Erlang系統是由多個Erlang節點組成的網絡。使用Erlang來編寫分布式應用要簡單的多,因為它的分布式機制是透明的:對于程序來說并不知道自己是在分布式運行。
2.4健壯性
Erlang可以說是一門為健壯而生動語言。它不像其它語言一樣假設不會發生錯誤,而是假定錯誤隨時可能發生,并隨時準備修正錯誤。
在一個分布式系統中,如果一個進程出錯,讓這個進程自己修復錯誤有時并不可行,有時節點可能徹底崩潰(死機),因此將錯誤交給其他進程去處理是更恰當的選擇。
在現實世界中,如果一個人停止工作,他就會告訴其他人“我頭疼無法工作了”或者“我心臟病發作了”等。
Erlang錯誤偵測的工作機制也類似于此,通過進程間的相互“鏈接”來實現進程監控(這些進程可以運行于不同節點之上)機制,將錯誤進行分層處理,從而實現構造“容錯系統”。Erlang引入了“速錯(Fail fast)”的理念,在一個進程出錯時不會試圖自行處理,而是直接退出,并發出錯誤消息如“有人讓我去除0”或者“有人讓我從空列表中讀取數據”等,讓其他進程進行錯誤處理、故障恢復等工作。
2.5 軟實時性
Erlang支持可編程的“軟”實時系統,這種系統需要反應時間在毫秒級。而在這種系統中,長時間的垃圾收集(garbage collection)延遲是無法接受的,因此Erlang使用了遞增式垃圾收集技術。
2.6熱部署(Hot swap)
有些系統是不能夠由于軟件維護而停止運行的。Erlang允許程序代碼在系統運行中被修改。舊第代碼能被逐步淘汰而后被新代碼替換。在此過渡期間,新舊代碼是共存的。這也使得安裝bug補丁、系統升級而不干擾系統運行成為了可能。
2.7 遞增式代碼裝載
用戶能夠控制代碼如何被裝載的細節。在嵌入式系統中,所有代碼通常是在啟動時就被完全裝載。而在開發系統中,代碼是按需裝載的,甚至在系統運行時被裝載的。如果測試到了未覆蓋的bug,那么只有具有bug的代碼需要被替換。
2.8 外部接口
Erlang進程與外部世界之間的通訊機制與Erlang進程之間相同的消息傳送機制相同。這種機制被用于和操作系統通訊、與其它語言編寫的程序交互。如果出于高效率的需要,這種機制的一個特殊版本也允許例如C程序這樣的代碼直接鏈接到Erlang運行時系統中來。
3 Erlang語言的分布式編程
3.1 分布式系統的特性
George Coulouris認為:分布式系統是由位于網絡中的一些計算機組成的系統,這些網絡中的計算機之間只能通過傳遞消息來進行溝通和協調。Andrew S.Tanenbaum說:一個分布式系統是一些獨立計算機的集合,但是對這個系統的用戶來說,系統就像一臺計算機一樣。
盡管分布式系統還沒有一個公認令人滿意的定義,但是所有分布式系統都有以下兩個特點:一、系統的各個節點(計算機)是相互獨立的;二、各個節點之間只能通過消息傳遞來進行溝通和協調。
這兩個特點很耳熟吧?沒錯,這也正是Erlang程序的特點!各進程相互獨立,且只能通過消息傳遞來進行交互。
Erlang與分布式系統天性互相吻合,決定了使用Erlang開發分布式系統具有得天獨厚的優勢。Erlang天生面向并發,在Erlang開發的分布式系統增加和刪除節點非常簡便,具有良好的伸縮性。
3.2 Erlang語言分布式編程的兩種基本模型
3.2.1
這種模型用于在一個緊密耦合的計算機集群上編寫程序。這種模型幾乎不需要有什么額外操作,一切就像在單節點編程時一樣:我們可以在任何一個節點創建進程,所有的消息傳遞和錯誤處理原語都可以照搬。
分布式Erlang運行于一個可信任的環境中——因為任何節點都可以運行其他節點上的操作,這就需要一個高度信任的網絡環境。
3.2.2
使用TCP/IP套接字,我們可以編寫運行在非信任環境中的分布式應用程序。這種編程模型的功能比分布式Erlang要弱一些,但是卻更加可靠。
3.3分布式編程實例:名字服務和遠程過程調用
下面我們用Erlang語言來實現一個分布式應用實例:名字服務(Naming Service)。這里面將會用到遠程過程調用(Remote Procedure Call,RPC),你會發現使用Erlang實現RPC簡直是易如反掌。
名字服務就是一種“Key-Value”類型的服務。客戶機向服務器發送一個名字(Key),服務器返回與這個名字(Key)相關聯的值(Value),這樣的服務就叫做名字服務。
3.3.1
文件名kvs.erl:
-module(kvs). |
-export([start/0, store/2, lookup/1]). |
|
start() -> register(kvs, spawn(fun() -> loop() |
|
store(Key, Value) -> rpc({store, Key, Value}). |
|
lookup(Key) -> rpc({lookup, Key}). |
|
rpc(Q) -> |
|
|
|
|
end. |
|
loop() -> |
|
|
|
|
|
|
|
|
|
3.3.2
我所使用的系統是Microsoft Windows 7,所以下面的例子都是在Windows操作系統上完成的。應首先保證系統上安裝有一個可運行的Erlang版本,我所安裝的版本是5.8.1.1。
首先準備使用兩個節點:第一個節點位于名為SERVER計算機上充當服務器的ss節點,另外一個節點是位于位于名為CLIENT的計算機上充當客戶端的cc。
步驟1:在SERVER上啟動一個名為ss的Erlang節點。kvs模塊部署于ss節點上。
C:\ erl –sname ss –setcookie abc Eshell V5.8.1.1 (ss@SERVER)1> |
步驟2:在節點ss上啟動服務。
(ss@SERVER)1>kvs:start(). true |
步驟3:在CLIENT上啟動一個名為cc的Erlang節點,應保證與服務器節點使用相同的cookie。
C:\ erl –sname cc –setcookie abc Eshell V5.8.1.1 (cc@CLIENT)1> |
步驟4:現在我們就可以在客戶端節點cc上進行遠程過程調用了。首先調用服務器節點ss上kvs模塊的store進程,存儲一個名字 “wheather”,其關聯值為“cold”。
(cc@CLIENT)1>rpc:call(ss@SERVER true (cc@CLIENT)2> |
注意:這里的rpc是Erlang標準庫里的一個模塊,與kvs模塊中的rpc毫無關系。
現在就可以遠程調用ss上kvs模塊的lookup進程取得“wheather”的關聯值。
(cc@CLIENT)2>rpc:call(ss@SERVER {ok,cold} (cc@CLIENT)3> |
這樣,我們就輕而易舉地實現了一個簡單名字服務。
4 Erlang語言的應用前景
4.1 Erlang至今沒有廣泛應用的原因
盡管Erlang已經有二十多高齡了,利用它也成功地實現了一批項目。但是,它迄今為止還是一門小眾語言。連Joe Armstrong 也抱怨說,Erlang總是充當“救火隊員”的角色,總是碰到用其它語言難以解決的難題時,人們才會想起Erlang。那么是什么阻礙了Erlang的廣泛應用呢?我覺得主要有以下一些原因:
4.1.1
傳統程序員的思維方式還不能適應并行程序設計的理念;經過了OO訓練的程序員也難以適應COP的模式。習慣了OO(面向對象)語言的程序員在剛接觸到Erlang時一定會覺得很不適應。歷史告訴我們,習慣的力量是強大的,不順應大眾習慣的東西,成長總是會很艱難。此外,Erlang是小眾語言,使用它還存在著維護上的風險。
4.1.2
Erlang的語法的確有些“怪異”,學習困難。它實際上和我們主流的編程的方法、思路是不一樣的。主流編程語言如C++、Java等是基于圖靈機的命令式語言,而Erlang是基于λ-演算的函數式語言。
使用Erlang編寫的代碼非常簡潔(幾乎可以簡潔到極致),代碼量可以非常少。但是我個人認為它并不夠優雅(Joe Armstrong一定不同意我的看法),難以讀懂(在沒有注釋的情況下更是如此),并且也難以學習。
Erlang語言對二進制數據操作支持較好,但是在字符編碼這方面的支持是比較薄弱的。當前版本的Erlang(v5.5.2)使用的是Latin-1(ISO-8859-1)編碼的字符。使用Erlang操作多字節字符存在一定困難。這點對于使用中文的我們來說,非常不便。
4.1.3
Erlang至今仍沒有一個優秀的集成開發環境(IDE),仍然停留在命令行階段。盡管有些人(我覺得他們屬于技術狂一類)認為IDE的功能太強大,會讓程序員越來越懶,但是這的確造成Erlang的吸引力不足。使用Erlang進行桌面應用的界面設計也很麻煩。
4.2Erlang與其它面向并發的編程語言
除了Erlang以外,面向并發的編程語言還有很多。下面簡單介紹我找到的幾種:
q
Scala語言近年來的流行度也在不斷提升。它是一種JVM語言,運行于Java虛擬機之上。Scala語言是靜態類型的,兼具函數式語言(FP)和面向對象編程(OO)的特點。它比Java有著更簡潔的語法,但運行速度不遜于Java甚至超過Java。但是Scala進程與Erlang相比還是不夠輕量級。
q
Go語言是谷歌2009發布的第二款開源編程語言。它專門針對多處理器系統應用程序的編程進行了優化,使用Go編譯的程序可以媲美C或C++代碼的速度,而且更加安全、支持并行進程。Go語言一誕生就備受青睞,由于在2009年市場份額增長最多而一舉摘得了TIOBE公布的年度大獎。
q
F#語言是微軟推出的一門函數式語言(FP),是一種運行在.Net CLR平臺上的新語言。它是OCaml的一個分支,兼具了函數式和命令式面向對象語言的特點;同時它也是一種強類型的編程語言。作為微軟唯一的FP語言,F#的特殊性引起了很多人的關注,而微軟也致力于將F#打造成為.NET平臺的頂峰語言,我們也期待著F#能夠給函數式編程語言帶來一次重大革命。
Scala、Go、F#、Erlang各有特點,很難分出高下。盡管不能斷定Erlang是不是在并行領域是最優的語言,但至少目前來說我覺得Erlang是比較好的。Erlang憑借20多年的發展經驗,在Erlang的層級上支持高并發、濃縮性和數據的持續性存儲都有豐富的庫有著相當多的成熟庫(OTP)和開源軟件,這些資產使得它有極高的實用價值。此外,Erlang還有非常活躍和成熟的社區為開發者解疑釋惑,甚至在社區經常能看到。在熟悉了Erlang的思維方法和社區后,用Erlang開發將是一件非常輕松、非常高效的事情。如果你想在將來的構建多核的應用程序或者是健壯的分布式系統,你應該學習Erlang。
最近關注erlang游戲服務器開發 erlang大牛寫的游戲服務器值得參考
介紹
本文以我的OpenPoker項目為例子,講述了一個構建超強伸縮性的在線多游戲玩家系統。
OpenPoker是一個超強多玩家紙牌服務器,具有容錯、負載均衡和無限伸縮性等特性。
源代碼位于我的個人站點上,大概10,000行代碼,其中1/3是測試代碼。
在OpenPoker最終版本敲定之前我做了大量調研,我嘗試了Delphi、Python、C#、C/C++和Scheme。我還用Common Lisp寫了紙牌引擎。
雖然我花費了9個月的時間研究原型,但是最終重寫時只花了6個星期的時間。
我認為我所節約的大部分時間都得益于選擇Erlang作為平臺。
相比之下,舊版本的OpenPoker花費了一個4~5人的團隊9個月時間。
Erlang是什么東東?
我建議你在繼續閱讀本文之前瀏覽下Erlang FAQ,這里我給你一個簡單的總結...
Erlang是一個函數式動態類型編程語言并自帶并發支持。它是由Ericsson特別為控制開關、轉換協議等電信應用設計的。
Erlang十分適合構建分布式、軟實時的并發系統。
由Erlang所寫的程序通常由成百上千的輕量級進程組成,這些進程通過消息傳遞來通訊。
Erlang進程間的上下文切換通常比C程序線程的上下文切換要廉價一到兩個數量級。
使用Erlang寫分布式程序很簡單,因為它的分布式機制是透明的:程序不需要了解它們是否分布。
Erlang運行時環境是一個虛擬機,類似于Java虛擬機。這意味著在一個價格上編譯的代碼可以在任何地方運行。
運行時系統也允許在一個運行著的系統上不間斷的更新代碼。
如果你需要額外的性能提升,字節碼也可以編譯成本地代碼。
請移步Erlang site,參考Getting started、Documentation和Exampes章節等資源。
為何選擇Erlang?
構建在Erlang骨子里的并發模型特別適合寫在線多玩家服務器。
一個超強伸縮性的多玩家Erlang后端構建為擁有不同“節點”的“集群”,不同節點做不同的任務。
一個Erlang節點是一個Erlang VM實例,你可以在你的桌面、筆記本電腦或服務器上上運行多個Erlang節點/VM。
推薦一個CPU一個節點。
Erlang節點會追蹤所有其他和它相連的節點。向集群里添加一個新節點所需要的只是將該新節點指向一個已有的節點。
一旦這兩個節點建立連接,集群里所有其他的節點都會知曉這個新節點。
Erlang進程使用一個進程id來相互發消息,進程id包含了節點在哪里運行的信息。進程不需要知道其他進程在哪里就可以通訊。
連接在一起的Erlang節點集可以看作一個網格或者超級計算設備。
超多玩家游戲里玩家、NPC和其他實體最好建模為并行運行的進程,但是并行很難搞是眾所皆知的。Erlang讓并行變得簡單。
Erlang的位語法∞讓它在處理結構封裝/拆解的能力上比Perl和Python都要強大。這讓Erlang特別適合處理二進制網絡協議。
OpenPoker架構
OpenPoker里的任何東西都是進程。玩家、機器人、游戲等等多是進程。
對于每個連接到OpenPoker的客戶端都有一個玩家“代理”來處理網絡消息。
根據玩家是否登錄來決定部分消息忽略,而另一部分消息則發送給處理紙牌游戲邏輯的進程。
紙牌游戲進程是一個狀態機,包含了游戲每一階段的狀態。
這可以讓我們將紙牌游戲邏輯當作堆積木,只需將狀態機構建塊放在一起就可以添加新的紙牌游戲。
如果你想了解更多的話可以看看cardgame.erl的start方法。
紙牌游戲狀態機根據游戲狀態來決定不同的消息是否通過。
同時也使用一個單獨的游戲進程來處理所有游戲共有的一些東西,如跟蹤玩家、pot和限制等等。
當在我的筆記本電腦上模擬27,000個紙牌游戲時我發現我擁有大約136,000個玩家以及總共接近800,000個進程。
下面我將以OpenPoker為例子,專注于講述怎樣基于Erlang讓實現伸縮性、容錯和負載均衡變簡單。
我的方式不是特別針對紙牌游戲。同樣的方式可以用在其他地方。
伸縮性
我通過多層架構來實現伸縮性和負載均衡。
第一層是網關節點。
游戲服務器節點組成第二層。
Mnesia“master”節點可以認為是第三層。
Mnesia是Erlang實時分布式數據庫。Mnesia FAQ有一個很詳細的解釋。Mnesia基本上是一個快速的、可備份的、位于內存中的數據庫。
Erlang里沒有對象,但是Mnesia可以認為是面向對象的,因為它可以存儲任何Erlang數據。
有兩種類型的Mnesia節點:寫到硬盤的節點和不寫到硬盤的節點。除了這些節點,所有其他的Mnesia節點將數據保存在內存中。
在OpenPoker里Mnesia master節點會將數據寫入硬盤。網關和游戲服務器從Mnesia master節點獲得數據庫并啟動,它們只是內存節點。
當啟動Mnesia時,你可以給Erlang VM和解釋器一些命令行參數來告訴Mnesia master數據庫在哪里。
當一個新的本地Mnesia節點與master Mnesia節點建立連接之后,新節點變成master節點集群的一部分。
假設master節點位于apple和orange節點上,添加一個新的網關、游戲服務器等等。OpenPoker集群簡單的如下所示:
- 代碼: 全選
erl -mnesia extra_db_nodes /['db@apple','db@orange'/] -s mnesia start
-s mnesia start相當于這樣在erlang shell里啟動Mnedia:
- 代碼: 全選
erl -mnesia extra_db_nodes /['db@apple','db@orange'/]
Erlang (BEAM) emulator version 5.4.8 [source] [hipe] [threads:0]
Eshell V5.4.8 (abort with ^G)
1> mnesia:start().
ok
OpenPoker在Mnesia表里保存配置信息,并且這些信息在Mnesia啟動后立即自動被新的節點下載。零配置!
容錯
通過添加廉價的Linux機器到我的服務器集群,OpenPoker讓我隨心所欲的變大。
將幾架1U的服務器放在一起,這樣你就可以輕易的處理500,000甚至1,000,000的在線玩家。這對MMORPG也是一樣。
我讓一些機器運行網關節點,另一些運行數據庫master來寫數據庫事務到硬盤,讓其他的機器運行游戲服務器。
我限制游戲服務器接受最多5000個并發的玩家,這樣當游戲服務器崩潰時最多影響5000個玩家。
值得注意的是,當游戲服務器崩潰時沒有任何信息丟失,因為所有的Mnesia數據庫事務都是實時備份到其他運行Mnesia以及游戲服務器的節點上的。
為了預防出錯,游戲客戶端必須提供一些援助來平穩的重連接OpenPoker集群。
一旦客戶端發現一個網絡錯誤,它應該連接網關,接受一個新的游戲服務器地址,然后重新連接新的游戲服務器。
下面發生的事情需要一定技巧,因為不同類型的重連接場景需要不同的處理。
OpenPoker會處理如下幾種重連接的場景:
1,游戲服務器崩潰
2,客戶端崩潰或者由于網絡原因超時
3,玩家在線并且在一個不同的連接上
4,玩家在線并且在一個不同的連接上并在一個游戲中
最常見的場景是一個客戶端由于網絡出錯而重新連接。
比較少見但仍然可能的場景是客戶端已經在一臺機器上玩游戲,而此時從另一臺機器上重連接。
每個發送給玩家的OpenPoker游戲緩沖包和每個重連接的客戶端將首先接受所有的游戲包,因為游戲不是像通常那樣正常啟動然后接受包。
OpenPoker使用TCP連接,這樣我不需要擔心包的順序——包會按正確的順序到達。
每個客戶端連接由兩個OpenPoker進程來表現:socket進程和真正的玩家進程。
先使用一個功能受限的visitor進程,直到玩家登錄。例如visitor不能參加游戲。
在客戶端斷開連接后,socket進程死掉,而玩家進程仍然活著。
當玩家進程嘗試發送一個游戲包時可以通知一個死掉的socket,并讓它自己進入auto-play模式或者掛起。
在重新連接時登錄代碼將檢查死掉的socket和活著的玩家進程的結合。代碼如下:
- 代碼: 全選
login({atomic, [Player]}, [_Nick, Pass|_] = Args)
when is_record(Player, player) ->
Player1 = Player#player {
socket = fix_pid(Player#player.socket),
pid = fix_pid(Player#player.pid)
},
Condition = check_player(Player1, [Pass],
[
fun is_account_disabled/2,
fun is_bad_password/2,
fun is_player_busy/2,
fun is_player_online/2,
fun is_client_down/2,
fun is_offline/2
]),
...
condition本身由如下代碼決定:
- 代碼: 全選
is_player_busy(Player, _) ->
{Online, _} = is_player_online(Player, []),
Playing = Player#player.game /= none,
{Online and Playing, player_busy}.
is_player_online(Player, _) ->
SocketAlive = Player#player.socket /= none,
PlayerAlive = Player#player.pid /= none,
{SocketAlive and PlayerAlive, player_online}.
is_client_down(Player, _) ->
SocketDown = Player#player.socket == none,
PlayerAlive = Player#player.pid /= none,
{SocketDown and PlayerAlive, client_down}.
is_offline(Player, _) ->
SocketDown = Player#player.socket == none,
PlayerDown = Player#player.pid == none,
{SocketDown and PlayerDown, player_offline}.
注意login方法的第一件事是修復死掉的進程id:
- 代碼: 全選
fix_pid(Pid)
when is_pid(Pid) ->
case util:is_process_alive(Pid) of
true ->
Pid;
_->
none
end;
fix_pid(Pid) ->
Pid.
以及:
- 代碼: 全選
-module(util).
-export([is_process_alive/1]).
is_process_alive(Pid)
when is_pid(Pid) ->
rpc:call(node(Pid), erlang, is_process_alive, [Pid]).
Erlang里一個進程id包括正在運行的進程的節點的id。
is_pid(Pid)告訴我它的參數是否是一個進程id(pid),但是不能告訴我進程是活著還是死了。
Erlang自帶的erlang:is_process_alive(Pid)告訴我一個本地進程(運行在同一節點上)是活著還是死了,但沒有檢查遠程節點是或者還是死了的is_process_alive變種。
還好,我可以使用Erlang rpc工具和node(pid)來在遠程節點上調用is_process_alive()。
事實上,這跟在本地節點上一樣工作,這樣上面的代碼就可以作為全局分布式進程檢查器。
剩下的唯一的事情是在不同的登錄條件上活動。
最簡單的情況是玩家離線,我期待一個玩家進程,連接玩家到socket并更新player record。
- 代碼: 全選
login(Player, player_offline, [Nick, _, Socket]) ->
{ok, Pid} = player:start(Nick),
OID = gen_server:call(Pid, 'ID'),
gen_server:cast(Pid, {'SOCKET', Socket}),
Player1 = Player#player {
oid = OID,
pid = Pid,
socket = Socket
},
{Player1, {ok, Pid}}.
假如玩家登陸信息不匹配,我可以返回一個錯誤并增加錯誤登錄次數。如果次數超過一個預定義的最大值,我就禁止該帳號:
- 代碼: 全選
login(Player, bad_password, _) ->
N = Player#player.login_errors + 1,
{atomic, MaxLoginErrors} =
db:get(cluster_config, 0, max_login_errors),
if
N > MaxLoginErrors ->
Player1 = Player#player {
disabled = true
},
{Player1, {error, ?ERR_ACCOUNT_DISABLED}};
true ->
Player1 = Player#player {
login_errors =N
},
{Player1, {error, ?ERR_BAD_LOGIN}}
end;
login(Player, account_disabled, _) ->
{Player, {error, ?ERR_ACCOUNT_DISABLED}};
注銷玩家包括使用Object ID(只是一個數字)找到玩家進程id,停止玩家進程,然后在數據庫更新玩家record:
- 代碼: 全選
logout(OID) ->
case db:find(player, OID) of
{atomic, [Player]} ->
player:stop(Player#player.pid),
{atomic, ok} = db:set(player, OID,
[{pid, none},
{socket, none}];
_->
oops
end.
這樣我就可以完成多種重連接condition,例如從不同的機器重連接,我只需先注銷再登錄:
- 代碼: 全選
login(Player, player_online, Args) ->
logout(Player#player.oid),
login(Player, player_offline, Args);
如果玩家空閑時客戶端重連接,我所需要做的只是在玩家record里替換socket進程id然后告訴玩家進程新的socket:
- 代碼: 全選
login(Player, client_down, [_, _, SOcket]) ->
gen_server:cast(Player#player.pid, {'SOCKET', Socket}),
Player1 = Player#player {
socket = Socket
},
{Player1, {ok, Player#player.pid}};
如果玩家在游戲中,這是我們運行上面的代碼,然后告訴游戲重新發送時間歷史:
- 代碼: 全選
login(Player, player_busy, Args) ->
Temp = login(Player, client_down, Args),
cardgame:cast(Player#player.game,
{'RESEND UPDATES', Player#player.pid}),
Temp;
總體來說,一個實時備份數據庫,一個知道重新建立連接到不同的游戲服務器的客戶端和一些有技巧的登錄代碼運行我提供一個高級容錯系統并且對玩家透明。
負載均衡
我可以構建自己的OpenPoker集群,游戲服務器數量大小隨心所欲。
我希望每臺游戲服務器分配5000個玩家,然后在集群的活動游戲服務器間分散負載。
我可以在任何時間添加一個新的游戲服務器,并且它們將自動賦予自己接受新玩家的能力。
網關節點分散玩家負載到OpenPoker集群里活動的游戲服務器。
網關節點的工作是選擇一個隨機的游戲服務器,詢問它所連接的玩家數量和它的地址、主機和端口號。
一旦網關找到一個游戲服務器并且連接的玩家數量少于最大值,它將返回該游戲服務器的地址到連接的客戶端,然后關閉連接。
網關上絕對沒有壓力,網關的連接都非常短。你可以使用非常廉價的機器來做網關節點。
節點一般都成雙成對出現,這樣一個節點崩潰后還有另一個繼續工作。你可能需要一個類似于Round-robin DNS的機制來保證不只一個單獨的網關節點。
- 代碼: 全選
網關怎么知曉游戲服務器?
OpenPoker使用Erlang Distirbuted Named Process Groups工具來為游戲服務器分組。
該組自動對所有的節點全局可見。
新的游戲服務器進入游戲服務器后,當一個游戲服務器節點崩潰時它被自動刪除。
這是尋找容量最大為MaxPlayers的游戲服務器的代碼:
- 代碼: 全選
find_server(MaxPlayers) ->
case pg2:get_closest_pid(?GAME_SERVER) of
Pid when is_pid(Pid) ->
{Time, {Host, Port}} = timer:tc(gen_server, call, [Pid, 'WHERE']),
Coutn = gen_server:call(Pid, 'USER COUNT'),
if
Count < MaxPlayers ->
io:format("~s:~w ~w players~n", [Host, Port, Count]),
{Host, Port};
true ->
io:format("~s:~w is full...~n", [Host, Port]),
find_server(MaxPlayers)
end;
Any ->
Any
end.
pg2:get_closest_pid()返回一個隨機的游戲服務器進程id,因為網關節點上不允許跑任何游戲服務器。
如果一個游戲服務器進程id返回,我詢問游戲服務器的地址(host和port)和連接的玩家數量。
只要連接的玩家數量少于最大值,我返回游戲服務器地址給調用者,否則繼續查找。
- 代碼: 全選
多功能熱插拔中間件
OpenPoker是一個開源軟件,我最近也正在將其投向許多棋牌類運營商。所有商家都存在容錯性和可伸縮性的問題,即使有些已經經過了長年的開發維護。有些已經重寫了代碼,而有些才剛剛起步。所有商家都在Java體系上大筆投入,所以他們不愿意換到Erlang也是可以理解的。
但是,對我來說這是一種商機。我越是深入研究,越發現Erlang更適合提供一個簡單直接卻又高效可靠的解決方案。我把這個解決方案看成一個多功能插座,就像你現在電源插頭上連著的一樣。
你的游戲服務器可以像簡單的單一套接字服務器一樣的寫,只用一個數據庫后臺。實際上,可能比你現在的游戲服務器寫得還要簡單。你的游戲服務器就好比一個電源插頭,多種電源插頭接在我的插線板上,而玩家就從另一端流入。
你提供游戲服務,而我提供可伸縮性,負載平衡,還有容錯性。我保持玩家連到插線板上并監視你的游戲服務器們,在需要的時候重啟任何一個。我還可以在某個服務器當掉的情況下把玩家從一個服務器切換到另一個,而你可以隨時插入新的服務器。
這么一個多功能插線板中間件就像一個黑匣子設置在玩家與服務器之間,而且你的游戲代碼不需要做出任何修改。你可以享用這個方案帶來的高伸縮性,負載平衡,可容錯性等好處,與此同時節約投資并寫僅僅修改一小部分體系結構。
你可以今天就開始寫這個Erlang的中間件,在一個特別為TCP連接數做了優化的Linux機器上運行,把這臺機器放到公眾網上的同時保持你的游戲服務器群組在防火墻背后。就算你不打算用,我也建議你抽空看看Erlang考慮一下如何簡化你的多人在線服務器架構。而且我隨時愿意幫忙!