游戲服務(wù)器端所完成的事(四)_第1頁(yè)
游戲服務(wù)器端所完成的事(四)_第2頁(yè)
游戲服務(wù)器端所完成的事(四)_第3頁(yè)
游戲服務(wù)器端所完成的事(四)_第4頁(yè)
游戲服務(wù)器端所完成的事(四)_第5頁(yè)
已閱讀5頁(yè),還剩13頁(yè)未讀, 繼續(xù)免費(fèi)閱讀

下載本文檔

版權(quán)說明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權(quán),請(qǐng)進(jìn)行舉報(bào)或認(rèn)領(lǐng)

文檔簡(jiǎn)介

1、游戲服務(wù)端所完成的事情(四)游戲世界狀態(tài)的維護(hù)方式2.1數(shù)據(jù)服務(wù)的定位游戲世界的狀態(tài)可以簡(jiǎn)單分為兩個(gè)部分,一部分是需要存檔的,比如玩家數(shù)據(jù);一部分是不需要存檔的,比如場(chǎng)景狀態(tài)。對(duì)于訪問較頻繁的部分,比如場(chǎng)景狀態(tài),會(huì)維護(hù)成純內(nèi)存數(shù)據(jù);對(duì)于訪問較不頻繁的部分,比如玩家存檔,就可以考慮維護(hù)在第三方。這個(gè)第三方,就是數(shù)據(jù)服務(wù)。數(shù)據(jù)服務(wù)與之前所提到的場(chǎng)景服務(wù)、IM服務(wù)等都屬于應(yīng)用層的概念。數(shù)據(jù)服務(wù)通常也會(huì)依賴于一種基礎(chǔ)設(shè)施抽象,那就是緩存。2.1.1 傳統(tǒng)架構(gòu)中的數(shù)據(jù)服務(wù)傳統(tǒng)MMO架構(gòu)中,數(shù)據(jù)服務(wù)的概念非常模糊。我們還是先通過回顧發(fā)展歷史的形式來厘清數(shù)據(jù)服務(wù)的定義?;氐綀?chǎng)景進(jìn)程的發(fā)展階段,玩家狀態(tài)是內(nèi)存

2、中的數(shù)據(jù),但是服務(wù)器不會(huì)一直開著,因此就有了存盤(文件或db)需求。但是隨著業(yè)務(wù)變復(fù)雜,存盤邏輯需要數(shù)據(jù)層暴露越來越多的存儲(chǔ)API細(xì)節(jié),非常難擴(kuò)展。因此發(fā)展出了Db代理進(jìn)程,場(chǎng)景進(jìn)程直接將存檔推給Db代理進(jìn)程,由Db代理進(jìn)程定期存盤。這樣,存儲(chǔ)API的細(xì)節(jié)在Db代理進(jìn)程內(nèi)部閉合,游戲邏輯無須再關(guān)注。場(chǎng)景進(jìn)程只需要通過協(xié)議封包或者RPC的形式與Db代理進(jìn)程交互,其他的就不用管了。Db代理進(jìn)程由于是定期存盤,因此它相當(dāng)于維護(hù)了玩家存檔的緩存。這個(gè)時(shí)候,Db代理進(jìn)程就具有了數(shù)據(jù)服務(wù)的雛形。跟之前的討論一樣,我在這里又要開始批判一番了。很多團(tuán)隊(duì)至今,新立項(xiàng)的項(xiàng)目都仍然采用這種Db代理進(jìn)程。雖然確實(shí)可以

3、用來滿足一定程度的需求,但是,存在幾個(gè)致命問題。 第一,Db代理進(jìn)程讓整個(gè)團(tuán)隊(duì)的代碼復(fù)用級(jí)別保持在copy-paste層面。玩家存檔一定是項(xiàng)目特定的,而采用Db代理進(jìn)程的團(tuán)隊(duì),通常并不會(huì)將Db代理進(jìn)程設(shè)計(jì)成普適、通用的,畢竟對(duì)于他們來說,Db代理進(jìn)程是場(chǎng)景進(jìn)程和存盤之間的唯一中間層。舉個(gè)例子,Db代理進(jìn)程提供一個(gè)LoadPlayer的RPC接口,那么,接口實(shí)現(xiàn)就一定是具體游戲相關(guān)的。 第二,Db代理進(jìn)程嚴(yán)重耦合了兩個(gè)概念:一個(gè)是面向游戲邏輯的存儲(chǔ)API;一個(gè)是數(shù)據(jù)緩存。數(shù)據(jù)緩存本質(zhì)上是一種新的基礎(chǔ)設(shè)施抽象,kv發(fā)展了這么多年,已經(jīng)涌現(xiàn)出無數(shù)高度成熟的工業(yè)級(jí)緩存基礎(chǔ)設(shè)施,居然還有新立項(xiàng)游戲?qū)Υ撕?/p>

4、知后覺。殊不知,自己對(duì)Db代理進(jìn)程再怎么做擴(kuò)展,也不過是在feature set上逐漸接近成熟的KV,但是在可用性上就是玩具和工業(yè)級(jí)生產(chǎn)資料的差距。舉個(gè)最簡(jiǎn)單的例子,有多少團(tuán)隊(duì)的Db代理進(jìn)程能提供一個(gè)規(guī)范化的容忍多少秒掉線的保證? 第三,Db代理進(jìn)程在分區(qū)分服架構(gòu)下通常是一區(qū)一個(gè)的,一個(gè)很重要的原因就是Db代理進(jìn)程通常是自己YY寫出來的,很少能夠解決擴(kuò)容問題。如果多服共用一個(gè)Db代理進(jìn)程,全局單點(diǎn)給系統(tǒng)增加不穩(wěn)定性的問題暫且按下不表,負(fù)載早就撐爆了。但是只是負(fù)責(zé)緩存玩家存檔以及將存檔存盤,這跟之前討論過的全局IM服務(wù)定位非常類似,又有什么必要分區(qū)分服?我們可以構(gòu)建一個(gè)數(shù)據(jù)服務(wù)解決這些問題。至于

