版權(quán)說明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權(quán),請進行舉報或認領(lǐng)
文檔簡介
1、游戲服務(wù)端所完成的事情(四)游戲世界狀態(tài)的維護方式2.1數(shù)據(jù)服務(wù)的定位游戲世界的狀態(tài)可以簡單分為兩個部分,一部分是需要存檔的,比如玩家數(shù)據(jù);一部分是不需要存檔的,比如場景狀態(tài)。對于訪問較頻繁的部分,比如場景狀態(tài),會維護成純內(nèi)存數(shù)據(jù);對于訪問較不頻繁的部分,比如玩家存檔,就可以考慮維護在第三方。這個第三方,就是數(shù)據(jù)服務(wù)。數(shù)據(jù)服務(wù)與之前所提到的場景服務(wù)、IM服務(wù)等都屬于應(yīng)用層的概念。數(shù)據(jù)服務(wù)通常也會依賴于一種基礎(chǔ)設(shè)施抽象,那就是緩存。2.1.1 傳統(tǒng)架構(gòu)中的數(shù)據(jù)服務(wù)傳統(tǒng)MMO架構(gòu)中,數(shù)據(jù)服務(wù)的概念非常模糊。我們還是先通過回顧發(fā)展歷史的形式來厘清數(shù)據(jù)服務(wù)的定義?;氐綀鼍斑M程的發(fā)展階段,玩家狀態(tài)是內(nèi)存
2、中的數(shù)據(jù),但是服務(wù)器不會一直開著,因此就有了存盤(文件或db)需求。但是隨著業(yè)務(wù)變復(fù)雜,存盤邏輯需要數(shù)據(jù)層暴露越來越多的存儲API細節(jié),非常難擴展。因此發(fā)展出了Db代理進程,場景進程直接將存檔推給Db代理進程,由Db代理進程定期存盤。這樣,存儲API的細節(jié)在Db代理進程內(nèi)部閉合,游戲邏輯無須再關(guān)注。場景進程只需要通過協(xié)議封包或者RPC的形式與Db代理進程交互,其他的就不用管了。Db代理進程由于是定期存盤,因此它相當(dāng)于維護了玩家存檔的緩存。這個時候,Db代理進程就具有了數(shù)據(jù)服務(wù)的雛形。跟之前的討論一樣,我在這里又要開始批判一番了。很多團隊至今,新立項的項目都仍然采用這種Db代理進程。雖然確實可以
3、用來滿足一定程度的需求,但是,存在幾個致命問題。· 第一,Db代理進程讓整個團隊的代碼復(fù)用級別保持在copy-paste層面。玩家存檔一定是項目特定的,而采用Db代理進程的團隊,通常并不會將Db代理進程設(shè)計成普適、通用的,畢竟對于他們來說,Db代理進程是場景進程和存盤之間的唯一中間層。舉個例子,Db代理進程提供一個LoadPlayer的RPC接口,那么,接口實現(xiàn)就一定是具體游戲相關(guān)的。· 第二,Db代理進程嚴重耦合了兩個概念:一個是面向游戲邏輯的存儲API;一個是數(shù)據(jù)緩存。數(shù)據(jù)緩存本質(zhì)上是一種新的基礎(chǔ)設(shè)施抽象,kv發(fā)展了這么多年,已經(jīng)涌現(xiàn)出無數(shù)高度成熟的工業(yè)級緩存基礎(chǔ)設(shè)施,
4、居然還有新立項游戲?qū)Υ撕笾笥X。殊不知,自己對Db代理進程再怎么做擴展,也不過是在feature set上逐漸接近成熟的KV,但是在可用性上就是玩具和工業(yè)級生產(chǎn)資料的差距。舉個最簡單的例子,有多少團隊的Db代理進程能提供一個規(guī)范化的容忍多少秒掉線的保證?· 第三,Db代理進程在分區(qū)分服架構(gòu)下通常是一區(qū)一個的,一個很重要的原因就是Db代理進程通常是自己YY寫出來的,很少能夠解決擴容問題。如果多服共用一個Db代理進程,全局單點給系統(tǒng)增加不穩(wěn)定性的問題暫且按下不表,負載早就撐爆了。但是只是負責(zé)緩存玩家存檔以及將存檔存盤,這跟之前討論過的全局IM服務(wù)定位非常類似,又有什么必要分區(qū)分服?我們可
5、以構(gòu)建一個數(shù)據(jù)服務(wù)解決這些問題。至于依賴的具體緩存基礎(chǔ)設(shè)施,我之后會以redis為例。redis相比于傳統(tǒng)的KV比如memcache、tc,具有不同的設(shè)計理念,redis的定位是一種數(shù)據(jù)結(jié)構(gòu)服務(wù)器。游戲服務(wù)端開發(fā)可以拿redis當(dāng)緩存用,也可以直接當(dāng)一個數(shù)據(jù)庫用。數(shù)據(jù)服務(wù)解決了什么問題數(shù)據(jù)服務(wù)首先要解決的就是玩家存檔問題。redis作為一個高性能緩存基礎(chǔ)設(shè)施,可以滿足邏輯層的存檔需求。同時還可以實現(xiàn)額外的落地服務(wù),比如將redis中的數(shù)據(jù)定期存回mysql。之所以這樣做,一方面是因為redis的定位是高性能緩存設(shè)施,那就不希望它被rdb、aofrewrite機制拖慢表現(xiàn),或者卡IO;另一方面是
6、對于一些數(shù)據(jù)分析系統(tǒng),用SQL來描述數(shù)據(jù)查詢需求更合適,如果只用redis,還得單獨開發(fā)查詢工具,得不償失。數(shù)據(jù)服務(wù)其次要解決的問題是可以做到服務(wù)級別的復(fù)用。這一點我們可以借助企業(yè)應(yīng)用開發(fā)中的ORM來設(shè)計一套對象-kv-關(guān)系映射。也就是數(shù)據(jù)服務(wù)是統(tǒng)一的,而不同的業(yè)務(wù)可以用不同的數(shù)據(jù)結(jié)構(gòu)描述自己的領(lǐng)域模型,然后數(shù)據(jù)服務(wù)的配套工具會自動生成數(shù)據(jù)訪問層API、redis中cache關(guān)系以及mysql中的table schema。也就是說,同樣的數(shù)據(jù)服務(wù),我在項目A中引用并定義了Player結(jié)構(gòu),就會自動生成LoadPlayer的API;在項目B中定義User同理生成LoadUser的API。這兩個問
7、題是比較容易解決的,最關(guān)鍵的還是一個思路的轉(zhuǎn)換。下面看一種non-trivial的實現(xiàn)。Phial中的DataAccess部分,Phial的Model代碼生成器。實際上,數(shù)據(jù)服務(wù)除去緩存基礎(chǔ)設(shè)施的部分,都屬于外圍機制。在有些設(shè)計中,我們可以看到還是存在緩存服務(wù)與邏輯服務(wù)的中間層。這種中間層的單點問題很容易解決只要不同的邏輯服務(wù)訪問不同的中間層節(jié)點即可。中間層的意義通常是進行RPC到具體緩存協(xié)議API的轉(zhuǎn)換,在我的實現(xiàn)中,由于已經(jīng)有了數(shù)據(jù)訪問API的自動生成,因此沒有這種中間層存在的必要。所有需要訪問數(shù)據(jù)服務(wù)的邏輯服務(wù)都可以直接通過數(shù)據(jù)訪問API訪問。其中還有幾點細節(jié):· 數(shù)據(jù)訪問層A
8、PI的調(diào)用規(guī)范與RPC的調(diào)用規(guī)范保持了統(tǒng)一,都是基于async/await模式。· 通過數(shù)據(jù)服務(wù)對任意存檔進行增加或修改都會記錄一個job,由落地服務(wù)定期檢查job進行落地。引入新的問題目前仍然遺留了幾個問題:· redis單實例的性能確實很強悍,但是如果全區(qū)全服只開一個redis實例確實是存在問題的,這個問題需要解決。· 數(shù)據(jù)服務(wù)對于傳統(tǒng)MMO架構(gòu)來說可以無縫替換掉丑陋的Db代理進程,但是,既然數(shù)據(jù)服務(wù)已經(jīng)能提供抽象程度如此高的存儲接口,那是否還可以應(yīng)用在其他地方?2.1.2 無狀態(tài)服務(wù)中數(shù)據(jù)服務(wù)的定位定義問題之前提到過,游戲世界的狀態(tài)除了需要存檔的玩家數(shù)據(jù),還
9、有一部分是不需要存檔的邏輯服務(wù)的狀態(tài)。數(shù)據(jù)服務(wù)如果只是用來替代MMO中的Db代理進程的,那么它的全部職責(zé)就僅僅是為需要存檔的數(shù)據(jù)提供服務(wù)。從更高的抽象層次來看的話,數(shù)據(jù)服務(wù)相當(dāng)于是維護了client在服務(wù)端的狀態(tài)。但是,數(shù)據(jù)服務(wù)提供了更強大的抽象能力?,F(xiàn)在數(shù)據(jù)服務(wù)的API結(jié)構(gòu)是任意定制的、code first,而且數(shù)據(jù)服務(wù)依賴的基礎(chǔ)設(shè)施redis又被證明非常強大,不僅僅是性能極佳,而且提供了多種數(shù)據(jù)結(jié)構(gòu)抽象。那么,數(shù)據(jù)服務(wù)是否可以維護其他服務(wù)的狀態(tài)?在web開發(fā)中,用緩存維護服務(wù)狀態(tài)是一種很常規(guī)的開發(fā)思路。而在游戲服務(wù)端開發(fā)中,由于場景服務(wù)的存在,這種思路通常并不靠譜。為什么要用緩存維護服務(wù)狀
10、態(tài)?考慮這樣一個問題:如果服務(wù)的狀態(tài)維護在服務(wù)進程中,那么服務(wù)進程掛掉,狀態(tài)就不存在了。而對于我們來說,服務(wù)的狀態(tài)是比服務(wù)進程本身更加重要的因為進程掛了可以趕緊重啟,哪怕耽誤個1、2s,但是狀態(tài)沒了卻意味著這個服務(wù)在整個分布式服務(wù)端中所處的全局一致性已經(jīng)不正確了,即使瞬間就重啟好了也沒用。那么為了讓服務(wù)進程掛掉時不會導(dǎo)致服務(wù)狀態(tài)丟掉,只要分離服務(wù)進程的生命周期和服務(wù)狀態(tài)的生命周期就可以了。將進程和狀態(tài)的生命周期分離帶來的另一個好處就是讓這類服務(wù)的橫向擴展成本降到最低。比較簡單的分離方法是將服務(wù)狀態(tài)維護在共享內(nèi)存里事實上很多項目也確實是這樣做的。但是這種做法擴展性不強,比如很難跨物理機,而且共享
11、內(nèi)存就這樣一個文件安全性很難保障。我們可以將服務(wù)狀態(tài)存放在外部設(shè)施中,比如數(shù)據(jù)服務(wù)。這種可以將狀態(tài)存放在外部設(shè)施的服務(wù)就是無狀態(tài)服務(wù)(stateless service)。而與之對應(yīng)的,場景服務(wù)這種狀態(tài)需要在進程內(nèi)維護的就是有狀態(tài)服務(wù)(stateful service)。有時候跟只接觸過游戲服務(wù)端開發(fā)的業(yè)務(wù)狗談起無狀態(tài)服務(wù),對方竟然會產(chǎn)生 一種“無狀態(tài)服務(wù)是為了解決游戲斷線重連的吧”這種論點,真的很哭笑不得。斷線重連在游戲開發(fā)中固然是大坑之一,但是解決方案從來都跟有無狀態(tài)毫無關(guān)系, 無狀態(tài)服務(wù)畢竟是服務(wù)而不是客戶端。如果真的能實現(xiàn)一個無狀態(tài)游戲客戶端,那真的是能直接解決坑人無數(shù)的斷線重連問題。
12、無狀態(tài)游戲客戶端意味著網(wǎng)絡(luò)通信的成本跟內(nèi)存數(shù)據(jù)訪問的成本一樣低這當(dāng)然是不可能實現(xiàn)的。無狀態(tài)服務(wù)就是為了scalability而出現(xiàn)的,無狀態(tài)服務(wù)橫向擴展的能力相比于有狀態(tài)服務(wù)大大增強,同時實現(xiàn)負載均衡的成本又遠低于有狀態(tài)服務(wù)。分布式系統(tǒng)中有一個基本的CAP原理,也就是一致性C、響應(yīng)性能A、分區(qū)容錯P,無法三者兼顧。無狀態(tài)服務(wù)更傾向于CP,有狀態(tài)服務(wù)更傾向于AP。但是要補充一點,有狀態(tài)服務(wù)的P與無狀態(tài)服務(wù)的P所能達到的程度是不一樣的,后者是真的容錯,前者只能做到不把雞蛋放在一個籃子里。兩種服務(wù)的設(shè)計意圖不同。無狀態(tài)服務(wù)的所有狀態(tài)訪問與修改都增加了內(nèi)網(wǎng)時延,這對于場景服務(wù)這種性能優(yōu)先的服務(wù)是不可忍
13、受的。而有狀態(tài)服務(wù)非常適合場景同步與交互這種數(shù)據(jù)密集的情景,一方面是數(shù)據(jù)交互的延遲僅僅是進程內(nèi)方法調(diào)用的開銷,另一方面由于數(shù)據(jù)局部性原理,對同樣數(shù)據(jù)的訪問非???。既然設(shè)計意圖本來就是不同的,我們這一節(jié)就只討論數(shù)據(jù)服務(wù)與無狀態(tài)服務(wù)的關(guān)系。游戲中可以拆分為無狀態(tài)服務(wù)的業(yè)務(wù)需求其實有很多,基本上所有服務(wù)間交互需求都可以實現(xiàn)為無狀態(tài)服務(wù)。比如切場景服務(wù),因為切場景的請求是有限的,對時延的要求也不會特別高,同理的還有分配房間服務(wù);或者是面向客戶端的IM服務(wù)、拍賣行服務(wù)等等。數(shù)據(jù)服務(wù)對于無狀態(tài)服務(wù)來說,解決了什么問題?簡單來說,就是轉(zhuǎn)移了無狀態(tài)服務(wù)的狀態(tài)維護成本,同時讓無狀態(tài)服務(wù)具有了橫向擴展的能力。因為
14、狀態(tài)維護在數(shù)據(jù)服務(wù)中,所以無狀態(tài)服務(wù)開多少個都無所謂。因此無狀態(tài)服務(wù)非常適合計算密集的業(yè)務(wù)需求。你可能覺得我之前在服務(wù)劃分一節(jié)之后直接提出要引入MQ有些突兀,實際上,服務(wù)劃分要解決的根本問題就是讓程序員能清楚自己定義每種服務(wù)的意圖是什么,哪一種服務(wù)更適合Request-Reply,哪一種服務(wù)更適合Ask-Sync。假設(shè)策劃對游戲沒有分服的需求,理論上講,有節(jié)操的程序是不應(yīng)該以“其他游戲就這樣做的”或“做不到”之類的借口搪塞。每一種服務(wù)都由分布式的多個節(jié)點共同提供服務(wù),如果服務(wù)的消息流更適合Request-Reply pattern,那么實現(xiàn)為無狀態(tài)服務(wù)就更合適,原因有二:· 一個Re
15、quest上來,取相關(guān)數(shù)據(jù),處理,直接返回。整個狀態(tài)的生命周期保持在一次RPC調(diào)用過程中,這描述的就是Request-Reply的工作方式。· 目前只有走MQ的消息pipeline支持Request-Reply pattern,而MQ通常都能很好地支持無狀態(tài)服務(wù)的round-robin work distribution。針對第二點,可能需要稍微介紹下rabbitMQ。rabbitMQ中有exchange(交換機)、queue、binding(綁定規(guī)則)三個主要概念。其中,exchange是對應(yīng)生產(chǎn)者的,queue是對應(yīng)消費者的,binding則是描述消息從exchange到queu
16、e的路由關(guān)系的。exchange有兩種常用類型direct、topic。其中direct exchange接收到的消息是不會dup的,而topic exchange則會將接收到的消息根據(jù)匹配的binding確定要dup到哪個target queue上。這樣,對于無狀態(tài)服務(wù),比如同一命名空間下的切場景服務(wù),可以共用同一個queue,然后client發(fā)來的消息走direct exchange,就可以在MQ層面做到round-robin,將消息輪流分配到不同的切場景服務(wù)上。而且無狀態(tài)服務(wù)本質(zhì)上是沒有擴容成本的,波峰就多開,波谷就少開。程序員負責(zé)為不同服務(wù)規(guī)劃不同的橫向擴展方式。比如類似公會服務(wù)這種走
17、MQ的,橫向擴展的觸發(fā)條件就是現(xiàn)在請求數(shù)量級或者是節(jié)點壓力。比如場景服務(wù)這種Ask-Sync的,橫向擴展就需要借助第三方的服務(wù)作為仲裁者,而這個仲裁者可以實現(xiàn)為基于MQ的服務(wù)。這里有個問題需要注意一下。由于現(xiàn)在同一個client上來的request消息可能由無狀態(tài)服務(wù)的不同節(jié)點處理,那么就會出現(xiàn)這樣的情況:1. 某個client由于一些原因,快速發(fā)了兩個message1、message2。2. message1先到了服務(wù)A,服務(wù)A去數(shù)據(jù)服務(wù)拉相關(guān)數(shù)據(jù)集合Sa,并進行后續(xù)處理。3. 此時message2到了服務(wù)B,服務(wù)B去數(shù)據(jù)服務(wù)拉相關(guān)數(shù)據(jù)集合Sb,進行后續(xù)處理,處理完畢,將結(jié)果存回數(shù)據(jù)服務(wù)。4
18、. 然后服務(wù)A才處理完,并嘗試將處理結(jié)果存回數(shù)據(jù)服務(wù)。假如Sa與Sb有交集,那就會出現(xiàn)競態(tài)條件,如果這時允許服務(wù)A存回結(jié)果,那數(shù)據(jù)就有可能存在不一致。類似的情況還會出現(xiàn)在像率土之濱或者cok這種策略游戲的大世界刷怪需求中。當(dāng)然前提是玩家與大地圖上的元素交互和后臺刷怪邏輯都是基于無狀態(tài)服務(wù)做的。 這其實是一個跨進程共享狀態(tài)問題,而且是一個高度簡化的版本因為這個共享狀態(tài)只在一個實例上維護。可以引入鎖來解決問題,思路通常有兩個:最直觀的一種方案是悲觀鎖。也就是如果要進行修改操作,就需要在讀相關(guān)數(shù)據(jù)的時候就都加上鎖,最后寫成功的時候釋放鎖。獲得鎖所有權(quán)期間其他impure服務(wù)任意讀寫請求都是
19、非法的。但是,這畢竟不是多線程執(zhí)行環(huán)境,沒有語言或平臺幫你做自動鎖釋放的保證。獲取悲觀鎖的服務(wù)節(jié)點不能保證一定會將鎖釋放掉,拿到鎖之后節(jié)點掛掉的可能性非常大。這樣,就需要給悲觀鎖增加超時機制。第二種方案是樂觀鎖。也就是impure服務(wù)可以隨意進行讀請求,讀到的數(shù)據(jù)會額外帶個版本號,等寫的時候?qū)Ρ劝姹咎枺绻恢戮涂梢猿晒懟?,否則就通知到應(yīng)用層失敗,由應(yīng)用層決定后續(xù)操作。帶過期機制的悲觀鎖和樂觀鎖本質(zhì)上都屬于可搶占的分布式鎖,相當(dāng)于是將paxos要解決的問題退化為單Acceptor,因此實現(xiàn)起來非常簡單??蛇^期的悲觀鎖和樂觀鎖唯一的區(qū)別就是前者在申請鎖的時候有可能申請失敗,而后者申請鎖時永遠不
20、會失敗。兩種方案具體的表現(xiàn)優(yōu)劣跟業(yè)務(wù)需求有關(guān),不論一開始選擇的是哪一種,都非常容易切換到另一種。我在示例中實現(xiàn)了一個簡單的樂觀鎖,在提交修改的時候用一個lua腳本做原子檢查就能簡單實現(xiàn)。如果要實現(xiàn)帶過期機制的悲觀鎖,需要保證應(yīng)用層有簡單的時鐘同步機制,而且在申請鎖的時候也要寫一個lua腳本。在應(yīng)用層也做了對應(yīng)修改,調(diào)用數(shù)據(jù)訪問層API可以按如下這種方式調(diào)用。之所以用了RTTI,是考慮到有可能會改成悲觀鎖實現(xiàn),在Dispose的時候會自動release lock。現(xiàn)在pure服務(wù)與impure服務(wù)對數(shù)據(jù)服務(wù)調(diào)用的接口是不一樣的,我們甚至還可以基于這一點在底層做一些擴展,最典型的比如讀寫分離。當(dāng)然
21、,這些都是引入主從之后要考慮的問題了。 View Code 有了這樣一個簡易的鎖機制,我們可以保證單redis實例內(nèi)的一致性。引入新的問題有了無狀態(tài)服務(wù)的概念,我們的架構(gòu)中就可以逐步干掉類似切場景管理這種單點進程。無狀態(tài)服務(wù)是高可用的,也就是說,任意掛掉一個,仍然能持續(xù)提供服務(wù)。整個游戲服務(wù)端理論上應(yīng)該具有整體持續(xù)提供服務(wù)的能力。也就是說,隨便掛掉一個節(jié)點,不需要停服。場景服務(wù)掛掉一個節(jié)點,不會影響其他任何服務(wù),只是玩家短期內(nèi)無法進行場景相關(guān)操作了而已。而我們見過的大多數(shù)架構(gòu),處處皆單點,這完全不能叫可用的架構(gòu)。有的時候一個服務(wù)端跑的好好的,有人硬是要額外加一個全局單點,而
22、且理由是更容易管理,讓人哭笑不得。分布式系統(tǒng)中動不動就想加單點,這是病,得治。判斷一整個游戲服務(wù)端是否具有可用性很簡單,隨便kill掉一個節(jié)點,如果服務(wù)端仍然能持續(xù)提供服務(wù),即使是部分client受到了影響,也能稱為是可用的。但是,現(xiàn)在邏輯服務(wù)具有可用性了,可是數(shù)據(jù)服務(wù)還沒有具有可用性,數(shù)據(jù)服務(wù)依賴于一個redis實例,這個redis實例反而成為了整個服務(wù)端中的單點。幸好,redis像其他大多數(shù)工業(yè)級緩存基礎(chǔ)設(shè)施一樣,已經(jīng)提供了足夠用的可用性機制。但是,在討論redis的可用性機制之前,我們先解決一下數(shù)據(jù)服務(wù)的一個遺留問題,那就是如何構(gòu)建一個可以擴展的全局數(shù)據(jù)服務(wù)。2.2 數(shù)據(jù)服務(wù)的擴展red
23、is是一種stateful service,繼續(xù)應(yīng)用之前的CAP原則,redis是傾向于AP的。之后我們可以看到,redis的各種擴展,實際上都是基于這個原則來做的。2.2.1 分片方案定義問題我們遇到的問題是,如果將數(shù)據(jù)服務(wù)定位為全局服務(wù),那僅用單實例的redis就難以應(yīng)對多變的負載情況。畢竟redis是單線程的。從mysql一路用過來的同學(xué)這時都會習(xí)慣性地水平拆分,redis中也是類似的原理,將整體的數(shù)據(jù)進行切分,每一部分是一個分片shard,不同的shard維護的key集合是不同的。那么,問題的實質(zhì)就是如何基于多個redis實例設(shè)計全局統(tǒng)一的數(shù)據(jù)服務(wù)。同時,有一個約束條件,那就是我們?yōu)榱?/p>
24、性能需要犧牲全局一致性。也就是說,數(shù)據(jù)服務(wù)進行分片擴展的前提是,不提供跨分片事務(wù)的保障。redis cluster也沒有提供類似支持,因為分布式事務(wù)本來就跟redis的定位是有沖突的。因此,我們之后的討論會有一個預(yù)設(shè)前提:不同shard中的數(shù)據(jù)一定是嚴格隔離的,比如是不同組服的數(shù)據(jù),或者是完全不相干的數(shù)據(jù)。要想實現(xiàn)跨shard的數(shù)據(jù)交互,必須依賴更上層的協(xié)調(diào)機制保證,底層不做任何承諾。這樣,我們的分片數(shù)據(jù)服務(wù)就能通過之前提到的簡易鎖機制提供單片內(nèi)的一致性保證,而不再提供全局的一致性保證?;谕瑯拥脑?,我們的分片方案也不會在分片間做類似分布式存儲系統(tǒng)的數(shù)據(jù)冗余機制。分片方案解決了什么問題分片需
25、要解決兩個問題:· 第一個問題,分片方案需要描述shard與shard之間的聯(lián)系,也就是cluster membership。· 第二個問題,分片方案需要描述dbClient的一個請求應(yīng)該交給哪個shard,也就是work distribution。針對第一個問題,解決方案通常有三:· presharding,也就是sharding靜態(tài)配置。· gossip protocol,其實就是redis cluster采用的方案。簡單地說就是集群中每個節(jié)點會由于網(wǎng)絡(luò)分化、節(jié)點抖動等原因而具有不同的集群全局視圖。節(jié)點之間通過gossip protocol進行節(jié)點信
26、息共享。這種方案更強調(diào)CAP中的A原則,因為不需要有仲裁者。· consensus system,這種方案跟上一種正相反,更強調(diào)CAP中的C原則,就是借助分布式系統(tǒng)中的仲裁者來決定集群中各節(jié)點的身份。需求決定解決方案,對于游戲服務(wù)端來說,后兩者的成本太高,而且增加了很多不確定的復(fù)雜性,因此現(xiàn)階段這兩種方案并不是合適的選擇。比如gossip protocol,redis cluster現(xiàn)在都不算是release,確實不太適合游戲服務(wù)端。而且,游戲服務(wù)端畢竟不是web服務(wù),通常是可以在設(shè)計階段確定每個分片的容量上限的,也不需要太復(fù)雜的機制支持。但是第一種方案的缺點也很明顯,做不到動態(tài)增容
27、減容,而且無法高可用。但是如果稍加改造,就足以滿足需求了。在談具體的改造措施之前,先看之前提出的第二個問題。第二個問題實際上是從另一種維度看分片,解決方案很多,但是如果從對架構(gòu)的影響上來看,大概分為兩種:· 一種是proxy-based,基于額外的轉(zhuǎn)發(fā)代理。例子有twemproxy/Codis。· 一種是client sharding,也就是dbClient(每個對數(shù)據(jù)服務(wù)有需求的服務(wù))維護sharding規(guī)則,自助式選擇要去哪個redis實例。redis cluster本質(zhì)上就屬于這種,client側(cè)緩存了部分sharding信息。第一種方案的缺點顯而易見,在整個架構(gòu)中增
28、加了額外的間接層,pipeline中增加了一趟round-trip。如果是像twemproxy或者Codis這種支持高可用的還好,但是github上隨便一翻還能找到特別多的沒法做到高可用的proxy-based方案,無緣無故多個單點,這樣就完全搞不明白sharding的意義何在了。第二種方案的缺點就是集群狀態(tài)發(fā)生變化的時候沒法即時通知到dbClient。第一種方案,我們其實可以直接pass掉了。因為這種方案本質(zhì)上還是更適合web開發(fā)的。web開發(fā)部門眾多,開發(fā)數(shù)據(jù)服務(wù)的部門有可能和業(yè)務(wù)部門相去甚遠,因此需要統(tǒng)一的轉(zhuǎn)發(fā)代理服務(wù)。但是游戲開發(fā)不一樣,數(shù)據(jù)服務(wù)邏輯服務(wù)都是一幫人開發(fā)的,沒什么增加額外
29、中間層的必要。那么,看起來只能選擇第二種方案了。將presharding與client sharding結(jié)合起來后,現(xiàn)在我們的改造成果是:數(shù)據(jù)服務(wù)是全局的,redis可以開多個實例,不相干的數(shù)據(jù)需要到不同的shard上存取,dbClient掌握這個映射關(guān)系。引入新的問題目前的方案只能滿足游戲?qū)?shù)據(jù)服務(wù)的基本需求。大部分采用redis的游戲團隊,一般最終會選定這個方案作為自己的數(shù)據(jù)服務(wù)。后續(xù)的擴展其實對他們來說不是不可以做,但是可能有維護上的復(fù)雜性與不確定性。今天這篇文章,我就繼續(xù)對數(shù)據(jù)服務(wù)做擴展,后面的內(nèi)容權(quán)當(dāng)拋磚引玉?,F(xiàn)在的這個方案存在兩個問題:· 首先,雖然我們沒有支持在線數(shù)據(jù)遷
30、移的必要,但是離線數(shù)據(jù)遷移是必須得有的,畢竟presharding做不到萬無一失。而在這個方案中,如果用單純的哈希算法,增加一個shard會導(dǎo)致原先的key到shard的對應(yīng)關(guān)系變得非常亂,抬高數(shù)據(jù)遷移成本。· 其次,分片方案固然可以將整個數(shù)據(jù)服務(wù)的崩潰風(fēng)險分散在不同shard中,比如相比于不分片的數(shù)據(jù)服務(wù),一臺機器掛掉了,只影響到一部分玩家。但是,我們理應(yīng)可以對數(shù)據(jù)服務(wù)做更深入的擴展,讓其可用程度更強。針對第一個問題,處理方式跟proxy-based采用的處理方式?jīng)]太大區(qū)別,由于目前的數(shù)據(jù)服務(wù)方案比較簡單,采用一致性哈希即可?;蛘卟捎靡环N比較簡單的兩段映射,第一段是靜態(tài)的固定哈希,
31、第二段是動態(tài)的可配置map。前者通過算法,后者通過map配置維護的方式,都能最小化影響到的key集合。而對于第二個問題,實際上就是上一節(jié)末提到的數(shù)據(jù)服務(wù)可用性問題。4.2.2 可用性方案定義問題討論數(shù)據(jù)服務(wù)的可用性之前,我們首先看redis的可用性。對于redis來說,可用性的本質(zhì)是什么?其實就是redis實例掛掉之后可以有后備節(jié)點頂上。 redis通過兩種機制支持這一點。· 一種機制是replication。通常的replication方案主要分為兩種。一種是active-passive,也就是active節(jié)點先修改自身狀態(tài),然后寫統(tǒng)一持久化log,然后passive節(jié)點
32、讀log跟進狀態(tài)。另一種是active-active,寫請求統(tǒng)一寫到持久化log,然后每個active節(jié)點自動同步log進度。還是由于CAP原則,redis的replication方案采用的是一種一致性較弱的active-passive方案。也就是master自身維護log,將log向其他slave同步,master掛掉有可能導(dǎo)致部分log丟失,client寫完master即可收到成功返回,是一種異步replication。這個機制只能解決節(jié)點數(shù)據(jù)冗余的問題,redis要具有可用性就還得解決redis實例掛掉讓備胎自動頂上的問題,畢竟由人肉去監(jiān)控master狀態(tài)再人肉切換是不現(xiàn)實的。 因此還需
33、要第二種機制。· 第二種機制是redis自帶的能夠自動化fail-over的redis sentinel。reds sentinel實際上是一種特殊的reds實例,其本身就是一種高可用服務(wù),可以多開,可以自動服務(wù)發(fā)現(xiàn)(基于redis內(nèi)置的pub-sub支持,sentinel并沒有禁用掉pub-sub的command map),可以自主leader election(基于sentinel實現(xiàn)的raft算法),然后在發(fā)現(xiàn)master掛掉時由leader發(fā)起fail-over,并將掉線后再上線的master降為新master的slave。redis基于自帶的這兩種機制,已經(jīng)能夠?qū)崿F(xiàn)一定程度
34、的可用性。那么接下來,我們來看數(shù)據(jù)服務(wù)如何高可用。數(shù)據(jù)服務(wù)具有可用性的本質(zhì)是什么?除了能實現(xiàn)redis可用性的需求redis實例數(shù)據(jù)冗余、故障自動切換之外,還需要將切換的消息通知到每個dbClient。由于是redis sentinel負責(zé)主從切換,因此最自然的想法就是問sentinel請求當(dāng)前節(jié)點主從連接信息。但是redis sentinel本身也是redis實例,數(shù)量也是動態(tài)的,redis sentinel的連接信息不僅在配置上成了一個難題,動態(tài)更新時也會有各種問題。而且,redis sentinel本質(zhì)上是整個服務(wù)端的static parts(要像dbClient提供服務(wù)),但是卻依賴于
35、redis的啟動,并不是特別優(yōu)雅。另一方面,dbClient要想問redis sentinel要到當(dāng)前連接信息,只能依賴其內(nèi)置的pub-sub機制。redis的pub-sub只是一個簡單的消息分發(fā),沒有消息持久化,因此需要輪詢式的請求連接信息模型。上一節(jié)末提到過,要想最小化數(shù)據(jù)遷移成本可以采用兩段映射或一致性哈希。這時還有另一種可以擴展的思路,如果采用兩段映射,那么我們可以動態(tài)下發(fā)第二段的配置數(shù)據(jù);如果采用一致性哈希,那么我們可以動態(tài)下發(fā)分片的連接信息。這其中的動態(tài),就可以基于新的符合Phial規(guī)范的服務(wù)來做。而這個通知機制,就非常適合采用Phial中的Notify pattern實現(xiàn)。而且r
36、edis sentinel的實現(xiàn)難度比較低,我們完全可以以較低的成本實現(xiàn)一個擴展性更強,定制性更強,還能額外支持分片服務(wù)的部分在線數(shù)據(jù)遷移機制的服務(wù)。同時,有一部分我在這篇文章里也沒提過,那就是落地服務(wù)所依賴的mysql的可用性保障機制。相比于再開一個額外的mysql高可用組件,倒不如整合到同樣的一個數(shù)據(jù)服務(wù)監(jiān)控服務(wù)中。這個監(jiān)控服務(wù)就是watcher。由于原理類似,接下來的討論就不再涉及對mysql的監(jiān)控部分,只針對redis的。watcher解決了什么問題?· 要能夠監(jiān)控redis的生存狀態(tài)。這一點實現(xiàn)起來很簡單,定期的PING redis實例即可。需要的信息以及做出客觀下線和主觀
37、下線的判斷依據(jù)都可以直接照搬sentinel實現(xiàn)。· 要做到自主服務(wù)發(fā)現(xiàn),包括其他watcher的發(fā)現(xiàn)與所監(jiān)控的master-slave組中的新節(jié)點的發(fā)現(xiàn)。前者基于MQ定期Notify通知,后者定期INFO 監(jiān)控的master實例即可。· 要在發(fā)現(xiàn)master客觀下線的時候選出leader進行后續(xù)的故障轉(zhuǎn)移流程。這部分實現(xiàn)起來算是最復(fù)雜的部分,接下來會集中討論。· 選出leader之后將一個最合適的slave提升為master,然后等老的master再上線了就把它降級為新master的slave。解決這些問題,watcher的職責(zé)就已經(jīng)達成,我們的數(shù)據(jù)服務(wù)也就更加
38、健壯,可用程度更高。引入新的問題但是,如果我們引入了新的服務(wù),那就引入了新的不確定性。如果引入這個服務(wù)的同時還要保證數(shù)據(jù)服務(wù)具有可用性,那我們就還得保證這個服務(wù)本身是可用的。先簡單介紹一下redis sentinel的可用性是如何做到的。同時監(jiān)控同一組主從的sentinel可以有多個,master掛掉的時候,這些sentinel會根據(jù)一種raft算法的工業(yè)級實現(xiàn)選舉出leader,算法流程也不是特別復(fù)雜,至少比paxos簡單多了。所有sentinel都是follower,判斷出master客觀下線的sentinel會升級成candidate同時向其他follower拉票,所有follower同
39、一epoch內(nèi)只能投給第一個向自己拉票的candidate。在具體表現(xiàn)中,通常一兩個epoch就能保證形成多數(shù)派,選出leader。有了leader,后面再對redis做SLAVEOF的時候就容易多了。如果想用watcher取代sentinel,最復(fù)雜的實現(xiàn)細節(jié)可能就是這部分邏輯了。這部分邏輯說白了就是要在分布式系統(tǒng)中維護一個一致狀態(tài),舉個例子,可以將“誰是leader”這個概念當(dāng)作一個狀態(tài)量,由分布式系統(tǒng)中的身份相等的幾個節(jié)點共同維護,既然誰都有可能修改這個變量,那究竟誰的修改才奏效呢?幸好,針對這種常見的問題情景,我們有現(xiàn)成的基礎(chǔ)設(shè)施抽象可以解決。這種基礎(chǔ)設(shè)施就是分布式系統(tǒng)的協(xié)調(diào)器組件(c
40、oordinator),老牌的有zookeeper(zab),新一點的有etcd(raft)。這種組件通常沒有重復(fù)開發(fā)的必要,像paxos這種算法理解起來都得老半天,實現(xiàn)起來的細節(jié)數(shù)量級更是難以想象。因此很多現(xiàn)成的開源項目都是依賴這兩者實現(xiàn)高可用的,比如codis就是用的zk。zk解決了什么問題?就我們的游戲服務(wù)端需求來說,zk可以用來選leader,還可以用來維護dbClient的配置數(shù)據(jù)dbClient直接去找zk要數(shù)據(jù)就行了。zk的具體原理我就不再介紹了,具體的可以參考lamport的paxos paper,沒時間沒精力的話搜一下看看zk實現(xiàn)原理的博客就行了。 簡單介紹下如何基
41、于zk實現(xiàn)leader election。zk提供了一個類似于os文件系統(tǒng)的目錄結(jié)構(gòu),目錄結(jié)構(gòu)上的每個節(jié)點都有類型的概念同時可以存儲一些數(shù)據(jù)。zk還提供了一次性觸發(fā)的watch機制。leader election就是基于這幾點概念實現(xiàn)的。假設(shè)有某個目錄節(jié)點/election,watcher1啟動的時候在這個節(jié)點下面創(chuàng)建一個子節(jié)點,節(jié)點類型是臨時順序節(jié)點,也就是說這個節(jié)點會隨創(chuàng)建者掛掉而掛掉,順序的意思就是會在節(jié)點的名字后面加個數(shù)字后綴,唯一標識這個節(jié)點在/election的子節(jié)點中的id。一個簡單的方案是我們可以每個watcher都watch /election的所有子節(jié)點,然后看自己的id是
42、否是最小的,如果是就說明自己是leader,然后告訴應(yīng)用層自己是leader,讓應(yīng)用層進行后續(xù)操作就行了。但是這樣會產(chǎn)生驚群效應(yīng),因為一個子節(jié)點刪除,每個watcher都會收到通知,但是至多一個watcher會從follower變?yōu)閘eader。優(yōu)化一些的方案是每個節(jié)點都關(guān)注比自己小一個排位的節(jié)點。這樣如果id最小的節(jié)點掛掉之后,id次小的節(jié)點會收到通知然后了解到自己成為了leader,避免了驚群效應(yīng)。還有一點需要注意的是,臨時順序節(jié)點的臨時性體現(xiàn)在一次session而不是一次連接的終止。例如watcher1每次申請節(jié)點都叫watcher1,第一次它申請成功的節(jié)點全名假設(shè)是watcher100
43、02(后面的是zk自動加的序列號),然后下線,watcher10002節(jié)點還會存在一段時間,如果這段時間內(nèi)watcher1再上線,再嘗試創(chuàng)建watcher1就會失敗,然后之前的節(jié)點過一會兒就因為session超時而銷毀,這樣就相當(dāng)于這個watcher1消失了。解決方案有兩個,可以創(chuàng)建節(jié)點前先顯式delete一次,也可以通過其他機制保證每次創(chuàng)建節(jié)點的名字不同,比如guid。 至于配置下發(fā),就更簡單了。配置變更時直接更新節(jié)點數(shù)據(jù),就能借助zk通知到關(guān)注的dbClient,這種事件通知機制相比于輪詢請求sentinel要配置數(shù)據(jù)的機制更加優(yōu)雅。我在實現(xiàn)中將zk作為路由協(xié)議的一種整合進了Phial規(guī)范,這樣基于zk的消息通知可以直接走Phial的RPC協(xié)議。有興趣的同學(xué)可以看下我實現(xiàn)的zkAdaptor,leader election的功能作為zkAdaptor的特殊API,watcherService會直接調(diào)用。而配置下發(fā)直接走了RPC協(xié)議,集成在統(tǒng)一的Phia
溫馨提示
- 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
- 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯(lián)系上傳者。文件的所有權(quán)益歸上傳用戶所有。
- 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁內(nèi)容里面會有圖紙預(yù)覽,若沒有圖紙預(yù)覽就沒有圖紙。
- 4. 未經(jīng)權(quán)益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
- 5. 人人文庫網(wǎng)僅提供信息存儲空間,僅對用戶上傳內(nèi)容的表現(xiàn)方式做保護處理,對用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對任何下載內(nèi)容負責(zé)。
- 6. 下載文件中如有侵權(quán)或不適當(dāng)內(nèi)容,請與我們聯(lián)系,我們立即糾正。
- 7. 本站不保證下載資源的準確性、安全性和完整性, 同時也不承擔(dān)用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。
最新文檔
- 寶業(yè)墊付合同范例
- 雙方買賣狗廠合同范例
- 保險推廣合同范例
- 合法采砂船出租合同范例
- 低價購買租賃合同范例
- 徐匯附近廠房出租合同范例
- 房屋全款交易合同范例
- 買賣膠帶分裝合同范例
- 家具安裝用工合同范例
- 家具代加工合同范例
- 史學(xué)概論復(fù)習(xí)題
- 2023年中國鹽業(yè)集團有限公司校園招聘筆試題庫及答案解析
- 教師情緒和壓力疏導(dǎo)課件
- 教科版八年級上冊物理知識點
- 噴淋系統(tǒng)壓力測試記錄
- 微型消防站培訓(xùn)
- AEO認證系列-供應(yīng)鏈安全培訓(xùn)
- 信息技術(shù)融合課例案例平行四邊形的面積
- 階梯圓筒落料拉深模具設(shè)計
- 起重機安全操作培訓(xùn)課件
- 起重吊裝作業(yè)安全卡控標準
評論
0/150
提交評論