有一天,我突然不想做游戲了。于是寫點(diǎn)開發(fā)總結(jié),于是就有了這篇文檔。
假如,我現(xiàn)在接手一個(gè)新項(xiàng)目,我的身份還是主程序。在下屬人員一一到位之前,在和制作人以及主策劃充分溝通后,我需要先獨(dú)自思考以下問題:
1、服務(wù)器跑在什么樣的操作系統(tǒng)環(huán)境下?
2、采用哪幾種語言開發(fā)?主要是什么?
3、服務(wù)器和客戶端以什么樣的接口通訊?
4、采用哪些第三方的類庫?
除了技術(shù)背景之外,考慮這些問題的時(shí)候一定要充分考慮項(xiàng)目需求和所能擁有的資源。
我覺得,先不要想一組需要幾臺(tái)機(jī)器各有什么功能這樣的問題,也不要想需要多少個(gè)daemon進(jìn)程。假設(shè)就一臺(tái)服務(wù)器,就一個(gè)進(jìn)程,把所需要的資源往最小了考慮,把架構(gòu)往最簡(jiǎn)單的方向想,直到發(fā)現(xiàn),“哦,這么做無法滿足策劃要求的并發(fā)量”,再去修改設(shè)計(jì)方案。
操作系統(tǒng):越單一越好。雖然FreeBSD的網(wǎng)絡(luò)性能更好、雖然Solaris非常穩(wěn)定,但選什么就是什么,最好別混著來。前端是FreeBSD,后端是Solaris,運(yùn)營(yíng)的人會(huì)苦死。也不要瞧不起用Windows的人,用Windows照樣也能支持一組一萬人在線,總之,能滿足策劃需求,好招程序員,運(yùn)營(yíng)成本低是要點(diǎn)。不同的操作系統(tǒng)有不同的特性,如果你真的對(duì)它們都很熟悉,那么必定能找到一個(gè)理由,一個(gè)足夠充分的理由讓你選擇A而不是B而不是C。但做決策的時(shí)候要注意不要因小失大。
Programming Language:傳統(tǒng)來說,基本都是C/C++。但是你也知道,這東西門檻很高,好的C/C++程序員很難招。用Perl/Python/Lua行不行?當(dāng)然可以。但是純腳本也不好,通常來說是混合著來。你要明白哪些是關(guān)鍵部分,我是說執(zhí)行次數(shù)最多的地方而不是說元寶,這些必須用性能高的語言實(shí)現(xiàn)(比如C/C++比如Java),其它像節(jié)日活動(dòng)這樣很久才執(zhí)行一次的,隨便吧。腳本的好處是,可以快速搭原型。所以,盡早的,在你做完基本的地圖和戰(zhàn)斗模塊之后,立馬跑機(jī)器人測(cè)試吞吐量。這時(shí)候項(xiàng)目開發(fā)進(jìn)度還不到10%,不行就趕緊改。
此處特別舉個(gè)例子就是Java GC的問題。既然你要用java,而jvm需要通過執(zhí)行g(shù)arbage collection來回收內(nèi)存,而garbage collection會(huì)使整個(gè)應(yīng)用停頓,那你不妨試一試,內(nèi)存在達(dá)到峰值的時(shí)候會(huì)停多久?策劃可以接受嗎?如果不可以,你可以采用其它的GC策略再試一試。這個(gè)問題應(yīng)該不是Java獨(dú)有的。網(wǎng)游和網(wǎng)站應(yīng)用相比它很注重流暢性。這是你務(wù)必需要考慮的。
至于選擇什么樣的腳本語言,以及腳本在你的游戲中究竟是占80%還是20%?需要根據(jù)需求來看。有沒有游戲完全不用腳本?有。有沒有游戲?yàn)E用腳本?也有。如果你引入腳本的目的是因?yàn)椴邉澆粫?huì)C/C++而你希望策劃能自己獨(dú)立實(shí)現(xiàn)更多的游戲功能。你希望策劃去寫腳本?腳本也是程序,策劃寫的腳本難道就比程序員寫腳本好?還是因?yàn)椴邉澒べY便宜?策劃因?yàn)槟_本寫錯(cuò)了導(dǎo)致大故障還少嗎(此處特別以網(wǎng)易的產(chǎn)品舉例)?綜合權(quán)衡下,還是算了吧。問問你一起工作的程序員哥們兒,他們最喜歡什么語言,什么用起來最順手,就用什么當(dāng)腳本。注意不光要考慮開發(fā)速度快,還要考慮調(diào)試方便。
總體來說,操作系統(tǒng)和編程語言的選擇,隨大流即可。標(biāo)新立異沒什么好處。小地方的實(shí)現(xiàn)你可以玩玩,整體還是要越保守越好。
然后說通訊的問題。服務(wù)器和客戶端怎么連接上的?
往最下面看,物理和鏈路層。有可能是以太網(wǎng),有可能是ADSL,在北京還有很多像歌華寬帶這樣的采用75歐同軸電纜或者電力線上網(wǎng)的。你不要企圖在這一層做什么優(yōu)化,你要充分考慮的是不同的網(wǎng)絡(luò)傳輸媒質(zhì)網(wǎng)絡(luò)延遲不一樣。更惡心的是你正常的數(shù)據(jù)包可能會(huì)被某些網(wǎng)吧的SB路由器當(dāng)做P2P數(shù)據(jù)包給封掉,或是甚至被解析成Wake-On-Lan這樣的含義。楊建還會(huì)給你講,什么是MTU,把數(shù)據(jù)包限制在多大才能盡量讓請(qǐng)求在一個(gè)包內(nèi)發(fā)完。是的,這些很精細(xì)的東西,等咱游戲做的差不多了再慢慢研究。先略過。
往上看,IP層。再往上,你要考慮用TCP還是UDP或是二者混合。UDP的優(yōu)勢(shì)是overhead小、延遲低,典型的用例就是《天下貳》,據(jù)說是純UDP。再比如《龍之谷》,據(jù)說是有小部分是UDP。負(fù)面的一點(diǎn)呢,就是它太過于簡(jiǎn)單所以用起來太過于復(fù)雜。你要是對(duì)自己沒信心,TCP吧,隨大流就好。
往上,采用什么樣的應(yīng)用協(xié)議。大多數(shù)rpc協(xié)議都是既支持TCP又支持UDP的。我所用過的有sun rpc、corba、webservice、json、java RMI以及一些專有協(xié)議。如果你有精力,還是自己搞一套吧,網(wǎng)游所用的東西,還是越專有越好,給抓包做外掛的人加一點(diǎn)門檻。這里非常強(qiáng)調(diào)的一點(diǎn),你采用什么樣的序列化方式與你采用什么樣的網(wǎng)絡(luò)協(xié)議是無關(guān)的,你的應(yīng)用協(xié)議和你傳輸協(xié)議應(yīng)該也是無關(guān)的(既支持TCP又支持UDP的)。如果做框架的人把自己限制的太死或者耦合太緊,那么用框架的人會(huì)非常痛苦。所以,沒必要在此為了性能做過多優(yōu)化。結(jié)構(gòu)簡(jiǎn)單清晰是王道。
很多人對(duì)網(wǎng)絡(luò)開發(fā)的認(rèn)識(shí)還停留在定義一個(gè)struct、memcpy到socket buffer、send,然后一個(gè)勁的給別人強(qiáng)調(diào)遇到指針怎么辦、數(shù)組的長(zhǎng)度不能超過多少、整個(gè)包的長(zhǎng)度不能超過多少等等。序列化其實(shí)是面向?qū)ο蟪绦蛟O(shè)計(jì)的一個(gè)很核心的要素。連glib/gtk/Berkeley DB這些純C的框架都是基于OOP設(shè)計(jì)的,所以我覺得您就算是C程序員也沒必要排斥它。我講這個(gè)是說,你應(yīng)當(dāng)做應(yīng)用的人盡可能的避免用memcpy/memset這樣的方式初始化數(shù)據(jù)、傳送數(shù)據(jù)。如果你是C程序員,你多提供一些g_object_new這樣的函數(shù);如果你是C++程序員,寫好你的構(gòu)造和析構(gòu)函數(shù);如果你是JAVA程序員還死活不懂OOP,那算了吧,改行吧。
網(wǎng)絡(luò)這一層有些很精妙的東西,尤其是當(dāng)你規(guī)模擴(kuò)大需要分布式擴(kuò)展的時(shí)候。你想想看為什么sun rpc需要先去rpcbind詢問一次然后才連真正的進(jìn)程呢?RMI返回的時(shí)候?yàn)槭裁葱枰瑫r(shí)返回IP和端口號(hào)呢?web service那么通用,大部分瀏覽器都支持直接從瀏覽器調(diào)用web service那么為什么主流的方式卻是json呢?
sun rpc是所有RPC機(jī)制中歷史最久的吧?它在設(shè)計(jì)第一版的時(shí)候,每個(gè)rpc調(diào)用都是由一問一答來組成,稱為two-way messaging。客戶端在發(fā)出請(qǐng)求之后,一直等服務(wù)器的答復(fù),如果一直到指定時(shí)間后依然沒收到答復(fù),那么執(zhí)行timeout邏輯。在第一個(gè)請(qǐng)求收到答復(fù)(或者timeout)之前,無法發(fā)起第二個(gè)答復(fù)。直到某一天,Sun的程序發(fā)現(xiàn)他們需要異步處理一些事情,于是設(shè)計(jì)了one-way messaging,客戶端在發(fā)起請(qǐng)求的時(shí)候,只要把這個(gè)東西塞到本地的IO隊(duì)列里,就返回。但是如果socket buffer滿了怎么辦?還是會(huì)等在那里。于是覺得這個(gè)還不徹底,于是又做了Non-Blocking Messaging,在kernel的socket buffer前面加了一個(gè)用戶態(tài)的rpc buffer,大多數(shù)時(shí)候它都是空的,當(dāng)socket buffer堆滿了的時(shí)候,再往這里面塞。如果這個(gè)buffer也滿了怎么辦?我覺得無非就三種處理手段:
1、阻塞。如果這么做,就是說本來是套非阻塞的設(shè)計(jì)但是某些情況下還是會(huì)阻塞?那么給用的人解釋起來太麻煩用起來也太麻煩。算了。
2、悄然丟棄。 不是所有的數(shù)據(jù)都可以丟。聊天的無所謂,但是交易的就不行。所以需要在消息類型上加判斷。
3、關(guān)閉連接。 最簡(jiǎn)單粗暴,卻也最有效。
在使用two-way messaging的時(shí)候,一定要記住設(shè)置超時(shí),省得像某些傻瓜一樣因?yàn)橐粋€(gè)請(qǐng)求把整個(gè)server堵死。但是我覺得timeout設(shè)多久完全是個(gè)經(jīng)驗(yàn)值,太大了沒作用,太小了失敗的太多。
至少在有一點(diǎn)我們可以大松一口氣,就是不用擔(dān)心數(shù)據(jù)量大到需要多網(wǎng)卡同時(shí)分擔(dān)中斷。通常來說網(wǎng)絡(luò)游戲的流量都是很小的,對(duì)玩家來說一個(gè)56K的貓或者128K的DSL就夠了。如果你的策劃給你提了一個(gè)很BT的需求導(dǎo)致要耗費(fèi)大量帶寬,那么你最好把這個(gè)應(yīng)用分到單獨(dú)的tcp 連接上,省得因?yàn)樗枞鴮?dǎo)致關(guān)鍵的業(yè)務(wù)(比如地圖消息)停滯。
我一直想把rpc的部分實(shí)現(xiàn)塞到kernel里。對(duì)客戶端的好處是增加了逆向工程的成本,對(duì)服務(wù)器的好處是網(wǎng)關(guān)可以很高效。就像LVS那樣,前端收完包之后在kernel里處理完然后立刻轉(zhuǎn)出去,不用切換到用戶態(tài)。而GameServer處理完之后,甚至不用經(jīng)過網(wǎng)關(guān),直接回復(fù)。目的不在于分擔(dān)網(wǎng)關(guān)的壓力,而是說降低響應(yīng)延遲。就算讓GameServer承擔(dān)部分加密和壓縮的計(jì)算量,它的CPU也足夠用。
不過對(duì)于網(wǎng)游,考慮動(dòng)態(tài)擴(kuò)容為時(shí)太早。一般都是新開幾組服務(wù)器。
我在做服務(wù)器安裝包的時(shí)候,分的很清楚:程序、配置文件、數(shù)據(jù)庫。
程序,就是編譯好的二進(jìn)制文件。最好是全靜態(tài)編譯,因?yàn)樗?jiǎn)單。動(dòng)態(tài)鏈接的優(yōu)點(diǎn)以及其它一些高級(jí)話題我后面講,但是通常來說,動(dòng)態(tài)的復(fù)雜的結(jié)構(gòu)得不償失。
配置文件總體來說可以分為文本文件和二進(jìn)制文件(廢話)。文本文件的好處是開發(fā)過程中易于調(diào)試和修改,最終發(fā)布后也易于追蹤問題。二進(jìn)制文件的好處是小、精巧、不易把信息泄露給外人知道。java的打jar包的技術(shù)算是一個(gè)折衷的優(yōu)勢(shì)吧?我最看重的是易于調(diào)試和修改,所以基本都用文本文件。而這其中,表現(xiàn)力最強(qiáng)的就是xml,所以基本都是xml。
但是xml多了怎么管理就是個(gè)問題。我得整理份文檔,每個(gè)xml都是什么格式,做什么用途的,最好每個(gè)xml再寫一個(gè)xsd。事實(shí)是配置文件是隨著需求變化最頻繁的部分,而換個(gè)角度說我之前強(qiáng)調(diào)的序列化。所以,正確的思路是這樣:
1、程序員分析需求文檔,確定需要什么樣的對(duì)象來表示配置
2、某套序列化框架,它利用某種xml解析庫把xml變成內(nèi)存中的對(duì)象
3、策劃提供xml
只要這個(gè)框架做的好,根本不需要文檔或xsd來描述xml。我這里說策劃提供xml,那么策劃怎么提供xml呢?按照我所看見的策劃的習(xí)慣,他們最喜歡的是兩種方式:
1、對(duì)于結(jié)構(gòu)簡(jiǎn)單的數(shù)據(jù),編輯excel表
2、對(duì)于結(jié)構(gòu)復(fù)雜的(如涉及樹、環(huán)的),提供專門的編輯工具
對(duì)于1,我們可以給excel做plugin,或者做一個(gè)工具從excel表導(dǎo)出成xml。對(duì)于2,讓編輯工具可以導(dǎo)出成xml。但是最終很重要很重要很重要的一點(diǎn)就是要讓所有的工具集成在一起,做好版本管理以及跨版本diff和merge。如何管理數(shù)據(jù)要比如何定義數(shù)據(jù)如何描述數(shù)據(jù)更難更重要。
很多同事和我的共識(shí)都是:要做一款好游戲,工具很重要。多個(gè)項(xiàng)目做完后,外人能看見的最大的積累就是工具和流程。
數(shù)據(jù)庫在游戲中的重要性,是一個(gè)很令人玩味的東西。你可以聽見很多人告訴你說,我們做游戲根本不需要數(shù)據(jù)庫。是的,像單機(jī)游戲那樣,在某個(gè)目錄下創(chuàng)建一個(gè)文件,save/load就行了。這就是我所看到的當(dāng)今的大型網(wǎng)游的主流做法。
哦,你要反對(duì)了。你說你知道某某游戲用的是mysql,某某游戲用的是oracle,等等。是的,你手上的信息可能比我多很多很多倍,但是關(guān)鍵點(diǎn)在于,數(shù)據(jù)庫在整個(gè)系統(tǒng)中的角色到底是什么?
典型的場(chǎng)景是這樣:?jiǎn)?dòng)一個(gè)單獨(dú)的進(jìn)程稱之為DB Gate。當(dāng)用戶登錄的時(shí)候,邏輯服務(wù)器找DB Gate要數(shù)據(jù),DB Gate沒有于是就去找后面的Mysql要,然后讀過來之后就放在這里,DB Gate就是一個(gè)類似于memcached的東西。所以后面無論是用mysql還是oracle還是plain text都可以,但實(shí)際上會(huì)在其它方面有些細(xì)微的差別。
它和網(wǎng)站應(yīng)用相比,數(shù)據(jù)更容易做cache,把握好上線和下線這兩個(gè)點(diǎn)即可,cache的命中率很容易達(dá)到4個(gè)9或者更高。但是從另一個(gè)方面,網(wǎng)絡(luò)游戲的數(shù)據(jù)關(guān)聯(lián)邏輯遠(yuǎn)遠(yuǎn)比網(wǎng)站復(fù)雜,而且對(duì)原子性、一致性、隔離性要求更高?,F(xiàn)在是你自己來管理cache,于是并發(fā)控制就沒辦法交給數(shù)據(jù)庫來做。
問題一:我不自己做cache,我就直接讀寫數(shù)據(jù)庫。就像php+mysql那樣,中間也不套memcache,行不行? 我不知道。你可以試一試。
問題二:SQL or NoSQL ? 我還是回答不了。你做個(gè)demo跑機(jī)器人試一試。
總之,東西是活的。沒有必要非要怎么著非不能怎么著。檢驗(yàn)的標(biāo)準(zhǔn)很簡(jiǎn)單:1、是否完成了策劃提出的功能需求 2、效率是否達(dá)到了預(yù)期目標(biāo)
對(duì)于第一個(gè),QA和策劃都會(huì)去檢查。對(duì)于2,跑機(jī)器人以及封測(cè)期間調(diào)優(yōu)是王道。
對(duì)于數(shù)據(jù)庫開發(fā),我還是很強(qiáng)調(diào)面向?qū)ο竽翘子^點(diǎn)。把數(shù)據(jù)庫里的表映射到對(duì)象,把對(duì)象抽象成接口,每個(gè)模塊以接口對(duì)外提供服務(wù),不同模塊不要直接通過表共享數(shù)據(jù)。或者,你可以讀我的表,但不要寫!因?yàn)閿?shù)據(jù)的約束條件未必是可以由DBMS完全保證的,某些約束是難以用數(shù)據(jù)庫本身的語言表述的。
數(shù)據(jù)是網(wǎng)游的核心,網(wǎng)游基本都是數(shù)據(jù)驅(qū)動(dòng)的,所以數(shù)值策劃才會(huì)這么吃香。
或者換個(gè)角度想,DBMS它是什么?
1、它管理數(shù)據(jù)。幫助我們高效的讀取和修改數(shù)據(jù)。因?yàn)閿?shù)據(jù)的動(dòng)態(tài)性,所以我們需要Btree這樣的結(jié)構(gòu),而不是隨便找個(gè)TXT追加寫。但是換個(gè)角度想,網(wǎng)絡(luò)游戲有什么特點(diǎn)?插入多,但是刪除操作極少極少。那么是否可以采用其它的結(jié)構(gòu)呢?順序重要嗎?為什么不用Hash呢?
2、它負(fù)責(zé)備份和恢復(fù)數(shù)據(jù)。這基本是任何現(xiàn)代的數(shù)據(jù)庫系統(tǒng)必須提供的基本功能。但是網(wǎng)絡(luò)游戲又特殊一點(diǎn),它要求能按指定時(shí)間“回檔”。時(shí)間可以有半小時(shí)的誤差,但是這個(gè)功能必須有。于是數(shù)據(jù)庫能支持增量備份,或者它的備份能支持版本很重要。
3、它使用logging system保證在突然宕機(jī)的時(shí)候數(shù)據(jù)依然是完整和一致的??墒侨绻覀円约鹤鯿ache,那么就要求我們?cè)趹?yīng)用層面所做的原子性保證必須在cache中也能體現(xiàn)出來。這些cache要么全刷,要么全不刷。
4、它提供并發(fā)功能。拿傳統(tǒng)的php+mysql架構(gòu)來說,為什么同一個(gè)應(yīng)用可以被分布式的部署在多臺(tái)機(jī)器上?魔力就在數(shù)據(jù)庫上。
既然有人輕視數(shù)據(jù)庫,那么也可反其道重視數(shù)據(jù)庫。把90%的邏輯都放在數(shù)據(jù)庫里完成。多招一些熟悉SQL熟悉存儲(chǔ)過程的,主要的邏輯都由他們完成。
接著說我在并發(fā)上的考慮。
一臺(tái)機(jī)器還是多臺(tái)機(jī)器?單進(jìn)程還是多進(jìn)程?單線程還是多線程?等等。
我覺得并發(fā)問題是最沒章法可循的問題。你可以這么做也可以那么做。網(wǎng)絡(luò)游戲的重點(diǎn)是在邏輯開發(fā)上,而做邏輯開發(fā)的人不要關(guān)心到底是epoll還是select??傊贫蚣艿臅r(shí)候需要定好一個(gè)規(guī)矩:?jiǎn)尉€程還是多線程、訪問哪些數(shù)據(jù)的時(shí)候需要加鎖(可能還需要跨進(jìn)程的加鎖)、誰來做load balancer、如果有一臺(tái)機(jī)器宕了怎么辦、哪些任務(wù)必須要以特定的順序執(zhí)行,等等。規(guī)矩定下來,一切都順了。可這個(gè)規(guī)矩要足夠的簡(jiǎn)單。
如果是多線程,我想過兩種模式:Thread per Connection和Task based thread pool?,F(xiàn)在機(jī)器的內(nèi)存越來越大了,所以前者的開銷是可以忍受的,1000人在線,就算每個(gè)線程要被系統(tǒng)占去2M,那么也才2G。而一般的3D游戲做個(gè) 3-4千人在線就行了,配個(gè)大內(nèi)存的機(jī)器,還剩下足夠多的內(nèi)存給應(yīng)用使用。多簡(jiǎn)單啊!網(wǎng)絡(luò)游戲中,很多請(qǐng)求都是只需要訪問單個(gè)角色的數(shù)據(jù)就夠了,反過來說很多數(shù)據(jù)都可以做成Thread Local的,免去了同步代價(jià)。
而Task based thread pool的伸縮性相對(duì)來說就好的多,但是并發(fā)問題也麻煩一些,況且從rpc請(qǐng)求被unmarshal完到扔到task pool里面又多了一次線程切換,如果換成Leader-Follower那樣的模式,少了切換但是模型又更復(fù)雜了一些。
如果是單線程的,那么一切都是事件驅(qū)動(dòng)的并且事件的處理都是非阻塞的。那么就得避開數(shù)據(jù)庫讀寫或者在處理的過程中再產(chǎn)生新的rpc請(qǐng)求,否則非常麻煩。
并發(fā)問題的瓶頸往往是在于怎么降低鎖沖突上。Task Pool里面的所有線程都在執(zhí)行Task,但是都在等同一把鎖,多悲劇啊。難點(diǎn)在于降低模塊耦合、采用適當(dāng)?shù)呐抨?duì)機(jī)制等等。我覺得這里沒有什么萬金油,降低模塊耦合本來就沒什么套路可循,而排隊(duì)機(jī)制有很多種,沒有最好的,各有利弊。
對(duì)于死鎖,我的容忍度比以前大了很多。我覺得每臺(tái)機(jī)器每天的死鎖數(shù)量在10個(gè)以內(nèi)都是可以忍受的,要有死鎖檢測(cè)、打斷機(jī)制并且重做的時(shí)候不會(huì)產(chǎn)生副作用。對(duì)玩家的感受而言就是突然卡了一下,可是網(wǎng)絡(luò)不也經(jīng)常會(huì)突然卡一下嗎?不頻繁就好。
我最鐘愛的模式就是“生產(chǎn)者-消費(fèi)者”模式,萬能的利器。例如Task Pool就是基于這樣的模式。它的核心東西無非就是一個(gè)隊(duì)列,如果要支持定時(shí),那么就是一個(gè)優(yōu)先隊(duì)列(deadline time作為優(yōu)先級(jí))。講個(gè)細(xì)節(jié),我面試的時(shí)候問了很多面試者,優(yōu)先隊(duì)列應(yīng)該用什么樣的數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn),結(jié)果都挺讓我失望的。
順便發(fā)個(gè)牢騷,Sun JDK的executor的實(shí)現(xiàn),BUG太多了。還那么巧,都被我遇上了?
說些雜七雜八的東西吧。
我剛?cè)胄械臅r(shí)候就一直在問,為什么網(wǎng)游服務(wù)器經(jīng)常要停機(jī)維護(hù)?為什么經(jīng)常都是好幾個(gè)小時(shí)?為什么非要分成不同組的服務(wù)器并且數(shù)據(jù)基本不互通?為什么不構(gòu)造一個(gè)大世界把所有玩家放在一起?
我現(xiàn)在不問了,這些問題基本都找到了答案。不是技術(shù)做不到,而且有很多它以外的東西在左右這些。至少我在盡力不回檔這件事情上已經(jīng)做的比較好了。
我想說的就是,入這行就得遵守這行的規(guī)矩。如果你是個(gè)老手了,根本沒必要來看我這一系列的P話。如果你是新手,那么我是在向你介紹現(xiàn)狀。策劃是甲方,我們是乙方,在盡力滿足策劃的需求且不會(huì)顯著增加成本的前提下做有限的創(chuàng)新,這是我給自己定的設(shè)計(jì)原則。
(支付寶剛通知我,我又收到了5塊錢的捐贈(zèng)。謝謝,謝謝大家)
如果你是一個(gè)受過良好訓(xùn)練的程序員,那么以下基本規(guī)則是懂的:
1、不要把需要翻譯的常量字符串寫在代碼里
2、不要直接在代碼中間寫498595這樣的magic number
3、向版本控制系統(tǒng)提交代碼的時(shí)候應(yīng)該寫注釋
4、需求是經(jīng)常變的,并且經(jīng)常是災(zāi)難性的
可往往知道是一回事兒,做又是另外一回事。尤其是不要相信策劃那張嘴,寫成word文檔才算數(shù)。
和大家分享一些我在版本控制上的經(jīng)驗(yàn)和教訓(xùn)。
最早接觸這個(gè)問題,是在sina的時(shí)候,由QA部門的同事以及周琦單獨(dú)專門給我講jira、svn。當(dāng)時(shí)受益很大。
周琦一再給我強(qiáng)調(diào),在產(chǎn)品生命周期中,源代碼版本管理和發(fā)布部署是獨(dú)立的兩套東西。源代碼版本管理是用subversion這樣的東西來做(更早一點(diǎn)我們還在用cvs)。發(fā)布部署,一是編譯的過程,二是對(duì)外推送部署的過程,是一套相對(duì)獨(dú)立的東西。周琦的特色在于他把這二者通過svn hook腳本的方式給自動(dòng)串起來了。
我一直想要做一套OBS這樣的東西找一臺(tái)服務(wù)器專門作build server,可惜一直沒時(shí)間去寫。就自己寫了一個(gè)腳本(本來是sh的,后來成perl,后來成groovy),它的作用是根據(jù)分支名和版本號(hào)從subversion下載代碼,然后編譯,然后放到指定位置。然后通知發(fā)布服務(wù)器從那里拿東西推到外邊。缺點(diǎn)它缺乏并發(fā)控制,并且沒有UI界面。導(dǎo)致做完之后就成個(gè)人專屬的了。
為什么每次要選擇一個(gè)空目錄checkout然后編譯,而不是在上次的基礎(chǔ)上svn up然后編譯?這個(gè)和Java/Ant有點(diǎn)關(guān)系。在寫Makefile的時(shí)候,盡管可以指定把當(dāng)前目錄下的.cpp文件全部都編譯,但是這是不推薦的做法。因?yàn)橄啾扔趯懘a的時(shí)間,把代碼文件添加到Makefile中的時(shí)間可以忽略不計(jì)。而我當(dāng)時(shí)給ant寫build.xml時(shí),是用**/*.java的方式去匹配,于是把src下的所有能編譯的全編譯了。可我在編譯之前會(huì)執(zhí)行一些腳本用于生成一些代碼,某些是單獨(dú)存放的,但是某些和其它手寫的代碼放在了一起。所以為了保持最終的jar包干凈,寧可犧牲編譯的時(shí)間。
在提供給QA的測(cè)試環(huán)境中可以很方便的通過GM指令得到版本號(hào),這個(gè)是編譯的時(shí)候打包工具寫進(jìn)去的。而編譯系統(tǒng)務(wù)必保證相同版本號(hào)的東西每次編譯出來都是相同的東西。雖然二進(jìn)制比對(duì)結(jié)果可能不一致,但是邏輯功能上是一致的。
對(duì)于svn的分支管理,有兩種普遍策略:
1、每個(gè)人一個(gè)單獨(dú)的分支。做完自己的功能后往主干merge
2、都在主干上工作。需要發(fā)版本的時(shí)候創(chuàng)建新分支。
前一種需要大家都比較熟悉svn的用法,熟悉版本管理的基本概念。后一種則把所有活堆給一個(gè)專門發(fā)版本的人。他來創(chuàng)建分支,他來merge(或是誰的功能誰merge)。并且這樣的話,絕大多數(shù)代碼是不需要merge的,所以我根據(jù)實(shí)際情況選擇了后一種。
于是在正在運(yùn)行的系統(tǒng)中發(fā)現(xiàn)bug的時(shí)候,立馬獲取版本號(hào),從那個(gè)版本上創(chuàng)建分支并且把分支名喊一聲告訴大家,然后找問題,把補(bǔ)丁merge到過去,編譯,發(fā)布,測(cè)試,推到外面。
發(fā)版本很累,這件事情在去年秋天上線后,一直到春節(jié),占去了我90%的精力。其中最重要的就是比對(duì)功能和bug列表。經(jīng)常,你分不清楚這到底算是一個(gè)bug呢,還是提需求的時(shí)候就沒說清楚所以這是一個(gè)新功能,反正都列一起的。挨個(gè)和svn提交記錄比對(duì)。
部署也是一個(gè)很有講究的過程。我的原則是,先刪除老的程序和配置文件,然后復(fù)制新的過去,數(shù)據(jù)庫的數(shù)據(jù)和日志文件保留,審計(jì)日志保留。這件事情本來還爭(zhēng)論過老的要不要?jiǎng)h,可不可以直接覆蓋,最終他們答應(yīng)了我的需求。過程挺曲折的,中間有很多惡心的細(xì)節(jié)問題,比如NFS的本地cache的問題。
對(duì)于數(shù)據(jù)庫,我們能智能的感知數(shù)據(jù)庫結(jié)構(gòu)更改并自動(dòng)生成升級(jí)腳本(天哪,我這算不算泄密)。這居然也是一把雙刃劍。優(yōu)點(diǎn)是減輕了開發(fā)人員的工作量,缺點(diǎn)是更改數(shù)據(jù)庫變得太隨意,隨意的添表添字段導(dǎo)致數(shù)據(jù)膨脹的厲害。
我的遺憾是沒有把上面這些東西和數(shù)據(jù)編輯器串起來。那么做有點(diǎn)是數(shù)值策劃調(diào)整數(shù)據(jù)更容易看到真實(shí)效果,缺點(diǎn)是也很容易亂來。如果這中間要經(jīng)過svn,那么太慢太曲折。如果這中間不經(jīng)過svn,那么鬼知道他們現(xiàn)在測(cè)的是什么版本的東西,他經(jīng)常會(huì)發(fā)現(xiàn)最終出去的東西跟他當(dāng)時(shí)測(cè)的還是不一樣,畢竟,是很多人在同一個(gè)服務(wù)器上測(cè)試。很難給他們解釋這個(gè)事情。
所以我當(dāng)時(shí)還漏了一個(gè)東西一直想做但是沒做,就是一個(gè)很簡(jiǎn)單的web gui能讓所有策劃自己?jiǎn)?dòng)、停止服務(wù)器,自己編譯、同步數(shù)據(jù)。各弄各的,互不干擾。但是吧,策劃畢竟是策劃,它們?nèi)狈镜腝A知識(shí)。他們不明白為什么一個(gè)底層功能好好的怎么突然就不好使了(因?yàn)樯蠈幽程幰有鹿δ?,所以底下的代碼要重構(gòu)),他們不明白為了一個(gè)bug被改掉之后反復(fù)又出現(xiàn)了,甚至對(duì)于分支和版本號(hào)這個(gè)東西,絕大多數(shù)策劃都理解起來困難。但是整個(gè)產(chǎn)品的開發(fā)、發(fā)布模型就是這樣,所以這些概念必須從一開始就溝通好、貫徹好。相比而下,這些倒和美術(shù)沒什么事兒。
都是些小活兒。
另外我一直在想要不要在配置文件和game server之間套一個(gè)gconf這樣的東西,外部更改配置,gconf通知listener也就是game server,呃,一個(gè)很不成熟的想法。
另外很多人一直想,在不重啟進(jìn)程的情況下,替換掉映像中的某個(gè)函數(shù),修BUG。如果這個(gè)daemon程序是用C/C++寫的,這個(gè)時(shí)候用dlopen加載一個(gè)so,設(shè)置一個(gè)參數(shù)就可以了。如果是JAVA并且用JDWP開了DEBUG,那么too easy。如果沒有,那么unload jar/load jar吧。
我一直在構(gòu)思一個(gè)可動(dòng)態(tài)拆卸/替換/裝載的架構(gòu),一個(gè)簡(jiǎn)單的不像OSGi那么復(fù)雜的東西,可是想法一直不大成熟,因?yàn)闆]有找到太簡(jiǎn)單的方法。我的基本想法是有一個(gè)object container,把service抽象成object,service和serivce之間的交互都要去這個(gè)object container中通過name lookup的方式得到一個(gè)句柄,然后通訊。配置文件不能視成一成不變的,它們也是動(dòng)態(tài)數(shù)據(jù)的一部分,不能再通過靜態(tài)的getInstance獲得,也必須通過這個(gè)object container查找。但是未必是一個(gè)global object container,每個(gè)module可以有自己的object container。或是module instance持有reference,請(qǐng)求派發(fā)給module,module派發(fā)給object的時(shí)候把需要的reference傳給過去,意思就是module就是一個(gè)object container,不過不是被lookup,而是主動(dòng)構(gòu)造好塞進(jìn)去。
(暫且到這里,想起來什么再補(bǔ)充)
聯(lián)系客服