5、依賴的具體緩存基礎(chǔ)設(shè)施,我之后會(huì)以redis為例。redis相比于傳統(tǒng)的KV比如memcache、tc,具有不同的設(shè)計(jì)理念,redis的定位是一種數(shù)據(jù)結(jié)構(gòu)服務(wù)器。游戲服務(wù)端開發(fā)可以拿redis當(dāng)緩存用,也可以直接當(dāng)一個(gè)數(shù)據(jù)庫(kù)用。數(shù)據(jù)服務(wù)解決了什么問題數(shù)據(jù)服務(wù)首先要解決的就是玩家存檔問題。redis作為一個(gè)高性能緩存基礎(chǔ)設(shè)施,可以滿足邏輯層的存檔需求。同時(shí)還可以實(shí)現(xiàn)額外的落地服務(wù),比如將redis中的數(shù)據(jù)定期存回mysql。之所以這樣做,一方面是因?yàn)閞edis的定位是高性能緩存設(shè)施,那就不希望它被rdb、aofrewrite機(jī)制拖慢表現(xiàn),或者卡IO;另一方面是對(duì)于一些數(shù)據(jù)分析系統(tǒng),用SQL來描述

6、數(shù)據(jù)查詢需求更合適,如果只用redis,還得單獨(dú)開發(fā)查詢工具,得不償失。數(shù)據(jù)服務(wù)其次要解決的問題是可以做到服務(wù)級(jí)別的復(fù)用。這一點(diǎn)我們可以借助企業(yè)應(yīng)用開發(fā)中的ORM來設(shè)計(jì)一套對(duì)象-kv-關(guān)系映射。也就是數(shù)據(jù)服務(wù)是統(tǒng)一的,而不同的業(yè)務(wù)可以用不同的數(shù)據(jù)結(jié)構(gòu)描述自己的領(lǐng)域模型,然后數(shù)據(jù)服務(wù)的配套工具會(huì)自動(dòng)生成數(shù)據(jù)訪問層API、redis中cache關(guān)系以及mysql中的table schema。也就是說,同樣的數(shù)據(jù)服務(wù),我在項(xiàng)目A中引用并定義了Player結(jié)構(gòu),就會(huì)自動(dòng)生成LoadPlayer的API;在項(xiàng)目B中定義User同理生成LoadUser的API。這兩個(gè)問題是比較容易解決的,最關(guān)鍵的還是一個(gè)

7、思路的轉(zhuǎn)換。下面看一種non-trivial的實(shí)現(xiàn)。Phial中的DataAccess部分,Phial的Model代碼生成器。實(shí)際上,數(shù)據(jù)服務(wù)除去緩存基礎(chǔ)設(shè)施的部分,都屬于外圍機(jī)制。在有些設(shè)計(jì)中,我們可以看到還是存在緩存服務(wù)與邏輯服務(wù)的中間層。這種中間層的單點(diǎn)問題很容易解決只要不同的邏輯服務(wù)訪問不同的中間層節(jié)點(diǎn)即可。中間層的意義通常是進(jìn)行RPC到具體緩存協(xié)議API的轉(zhuǎn)換,在我的實(shí)現(xiàn)中,由于已經(jīng)有了數(shù)據(jù)訪問API的自動(dòng)生成,因此沒有這種中間層存在的必要。所有需要訪問數(shù)據(jù)服務(wù)的邏輯服務(wù)都可以直接通過數(shù)據(jù)訪問API訪問。其中還有幾點(diǎn)細(xì)節(jié): 數(shù)據(jù)訪問層API的調(diào)用規(guī)范與RPC的調(diào)用規(guī)范保持了統(tǒng)一,都是

8、基于async/await模式。 通過數(shù)據(jù)服務(wù)對(duì)任意存檔進(jìn)行增加或修改都會(huì)記錄一個(gè)job,由落地服務(wù)定期檢查job進(jìn)行落地。引入新的問題目前仍然遺留了幾個(gè)問題: redis單實(shí)例的性能確實(shí)很強(qiáng)悍,但是如果全區(qū)全服只開一個(gè)redis實(shí)例確實(shí)是存在問題的,這個(gè)問題需要解決。 數(shù)據(jù)服務(wù)對(duì)于傳統(tǒng)MMO架構(gòu)來說可以無縫替換掉丑陋的Db代理進(jìn)程,但是,既然數(shù)據(jù)服務(wù)已經(jīng)能提供抽象程度如此高的存儲(chǔ)接口,那是否還可以應(yīng)用在其他地方?2.1.2 無狀態(tài)服務(wù)中數(shù)據(jù)服務(wù)的定位定義問題之前提到過,游戲世界的狀態(tài)除了需要存檔的玩家數(shù)據(jù),還有一部分是不需要存檔的邏輯服務(wù)的狀態(tài)。數(shù)據(jù)服務(wù)如果只是用來替代MMO中的Db代理進(jìn)程

9、的,那么它的全部職責(zé)就僅僅是為需要存檔的數(shù)據(jù)提供服務(wù)。從更高的抽象層次來看的話,數(shù)據(jù)服務(wù)相當(dāng)于是維護(hù)了client在服務(wù)端的狀態(tài)。但是,數(shù)據(jù)服務(wù)提供了更強(qiáng)大的抽象能力。現(xiàn)在數(shù)據(jù)服務(wù)的API結(jié)構(gòu)是任意定制的、code first,而且數(shù)據(jù)服務(wù)依賴的基礎(chǔ)設(shè)施redis又被證明非常強(qiáng)大,不僅僅是性能極佳,而且提供了多種數(shù)據(jù)結(jié)構(gòu)抽象。那么,數(shù)據(jù)服務(wù)是否可以維護(hù)其他服務(wù)的狀態(tài)?在web開發(fā)中,用緩存維護(hù)服務(wù)狀態(tài)是一種很常規(guī)的開發(fā)思路。而在游戲服務(wù)端開發(fā)中,由于場(chǎng)景服務(wù)的存在,這種思路通常并不靠譜。為什么要用緩存維護(hù)服務(wù)狀態(tài)?考慮這樣一個(gè)問題:如果服務(wù)的狀態(tài)維護(hù)在服務(wù)進(jìn)程中,那么服務(wù)進(jìn)程掛掉,狀態(tài)就不存在

