1、引言
我們常常會(huì)聽說(shuō),某個(gè)互聯(lián)網(wǎng)應(yīng)用的服務(wù)器端系統(tǒng)多么牛逼,比如QQ、微信、淘寶。那么,一個(gè)大型互聯(lián)網(wǎng)應(yīng)用的服務(wù)器端系統(tǒng),到底牛逼在什么地方?為什么海量的用戶訪問(wèn),會(huì)讓一個(gè)服務(wù)器端系統(tǒng)變得更復(fù)雜?本文結(jié)合作者多年的互聯(lián)網(wǎng)系統(tǒng)設(shè)計(jì)實(shí)踐經(jīng)驗(yàn),從最基本的技術(shù)概念開始,帶你探尋服務(wù)器端系統(tǒng)架構(gòu)的方方面面。
本文適合有過(guò)幾年工作經(jīng)驗(yàn)、正處于技術(shù)上升期的程序員閱讀,內(nèi)容少有浮夸,多為實(shí)踐經(jīng)驗(yàn)總結(jié),希望能為您的技術(shù)成長(zhǎng)加油助力。
學(xué)習(xí)交流:
- 即時(shí)通訊開發(fā)交流3群:185926912[推薦]
- 移動(dòng)端IM開發(fā)入門文章:《新手入門一篇就夠:從零開發(fā)移動(dòng)端IM》
(本文同步發(fā)布于:http://www.52im.net/thread-1811-1-1.html)
2、關(guān)于作者
韓偉:1999年大學(xué)實(shí)習(xí)期加入初創(chuàng)期的網(wǎng)易,成為第30號(hào)員工,8年間從程序員開始,歷任項(xiàng)目經(jīng)理、產(chǎn)品總監(jiān);2007年后創(chuàng)業(yè)4年,開發(fā)過(guò)視頻直播社區(qū),及多款頁(yè)游產(chǎn)品;2011年后就職于騰訊游戲研發(fā)部公共技術(shù)中心架構(gòu)規(guī)劃組,負(fù)責(zé)騰訊游戲公共技術(shù)和底層平臺(tái)的架構(gòu)設(shè)計(jì)。
韓偉是難得的技術(shù)+管理的復(fù)合型人才,他的《騰訊高級(jí)工程師自述:十年沉浮,我為什么選擇離開管理崗位?》一文,觀點(diǎn)獨(dú)到也很犀利,同樣值得一讀。
3、相關(guān)文章
《淺談IM系統(tǒng)的架構(gòu)設(shè)計(jì)》
《一套海量在線用戶的移動(dòng)端IM架構(gòu)設(shè)計(jì)實(shí)踐分享(含詳細(xì)圖文)》
《一套原創(chuàng)分布式即時(shí)通訊(IM)系統(tǒng)理論架構(gòu)方案》
《從零到卓越:京東客服即時(shí)通訊系統(tǒng)的技術(shù)架構(gòu)演進(jìn)歷程》
《蘑菇街即時(shí)通訊/IM服務(wù)器開發(fā)之架構(gòu)選擇》
《WhatsApp技術(shù)實(shí)踐分享:32人工程團(tuán)隊(duì)創(chuàng)造的技術(shù)神話》
《微信朋友圈千億訪問(wèn)量背后的技術(shù)挑戰(zhàn)和實(shí)踐總結(jié)》
《王者榮耀2億用戶量的背后:產(chǎn)品定位、技術(shù)架構(gòu)、網(wǎng)絡(luò)方案等》
《騰訊QQ1.4億在線用戶的技術(shù)挑戰(zhàn)和架構(gòu)演進(jìn)之路PPT》
《微信后臺(tái)基于時(shí)間序的海量數(shù)據(jù)冷熱分級(jí)架構(gòu)設(shè)計(jì)實(shí)踐》
《微信技術(shù)總監(jiān)談架構(gòu):微信之道——大道至簡(jiǎn)(演講全文)》
《如何解讀《微信技術(shù)總監(jiān)談架構(gòu):微信之道——大道至簡(jiǎn)》》
《快速裂變:見證微信強(qiáng)大后臺(tái)架構(gòu)從0到1的演進(jìn)歷程(一)》
4、承載量是分布式系統(tǒng)存在的原因
當(dāng)一個(gè)互聯(lián)網(wǎng)業(yè)務(wù)獲得大眾歡迎的時(shí)候,最顯著碰到的技術(shù)問(wèn)題,就是服務(wù)器非常繁忙。當(dāng)每天有1000萬(wàn)個(gè)用戶訪問(wèn)你的網(wǎng)站時(shí),無(wú)論你使用什么樣的服務(wù)器硬件,都不可能只用一臺(tái)機(jī)器就承載的了。因此,在互聯(lián)網(wǎng)程序員解決服務(wù)器端問(wèn)題的時(shí)候,必須要考慮如何使用多臺(tái)服務(wù)器,為同一種互聯(lián)網(wǎng)應(yīng)用提供服務(wù),這就是所謂“分布式系統(tǒng)”的來(lái)源。
然而,大量用戶訪問(wèn)同一個(gè)互聯(lián)網(wǎng)業(yè)務(wù),所造成的問(wèn)題并不簡(jiǎn)單。從表面上看,要能滿足很多用戶來(lái)自互聯(lián)網(wǎng)的請(qǐng)求,最基本的需求就是所謂性能需求:用戶反應(yīng)網(wǎng)頁(yè)打開很慢,或者網(wǎng)游中的動(dòng)作很卡等等。而這些對(duì)于“服務(wù)速度”的要求,實(shí)際上包含的部分卻是以下幾個(gè):高吞吐、高并發(fā)、低延遲和負(fù)載均衡。
高吞吐:意味著你的系統(tǒng),可以同時(shí)承載大量的用戶使用。這里關(guān)注的整個(gè)系統(tǒng)能同時(shí)服務(wù)的用戶數(shù)。這個(gè)吞吐量肯定是不可能用單臺(tái)服務(wù)器解決的,因此需要多臺(tái)服務(wù)器協(xié)作,才能達(dá)到所需要的吞吐量。而在多臺(tái)服務(wù)器的協(xié)作中,如何才能有效的利用這些服務(wù)器,不致于其中某一部分服務(wù)器成為瓶頸,從而影響整個(gè)系統(tǒng)的處理能力,這就是一個(gè)分布式系統(tǒng),在架構(gòu)上需要仔細(xì)權(quán)衡的問(wèn)題。
高并發(fā):是高吞吐的一個(gè)延伸需求。當(dāng)我們?cè)诔休d海量用戶的時(shí)候,我們當(dāng)然希望每個(gè)服務(wù)器都能盡其所能的工作,而不要出現(xiàn)無(wú)謂的消耗和等待的情況。然而,軟件系統(tǒng)并不是簡(jiǎn)單的設(shè)計(jì),就能對(duì)同時(shí)處理多個(gè)任務(wù),做到“盡量多”的處理。很多時(shí)候,我們的程序會(huì)因?yàn)橐x擇處理哪個(gè)任務(wù),而導(dǎo)致額外的消耗。這也是分布式系統(tǒng)解決的問(wèn)題。
低延遲:對(duì)于人數(shù)稀少的服務(wù)來(lái)說(shuō)不算什么問(wèn)題。然而,如果我們需要在大量用戶訪問(wèn)的時(shí)候,也能很快的返回計(jì)算結(jié)果,這就要困難的多。因?yàn)槌舜罅坑脩粼L問(wèn)可能造成請(qǐng)求在排隊(duì)外,還有可能因?yàn)榕抨?duì)的長(zhǎng)度太長(zhǎng),導(dǎo)致內(nèi)存耗盡、帶寬占滿等空間性的問(wèn)題。如果因?yàn)榕抨?duì)失敗而采取重試的策略,則整個(gè)延遲會(huì)變的更高。所以分布式系統(tǒng)會(huì)采用很多請(qǐng)求分揀和分發(fā)的做法,盡快的讓更多的服務(wù)器來(lái)出來(lái)用戶的請(qǐng)求。但是,由于一個(gè)數(shù)量龐大的分布式系統(tǒng),必然需要把用戶的請(qǐng)求經(jīng)過(guò)多次的分發(fā),整個(gè)延遲可能會(huì)因?yàn)檫@些分發(fā)和轉(zhuǎn)交的操作,變得更高,所以分布式系統(tǒng)除了分發(fā)請(qǐng)求外,還要盡量想辦法減少分發(fā)的層次數(shù),以便讓請(qǐng)求能盡快的得到處理
由于互聯(lián)網(wǎng)業(yè)務(wù)的用戶來(lái)自全世界,因此在物理空間上可能來(lái)自各種不同延遲的網(wǎng)絡(luò)和線路,在時(shí)間上也可能來(lái)自不同的時(shí)區(qū),所以要有效的應(yīng)對(duì)這種用戶來(lái)源的復(fù)雜性,就需要把多個(gè)服務(wù)器部署在不同的空間來(lái)提供服務(wù)。同時(shí),我們也需要讓同時(shí)發(fā)生的請(qǐng)求,有效的讓多個(gè)不同服務(wù)器承載。所謂的負(fù)載均衡,就是分布式系統(tǒng)與生俱來(lái)需要完成的功課。
由于分布式系統(tǒng),幾乎是解決互聯(lián)網(wǎng)業(yè)務(wù)承載量問(wèn)題,的最基本方法,所以作為一個(gè)服務(wù)器端程序員,掌握分布式系統(tǒng)技術(shù)就變得異常重要了。然而,分布式系統(tǒng)的問(wèn)題,并非是學(xué)會(huì)用幾個(gè)框架和使用幾個(gè)庫(kù),就能輕易解決的,因?yàn)楫?dāng)一個(gè)程序在一個(gè)電腦上運(yùn)行,變成了又無(wú)數(shù)個(gè)電腦上同時(shí)協(xié)同運(yùn)行,在開發(fā)、運(yùn)維上都會(huì)帶來(lái)很大的差別。
5、分布式系統(tǒng)提高承載量的基本手段
5.1 分層模型(路由、代理)
使用多態(tài)服務(wù)器來(lái)協(xié)同完成計(jì)算任務(wù),最簡(jiǎn)單的思路就是,讓每個(gè)服務(wù)器都能完成全部的請(qǐng)求,然后把請(qǐng)求隨機(jī)的發(fā)給任何一個(gè)服務(wù)器處理。最早期的互聯(lián)網(wǎng)應(yīng)用中,DNS輪詢就是這樣的做法:當(dāng)用戶輸入一個(gè)域名試圖訪問(wèn)某個(gè)網(wǎng)站,這個(gè)域名會(huì)被解釋成多個(gè)IP地址中的一個(gè),隨后這個(gè)網(wǎng)站的訪問(wèn)請(qǐng)求,就被發(fā)往對(duì)應(yīng)IP的服務(wù)器了,這樣多個(gè)服務(wù)器(多個(gè)IP地址)就能一起解決處理大量的用戶請(qǐng)求。(詳細(xì)的原理,可以看看《通俗易懂:基于集群的移動(dòng)端IM接入層負(fù)載均衡方案分享》)
然而,單純的請(qǐng)求隨機(jī)轉(zhuǎn)發(fā),并不能解決一切問(wèn)題。比如我們很多互聯(lián)網(wǎng)業(yè)務(wù),都是需要用戶登錄的。在登錄某一個(gè)服務(wù)器后,用戶會(huì)發(fā)起多個(gè)請(qǐng)求,如果我們把這些請(qǐng)求隨機(jī)的轉(zhuǎn)發(fā)到不同的服務(wù)器上,那么用戶登錄的狀態(tài)就會(huì)丟失,造成一些請(qǐng)求處理失敗。簡(jiǎn)單的依靠一層服務(wù)轉(zhuǎn)發(fā)是不夠的,所以我們會(huì)增加一批服務(wù)器,這些服務(wù)器會(huì)根據(jù)用戶的Cookie,或者用戶的登錄憑據(jù),來(lái)再次轉(zhuǎn)發(fā)給后面具體處理業(yè)務(wù)的服務(wù)器。(更詳細(xì)的技術(shù)原理,請(qǐng)閱讀:《[url=http://www.52im.net/thread-1686-1-1.html]小白必讀:閑話HTTP短連接中的Session和Token[/url]》、《IM開發(fā)基礎(chǔ)知識(shí)補(bǔ)課:正確理解前置HTTP SSO單點(diǎn)登陸接口的原理》、《IM開發(fā)基礎(chǔ)知識(shí)補(bǔ)課(四):正確理解HTTP短連接中的Cookie、Session和Token》)
除了登錄的需求外,我們還發(fā)現(xiàn),很多數(shù)據(jù)是需要數(shù)據(jù)庫(kù)來(lái)處理的,而我們的這些數(shù)據(jù)往往都只能集中到一個(gè)數(shù)據(jù)庫(kù)中,否則在查詢的時(shí)候就會(huì)丟失其他服務(wù)器上存放的數(shù)據(jù)結(jié)果。所以往往我們還會(huì)把數(shù)據(jù)庫(kù)單獨(dú)出來(lái)成為一批專用的服務(wù)器。
至此,我們就會(huì)發(fā)現(xiàn),一個(gè)典型的三層結(jié)構(gòu)出現(xiàn)了:接入、邏輯、存儲(chǔ)。
然而,這種三層結(jié)果,并不就能包醫(yī)百病。例如:當(dāng)我們需要讓用戶在線互動(dòng)(網(wǎng)游和IM就是典型) ,那么分割在不同邏輯服務(wù)器上的在線狀態(tài)數(shù)據(jù),是無(wú)法知道對(duì)方的,這樣我們就需要專門做一個(gè)類似互動(dòng)或接層服務(wù)器的專門系統(tǒng),讓用戶登錄的時(shí)候,也同時(shí)記錄一份數(shù)據(jù)到它那里,表明某個(gè)用戶登錄在某個(gè)服務(wù)器上,而所有的互動(dòng)操作,要先經(jīng)過(guò)這個(gè)互動(dòng)服務(wù)器,才能正確的把消息轉(zhuǎn)發(fā)到目標(biāo)用戶的服務(wù)器上。
又例如,當(dāng)我們?cè)谑褂镁W(wǎng)上論壇(BBS)系統(tǒng)的時(shí)候,我們發(fā)的文章,不可能只寫入一個(gè)數(shù)據(jù)庫(kù)里,因?yàn)樘嗳说拈喿x請(qǐng)求會(huì)拖死這個(gè)數(shù)據(jù)庫(kù)。我們常常會(huì)按論壇板塊來(lái)寫入不同的數(shù)據(jù)庫(kù),又或者是同時(shí)寫入多個(gè)數(shù)據(jù)庫(kù)。這樣把文章數(shù)據(jù)分別存放到不同的服務(wù)器上,才能應(yīng)對(duì)大量的操作請(qǐng)求。然而,用戶在讀取文章的時(shí)候,就需要有一個(gè)專門的程序,去查找具體文章在哪一個(gè)服務(wù)器上,這時(shí)候我們就要架設(shè)一個(gè)專門的代理層,把所有的文章請(qǐng)求先轉(zhuǎn)交給它,由它按照我們預(yù)設(shè)的存儲(chǔ)計(jì)劃,去找對(duì)應(yīng)的數(shù)據(jù)庫(kù)獲取數(shù)據(jù)。
根據(jù)上面的例子來(lái)看,分布式系統(tǒng)雖然具有三層典型的結(jié)構(gòu),但是實(shí)際上往往不止有三層,而是根據(jù)業(yè)務(wù)需求,會(huì)設(shè)計(jì)成多個(gè)層次的。為了把請(qǐng)求轉(zhuǎn)交給正確的進(jìn)程處理,我們而設(shè)計(jì)很多專門用于轉(zhuǎn)發(fā)請(qǐng)求的進(jìn)程和服務(wù)器。這些進(jìn)程我們常常以Proxy或者Router來(lái)命名,一個(gè)多層結(jié)構(gòu)常常會(huì)具備各種各樣的Proxy進(jìn)程。這些代理進(jìn)程,很多時(shí)候都是通過(guò)TCP來(lái)連接前后兩端。然而,TCP雖然簡(jiǎn)單,但是卻會(huì)有故障后不容易恢復(fù)的問(wèn)題。而且TCP的網(wǎng)絡(luò)編程,也是有點(diǎn)復(fù)雜的。所以,人們?cè)O(shè)計(jì)出更好進(jìn)程間通訊機(jī)制:消息隊(duì)列。
盡管通過(guò)各種Proxy或者Router進(jìn)程能組建出強(qiáng)大的分布式系統(tǒng),但是其管理的復(fù)雜性也是非常高的。所以人們?cè)诜謱幽J降幕A(chǔ)上,想出了更多的方法,來(lái)讓這種分層模式的程序變得更簡(jiǎn)單高效的方法。
5.2 并發(fā)模型(多線程、異步)
當(dāng)我們?cè)诰帉懛?wù)器端程序是,我們會(huì)明確的知道,大部分的程序,都是會(huì)處理同時(shí)到達(dá)的多個(gè)請(qǐng)求的。因此我們不能好像HelloWorld那么簡(jiǎn)單的,從一個(gè)簡(jiǎn)單的輸入計(jì)算出輸出來(lái)。因?yàn)槲覀儠?huì)同時(shí)獲得很多個(gè)輸入,需要返回很多個(gè)輸出。在這些處理的過(guò)程中,往往我們還會(huì)碰到需要“等待”或“阻塞”的情況,比如我們的程序要等待數(shù)據(jù)庫(kù)處理結(jié)果,等待向另外一個(gè)進(jìn)程請(qǐng)求結(jié)果等等……如果我們把請(qǐng)求一個(gè)挨著一個(gè)的處理,那么這些空閑的等待時(shí)間將白白浪費(fèi),造成用戶的響應(yīng)延時(shí)增加,以及整體系統(tǒng)的吞吐量極度下降。
所以在如何同時(shí)處理多個(gè)請(qǐng)求的問(wèn)題上,業(yè)界有2個(gè)典型的方案:
一種是多線程;
一種是異步。
在早期的系統(tǒng)中,多線程或多進(jìn)程是最常用的技術(shù)。這種技術(shù)的代碼編寫起來(lái)比較簡(jiǎn)單,因?yàn)槊總€(gè)線程中的代碼都肯定是按先后順序執(zhí)行的。但是由于同時(shí)運(yùn)行著多個(gè)線程,所以你無(wú)法保障多個(gè)線程之間的代碼的先后順序。這對(duì)于需要處理同一個(gè)數(shù)據(jù)的邏輯來(lái)說(shuō),是一個(gè)非常嚴(yán)重的問(wèn)題,最簡(jiǎn)單的例子就是顯示某個(gè)新聞的閱讀量。兩個(gè)++操作同時(shí)運(yùn)行,有可能結(jié)果只加了1,而不是2。所以多線程下,我們常常要加很多數(shù)據(jù)的鎖,而這些鎖又反過(guò)來(lái)可能導(dǎo)致線程的死鎖。
因此異步回調(diào)模型在隨后比多線程更加流行,除了多線程的死鎖問(wèn)題外,異步還能解決多線程下,線程反復(fù)切換導(dǎo)致不必要的開銷的問(wèn)題:每個(gè)線程都需要一個(gè)獨(dú)立的棧空間,在多線程并行運(yùn)行的時(shí)候,這些棧的數(shù)據(jù)可能需要來(lái)回的拷貝,這額外消耗了CPU。同時(shí)由于每個(gè)線程都需要占用棧空間,所以在大量線程存在的時(shí)候,內(nèi)存的消耗也是巨大的。而異步回調(diào)模型則能很好的解決這些問(wèn)題,不過(guò)異步回調(diào)更像是“手工版”的并行處理,需要開發(fā)者自己去實(shí)現(xiàn)如何“并行”的問(wèn)題。
異步回調(diào)基于非阻塞的I/O操作(網(wǎng)絡(luò)和文件),這樣我們就不用在調(diào)用讀寫函數(shù)的時(shí)候“卡”在那一句函數(shù)調(diào)用,而是立刻返回“有無(wú)數(shù)據(jù)”的結(jié)果。而Linux的epoll技術(shù),則利用底層內(nèi)核的機(jī)制,讓我們可以快速的“查找”到有數(shù)據(jù)可以讀寫的連接\文件。由于每個(gè)操作都是非阻塞的,所以我們的程序可以只用一個(gè)進(jìn)程,就處理大量并發(fā)的請(qǐng)求。因?yàn)橹挥幸粋€(gè)進(jìn)程,所以所有的數(shù)據(jù)處理,其順序都是固定的,不可能出現(xiàn)多線程中,兩個(gè)函數(shù)的語(yǔ)句交錯(cuò)執(zhí)行的情況,因此也不需要各種“鎖”。從這個(gè)角度看,異步非阻塞的技術(shù),是大大簡(jiǎn)化了開發(fā)的過(guò)程。由于只有一個(gè)線程,也不需要有線程切換之類的開銷,所以異步非阻塞成為很多對(duì)吞吐量、并發(fā)有較高要求的系統(tǒng)首選。
▲ Linux epoll網(wǎng)絡(luò)模型原理圖
int epoll_create(int size);// 創(chuàng)建一個(gè)epoll的句柄,size用來(lái)告訴內(nèi)核這個(gè)監(jiān)聽的數(shù)目一共有多大
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
5.3 緩沖技術(shù)
在互聯(lián)網(wǎng)服務(wù)中,大部分的用戶交互,都是需要立刻返回結(jié)果的,所以對(duì)于延遲有一定的要求。而類似網(wǎng)絡(luò)游戲之類服務(wù),延遲更是要求縮短到幾十毫秒以內(nèi)。所以為了降低延遲,緩沖是互聯(lián)網(wǎng)服務(wù)中最常見的技術(shù)之一。
早期的WEB系統(tǒng)中,如果每個(gè)HTTP請(qǐng)求的處理,都去數(shù)據(jù)庫(kù)(MySQL)讀寫一次,那么數(shù)據(jù)庫(kù)很快就會(huì)因?yàn)檫B接數(shù)占滿而停止響應(yīng)。因?yàn)橐话愕臄?shù)據(jù)庫(kù),支持的連接數(shù)都只有幾百,而WEB的應(yīng)用的并發(fā)請(qǐng)求,輕松能到幾千。這也是很多設(shè)計(jì)不良的網(wǎng)站人一多就卡死的最直接原因。為了盡量減少對(duì)數(shù)據(jù)庫(kù)的連接和訪問(wèn),人們?cè)O(shè)計(jì)了很多緩沖系統(tǒng)——把從數(shù)據(jù)庫(kù)中查詢的結(jié)果存放到更快的設(shè)施上,如果沒有相關(guān)聯(lián)的修改,就直接從這里讀。
最典型的WEB應(yīng)用緩沖系統(tǒng)是Memcache(更新一點(diǎn)的技術(shù)方案是Redis)。由于PHP本身的線程結(jié)構(gòu),是不帶狀態(tài)的。早期PHP本身甚至連操作“堆”內(nèi)存的方法都沒有,所以那些持久的狀態(tài),就一定要存放到另外一個(gè)進(jìn)程里。而Memcache就是一個(gè)簡(jiǎn)單可靠的存放臨時(shí)狀態(tài)的開源軟件。很多PHP應(yīng)用現(xiàn)在的處理邏輯,都是先從數(shù)據(jù)庫(kù)讀取數(shù)據(jù),然后寫入Memcache;當(dāng)下次請(qǐng)求來(lái)的時(shí)候,先嘗試從Memcache里面讀取數(shù)據(jù),這樣就有可能大大減少對(duì)數(shù)據(jù)庫(kù)的訪問(wèn)。
然而Memcache本身是一個(gè)獨(dú)立的服務(wù)器進(jìn)程,這個(gè)進(jìn)程自身并不帶特別的集群功能。也就是說(shuō)這些Memcache進(jìn)程,并不能直接組建成一個(gè)統(tǒng)一的集群。如果一個(gè)Memcache不夠用,我們就要手工用代碼去分配,哪些數(shù)據(jù)應(yīng)該去哪個(gè)Memcache進(jìn)程。——這對(duì)于真正的大型分布式網(wǎng)站來(lái)說(shuō),管理一個(gè)這樣的緩沖系統(tǒng),是一個(gè)很繁瑣的工作。
因此人們開始考慮設(shè)計(jì)一些更高效的緩沖系統(tǒng):從性能上來(lái)說(shuō),Memcache的每筆請(qǐng)求,都要經(jīng)過(guò)網(wǎng)絡(luò)傳輸,才能去拉取內(nèi)存中的數(shù)據(jù)。這無(wú)疑是有一點(diǎn)浪費(fèi)的,因?yàn)檎?qǐng)求者本身的內(nèi)存,也是可以存放數(shù)據(jù)的。——這就是促成了很多利用請(qǐng)求方內(nèi)存的緩沖算法和技術(shù),其中最簡(jiǎn)單的就是使用LRU算法,把數(shù)據(jù)放在一個(gè)哈希表結(jié)構(gòu)的堆內(nèi)存中。
而Memcache的不具備集群功能,也是一個(gè)用戶的痛點(diǎn)。于是很多人開始設(shè)計(jì),如何讓數(shù)據(jù)緩存分不到不同的機(jī)器上。最簡(jiǎn)單的思路是所謂讀寫分離,也就是緩存每次寫,都寫到多個(gè)緩沖進(jìn)程上記錄,而讀則可以隨機(jī)讀任何一個(gè)進(jìn)程。在業(yè)務(wù)數(shù)據(jù)有明顯的讀寫不平衡差距上,效果是非常好的。
然而,并不是所有的業(yè)務(wù)都能簡(jiǎn)單的用讀寫分離來(lái)解決問(wèn)題,比如一些在線互動(dòng)的互聯(lián)網(wǎng)業(yè)務(wù),比如社區(qū)、游戲。這些業(yè)務(wù)的數(shù)據(jù)讀寫頻率并沒很大的差異,而且也要求很高的延遲。因此人們又再想辦法,把本地內(nèi)存和遠(yuǎn)端進(jìn)程的內(nèi)存緩存結(jié)合起來(lái)使用,讓數(shù)據(jù)具備兩級(jí)緩存。同時(shí),一個(gè)數(shù)據(jù)不在同時(shí)的復(fù)制存在所有的緩存進(jìn)程上,而是按一定規(guī)律分布在多個(gè)進(jìn)程上。——這種分布規(guī)律使用的算法,最流行的就是所謂“一致性哈希”。這種算法的好處是,當(dāng)某一個(gè)進(jìn)程失效掛掉,不需要把整個(gè)集群中所有的緩存數(shù)據(jù),都重新修改一次位置。你可以想象一下,如果我們的數(shù)據(jù)緩存分布,是用簡(jiǎn)單的以數(shù)據(jù)的ID對(duì)進(jìn)程數(shù)取模,那么一旦進(jìn)程數(shù)變化,每個(gè)數(shù)據(jù)存放的進(jìn)程位置都可能變化,這對(duì)于服務(wù)器的故障容忍是不利的。
Orcale公司旗下有一款叫Coherence的產(chǎn)品(詳見《分布式網(wǎng)格緩存Coherence簡(jiǎn)介》),是在緩存系統(tǒng)上設(shè)計(jì)比較好的。這個(gè)產(chǎn)品是一個(gè)商業(yè)產(chǎn)品,支持利用本地內(nèi)存緩存和遠(yuǎn)程進(jìn)程緩存協(xié)作。集群進(jìn)程是完全自管理的,還支持在數(shù)據(jù)緩存所在進(jìn)程,進(jìn)行用戶定義的計(jì)算(處理器功能),這就不僅僅是緩存了,還是一個(gè)分布式的計(jì)算系統(tǒng)。
5.4 存儲(chǔ)技術(shù)(NoSQL)
相信CAP理論大家已經(jīng)耳熟能詳,然而在互聯(lián)發(fā)展的早期,大家都還在使用MySQL的時(shí)候,如何讓數(shù)據(jù)庫(kù)存放更多的數(shù)據(jù),承載更多的連接,很多團(tuán)隊(duì)都是絞盡腦汁。甚至于有很多業(yè)務(wù),主要的數(shù)據(jù)存儲(chǔ)方式是文件,數(shù)據(jù)庫(kù)反而變成是輔助的設(shè)施了。
然而,當(dāng)NoSQL興起,大家突然發(fā)現(xiàn),其實(shí)很多互聯(lián)網(wǎng)業(yè)務(wù),其數(shù)據(jù)格式是如此的簡(jiǎn)單,很多時(shí)候根部不需要關(guān)系型數(shù)據(jù)庫(kù)那種復(fù)雜的表格。對(duì)于索引的要求往往也只是根據(jù)主索引搜索。而更復(fù)雜的全文搜索,本身數(shù)據(jù)庫(kù)也做不到。所以現(xiàn)在相當(dāng)多的高并發(fā)的互聯(lián)網(wǎng)業(yè)務(wù),首選NoSQL來(lái)做存儲(chǔ)設(shè)施。最早的NoSQL數(shù)據(jù)庫(kù)有MangoDB等,現(xiàn)在最流行的似乎就是Redis了。甚至有些團(tuán)隊(duì),把Redis也當(dāng)成緩沖系統(tǒng)的一部分,實(shí)際上也是認(rèn)可Redis的性能優(yōu)勢(shì)。
NoSQL除了更快、承載量更大以外,更重要的特點(diǎn)是,這種數(shù)據(jù)存儲(chǔ)方式,只能按照一條索引來(lái)檢索和寫入。這樣的需求約束,帶來(lái)了分布上的好處,我們可以按這條主索引,來(lái)定義數(shù)據(jù)存放的進(jìn)程(服務(wù)器)。這樣一個(gè)數(shù)據(jù)庫(kù)的數(shù)據(jù),就能很方便的存放在不同的服務(wù)器上。在分布式系統(tǒng)的必然趨勢(shì)下,數(shù)據(jù)存儲(chǔ)層終于也找到了分布的方法。
6、分布式系統(tǒng)在可管理性上造成的問(wèn)題
分布式系統(tǒng)并不是簡(jiǎn)單的把一堆服務(wù)器一起運(yùn)行起來(lái)就能滿足需求的。對(duì)比單機(jī)或少量服務(wù)器的集群,有一些特別需要解決的問(wèn)題等待著我們。
6.1 硬件故障率
所謂分布式系統(tǒng),肯定就不是只有一臺(tái)服務(wù)器。假設(shè)一臺(tái)服務(wù)器的平均故障時(shí)間是1%,那么當(dāng)你有100臺(tái)服務(wù)器的時(shí)候,那就幾乎總有一臺(tái)是在故障的。雖然這個(gè)比方不一定很準(zhǔn)確,但是,當(dāng)你的系統(tǒng)所涉及的硬件越來(lái)越多,硬件的故障也會(huì)從偶然事件變成一個(gè)必然事件。一般我們?cè)趯懝δ艽a的時(shí)候,是不會(huì)考慮到硬件故障的時(shí)候應(yīng)該怎么辦的。而如果在編寫分布式系統(tǒng)的時(shí)候,就一定需要面對(duì)這個(gè)問(wèn)題了。否則,很可能只有一臺(tái)服務(wù)器出故障,整個(gè)數(shù)百臺(tái)服務(wù)器的集群都工作不正常了。
▲ 為了讓服務(wù)器不宕機(jī),“開光”、“祈禱”必不可少啊!
除了服務(wù)器自己的內(nèi)存、硬盤等故障,服務(wù)器之間的網(wǎng)絡(luò)線路故障更加常見。而且這種故障還有可能是偶發(fā)的,或者是會(huì)自動(dòng)恢復(fù)的。面對(duì)這種問(wèn)題,如果只是簡(jiǎn)單的把“出現(xiàn)故障”的機(jī)器剔除出去,那還是不夠的。因?yàn)榫W(wǎng)絡(luò)可能過(guò)一會(huì)兒就又恢復(fù)了,而你的集群可能因?yàn)檫@一下的臨時(shí)故障,丟失了過(guò)半的處理能力。
如何讓分布式系統(tǒng),在各種可能隨時(shí)出現(xiàn)故障的情況下,盡量的自動(dòng)維護(hù)和維持對(duì)外服務(wù),成為了編寫程序就要考慮的問(wèn)題。由于要考慮到這種故障的情況,所以我們?cè)谠O(shè)計(jì)架構(gòu)的時(shí)候,也要有意識(shí)的預(yù)設(shè)一些冗余、自我維護(hù)的功能。這些都不是產(chǎn)品上的業(yè)務(wù)需求,完全就是技術(shù)上的功能需求。能否在這方面提出對(duì)的需求,然后正確的實(shí)現(xiàn),是服務(wù)器端程序員最重要的職責(zé)之一。
6.2 資源利用率優(yōu)化
在分布式系統(tǒng)的集群,包含了很多個(gè)服務(wù)器,當(dāng)這樣一個(gè)集群的硬件承載能力到達(dá)極限的時(shí)候,最自然的想法就是增加更多的硬件。然而,一個(gè)軟件系統(tǒng)不是那么容易就可以通過(guò)“增加”硬件來(lái)提高承載性能的。因?yàn)檐浖诙鄠€(gè)服務(wù)器上的工作,是需要有復(fù)雜細(xì)致的協(xié)調(diào)工作。在對(duì)一個(gè)集群擴(kuò)容的時(shí)候,我們往往會(huì)要停掉整個(gè)集群的服務(wù),然后修改各種配置,最后才能重新啟動(dòng)一個(gè)加入了新的服務(wù)器的集群。
由于在每個(gè)服務(wù)器的內(nèi)存里,都可能會(huì)有一些用戶使用的數(shù)據(jù),所以如果冒然在運(yùn)行的時(shí)候,就試圖修改集群中提供服務(wù)的配置,很可能會(huì)造成內(nèi)存數(shù)據(jù)的丟失和錯(cuò)誤。因此,運(yùn)行時(shí)擴(kuò)容在對(duì)無(wú)狀態(tài)的服務(wù)上,是比較容易的,比如增加一些Web服務(wù)器。但如果是在有狀態(tài)的服務(wù)上,比如網(wǎng)絡(luò)游戲,幾乎是不可能進(jìn)行簡(jiǎn)單的運(yùn)行時(shí)擴(kuò)容的。
分布式集群除了擴(kuò)容,還有縮容的需求。當(dāng)用戶人數(shù)下降,服務(wù)器硬件資源出現(xiàn)空閑的時(shí)候,我們往往需要這些空閑的資源能利用起來(lái),放到另外一些新的服務(wù)集群里去。縮容和集群中有故障需要容災(zāi)有一定類似之處,區(qū)別是縮容的時(shí)間點(diǎn)和目標(biāo)是可預(yù)期的。
由于分布式集群中的擴(kuò)容、縮容,以及希望盡量能在線操作,這導(dǎo)致了非常復(fù)雜的技術(shù)問(wèn)題需要處理,比如集群中互相關(guān)聯(lián)的配置如何正確高效的修改、如何對(duì)有狀態(tài)的進(jìn)程進(jìn)行操作、如何在擴(kuò)容縮容的過(guò)程中保證集群中節(jié)點(diǎn)之間通信的正常。作為服務(wù)器端程序員,會(huì)需要花費(fèi)大量的經(jīng)歷,來(lái)對(duì)多個(gè)進(jìn)程的集群狀態(tài)變化,造成的一系列問(wèn)題進(jìn)行專門的開發(fā)。
6.3 軟件服務(wù)內(nèi)容更新
現(xiàn)在都流行用敏捷開發(fā)模式中的“迭代”,來(lái)表示一個(gè)服務(wù)不斷的更新程序,滿足新的需求,修正BUG。如果我們僅僅管理一臺(tái)服務(wù)器,那么更新這一臺(tái)服務(wù)器上的程序,是非常簡(jiǎn)單的:只要把軟件包拷貝過(guò)去,然后修改下配置就好。但是如果你要對(duì)成百上千的服務(wù)器去做同樣的操作,就不可能每臺(tái)服務(wù)器登錄上去處理。
服務(wù)器端的程序批量安裝部署工具,是每個(gè)分布式系統(tǒng)開發(fā)者都需要的。然而,我們的安裝工作除了拷貝二進(jìn)制文件和配置文件外,還會(huì)有很多其他的操作。比如打開防火墻、建立共享內(nèi)存文件、修改數(shù)據(jù)庫(kù)表結(jié)構(gòu)、改寫一些數(shù)據(jù)文件等等……甚至有一些還要在服務(wù)器上安裝新的軟件。
如果我們?cè)陂_發(fā)服務(wù)器端程序的時(shí)候,就考慮到軟件更新、版本升級(jí)的問(wèn)題,那么我們對(duì)于配置文件、命令行參數(shù)、系統(tǒng)變量的使用,就會(huì)預(yù)先做一定的規(guī)劃,這能讓安裝部署的工具運(yùn)行更快,可靠性更高。
除了安裝部署的過(guò)程,還有一個(gè)重要的問(wèn)題,就是不同版本間數(shù)據(jù)的問(wèn)題。我們?cè)谏?jí)版本的時(shí)候,舊版本程序生成的一些持久化數(shù)據(jù),一般都是舊的數(shù)據(jù)格式的;而我們升級(jí)版本中如果涉及修改了數(shù)據(jù)格式,比如數(shù)據(jù)表結(jié)果,那么這些舊格式的數(shù)據(jù),都要轉(zhuǎn)換改寫成新版本的數(shù)據(jù)格式才行。這導(dǎo)致了我們?cè)谠O(shè)計(jì)數(shù)據(jù)結(jié)構(gòu)的時(shí)候,就要考慮清楚這些表格的結(jié)構(gòu),是用最簡(jiǎn)單直接的表達(dá)方式,來(lái)讓將來(lái)的修改更簡(jiǎn)單;還是一早就預(yù)計(jì)到修改的范圍,專門預(yù)設(shè)一些字段,或者使用其他形式存放數(shù)據(jù)。
除了持久化數(shù)據(jù)以外,如果存在客戶端程序(如受擊APP),這些客戶端程序的升級(jí)往往不能和服務(wù)器同步,如果升級(jí)的內(nèi)容包含了通信協(xié)議的修改,這就造成了我們必須為不同的版本部署不同的服務(wù)器端系統(tǒng)的問(wèn)題。為了避免同時(shí)維護(hù)多套服務(wù)器,我們?cè)谲浖_發(fā)的時(shí)候,往往傾向于所謂“版本兼容”的協(xié)議定義方式。而怎樣設(shè)計(jì)的協(xié)議才能有很好的兼容性,又是服務(wù)器端程序需要仔細(xì)考慮的問(wèn)題。
6.4 數(shù)據(jù)統(tǒng)計(jì)和決策
一般來(lái)說(shuō),分布式系統(tǒng)的日志數(shù)據(jù),都是被集中到一起,然后統(tǒng)一進(jìn)行統(tǒng)計(jì)的。然而,當(dāng)集群的規(guī)模到一定程度的時(shí)候,這些日志的數(shù)據(jù)量會(huì)變得非常恐怖。很多時(shí)候,統(tǒng)計(jì)一天的日志量,要消耗計(jì)算機(jī)運(yùn)行一天以上的時(shí)間。所以,日志統(tǒng)計(jì)這項(xiàng)工作,也變成一門非常專業(yè)的活動(dòng)。
經(jīng)典的分布式統(tǒng)計(jì)模型,有Google的Map Reduce模型。這種模型既有靈活性,也能利用大量服務(wù)器進(jìn)行統(tǒng)計(jì)工作。但是缺點(diǎn)是易用性往往不夠好,因?yàn)檫@些數(shù)據(jù)的統(tǒng)計(jì)和我們常見的SQL數(shù)據(jù)表統(tǒng)計(jì)有非常大的差異,所以我們最后還是常常把數(shù)據(jù)丟到MySQL里面去做更細(xì)層面的統(tǒng)計(jì)。
由于分布式系統(tǒng)日志數(shù)量的龐大,以及日志復(fù)雜程度的提高。我們變得必須要掌握類似Map Reduce技術(shù),才能真正的對(duì)分布式系統(tǒng)進(jìn)行數(shù)據(jù)統(tǒng)計(jì)。而且我們還需要想辦法提高統(tǒng)計(jì)工作的工作效率。
7、解決分布式系統(tǒng)可管理性的基本手段
7.1 目錄服務(wù)(ZooKeeper)
分布式系統(tǒng)是一個(gè)由很多進(jìn)程組成的整體,這個(gè)整體中每個(gè)成員部分,都會(huì)具備一些狀態(tài),比如自己的負(fù)責(zé)模塊,自己的負(fù)載情況,對(duì)某些數(shù)據(jù)的掌握等等。而這些和其他進(jìn)程相關(guān)的數(shù)據(jù),在故障恢復(fù)、擴(kuò)容縮容的時(shí)候變得非常重要。
簡(jiǎn)單的分布式系統(tǒng),可以通過(guò)靜態(tài)的配置文件,來(lái)記錄這些數(shù)據(jù):進(jìn)程之間的連接對(duì)應(yīng)關(guān)系,他們的IP地址和端口,等等。然而一個(gè)自動(dòng)化程度高的分布式系統(tǒng),必然要求這些狀態(tài)數(shù)據(jù)都是動(dòng)態(tài)保存的。這樣才能讓程序自己去做容災(zāi)和負(fù)載均衡的工作。
一些程序員會(huì)專門自己編寫一個(gè)DIR服務(wù)(目錄服務(wù)),來(lái)記錄集群中進(jìn)程的運(yùn)行狀態(tài)。集群中進(jìn)程會(huì)和這個(gè)DIR服務(wù)產(chǎn)生自動(dòng)關(guān)聯(lián),這樣在容災(zāi)、擴(kuò)容、負(fù)載均衡的時(shí)候,就可以自動(dòng)根據(jù)這些DIR服務(wù)里的數(shù)據(jù),來(lái)調(diào)整請(qǐng)求的發(fā)送目地,從而達(dá)到繞開故障機(jī)器、或連接到新的服務(wù)器的操作。
然而,如果我們只是用一個(gè)進(jìn)程來(lái)充當(dāng)這個(gè)工作。那么這個(gè)進(jìn)程就成為了這個(gè)集群的“單點(diǎn)”——意思就是,如果這個(gè)進(jìn)程故障了,那么整個(gè)集群可能都無(wú)法運(yùn)行的。所以存放集群狀態(tài)的目錄服務(wù),也需要是分布式的。幸好我們有ZooKeeper這個(gè)優(yōu)秀的開源軟件,它正是一個(gè)分布式的目錄服務(wù)區(qū)。
ZooKeeper可以簡(jiǎn)單啟動(dòng)奇數(shù)個(gè)進(jìn)程,來(lái)形成一個(gè)小的目錄服務(wù)集群。這個(gè)集群會(huì)提供給所有其他進(jìn)程,進(jìn)行讀寫其巨大的“配置樹”的能力。這些數(shù)據(jù)不僅僅會(huì)存放在一個(gè)ZooKeeper進(jìn)程中,而是會(huì)根據(jù)一套非常安全的算法,讓多個(gè)進(jìn)程來(lái)承載。這讓ZooKeeper成為一個(gè)優(yōu)秀的分布式數(shù)據(jù)保存系統(tǒng)。
由于ZooKeeper的數(shù)據(jù)存儲(chǔ)結(jié)構(gòu),是一個(gè)類似文件目錄的樹狀系統(tǒng),所以我們常常會(huì)利用它的功能,把每個(gè)進(jìn)程都綁定到其中一個(gè)“分枝”上,然后通過(guò)檢查這些“分支”,來(lái)進(jìn)行服務(wù)器請(qǐng)求的轉(zhuǎn)發(fā),就能簡(jiǎn)單的解決請(qǐng)求路由(由誰(shuí)去做)的問(wèn)題。另外還可以在這些“分支”上標(biāo)記進(jìn)程的負(fù)載的狀態(tài),這樣負(fù)載均衡也很容易做了。
目錄服務(wù)是分布式系統(tǒng)中最關(guān)鍵的組件之一。而ZooKeeper是一個(gè)很好的開源軟件,正好是用來(lái)完成這個(gè)任務(wù)。
7.2 消息隊(duì)列服務(wù)(ActiveMQ、ZeroMQ、Jgroups、Kafka、RabbitMQ)
兩個(gè)進(jìn)程間如果要跨機(jī)器通訊,我們幾乎都會(huì)用TCP/UDP這些協(xié)議。但是直接使用網(wǎng)絡(luò)API去編寫跨進(jìn)程通訊,是一件非常麻煩的事情。除了要編寫大量的底層socket代碼外,我們還要處理諸如:如何找到要交互數(shù)據(jù)的進(jìn)程,如何保障數(shù)據(jù)包的完整性不至于丟失,如果通訊的對(duì)方進(jìn)程掛掉了,或者進(jìn)程需要重啟應(yīng)該怎樣等等這一系列問(wèn)題。這些問(wèn)題包含了容災(zāi)擴(kuò)容、負(fù)載均衡等一系列的需求。
為了解決分布式系統(tǒng)進(jìn)程間通訊的問(wèn)題,人們總結(jié)出了一個(gè)有效的模型,就是“消息隊(duì)列”模型。消息隊(duì)列模型,就是把進(jìn)程間的交互,抽象成對(duì)一個(gè)個(gè)消息的處理,而對(duì)于這些消息,我們都有一些“隊(duì)列”,也就是管道,來(lái)對(duì)消息進(jìn)行暫存。每個(gè)進(jìn)程都可以訪問(wèn)一個(gè)或者多個(gè)隊(duì)列,從里面讀取消息(消費(fèi))或?qū)懭胂ⅲㄉa(chǎn))。由于有一個(gè)緩存的管道,我們可以放心的對(duì)進(jìn)程狀態(tài)進(jìn)行變化。當(dāng)進(jìn)程起來(lái)的時(shí)候,它會(huì)自動(dòng)去消費(fèi)消息就可以了。而消息本身的路由,也是由存放的隊(duì)列決定的,這樣就把復(fù)雜的路由問(wèn)題,變成了如何管理靜態(tài)的隊(duì)列的問(wèn)題。
一般的消息隊(duì)列服務(wù),都是提供簡(jiǎn)單的“投遞”和“收取”兩個(gè)接口,但是消息隊(duì)列本身的管理方式卻比較復(fù)雜,一般來(lái)說(shuō)有兩種。一部分的消息隊(duì)列服務(wù),提倡點(diǎn)對(duì)點(diǎn)的隊(duì)列管理方式:每對(duì)通信節(jié)點(diǎn)之間,都有一個(gè)單獨(dú)的消息隊(duì)列。這種做法的好處是不同來(lái)源的消息,可以互不影響,不會(huì)因?yàn)槟硞€(gè)隊(duì)列的消息過(guò)多,擠占了其他隊(duì)列的消息緩存空間。而且處理消息的程序也可以自己來(lái)定義處理的優(yōu)先級(jí)——先收取、多處理某個(gè)隊(duì)列,而少處理另外一些隊(duì)列。
但是這種點(diǎn)對(duì)點(diǎn)的消息隊(duì)列,會(huì)隨著集群的增長(zhǎng)而增加大量的隊(duì)列,這對(duì)于內(nèi)存占用和運(yùn)維管理都是一個(gè)復(fù)雜的事情。因此更高級(jí)的消息隊(duì)列服務(wù),開始可以讓不同的隊(duì)列共享內(nèi)存空間,而消息隊(duì)列的地址信息、建立和刪除,都采用自動(dòng)化的手段。——這些自動(dòng)化往往需要依賴上文所述的“目錄服務(wù)”,來(lái)登記隊(duì)列的ID對(duì)應(yīng)的物理IP和端口等信息。比如很多開發(fā)者使用ZooKeeper來(lái)充當(dāng)消息隊(duì)列服務(wù)的中央節(jié)點(diǎn);而類似Jgropus這類軟件,則自己維護(hù)一個(gè)集群狀態(tài)來(lái)存放各節(jié)點(diǎn)今昔。
另外一種消息隊(duì)列,則類似一個(gè)公共的郵箱。一個(gè)消息隊(duì)列服務(wù)就是一個(gè)進(jìn)程,任何使用者都可以投遞或收取這個(gè)進(jìn)程中的消息。這樣對(duì)于消息隊(duì)列的使用更簡(jiǎn)便,運(yùn)維管理也比較方便。不過(guò)這種用法下,任何一個(gè)消息從發(fā)出到處理,最少進(jìn)過(guò)兩次進(jìn)程間通信,其延遲是相對(duì)比較高的。并且由于沒有預(yù)定的投遞、收取約束,所以也比較容易出BUG。
不管使用那種消息隊(duì)列服務(wù),在一個(gè)分布式服務(wù)器端系統(tǒng)中,進(jìn)程間通訊都是必須要解決的問(wèn)題,所以作為服務(wù)器端程序員,在編寫分布式系統(tǒng)代碼的時(shí)候,使用的最多的就是基于消息隊(duì)列驅(qū)動(dòng)的代碼,這也直接導(dǎo)致了EJB3.0把“消息驅(qū)動(dòng)的Bean”加入到規(guī)范之中。
7.3 事務(wù)系統(tǒng)
在分布式的系統(tǒng)中,事務(wù)是最難解決的技術(shù)問(wèn)題之一。由于一個(gè)處理可能分布在不同的處理進(jìn)程上,任何一個(gè)進(jìn)程都可能出現(xiàn)故障,而這個(gè)故障問(wèn)題則需要導(dǎo)致一次回滾。這種回滾大部分又涉及多個(gè)其他的進(jìn)程。這是一個(gè)擴(kuò)散性的多進(jìn)程通訊問(wèn)題。要在分布式系統(tǒng)上解決事務(wù)問(wèn)題,必須具備兩個(gè)核心工具:一個(gè)是穩(wěn)定的狀態(tài)存儲(chǔ)系統(tǒng);另外一個(gè)是方便可靠的廣播系統(tǒng)。
事務(wù)中任何一步的狀態(tài),都必須在整個(gè)集群中可見,并且還要有容災(zāi)的能力。這個(gè)需求,一般還是由集群的“目錄服務(wù)”來(lái)承擔(dān)。如果我們的目錄服務(wù)足夠健壯,那么我們可以把每步事務(wù)的處理狀態(tài),都同步寫到目錄服務(wù)上去。ZooKeeper再次在這個(gè)地方能發(fā)揮重要的作用。
如果事務(wù)發(fā)生了中斷,需要回滾,那么這個(gè)過(guò)程會(huì)涉及到多個(gè)已經(jīng)執(zhí)行過(guò)的步驟。也許這個(gè)回滾只需要在入口處回滾即可(加入那里有保存回滾所需的數(shù)據(jù)),也可能需要在各個(gè)處理節(jié)點(diǎn)上回滾。如果是后者,那么就需要集群中出現(xiàn)異常的節(jié)點(diǎn),向其他所有相關(guān)的節(jié)點(diǎn)廣播一個(gè)“回滾!事務(wù)ID是XXXX”這樣的消息。這個(gè)廣播的底層一般會(huì)由消息隊(duì)列服務(wù)來(lái)承載,而類似Jgroups這樣的軟件,直接提供了廣播服務(wù)。
雖然現(xiàn)在我們?cè)谟懻撌聞?wù)系統(tǒng),但實(shí)際上分布式系統(tǒng)經(jīng)常所需的“分布式鎖”功能,也是這個(gè)系統(tǒng)可以同時(shí)完成的。所謂的“分布式鎖”,也就是一種能讓各個(gè)節(jié)點(diǎn)先檢查后執(zhí)行的限制條件。如果我們有高效而單子操作的目錄服務(wù),那么這個(gè)鎖狀態(tài)實(shí)際上就是一種“單步事務(wù)”的狀態(tài)記錄,而回滾操作則默認(rèn)是“暫停操作,稍后再試”。這種“鎖”的方式,比事務(wù)的處理更簡(jiǎn)單,因此可靠性更高,所以現(xiàn)在越來(lái)越多的開發(fā)人員,愿意使用這種“鎖”服務(wù),而不是去實(shí)現(xiàn)一個(gè)“事務(wù)系統(tǒng)”。
7.4 自動(dòng)部署工具(Docker)
由于分布式系統(tǒng)最大的需求,是在運(yùn)行時(shí)(有可能需要中斷服務(wù))來(lái)進(jìn)行服務(wù)容量的變更:擴(kuò)容或者縮容。而在分布式系統(tǒng)中某些節(jié)點(diǎn)故障的時(shí)候,也需要新的節(jié)點(diǎn)來(lái)恢復(fù)工作。這些如果還是像老式的服務(wù)器管理方式,通過(guò)填表、申報(bào)、進(jìn)機(jī)房、裝服務(wù)器、部署軟件……這一套做法,那效率肯定是不行。
在分布式系統(tǒng)的環(huán)境下,我們一般都是采用“池”的方式來(lái)管理服務(wù)。我們預(yù)先會(huì)申請(qǐng)一批機(jī)器,然后在某些機(jī)器上運(yùn)行服務(wù)軟件,另外一些則作為備份。顯然我們這一批服務(wù)器不可能只為某一個(gè)業(yè)務(wù)服務(wù),而是會(huì)提供多個(gè)不同的業(yè)務(wù)承載。那些備份的服務(wù)器,則會(huì)成為多個(gè)業(yè)務(wù)的通用備份“池”。隨著業(yè)務(wù)需求的變化,一些服務(wù)器可能“退出”A服務(wù)而“加入”B服務(wù)。
這種頻繁的服務(wù)變化,依賴高度自動(dòng)的軟件部署工具。我們的運(yùn)維人員,應(yīng)該掌握這開發(fā)人員提供的部署工具,而不是厚厚的手冊(cè),來(lái)進(jìn)行這類運(yùn)維操作。一些比較有經(jīng)驗(yàn)的開發(fā)團(tuán)隊(duì),會(huì)統(tǒng)一所有的業(yè)務(wù)底層框架,以期大部分的部署、配置工具,都能用一套通用的系統(tǒng)來(lái)進(jìn)行管理。而開源界,也有類似的嘗試,最廣為人知的莫過(guò)于RPM安裝包格式,然而RPM的打包方式還是太復(fù)雜,不太符合服務(wù)器端程序的部署需求。所以后來(lái)又出現(xiàn)了Chef為代表的,可編程的通用部署系統(tǒng)。
在虛擬機(jī)技術(shù)出現(xiàn)之后,PaaS平臺(tái)為自動(dòng)部署提供了強(qiáng)大的支持:如果我們是按某個(gè)PaaS平臺(tái)的規(guī)范來(lái)編寫的應(yīng)用,可以完全把程序丟給平臺(tái)去部署,其承載量計(jì)算、部署規(guī)劃,都自動(dòng)完成了。這方面的佼佼者是Google的AppEngine:我們可以直接用Eclipse開發(fā)一個(gè)本地的Web應(yīng)用,然后上傳到AppEngine里面,所有的部署就完成了!AppEngine會(huì)自動(dòng)的根據(jù)對(duì)這個(gè)Web應(yīng)用的訪問(wèn)量,來(lái)進(jìn)行擴(kuò)容、縮容、故障恢復(fù)。
然而,真正有革命性的工具,是Docker的出現(xiàn)。雖然虛擬機(jī)、沙箱技術(shù)早就不是什么新技術(shù),但是真正使用這些技術(shù)來(lái)作為部署工具的時(shí)間卻不長(zhǎng)。Linux高效的輕量級(jí)容器技術(shù),提供了部署上巨大的便利性——我們可以在各種庫(kù)、各種協(xié)作軟件的環(huán)境下打包我們的應(yīng)用程序,然后隨意的部署在任何一個(gè)Linux系統(tǒng)上。
為了管理大量的分布式服務(wù)器端進(jìn)程,我們確實(shí)需要花很多功夫,其優(yōu)化其部署管理的工作。統(tǒng)一服務(wù)器端進(jìn)程的運(yùn)行規(guī)范,是實(shí)現(xiàn)自動(dòng)化部署管理的基本條件。我們可以根據(jù)“操作系統(tǒng)”作為規(guī)范,采用Docker技術(shù);也可以根據(jù)“Web應(yīng)用”作為規(guī)范,采用某些PaaS平臺(tái)技術(shù);或者自己定義一些更具體的規(guī)范,自己開發(fā)完整的分布式計(jì)算平臺(tái)。
7.5 日志服務(wù)(log4j)
服務(wù)器端的日志,一直是一個(gè)既重要又容易被忽視的問(wèn)題。很多團(tuán)隊(duì)在剛開始的時(shí)候,僅僅把日志視為開發(fā)調(diào)試、排除BUG的輔助工具。但是很快會(huì)發(fā)現(xiàn),在服務(wù)運(yùn)營(yíng)起來(lái)之后,日志幾乎是服務(wù)器端系統(tǒng),在運(yùn)行時(shí)可以用來(lái)了解程序情況的唯一有效手段。
盡管我們有各種profile工具(比如JProfile),但是這些工具大部分都不適合在正式運(yùn)營(yíng)的服務(wù)上開啟,因?yàn)闀?huì)嚴(yán)重降低其運(yùn)行性能。所以我們更多的時(shí)候需要根據(jù)日志來(lái)分析。盡管日志從本質(zhì)上,就是一行行的文本信息,但是由于其具有很大的靈活性,所以會(huì)很受開發(fā)和運(yùn)維人員的重視。
日志本身從概念上,是一個(gè)很模糊的東西。你可以隨便打開一個(gè)文件,然后寫入一些信息。但是現(xiàn)代的服務(wù)器系統(tǒng),一般都會(huì)對(duì)日志做一些標(biāo)準(zhǔn)化的需求規(guī)范:日志必須是一行一行的,這樣比較方便日后的統(tǒng)計(jì)分析;每行日志文本,都應(yīng)該有一些統(tǒng)一的頭部,比如日期時(shí)間就是基本的需求;日志的輸出應(yīng)該是分等級(jí)的,比如fatal/error/warning/info/debug/trace等等,程序可以在運(yùn)行時(shí)調(diào)整輸出的等級(jí),以便可以節(jié)省日志打印的消耗;日志的頭部一般還需要一些類似用戶ID或者IP地址之類的頭信息,用于快速查找定位過(guò)濾某一批日志記錄,或者有一些其他的用于過(guò)濾縮小日志查看范圍的字段,這叫做染色功能;日志文件還需要有“回滾”功能,也就是保持固定大小的多個(gè)文件,避免長(zhǎng)期運(yùn)行后,把硬盤寫滿。
由于有上述的各種需求,所以開源界提供了很多游戲的日志組件庫(kù),比如大名鼎鼎的log4j,以及成員眾多的log4X家族庫(kù),這些都是應(yīng)用廣泛而飽受好評(píng)的工具。
不過(guò)對(duì)比日志的打印功能,日志的搜集和統(tǒng)計(jì)功能卻往往比較容易被忽視。作為分布式系統(tǒng)的程序員,肯定是希望能從一個(gè)集中節(jié)點(diǎn),能搜集統(tǒng)計(jì)到整個(gè)集群日志情況。而有一些日志的統(tǒng)計(jì)結(jié)果,甚至希望能在很短時(shí)間內(nèi)反復(fù)獲取,用來(lái)監(jiān)控整個(gè)集群的健康情況。要做到這一點(diǎn),就必須有一個(gè)分布式的文件系統(tǒng),用來(lái)存放源源不斷到達(dá)的日志(這些日志往往通過(guò)UDP協(xié)議發(fā)送過(guò)來(lái))。而在這個(gè)文件系統(tǒng)上,則需要有一個(gè)類似Map Reduce架構(gòu)的統(tǒng)計(jì)系統(tǒng),這樣才能對(duì)海量的日志信息,進(jìn)行快速的統(tǒng)計(jì)以及報(bào)警。有一些開發(fā)者會(huì)直接使用Hadoop系統(tǒng),有一些則用Kafka來(lái)作為日志存儲(chǔ)系統(tǒng),上面再搭建自己的統(tǒng)計(jì)程序。
關(guān)于Kafka:
Kafka是最初由Linkedin公司開發(fā),是一個(gè)分布式、支持分區(qū)的(partition)、多副本的(replica),基于zookeeper協(xié)調(diào)的分布式消息系統(tǒng),它的最大的特性就是可以實(shí)時(shí)的處理大量數(shù)據(jù)以滿足各種需求場(chǎng)景:比如基于hadoop的批處理系統(tǒng)、低延遲的實(shí)時(shí)系統(tǒng)、storm/Spark流式處理引擎,web/nginx日志、訪問(wèn)日志,消息服務(wù)等等,用scala語(yǔ)言編寫,Linkedin于2010年貢獻(xiàn)給了Apache基金會(huì)并成為頂級(jí)開源 項(xiàng)目。
日志服務(wù)是分布式運(yùn)維的儀表盤、潛望鏡。如果沒有一個(gè)可靠的日志服務(wù),整個(gè)系統(tǒng)的運(yùn)行狀況可能會(huì)是失控的。所以無(wú)論你的分布式系統(tǒng)節(jié)點(diǎn)是多還是少,必須花費(fèi)重要的精力和專門的開發(fā)時(shí)間,去建立一個(gè)對(duì)日志進(jìn)行自動(dòng)化統(tǒng)計(jì)分析的系統(tǒng)。
8、分布式系統(tǒng)在開發(fā)效率上造成的問(wèn)題和解決思路
根據(jù)上文所述,分布式系統(tǒng)在業(yè)務(wù)需求的功能以為,還需要增加額外很多非功能的需求。這些非功能需求,往往都是為了一個(gè)多進(jìn)程系統(tǒng)能穩(wěn)定可靠運(yùn)行而去設(shè)計(jì)和實(shí)現(xiàn)的。這些“額外”的工作,一般都會(huì)讓你的代碼更加復(fù)雜,如果沒有很好的工具,就會(huì)讓你的開發(fā)效率嚴(yán)重下降。
8.1 微服務(wù)框架:EJB、WebService
當(dāng)我們?cè)谟懻摲?wù)器端軟件分布的時(shí)候,服務(wù)進(jìn)程之間的通信就難免了。然而服務(wù)進(jìn)程間的通訊,并不是簡(jiǎn)單的收發(fā)消息就能完成的。這里還涉及了消息的路由、編碼解碼、服務(wù)狀態(tài)的讀寫等等。如果整個(gè)流程都由自己開發(fā),那就太累人了。
所以業(yè)界很早就推出了各種分布式的服務(wù)器端開發(fā)框架,最著名的就是“EJB”——企業(yè)JavaBean。但凡冠以“企業(yè)”的技術(shù),往往都是分布式下所需的部分,而EJB這種技術(shù),也是一種分布式對(duì)象調(diào)用的技術(shù)。我們?nèi)绻枰尪鄠€(gè)進(jìn)程合作完成任務(wù),則需要把任務(wù)分解到多個(gè)“類”上,然后這些“類”的對(duì)象就會(huì)在各個(gè)進(jìn)程容器中存活,從而協(xié)作提供服務(wù)。這個(gè)過(guò)程很“面向?qū)ο?#8221;。每個(gè)對(duì)象都是一個(gè)“微服務(wù)”,可以提供某些分布式的功能。
而另外一些系統(tǒng),則走向?qū)W習(xí)互聯(lián)網(wǎng)的基本模型:HTTP。所以就有了各種的WebService框架,從開源的到商業(yè)軟件,都有各自的WebService實(shí)現(xiàn)。這種模型,把復(fù)雜的路由、編解碼等操作,簡(jiǎn)化成常見的一次HTTP操作,是一種非常有效的抽象。開發(fā)人員開發(fā)和部署多個(gè)WebService到Web服務(wù)器上,就完成了分布式系統(tǒng)的搭建。
不管我們是學(xué)習(xí)EJB還是WebService,實(shí)際上我們都需要簡(jiǎn)化分布式調(diào)用的復(fù)雜程度。而分布式調(diào)用的復(fù)雜之處,就是因?yàn)樾枰讶轂?zāi)、擴(kuò)容、負(fù)載均衡等功能,融合到跨進(jìn)程調(diào)用里。所以使用一套通用的代碼,來(lái)為所有的跨進(jìn)程通訊(調(diào)用),統(tǒng)一的實(shí)現(xiàn)容災(zāi)、擴(kuò)容、負(fù)載均衡、過(guò)載保護(hù)、狀態(tài)緩存命中等等非功能性需求,能大大簡(jiǎn)化整個(gè)分布式系統(tǒng)的復(fù)雜性。
一般我們的微服務(wù)框架,都會(huì)在路由階段,對(duì)整個(gè)集群所有節(jié)點(diǎn)的狀態(tài)進(jìn)行觀察,如哪些地址上運(yùn)行了哪些服務(wù)的進(jìn)程,這些服務(wù)進(jìn)程的負(fù)載狀況如何,是否可用,然后對(duì)于有狀態(tài)的服務(wù),還會(huì)使用類似一致性哈希的算法,去盡量試圖提高緩存的命中率。當(dāng)集群中的節(jié)點(diǎn)狀態(tài)發(fā)生變化的時(shí)候,微服務(wù)框架下的所有節(jié)點(diǎn),都能盡快的獲得這個(gè)變化的情況,從新根據(jù)當(dāng)前狀態(tài),重新規(guī)劃以后的服務(wù)路由方向,從而實(shí)現(xiàn)自動(dòng)化的路由選擇,避開那些負(fù)載過(guò)高或者失效的節(jié)點(diǎn)。
有一些微服務(wù)框架,還提供了類似IDL轉(zhuǎn)換成“骨架”、“樁”代碼的工具,這樣在編寫遠(yuǎn)程調(diào)用程序的時(shí)候,完全無(wú)需編寫那些復(fù)雜的網(wǎng)絡(luò)相關(guān)的代碼,所有的傳輸層、編碼層代碼都自動(dòng)的編寫好了。這方面EJB、Facebook的Thrift,Google gRPC都具備這種能力。在具備代碼生成能力的框架下,我們編寫一個(gè)分布式下可用的功能模塊(可能是一個(gè)函數(shù)或者是一個(gè)類),就好像編寫一個(gè)本地的函數(shù)那樣簡(jiǎn)單。這絕對(duì)是分布式系統(tǒng)下非常重要的效率提升。
8.2 異步編程工具:協(xié)程、Futrue、Lamda
在分布式系統(tǒng)中編程,你不可避免的會(huì)碰到大量的“回調(diào)”型API。因?yàn)榉植际较到y(tǒng)涉及非常多的網(wǎng)絡(luò)通信。任何一個(gè)業(yè)務(wù)命令,都可能被分解到多個(gè)進(jìn)程,通過(guò)多次網(wǎng)絡(luò)通信來(lái)組合完成。由于異步非阻塞的編程模型大行其道,所以我們的代碼也往往動(dòng)不動(dòng)就要碰到“回調(diào)函數(shù)”。然而,回調(diào)這種異步編程模型,是一種非常不利于代碼閱讀的編程方法。因?yàn)槟銦o(wú)法從頭到尾的閱讀代碼,去了解一個(gè)業(yè)務(wù)任務(wù),是怎樣被逐步的完成的。屬于一個(gè)業(yè)務(wù)任務(wù)的代碼,由于多次的非阻塞回調(diào),從而被分割成很多個(gè)回調(diào)函數(shù),在代碼的各處被串接起來(lái)。
更有甚者,我們有時(shí)候會(huì)選擇使用“觀察者模式”,我們會(huì)在一個(gè)地方注冊(cè)大量的“事件-響應(yīng)函數(shù)”,然后在所有需要回調(diào)的地方,都發(fā)出一個(gè)事件。——這樣的代碼,比單純的注冊(cè)回調(diào)函數(shù)更難理解。因?yàn)槭录?duì)應(yīng)的響應(yīng)函數(shù),通常在發(fā)出事件處是無(wú)法找到的。這些函數(shù)永遠(yuǎn)都會(huì)放在另外的一些文件里,而且有時(shí)候這些函數(shù)還會(huì)在運(yùn)行時(shí)改變。而事件名字本身,也往往是匪夷所思難以理解的,因?yàn)楫?dāng)你的程序需要成千上百的事件的時(shí)候,起一個(gè)容易理解名符其實(shí)的名字,幾乎是不可能的。
為了解決回調(diào)函數(shù)這種對(duì)于代碼可讀性的破壞作用,人們發(fā)明了很多不同的改進(jìn)方法。其中最著名的是“協(xié)程”。我們以前常常習(xí)慣于用多線程來(lái)解決問(wèn)題,所以非常熟悉以同步的方式去寫代碼。協(xié)程正是延續(xù)了我們的這一習(xí)慣,但不同于多線程的是,協(xié)程并不會(huì)“同時(shí)”運(yùn)行,它只是在需要阻塞的地方,用Yield()切換出去執(zhí)行其他協(xié)程,然后當(dāng)阻塞結(jié)束后,用Resume()回到剛剛切換的位置繼續(xù)往下執(zhí)行。這相當(dāng)于我們可以把回調(diào)函數(shù)的內(nèi)容,接到Y(jié)ield()調(diào)用的后面。這種編寫代碼的方法,非常類似于同步的寫法,讓代碼變得非常易讀。但是唯一的缺點(diǎn)是,Resume()的代碼還是需要在所謂“主線程”中運(yùn)行。用戶必須自己從阻塞恢復(fù)的時(shí)候,去調(diào)用Resume()。協(xié)程另外一個(gè)缺點(diǎn),是需要做棧保存,在切換到其他協(xié)程之后,棧上的臨時(shí)變量,也都需要額外占用空間,這限制了協(xié)程代碼的寫法,讓開發(fā)者不能用太大的臨時(shí)變量。
而另外一種改善回調(diào)函數(shù)的寫法,往往叫做Future/Promise模型。這種寫法的基本思路,就是“一次性把所有回調(diào)寫到一起”。這是一個(gè)非常實(shí)用的編程模型,它沒有讓你去徹底干掉回調(diào),而是讓你可以把回調(diào)從分散各處,集中到一個(gè)地方。在同一段代碼中,你可以清晰的看到各個(gè)異步的步驟是如何串接、或者并行執(zhí)行的。
最后說(shuō)一下lamda模型,這種寫法流行于js語(yǔ)言的廣泛應(yīng)用。由于在其他語(yǔ)言中,定一個(gè)回調(diào)函數(shù)是非常費(fèi)事的:Java語(yǔ)言要設(shè)計(jì)一個(gè)接口然后做一個(gè)實(shí)現(xiàn),簡(jiǎn)直是五星級(jí)的費(fèi)事程度;C/C++支持函數(shù)指針,算是比較簡(jiǎn)單,但是也很容易導(dǎo)致代碼看不懂;腳本語(yǔ)言相對(duì)好一些,也要定義個(gè)函數(shù)。而直接在調(diào)用回調(diào)的地方,寫回調(diào)函數(shù)的內(nèi)容,是最方便開發(fā),也比較利于閱讀的。更重要的,lamda一般意味著閉包,也就是說(shuō),這種回調(diào)函數(shù)的調(diào)用棧,是被分別保存的,很多需要在異步操作中,需要建立一個(gè)類似“會(huì)話池”的狀態(tài)保存變量,在這里都是不需要的,而是可以自然生效的。這一點(diǎn)和協(xié)程有異曲同工之妙。
不管使用哪一種異步編程方式,其編碼的復(fù)雜度,都是一定比同步調(diào)用的代碼高的。所以我們?cè)诰帉懛植际椒?wù)器代碼的時(shí)候,一定要仔細(xì)規(guī)劃代碼結(jié)構(gòu),避免出現(xiàn)隨意添加功能代碼,導(dǎo)致代碼的可讀性被破壞的情況。不可讀的代碼,就是不可維護(hù)的代碼,而大量異步回調(diào)的服務(wù)器端代碼,是更容易出現(xiàn)這種情況的。
8.3 云服務(wù)模型:IaaS/PaaS/SaaS
在復(fù)雜的分布式系統(tǒng)開發(fā)和使用過(guò)程中,如何對(duì)大量服務(wù)器和進(jìn)程的運(yùn)維,一直是一個(gè)貫穿其中的問(wèn)題。不管是使用微服務(wù)框架、還是統(tǒng)一的部署工具、日志監(jiān)控服務(wù),都是因?yàn)榇罅康姆?wù)器,要集中的管理,是非常不容易的。這里背后的原因,主要是大量的硬件和網(wǎng)絡(luò),把邏輯上的計(jì)算能力,切割成很多小塊。
隨著計(jì)算機(jī)運(yùn)算能力的提升,出現(xiàn)的虛擬化技術(shù),卻能把被分割的計(jì)算單元,更智能的統(tǒng)一起來(lái)。其中最常見的就是IaaS技術(shù):當(dāng)我們可以用一個(gè)服務(wù)器硬件,運(yùn)行多個(gè)虛擬的服務(wù)器操作系統(tǒng)的時(shí)候,我們需要維護(hù)的硬件數(shù)量就會(huì)成倍的下降。
而PaaS技術(shù)的流行,讓我們可以為某一種特定的編程模型,統(tǒng)一的進(jìn)行系統(tǒng)運(yùn)行環(huán)境的部署維護(hù)。而不需要再一臺(tái)臺(tái)服務(wù)器的去裝操作系統(tǒng)、配置運(yùn)行容器、上傳運(yùn)行代碼和數(shù)據(jù)。在沒有統(tǒng)一的PaaS之前,安裝大量的MySQL數(shù)據(jù)庫(kù),曾經(jīng)是消耗大量時(shí)間和精力的工作。
當(dāng)我們的業(yè)務(wù)模型,成熟到可以抽象為一些固定的軟件時(shí),我們的分布式系統(tǒng)就會(huì)變得更加易用。我們的計(jì)算能力不再是代碼和庫(kù),而是一個(gè)個(gè)通過(guò)網(wǎng)絡(luò)提供服務(wù)的云——SaaS,這樣使用者根本來(lái)維護(hù)、部署的工作都不需要,只要申請(qǐng)一個(gè)接口,填上預(yù)期的容量額度,就能直接使用了。這不僅節(jié)省了大量開發(fā)對(duì)應(yīng)功能的事件,還等于把大量的運(yùn)維工作,都交出去給SaaS的維護(hù)者——而他們做這樣的維護(hù)會(huì)更加專業(yè)。
在運(yùn)維模型的進(jìn)化上,從IaaS到PaaS到SaaS,其應(yīng)用范圍也許是越來(lái)越窄,但使用的便利性卻成倍的提高。這也證明了,軟件勞動(dòng)的工作,也是可以通過(guò)分工,向更專業(yè)化、更細(xì)分的方向去提高效率。
(本文同步發(fā)布于:http://www.52im.net/thread-1811-1-1.html)
附錄2:騰訊技術(shù)團(tuán)隊(duì)文章匯總