10、了。而對(duì)于我們來說,服務(wù)的狀態(tài)是比服務(wù)進(jìn)程本身更加重要的因?yàn)檫M(jìn)程掛了可以趕緊重啟,哪怕耽誤個(gè)1、2s,但是狀態(tài)沒了卻意味著這個(gè)服務(wù)在整個(gè)分布式服務(wù)端中所處的全局一致性已經(jīng)不正確了,即使瞬間就重啟好了也沒用。那么為了讓服務(wù)進(jìn)程掛掉時(shí)不會(huì)導(dǎo)致服務(wù)狀態(tài)丟掉,只要分離服務(wù)進(jìn)程的生命周期和服務(wù)狀態(tài)的生命周期就可以了。將進(jìn)程和狀態(tài)的生命周期分離帶來的另一個(gè)好處就是讓這類服務(wù)的橫向擴(kuò)展成本降到最低。比較簡(jiǎn)單的分離方法是將服務(wù)狀態(tài)維護(hù)在共享內(nèi)存里事實(shí)上很多項(xiàng)目也確實(shí)是這樣做的。但是這種做法擴(kuò)展性不強(qiáng),比如很難跨物理機(jī),而且共享內(nèi)存就這樣一個(gè)文件安全性很難保障。我們可以將服務(wù)狀態(tài)存放在外部設(shè)施中,比如數(shù)據(jù)服務(wù)。

11、這種可以將狀態(tài)存放在外部設(shè)施的服務(wù)就是無狀態(tài)服務(wù)(stateless service)。而與之對(duì)應(yīng)的,場(chǎng)景服務(wù)這種狀態(tài)需要在進(jìn)程內(nèi)維護(hù)的就是有狀態(tài)服務(wù)(stateful service)。有時(shí)候跟只接觸過游戲服務(wù)端開發(fā)的業(yè)務(wù)狗談起無狀態(tài)服務(wù),對(duì)方竟然會(huì)產(chǎn)生 一種“無狀態(tài)服務(wù)是為了解決游戲斷線重連的吧”這種論點(diǎn),真的很哭笑不得。斷線重連在游戲開發(fā)中固然是大坑之一,但是解決方案從來都跟有無狀態(tài)毫無關(guān)系, 無狀態(tài)服務(wù)畢竟是服務(wù)而不是客戶端。如果真的能實(shí)現(xiàn)一個(gè)無狀態(tài)游戲客戶端,那真的是能直接解決坑人無數(shù)的斷線重連問題。無狀態(tài)游戲客戶端意味著網(wǎng)絡(luò)通信的成本跟內(nèi)存數(shù)據(jù)訪問的成本一樣低這當(dāng)然是不可能實(shí)現(xiàn)的。

12、無狀態(tài)服務(wù)就是為了scalability而出現(xiàn)的,無狀態(tài)服務(wù)橫向擴(kuò)展的能力相比于有狀態(tài)服務(wù)大大增強(qiáng),同時(shí)實(shí)現(xiàn)負(fù)載均衡的成本又遠(yuǎn)低于有狀態(tài)服務(wù)。分布式系統(tǒng)中有一個(gè)基本的CAP原理,也就是一致性C、響應(yīng)性能A、分區(qū)容錯(cuò)P,無法三者兼顧。無狀態(tài)服務(wù)更傾向于CP,有狀態(tài)服務(wù)更傾向于AP。但是要補(bǔ)充一點(diǎn),有狀態(tài)服務(wù)的P與無狀態(tài)服務(wù)的P所能達(dá)到的程度是不一樣的,后者是真的容錯(cuò),前者只能做到不把雞蛋放在一個(gè)籃子里。兩種服務(wù)的設(shè)計(jì)意圖不同。無狀態(tài)服務(wù)的所有狀態(tài)訪問與修改都增加了內(nèi)網(wǎng)時(shí)延,這對(duì)于場(chǎng)景服務(wù)這種性能優(yōu)先的服務(wù)是不可忍受的。而有狀態(tài)服務(wù)非常適合場(chǎng)景同步與交互這種數(shù)據(jù)密集的情景,一方面是數(shù)據(jù)交互的延遲僅

13、僅是進(jìn)程內(nèi)方法調(diào)用的開銷,另一方面由于數(shù)據(jù)局部性原理,對(duì)同樣數(shù)據(jù)的訪問非常快。既然設(shè)計(jì)意圖本來就是不同的,我們這一節(jié)就只討論數(shù)據(jù)服務(wù)與無狀態(tài)服務(wù)的關(guān)系。游戲中可以拆分為無狀態(tài)服務(wù)的業(yè)務(wù)需求其實(shí)有很多,基本上所有服務(wù)間交互需求都可以實(shí)現(xiàn)為無狀態(tài)服務(wù)。比如切場(chǎng)景服務(wù),因?yàn)榍袌?chǎng)景的請(qǐng)求是有限的,對(duì)時(shí)延的要求也不會(huì)特別高,同理的還有分配房間服務(wù);或者是面向客戶端的IM服務(wù)、拍賣行服務(wù)等等。數(shù)據(jù)服務(wù)對(duì)于無狀態(tài)服務(wù)來說,解決了什么問題?簡(jiǎn)單來說,就是轉(zhuǎn)移了無狀態(tài)服務(wù)的狀態(tài)維護(hù)成本,同時(shí)讓無狀態(tài)服務(wù)具有了橫向擴(kuò)展的能力。因?yàn)闋顟B(tài)維護(hù)在數(shù)據(jù)服務(wù)中,所以無狀態(tài)服務(wù)開多少個(gè)都無所謂。因此無狀態(tài)服務(wù)非常適合計(jì)算密集

14、的業(yè)務(wù)需求。你可能覺得我之前在服務(wù)劃分一節(jié)之后直接提出要引入MQ有些突兀,實(shí)際上,服務(wù)劃分要解決的根本問題就是讓程序員能清楚自己定義每種服務(wù)的意圖是什么,哪一種服務(wù)更適合Request-Reply,哪一種服務(wù)更適合Ask-Sync。假設(shè)策劃對(duì)游戲沒有分服的需求,理論上講,有節(jié)操的程序是不應(yīng)該以“其他游戲就這樣做的”或“做不到”之類的借口搪塞。每一種服務(wù)都由分布式的多個(gè)節(jié)點(diǎn)共同提供服務(wù),如果服務(wù)的消息流更適合Request-Reply pattern,那么實(shí)現(xiàn)為無狀態(tài)服務(wù)就更合適,原因有二: 一個(gè)Request上來,取相關(guān)數(shù)據(jù),處理,直接返回。整個(gè)狀態(tài)的生命周期保持在一次RPC調(diào)用過程中,這描述

15、的就是Request-Reply的工作方式。 目前只有走M(jìn)Q的消息pipeline支持Request-Reply pattern,而MQ通常都能很好地支持無狀態(tài)服務(wù)的round-robin work distribution。針對(duì)第二點(diǎn),可能需要稍微介紹下rabbitMQ。rabbitMQ中有exchange(交換機(jī))、queue、binding(綁定規(guī)則)三個(gè)主要概念。其中,exchange是對(duì)應(yīng)生產(chǎn)者的,queue是對(duì)應(yīng)消費(fèi)者的,binding則是描述消息從exchange到queue的路由關(guān)系的。exchange有兩種常用類型direct、topic。其中direct exchange接

16、收到的消息是不會(huì)dup的,而topic exchange則會(huì)將接收到的消息根據(jù)匹配的binding確定要dup到哪個(gè)target queue上。這樣,對(duì)于無狀態(tài)服務(wù),比如同一命名空間下的切場(chǎng)景服務(wù),可以共用同一個(gè)queue,然后client發(fā)來的消息走direct exchange,就可以在MQ層面做到round-robin,將消息輪流分配到不同的切場(chǎng)景服務(wù)上。而且無狀態(tài)服務(wù)本質(zhì)上是沒有擴(kuò)容成本的,波峰就多開,波谷就少開。程序員負(fù)責(zé)為不同服務(wù)規(guī)劃不同的橫向擴(kuò)展方式。比如類似公會(huì)服務(wù)這種走M(jìn)Q的,橫向擴(kuò)展的觸發(fā)條件就是現(xiàn)在請(qǐng)求數(shù)量級(jí)或者是節(jié)點(diǎn)壓力。比如場(chǎng)景服務(wù)這種Ask-Sync的,橫向擴(kuò)展就需

17、要借助第三方的服務(wù)作為仲裁者,而這個(gè)仲裁者可以實(shí)現(xiàn)為基于MQ的服務(wù)。這里有個(gè)問題需要注意一下。由于現(xiàn)在同一個(gè)client上來的request消息可能由無狀態(tài)服務(wù)的不同節(jié)點(diǎn)處理,那么就會(huì)出現(xiàn)這樣的情況:1. 某個(gè)client由于一些原因,快速發(fā)了兩個(gè)message1、message2。2. message1先到了服務(wù)A,服務(wù)A去數(shù)據(jù)服務(wù)拉相關(guān)數(shù)據(jù)集合Sa,并進(jìn)行后續(xù)處理。3. 此時(shí)message2到了服務(wù)B,服務(wù)B去數(shù)據(jù)服務(wù)拉相關(guān)數(shù)據(jù)集合Sb,進(jìn)行后續(xù)處理,處理完畢,將結(jié)果存回?cái)?shù)據(jù)服務(wù)。4. 然后服務(wù)A才處理完,并嘗試將處理結(jié)果存回?cái)?shù)據(jù)服務(wù)。假如Sa與Sb有交集,那就會(huì)出現(xiàn)競(jìng)態(tài)條件,如果這時(shí)允許

18、服務(wù)A存回結(jié)果,那數(shù)據(jù)就有可能存在不一致。類似的情況還會(huì)出現(xiàn)在像率土之濱或者cok這種策略游戲的大世界刷怪需求中。當(dāng)然前提是玩家與大地圖上的元素交互和后臺(tái)刷怪邏輯都是基于無狀態(tài)服務(wù)做的。這其實(shí)是一個(gè)跨進(jìn)程共享狀態(tài)問題,而且是一個(gè)高度簡(jiǎn)化的版本因?yàn)檫@個(gè)共享狀態(tài)只在一個(gè)實(shí)例上維護(hù)??梢砸腈i來解決問題,思路通常有兩個(gè):最直觀的一種方案是悲觀鎖。也就是如果要進(jìn)行修改操作,就需要在讀相關(guān)數(shù)據(jù)的時(shí)候就都加上鎖,最后寫成功的時(shí)候釋放鎖。獲得鎖所有權(quán)期間其他impure服務(wù)任意讀寫請(qǐng)求都是非法的。但是,這畢竟不是多線程執(zhí)行環(huán)境,沒有語(yǔ)言或平臺(tái)幫你做自動(dòng)鎖釋放的保證。獲取悲觀鎖的服務(wù)節(jié)點(diǎn)不能保證一定會(huì)將鎖釋放

19、掉,拿到鎖之后節(jié)點(diǎn)掛掉的可能性非常大。這樣,就需要給悲觀鎖增加超時(shí)機(jī)制。第二種方案是樂觀鎖。也就是impure服務(wù)可以隨意進(jìn)行讀請(qǐng)求,讀到的數(shù)據(jù)會(huì)額外帶個(gè)版本號(hào),等寫的時(shí)候?qū)Ρ劝姹咎?hào),如果一致就可以成功寫回,否則就通知到應(yīng)用層失敗,由應(yīng)用層決定后續(xù)操作。帶過期機(jī)制的悲觀鎖和樂觀鎖本質(zhì)上都屬于可搶占的分布式鎖,相當(dāng)于是將paxos要解決的問題退化為單Acceptor,因此實(shí)現(xiàn)起來非常簡(jiǎn)單??蛇^期的悲觀鎖和樂觀鎖唯一的區(qū)別就是前者在申請(qǐng)鎖的時(shí)候有可能申請(qǐng)失敗,而后者申請(qǐng)鎖時(shí)永遠(yuǎn)不會(huì)失敗。兩種方案具體的表現(xiàn)優(yōu)劣跟業(yè)務(wù)需求有關(guān),不論一開始選擇的是哪一種,都非常容易切換到另一種。我在示例中實(shí)現(xiàn)了一個(gè)簡(jiǎn)單

20、的樂觀鎖,在提交修改的時(shí)候用一個(gè)lua腳本做原子檢查就能簡(jiǎn)單實(shí)現(xiàn)。如果要實(shí)現(xiàn)帶過期機(jī)制的悲觀鎖,需要保證應(yīng)用層有簡(jiǎn)單的時(shí)鐘同步機(jī)制,而且在申請(qǐng)鎖的時(shí)候也要寫一個(gè)lua腳本。在應(yīng)用層也做了對(duì)應(yīng)修改,調(diào)用數(shù)據(jù)訪問層API可以按如下這種方式調(diào)用。之所以用了RTTI,是考慮到有可能會(huì)改成悲觀鎖實(shí)現(xiàn),在Dispose的時(shí)候會(huì)自動(dòng)release lock?,F(xiàn)在pure服務(wù)與impure服務(wù)對(duì)數(shù)據(jù)服務(wù)調(diào)用的接口是不一樣的,我們甚至還可以基于這一點(diǎn)在底層做一些擴(kuò)展,最典型的比如讀寫分離。當(dāng)然,這些都是引入主從之后要考慮的問題了。View Code有了這樣一個(gè)簡(jiǎn)易的鎖機(jī)制,我們可以保證單redis實(shí)例內(nèi)的一致性

21、。引入新的問題有了無狀態(tài)服務(wù)的概念,我們的架構(gòu)中就可以逐步干掉類似切場(chǎng)景管理這種單點(diǎn)進(jìn)程。無狀態(tài)服務(wù)是高可用的,也就是說,任意掛掉一個(gè),仍然能持續(xù)提供服務(wù)。整個(gè)游戲服務(wù)端理論上應(yīng)該具有整體持續(xù)提供服務(wù)的能力。也就是說,隨便掛掉一個(gè)節(jié)點(diǎn),不需要停服。場(chǎng)景服務(wù)掛掉一個(gè)節(jié)點(diǎn),不會(huì)影響其他任何服務(wù),只是玩家短期內(nèi)無法進(jìn)行場(chǎng)景相關(guān)操作了而已。而我們見過的大多數(shù)架構(gòu),處處皆單點(diǎn),這完全不能叫可用的架構(gòu)。有的時(shí)候一個(gè)服務(wù)端跑的好好的,有人硬是要額外加一個(gè)全局單點(diǎn),而且理由是更容易管理,讓人哭笑不得。分布式系統(tǒng)中動(dòng)不動(dòng)就想加單點(diǎn),這是病,得治。判斷一整個(gè)游戲服務(wù)端是否具有可用性很簡(jiǎn)單,隨便kill掉一個(gè)節(jié)點(diǎn),

22、如果服務(wù)端仍然能持續(xù)提供服務(wù),即使是部分client受到了影響,也能稱為是可用的。但是,現(xiàn)在邏輯服務(wù)具有可用性了,可是數(shù)據(jù)服務(wù)還沒有具有可用性,數(shù)據(jù)服務(wù)依賴于一個(gè)redis實(shí)例,這個(gè)redis實(shí)例反而成為了整個(gè)服務(wù)端中的單點(diǎn)。幸好,redis像其他大多數(shù)工業(yè)級(jí)緩存基礎(chǔ)設(shè)施一樣,已經(jīng)提供了足夠用的可用性機(jī)制。但是,在討論redis的可用性機(jī)制之前,我們先解決一下數(shù)據(jù)服務(wù)的一個(gè)遺留問題,那就是如何構(gòu)建一個(gè)可以擴(kuò)展的全局?jǐn)?shù)據(jù)服務(wù)。2.2 數(shù)據(jù)服務(wù)的擴(kuò)展redis是一種stateful service,繼續(xù)應(yīng)用之前的CAP原則,redis是傾向于AP的。之后我們可以看到,redis的各種擴(kuò)展,實(shí)際上都

23、是基于這個(gè)原則來做的。2.2.1 分片方案定義問題我們遇到的問題是,如果將數(shù)據(jù)服務(wù)定位為全局服務(wù),那僅用單實(shí)例的redis就難以應(yīng)對(duì)多變的負(fù)載情況。畢竟redis是單線程的。從mysql一路用過來的同學(xué)這時(shí)都會(huì)習(xí)慣性地水平拆分,redis中也是類似的原理,將整體的數(shù)據(jù)進(jìn)行切分,每一部分是一個(gè)分片shard,不同的shard維護(hù)的key集合是不同的。那么,問題的實(shí)質(zhì)就是如何基于多個(gè)redis實(shí)例設(shè)計(jì)全局統(tǒng)一的數(shù)據(jù)服務(wù)。同時(shí),有一個(gè)約束條件,那就是我們?yōu)榱诵阅苄枰獱奚忠恢滦?。也就是說,數(shù)據(jù)服務(wù)進(jìn)行分片擴(kuò)展的前提是,不提供跨分片事務(wù)的保障。redis cluster也沒有提供類似支持,因?yàn)榉植际?/p>

24、事務(wù)本來就跟redis的定位是有沖突的。因此,我們之后的討論會(huì)有一個(gè)預(yù)設(shè)前提:不同shard中的數(shù)據(jù)一定是嚴(yán)格隔離的,比如是不同組服的數(shù)據(jù),或者是完全不相干的數(shù)據(jù)。要想實(shí)現(xiàn)跨shard的數(shù)據(jù)交互,必須依賴更上層的協(xié)調(diào)機(jī)制保證,底層不做任何承諾。這樣,我們的分片數(shù)據(jù)服務(wù)就能通過之前提到的簡(jiǎn)易鎖機(jī)制提供單片內(nèi)的一致性保證,而不再提供全局的一致性保證?;谕瑯拥脑?,我們的分片方案也不會(huì)在分片間做類似分布式存儲(chǔ)系統(tǒng)的數(shù)據(jù)冗余機(jī)制。分片方案解決了什么問題分片需要解決兩個(gè)問題: 第一個(gè)問題,分片方案需要描述shard與shard之間的聯(lián)系,也就是cluster membership。 第二個(gè)問題,分片方

25、案需要描述dbClient的一個(gè)請(qǐng)求應(yīng)該交給哪個(gè)shard,也就是work distribution。針對(duì)第一個(gè)問題,解決方案通常有三: presharding,也就是sharding靜態(tài)配置。 gossip protocol,其實(shí)就是redis cluster采用的方案。簡(jiǎn)單地說就是集群中每個(gè)節(jié)點(diǎn)會(huì)由于網(wǎng)絡(luò)分化、節(jié)點(diǎn)抖動(dòng)等原因而具有不同的集群全局視圖。節(jié)點(diǎn)之間通過gossip protocol進(jìn)行節(jié)點(diǎn)信息共享。這種方案更強(qiáng)調(diào)CAP中的A原則,因?yàn)椴恍枰兄俨谜摺?consensus system,這種方案跟上一種正相反,更強(qiáng)調(diào)CAP中的C原則,就是借助分布式系統(tǒng)中的仲裁者來決定集群中各節(jié)點(diǎn)的

26、身份。需求決定解決方案,對(duì)于游戲服務(wù)端來說,后兩者的成本太高,而且增加了很多不確定的復(fù)雜性,因此現(xiàn)階段這兩種方案并不是合適的選擇。比如gossip protocol,redis cluster現(xiàn)在都不算是release,確實(shí)不太適合游戲服務(wù)端。而且,游戲服務(wù)端畢竟不是web服務(wù),通常是可以在設(shè)計(jì)階段確定每個(gè)分片的容量上限的,也不需要太復(fù)雜的機(jī)制支持。但是第一種方案的缺點(diǎn)也很明顯,做不到動(dòng)態(tài)增容減容,而且無法高可用。但是如果稍加改造,就足以滿足需求了。在談具體的改造措施之前,先看之前提出的第二個(gè)問題。第二個(gè)問題實(shí)際上是從另一種維度看分片,解決方案很多,但是如果從對(duì)架構(gòu)的影響上來看,大概分為兩種:

27、 一種是proxy-based,基于額外的轉(zhuǎn)發(fā)代理。例子有twemproxy/Codis。 一種是client sharding,也就是dbClient(每個(gè)對(duì)數(shù)據(jù)服務(wù)有需求的服務(wù))維護(hù)sharding規(guī)則,自助式選擇要去哪個(gè)redis實(shí)例。redis cluster本質(zhì)上就屬于這種,client側(cè)緩存了部分sharding信息。第一種方案的缺點(diǎn)顯而易見,在整個(gè)架構(gòu)中增加了額外的間接層,pipeline中增加了一趟round-trip。如果是像twemproxy或者Codis這種支持高可用的還好,但是github上隨便一翻還能找到特別多的沒法做到高可用的proxy-based方案,無緣無故多個(gè)

28、單點(diǎn),這樣就完全搞不明白sharding的意義何在了。第二種方案的缺點(diǎn)就是集群狀態(tài)發(fā)生變化的時(shí)候沒法即時(shí)通知到dbClient。第一種方案,我們其實(shí)可以直接pass掉了。因?yàn)檫@種方案本質(zhì)上還是更適合web開發(fā)的。web開發(fā)部門眾多,開發(fā)數(shù)據(jù)服務(wù)的部門有可能和業(yè)務(wù)部門相去甚遠(yuǎn),因此需要統(tǒng)一的轉(zhuǎn)發(fā)代理服務(wù)。但是游戲開發(fā)不一樣,數(shù)據(jù)服務(wù)邏輯服務(wù)都是一幫人開發(fā)的,沒什么增加額外中間層的必要。那么,看起來只能選擇第二種方案了。將presharding與client sharding結(jié)合起來后,現(xiàn)在我們的改造成果是:數(shù)據(jù)服務(wù)是全局的,redis可以開多個(gè)實(shí)例,不相干的數(shù)據(jù)需要到不同的shard上存取,db

29、Client掌握這個(gè)映射關(guān)系。引入新的問題目前的方案只能滿足游戲?qū)?shù)據(jù)服務(wù)的基本需求。大部分采用redis的游戲團(tuán)隊(duì),一般最終會(huì)選定這個(gè)方案作為自己的數(shù)據(jù)服務(wù)。后續(xù)的擴(kuò)展其實(shí)對(duì)他們來說不是不可以做,但是可能有維護(hù)上的復(fù)雜性與不確定性。今天這篇文章,我就繼續(xù)對(duì)數(shù)據(jù)服務(wù)做擴(kuò)展,后面的內(nèi)容權(quán)當(dāng)拋磚引玉。現(xiàn)在的這個(gè)方案存在兩個(gè)問題: 首先,雖然我們沒有支持在線數(shù)據(jù)遷移的必要,但是離線數(shù)據(jù)遷移是必須得有的,畢竟presharding做不到萬無一失。而在這個(gè)方案中,如果用單純的哈希算法,增加一個(gè)shard會(huì)導(dǎo)致原先的key到shard的對(duì)應(yīng)關(guān)系變得非常亂,抬高數(shù)據(jù)遷移成本。 其次,分片方案固然可以將整個(gè)數(shù)

30、據(jù)服務(wù)的崩潰風(fēng)險(xiǎn)分散在不同shard中,比如相比于不分片的數(shù)據(jù)服務(wù),一臺(tái)機(jī)器掛掉了,只影響到一部分玩家。但是,我們理應(yīng)可以對(duì)數(shù)據(jù)服務(wù)做更深入的擴(kuò)展,讓其可用程度更強(qiáng)。針對(duì)第一個(gè)問題,處理方式跟proxy-based采用的處理方式?jīng)]太大區(qū)別,由于目前的數(shù)據(jù)服務(wù)方案比較簡(jiǎn)單,采用一致性哈希即可?;蛘卟捎靡环N比較簡(jiǎn)單的兩段映射,第一段是靜態(tài)的固定哈希,第二段是動(dòng)態(tài)的可配置map。前者通過算法,后者通過map配置維護(hù)的方式,都能最小化影響到的key集合。而對(duì)于第二個(gè)問題,實(shí)際上就是上一節(jié)末提到的數(shù)據(jù)服務(wù)可用性問題。4.2.2 可用性方案定義問題討論數(shù)據(jù)服務(wù)的可用性之前,我們首先看redis的可用性。對(duì)

31、于redis來說,可用性的本質(zhì)是什么?其實(shí)就是redis實(shí)例掛掉之后可以有后備節(jié)點(diǎn)頂上。redis通過兩種機(jī)制支持這一點(diǎn)。 一種機(jī)制是replication。通常的replication方案主要分為兩種。一種是active-passive,也就是active節(jié)點(diǎn)先修改自身狀態(tài),然后寫統(tǒng)一持久化log,然后passive節(jié)點(diǎn)讀log跟進(jìn)狀態(tài)。另一種是active-active,寫請(qǐng)求統(tǒng)一寫到持久化log,然后每個(gè)active節(jié)點(diǎn)自動(dòng)同步log進(jìn)度。還是由于CAP原則,redis的replication方案采用的是一種一致性較弱的active-passive方案。也就是master自身維護(hù)log,

32、將log向其他slave同步,master掛掉有可能導(dǎo)致部分log丟失,client寫完master即可收到成功返回,是一種異步replication。這個(gè)機(jī)制只能解決節(jié)點(diǎn)數(shù)據(jù)冗余的問題,redis要具有可用性就還得解決redis實(shí)例掛掉讓備胎自動(dòng)頂上的問題,畢竟由人肉去監(jiān)控master狀態(tài)再人肉切換是不現(xiàn)實(shí)的。 因此還需要第二種機(jī)制。 第二種機(jī)制是redis自帶的能夠自動(dòng)化fail-over的redis sentinel。reds sentinel實(shí)際上是一種特殊的reds實(shí)例,其本身就是一種高可用服務(wù),可以多開,可以自動(dòng)服務(wù)發(fā)現(xiàn)(基于redis內(nèi)置的pub-sub支持,sentinel并沒

33、有禁用掉pub-sub的command map),可以自主leader election(基于sentinel實(shí)現(xiàn)的raft算法),然后在發(fā)現(xiàn)master掛掉時(shí)由leader發(fā)起fail-over,并將掉線后再上線的master降為新master的slave。redis基于自帶的這兩種機(jī)制,已經(jīng)能夠?qū)崿F(xiàn)一定程度的可用性。那么接下來,我們來看數(shù)據(jù)服務(wù)如何高可用。數(shù)據(jù)服務(wù)具有可用性的本質(zhì)是什么?除了能實(shí)現(xiàn)redis可用性的需求redis實(shí)例數(shù)據(jù)冗余、故障自動(dòng)切換之外,還需要將切換的消息通知到每個(gè)dbClient。由于是redis sentinel負(fù)責(zé)主從切換,因此最自然的想法就是問sentinel

34、請(qǐng)求當(dāng)前節(jié)點(diǎn)主從連接信息。但是redis sentinel本身也是redis實(shí)例,數(shù)量也是動(dòng)態(tài)的,redis sentinel的連接信息不僅在配置上成了一個(gè)難題,動(dòng)態(tài)更新時(shí)也會(huì)有各種問題。而且,redis sentinel本質(zhì)上是整個(gè)服務(wù)端的static parts(要像dbClient提供服務(wù)),但是卻依賴于redis的啟動(dòng),并不是特別優(yōu)雅。另一方面,dbClient要想問redis sentinel要到當(dāng)前連接信息,只能依賴其內(nèi)置的pub-sub機(jī)制。redis的pub-sub只是一個(gè)簡(jiǎn)單的消息分發(fā),沒有消息持久化,因此需要輪詢式的請(qǐng)求連接信息模型。上一節(jié)末提到過,要想最小化數(shù)據(jù)遷移成本可

35、以采用兩段映射或一致性哈希。這時(shí)還有另一種可以擴(kuò)展的思路,如果采用兩段映射,那么我們可以動(dòng)態(tài)下發(fā)第二段的配置數(shù)據(jù);如果采用一致性哈希,那么我們可以動(dòng)態(tài)下發(fā)分片的連接信息。這其中的動(dòng)態(tài),就可以基于新的符合Phial規(guī)范的服務(wù)來做。而這個(gè)通知機(jī)制,就非常適合采用Phial中的Notify pattern實(shí)現(xiàn)。而且redis sentinel的實(shí)現(xiàn)難度比較低,我們完全可以以較低的成本實(shí)現(xiàn)一個(gè)擴(kuò)展性更強(qiáng),定制性更強(qiáng),還能額外支持分片服務(wù)的部分在線數(shù)據(jù)遷移機(jī)制的服務(wù)。同時(shí),有一部分我在這篇文章里也沒提過,那就是落地服務(wù)所依賴的mysql的可用性保障機(jī)制。相比于再開一個(gè)額外的mysql高可用組件,倒不如整

36、合到同樣的一個(gè)數(shù)據(jù)服務(wù)監(jiān)控服務(wù)中。這個(gè)監(jiān)控服務(wù)就是watcher。由于原理類似,接下來的討論就不再涉及對(duì)mysql的監(jiān)控部分,只針對(duì)redis的。watcher解決了什么問題? 要能夠監(jiān)控redis的生存狀態(tài)。這一點(diǎn)實(shí)現(xiàn)起來很簡(jiǎn)單,定期的PING redis實(shí)例即可。需要的信息以及做出客觀下線和主觀下線的判斷依據(jù)都可以直接照搬sentinel實(shí)現(xiàn)。 要做到自主服務(wù)發(fā)現(xiàn),包括其他watcher的發(fā)現(xiàn)與所監(jiān)控的master-slave組中的新節(jié)點(diǎn)的發(fā)現(xiàn)。前者基于MQ定期Notify通知,后者定期INFO 監(jiān)控的master實(shí)例即可。 要在發(fā)現(xiàn)master客觀下線的時(shí)候選出leader進(jìn)行后續(xù)的故障

37、轉(zhuǎn)移流程。這部分實(shí)現(xiàn)起來算是最復(fù)雜的部分,接下來會(huì)集中討論。 選出leader之后將一個(gè)最合適的slave提升為master,然后等老的master再上線了就把它降級(jí)為新master的slave。解決這些問題,watcher的職責(zé)就已經(jīng)達(dá)成,我們的數(shù)據(jù)服務(wù)也就更加健壯,可用程度更高。引入新的問題但是,如果我們引入了新的服務(wù),那就引入了新的不確定性。如果引入這個(gè)服務(wù)的同時(shí)還要保證數(shù)據(jù)服務(wù)具有可用性,那我們就還得保證這個(gè)服務(wù)本身是可用的。先簡(jiǎn)單介紹一下redis sentinel的可用性是如何做到的。同時(shí)監(jiān)控同一組主從的sentinel可以有多個(gè),master掛掉的時(shí)候,這些sentinel會(huì)根據(jù)

38、一種raft算法的工業(yè)級(jí)實(shí)現(xiàn)選舉出leader,算法流程也不是特別復(fù)雜,至少比paxos簡(jiǎn)單多了。所有sentinel都是follower,判斷出master客觀下線的sentinel會(huì)升級(jí)成candidate同時(shí)向其他follower拉票,所有follower同一epoch內(nèi)只能投給第一個(gè)向自己拉票的candidate。在具體表現(xiàn)中,通常一兩個(gè)epoch就能保證形成多數(shù)派,選出leader。有了leader,后面再對(duì)redis做SLAVEOF的時(shí)候就容易多了。如果想用watcher取代sentinel,最復(fù)雜的實(shí)現(xiàn)細(xì)節(jié)可能就是這部分邏輯了。這部分邏輯說白了就是要在分布式系統(tǒng)中維護(hù)一個(gè)一致狀態(tài)

39、,舉個(gè)例子,可以將“誰(shuí)是leader”這個(gè)概念當(dāng)作一個(gè)狀態(tài)量,由分布式系統(tǒng)中的身份相等的幾個(gè)節(jié)點(diǎn)共同維護(hù),既然誰(shuí)都有可能修改這個(gè)變量,那究竟誰(shuí)的修改才奏效呢?幸好,針對(duì)這種常見的問題情景,我們有現(xiàn)成的基礎(chǔ)設(shè)施抽象可以解決。這種基礎(chǔ)設(shè)施就是分布式系統(tǒng)的協(xié)調(diào)器組件(coordinator),老牌的有zookeeper(zab),新一點(diǎn)的有etcd(raft)。這種組件通常沒有重復(fù)開發(fā)的必要,像paxos這種算法理解起來都得老半天,實(shí)現(xiàn)起來的細(xì)節(jié)數(shù)量級(jí)更是難以想象。因此很多現(xiàn)成的開源項(xiàng)目都是依賴這兩者實(shí)現(xiàn)高可用的,比如codis就是用的zk。zk解決了什么問題?就我們的游戲服務(wù)端需求來說,zk可以用

40、來選leader,還可以用來維護(hù)dbClient的配置數(shù)據(jù)dbClient直接去找zk要數(shù)據(jù)就行了。zk的具體原理我就不再介紹了,具體的可以參考lamport的paxos paper,沒時(shí)間沒精力的話搜一下看看zk實(shí)現(xiàn)原理的博客就行了。簡(jiǎn)單介紹下如何基于zk實(shí)現(xiàn)leader election。zk提供了一個(gè)類似于os文件系統(tǒng)的目錄結(jié)構(gòu),目錄結(jié)構(gòu)上的每個(gè)節(jié)點(diǎn)都有類型的概念同時(shí)可以存儲(chǔ)一些數(shù)據(jù)。zk還提供了一次性觸發(fā)的watch機(jī)制。leader election就是基于這幾點(diǎn)概念實(shí)現(xiàn)的。假設(shè)有某個(gè)目錄節(jié)點(diǎn)/election,watcher1啟動(dòng)的時(shí)候在這個(gè)節(jié)點(diǎn)下面創(chuàng)建一個(gè)子節(jié)點(diǎn),節(jié)點(diǎn)類型是臨時(shí)順

41、序節(jié)點(diǎn),也就是說這個(gè)節(jié)點(diǎn)會(huì)隨創(chuàng)建者掛掉而掛掉,順序的意思就是會(huì)在節(jié)點(diǎn)的名字后面加個(gè)數(shù)字后綴,唯一標(biāo)識(shí)這個(gè)節(jié)點(diǎn)在/election的子節(jié)點(diǎn)中的id。一個(gè)簡(jiǎn)單的方案是我們可以每個(gè)watcher都watch /election的所有子節(jié)點(diǎn),然后看自己的id是否是最小的,如果是就說明自己是leader,然后告訴應(yīng)用層自己是leader,讓應(yīng)用層進(jìn)行后續(xù)操作就行了。但是這樣會(huì)產(chǎn)生驚群效應(yīng),因?yàn)橐粋€(gè)子節(jié)點(diǎn)刪除,每個(gè)watcher都會(huì)收到通知,但是至多一個(gè)watcher會(huì)從follower變?yōu)閘eader。優(yōu)化一些的方案是每個(gè)節(jié)點(diǎn)都關(guān)注比自己小一個(gè)排位的節(jié)點(diǎn)。這樣如果id最小的節(jié)點(diǎn)掛掉之后,id次小的節(jié)點(diǎn)會(huì)

42、收到通知然后了解到自己成為了leader,避免了驚群效應(yīng)。還有一點(diǎn)需要注意的是,臨時(shí)順序節(jié)點(diǎn)的臨時(shí)性體現(xiàn)在一次session而不是一次連接的終止。例如watcher1每次申請(qǐng)節(jié)點(diǎn)都叫watcher1,第一次它申請(qǐng)成功的節(jié)點(diǎn)全名假設(shè)是watcher10002(后面的是zk自動(dòng)加的序列號(hào)),然后下線,watcher10002節(jié)點(diǎn)還會(huì)存在一段時(shí)間,如果這段時(shí)間內(nèi)watcher1再上線,再嘗試創(chuàng)建watcher1就會(huì)失敗,然后之前的節(jié)點(diǎn)過一會(huì)兒就因?yàn)閟ession超時(shí)而銷毀,這樣就相當(dāng)于這個(gè)watcher1消失了。解決方案有兩個(gè),可以創(chuàng)建節(jié)點(diǎn)前先顯式delete一次,也可以通過其他機(jī)制保證每次創(chuàng)建節(jié)點(diǎn)的名字不同,比如guid。至于配置下發(fā),就更簡(jiǎn)單了。配置變更時(shí)直接更新節(jié)點(diǎn)數(shù)據(jù),就能借助zk通知到關(guān)注的dbClient,這種事件通知機(jī)制相比于輪詢請(qǐng)求sentinel要配置數(shù)據(jù)的機(jī)制更加優(yōu)雅。我在實(shí)現(xiàn)中將zk作為路由協(xié)議的一種整合進(jìn)了Phial規(guī)范,這樣基于zk的消息通知可以直接走Phial

溫馨提示

  • 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請(qǐng)下載最新的WinRAR軟件解壓。
  • 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請(qǐng)聯(lián)系上傳者。文件的所有權(quán)益歸上傳用戶所有。
  • 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁(yè)內(nèi)容里面會(huì)有圖紙預(yù)覽,若沒有圖紙預(yù)覽就沒有圖紙。
  • 4. 未經(jīng)權(quán)益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
  • 5. 人人文庫(kù)網(wǎng)僅提供信息存儲(chǔ)空間,僅對(duì)用戶上傳內(nèi)容的表現(xiàn)方式做保護(hù)處理,對(duì)用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對(duì)任何下載內(nèi)容負(fù)責(zé)。
  • 6. 下載文件中如有侵權(quán)或不適當(dāng)內(nèi)容,請(qǐng)與我們聯(lián)系,我們立即糾正。
  • 7. 本站不保證下載資源的準(zhǔn)確性、安全性和完整性, 同時(shí)也不承擔(dān)用戶因使用這些下載資源對(duì)自己和他人造成任何形式的傷害或損失。

評(píng)論

0/150

提交評(píng)論