文章作者:韓偉 — 騰訊游戲高級(jí)架構(gòu)師遥缕, 版權(quán)歸原作者所有窟赏,未經(jīng)作者同意母赵,請(qǐng)勿轉(zhuǎn)載
文章來源:騰訊云技術(shù)社區(qū)——騰云閣:https://www.qcloud.com/community
原文鏈接:https://www.qcloud.com/community/article/165
任何的服務(wù)器的性能都是有極限的蛙紫,面對(duì)海量的互聯(lián)網(wǎng)訪問需求,是不可能單靠一臺(tái)服務(wù)器或者一個(gè)CPU來承擔(dān)的。所以我們一般都會(huì)在運(yùn)行時(shí)架構(gòu)設(shè)計(jì)之初夺荒,就考慮如何能利用多個(gè)CPU察藐、多臺(tái)服務(wù)器來分擔(dān)負(fù)載,這就是所謂分布的策略技潘。分布式的服務(wù)器概念很簡單遥巴,但是實(shí)現(xiàn)起來卻比較復(fù)雜。因?yàn)槲覀儗懙某绦蛳碛模际且砸粋€(gè)CPU铲掐,一塊內(nèi)存為基礎(chǔ)來設(shè)計(jì)的,所以要讓多個(gè)程序同時(shí)運(yùn)行值桩,并且協(xié)調(diào)運(yùn)作摆霉,這需要更多的底層工作。
首先出現(xiàn)能支持分布式概念的技術(shù)是多進(jìn)程奔坟。在DOS時(shí)代携栋,計(jì)算機(jī)在一個(gè)時(shí)間內(nèi)只能運(yùn)行一個(gè)程序,如果你想一邊寫程序咳秉,同時(shí)一邊聽mp3婉支,都是不可能的。但是滴某,在WIN95操作系統(tǒng)下磅摹,你就可以同時(shí)開多個(gè)窗口,背后就是同時(shí)在運(yùn)行多個(gè)程序霎奢。在Unix和后來的Linux操作系統(tǒng)里面户誓,都普遍支持了多進(jìn)程的技術(shù)。所謂的多進(jìn)程幕侠,就是操作系統(tǒng)可以同時(shí)運(yùn)行我們編寫的多個(gè)程序帝美,每個(gè)程序運(yùn)行的時(shí)候,都好像自己獨(dú)占著CPU和內(nèi)存一樣晤硕。在計(jì)算機(jī)只有一個(gè)CPU的時(shí)候悼潭,實(shí)際上計(jì)算機(jī)會(huì)分時(shí)復(fù)用的運(yùn)行多個(gè)進(jìn)程庇忌,CPU在多個(gè)進(jìn)程之間切換。但是如果這個(gè)計(jì)算機(jī)有多個(gè)CPU或者多個(gè)CPU核舰褪,則會(huì)真正的有幾個(gè)進(jìn)程同時(shí)運(yùn)行皆疹。所以進(jìn)程就好像一個(gè)操作系統(tǒng)提供的運(yùn)行時(shí)“程序盒子”,可以用來在運(yùn)行時(shí)占拍,容納任何我們想運(yùn)行的程序略就。當(dāng)我們掌握了操作系統(tǒng)的多進(jìn)程技術(shù)后,我們就可以把服務(wù)器上的運(yùn)行任務(wù)晃酒,分為多個(gè)部分表牢,然后分別寫到不同的程序里,利用上多CPU或者多核贝次,甚至是多個(gè)服務(wù)器的CPU一起來承擔(dān)負(fù)載崔兴。
![](http://mc.qcloudimg.com/static/img/d23a1e3131b09a27d6d8793241fa6c07/image.gif)
多進(jìn)程利用多CPU
這種劃分多個(gè)進(jìn)程的架構(gòu),一般會(huì)有兩種策略:一種是按功能來劃分蛔翅,比如負(fù)責(zé)網(wǎng)絡(luò)處理的一個(gè)進(jìn)程敲茄,負(fù)責(zé)數(shù)據(jù)庫處理的一個(gè)進(jìn)程,負(fù)責(zé)計(jì)算某個(gè)業(yè)務(wù)邏輯的一個(gè)進(jìn)程搁宾。另外一種策略是每個(gè)進(jìn)程都是同樣的功能折汞,只是分擔(dān)不同的運(yùn)算任務(wù)而已。使用第一種策略的系統(tǒng)盖腿,運(yùn)行的時(shí)候,直接根據(jù)操作系統(tǒng)提供的診斷工具损同,就能直觀的監(jiān)測(cè)到每個(gè)功能模塊的性能消耗翩腐,因?yàn)椴僮飨到y(tǒng)提供進(jìn)程盒子的同時(shí),也能提供對(duì)進(jìn)程的全方位的監(jiān)測(cè)膏燃,比如CPU占用茂卦、內(nèi)存消耗、磁盤和網(wǎng)絡(luò)I/O等等组哩。但是這種策略的運(yùn)維部署會(huì)稍微復(fù)雜一點(diǎn)等龙,因?yàn)槿魏我粋€(gè)進(jìn)程沒有啟動(dòng),或者和其他進(jìn)程的通信地址沒配置好伶贰,都可能導(dǎo)致整個(gè)系統(tǒng)無法運(yùn)作蛛砰;而第二種分布策略,由于每個(gè)進(jìn)程都是一樣的黍衙,這樣的安裝部署就非常簡單泥畅,性能不夠就多找?guī)讉€(gè)機(jī)器,多啟動(dòng)幾個(gè)進(jìn)程就完成了琅翻,這就是所謂的平行擴(kuò)展位仁。
現(xiàn)在比較復(fù)雜的分布式系統(tǒng)柑贞,會(huì)結(jié)合這兩種策略,也就是說系統(tǒng)既按一些功能劃分出不同的具體功能進(jìn)程聂抢,而這些進(jìn)程又是可以平行擴(kuò)展的钧嘶。當(dāng)然這樣的系統(tǒng)在開發(fā)和運(yùn)維上的復(fù)雜度,都是比單獨(dú)使用“按功能劃分”和“平行劃分”要更高的琳疏。由于要管理大量的進(jìn)程康辑,傳統(tǒng)的依靠配置文件來配置整個(gè)集群的做法,會(huì)顯得越來越不實(shí)用:這些運(yùn)行中的進(jìn)程轿亮,可能和其他很多進(jìn)程產(chǎn)生通信關(guān)系疮薇,當(dāng)其中一個(gè)進(jìn)程變更通信地址時(shí),勢(shì)必影響所有其他進(jìn)程的配置我注。所以我們需要集中的管理所有進(jìn)程的通信地址按咒,當(dāng)有變化的時(shí)候,只需要修改一個(gè)地方但骨。在大量進(jìn)程構(gòu)建的集群中励七,我們還會(huì)碰到容災(zāi)和擴(kuò)容的問題:當(dāng)集群中某個(gè)服務(wù)器出現(xiàn)故障,可能會(huì)有一些進(jìn)程消失奔缠;而當(dāng)我們需要增加集群的承載能力時(shí)掠抬,我們又需要增加新的服務(wù)器以及進(jìn)程。這些工作在長期運(yùn)行的服務(wù)器系統(tǒng)中校哎,會(huì)是比較常見的任務(wù)两波,如果整個(gè)分布系統(tǒng)有一個(gè)運(yùn)行中的中心進(jìn)程,能自動(dòng)化的監(jiān)測(cè)所有的進(jìn)程狀態(tài)闷哆,一旦有進(jìn)程加入或者退出集群腰奋,都能即時(shí)的修改所有其他進(jìn)程的配置,這就形成了一套動(dòng)態(tài)的多進(jìn)程管理系統(tǒng)抱怔。開源的ZooKeeper給我們提供了一個(gè)可以充當(dāng)這種動(dòng)態(tài)集群中心的實(shí)現(xiàn)方案劣坊。由于ZooKeeper本身是可以平行擴(kuò)展的,所以它自己也是具備一定容災(zāi)能力的∏簦現(xiàn)在越來越多的分布式系統(tǒng)都開始使用以ZooKeeper為集群中心的動(dòng)態(tài)進(jìn)程管理策略了局冰。
![](http://mc.qcloudimg.com/static/img/dce34266afeb8cf2c95b69a2c881838e/image.gif)
動(dòng)態(tài)進(jìn)程集群
在調(diào)用多進(jìn)程服務(wù)的策略上,我們也會(huì)有一定的策略選擇灌危,其中最著名的策略有三個(gè):一個(gè)是動(dòng)態(tài)負(fù)載均衡策略康二;一個(gè)是讀寫分離策略;一個(gè)是一致性哈希策略乍狐。動(dòng)態(tài)負(fù)載均衡策略赠摇,一般會(huì)搜集多個(gè)進(jìn)程的服務(wù)狀態(tài),然后挑選一個(gè)負(fù)載最輕的進(jìn)程來分發(fā)服務(wù),這種策略對(duì)于比較同質(zhì)化的進(jìn)程是比較合適的藕帜。讀寫分離策略則是關(guān)注對(duì)持久化數(shù)據(jù)的性能烫罩,比如對(duì)數(shù)據(jù)庫的操作,我們會(huì)提供一批進(jìn)程專門用于提供讀數(shù)據(jù)的服務(wù)洽故,而另外一個(gè)(或多個(gè))進(jìn)程用于寫數(shù)據(jù)的服務(wù)贝攒,這些寫數(shù)據(jù)的進(jìn)程都會(huì)每次寫多份拷貝到“讀服務(wù)進(jìn)程”的數(shù)據(jù)區(qū)(可能就是單獨(dú)的數(shù)據(jù)庫),這樣在對(duì)外提供服務(wù)的時(shí)候时甚,就可以提供更多的硬件資源隘弊。一致性哈希策略是針對(duì)任何一個(gè)任務(wù),看看這個(gè)任務(wù)所涉及讀寫的數(shù)據(jù)荒适,是屬于哪一片的梨熙,是否有某種可以緩存的特征,然后按這個(gè)數(shù)據(jù)的ID或者特征值刀诬,進(jìn)行“一致性哈涎噬龋”的計(jì)算,分擔(dān)給對(duì)應(yīng)的處理進(jìn)程陕壹。這種進(jìn)程調(diào)用策略质欲,能非常的利用上進(jìn)程內(nèi)的緩存(如果存在),比如我們的一個(gè)在線游戲糠馆,由100個(gè)進(jìn)程承擔(dān)服務(wù)嘶伟,那么我們就可以把游戲玩家的ID,作為一致性哈希的數(shù)據(jù)ID又碌,作為進(jìn)程調(diào)用的KEY九昧,如果目標(biāo)服務(wù)進(jìn)程有緩存游戲玩家的數(shù)據(jù),那么所有這個(gè)玩家的操作請(qǐng)求赠橙,都會(huì)被轉(zhuǎn)到這個(gè)目標(biāo)服務(wù)進(jìn)程上耽装,緩存的命中率大大提高。而使用“一致性哈掀诰荆”,而不是其他哈希算法规个,或者取模算法凤薛,主要是考慮到,如果服務(wù)進(jìn)程有一部分因故障消失诞仓,剩下的服務(wù)進(jìn)程的緩存依然可以有效缤苫,而不會(huì)整個(gè)集群所有進(jìn)程的緩存都失效。具體有興趣的讀者可以搜索“一致性哈鲜茫”一探究竟活玲。
以多進(jìn)程利用大量的服務(wù)器,以及服務(wù)器上的多個(gè)CPU核心,是一個(gè)非常有效的手段舒憾。但是使用多進(jìn)程帶來的額外的編程復(fù)雜度的問題镀钓。一般來說我們認(rèn)為最好是每個(gè)CPU核心一個(gè)進(jìn)程,這樣能最好的利用硬件镀迂。如果同時(shí)運(yùn)行的進(jìn)程過多丁溅,操作系統(tǒng)會(huì)消耗很多CPU時(shí)間在不同進(jìn)程的切換過程上。但是探遵,我們?cè)缙谒@得的很多API都是阻塞的窟赏,比如文件I/O,網(wǎng)絡(luò)讀寫箱季,數(shù)據(jù)庫操作等涯穷。如果我們只用有限的進(jìn)程來執(zhí)行帶這些阻塞操作的程序,那么CPU會(huì)大量被浪費(fèi)藏雏,因?yàn)樽枞腁PI會(huì)讓有限的這些進(jìn)程停著等待結(jié)果拷况。那么,如果我們希望能處理更多的任務(wù)诉稍,就必須要啟動(dòng)更多的進(jìn)程蝠嘉,以便充分利用那些阻塞的時(shí)間,但是由于進(jìn)程是操作系統(tǒng)提供的“盒子”杯巨,這個(gè)盒子比較大蚤告,切換耗費(fèi)的時(shí)間也比較多,所以大量并行的進(jìn)程反而會(huì)無謂的消耗服務(wù)器資源服爷。加上進(jìn)程之間的內(nèi)存一般是隔離的杜恰,進(jìn)程間如果要交換一些數(shù)據(jù),往往需要使用一些操作系統(tǒng)提供的工具仍源,比如網(wǎng)絡(luò)socket心褐,這些都會(huì)額外消耗服務(wù)器性能。因此笼踩,我們需要一種切換代價(jià)更少逗爹,通信方式更便捷,編程方法更簡單的并行技術(shù)嚎于,這個(gè)時(shí)候掘而,多線程技術(shù)出現(xiàn)了。
![](http://mc.qcloudimg.com/static/img/d40baf7577404e5335f24131490bf734/image.gif)
在進(jìn)程盒子里面的線程盒子
多線程的特點(diǎn)是切換代價(jià)少于购,可以同時(shí)訪問內(nèi)存袍睡。我們可以在編程的時(shí)候,任意讓某個(gè)函數(shù)放入新的線程去執(zhí)行肋僧,這個(gè)函數(shù)的參數(shù)可以是任何的變量或指針斑胜。如果我們希望和這些運(yùn)行時(shí)的線程通信控淡,只要讀、寫這些指針指向的變量即可止潘。在需要大量阻塞操作的時(shí)候掺炭,我們可以啟動(dòng)大量的線程,這樣就能較好的利用CPU的空閑時(shí)間覆山;線程的切換代價(jià)比進(jìn)程低得多竹伸,所以我們能利用的CPU也會(huì)多很多。線程是一個(gè)比進(jìn)程更小的“程序盒子”簇宽,他可以放入某一個(gè)函數(shù)調(diào)用勋篓,而不是一個(gè)完整的程序。一般來說魏割,如果多個(gè)線程只是在一個(gè)進(jìn)程里面運(yùn)行譬嚣,那其實(shí)是沒有利用到多核CPU的并行好處的,僅僅是利用了單個(gè)空閑的CPU核心钞它。但是拜银,在JAVA和C#這類帶虛擬機(jī)的語言中,多線程的實(shí)現(xiàn)底層遭垛,會(huì)根據(jù)具體的操作系統(tǒng)的任務(wù)調(diào)度單位(比如進(jìn)程)尼桶,盡量讓線程也成為操作系統(tǒng)可以調(diào)度的單位,從而利用上多個(gè)CPU核心锯仪。比如Linux2.6之后泵督,提供了NPTL的內(nèi)核線程模型,JVM就提供了JAVA線程到NPTL內(nèi)核線程的映射庶喜,從而利用上多核CPU小腊。而Windows系統(tǒng)中,據(jù)說本身線程就是系統(tǒng)的最小調(diào)度單位久窟,所以多線程也是利用上多核CPU的秩冈。所以我們?cè)谑褂肑AVA\C#編程的時(shí)候,多線程往往已經(jīng)同時(shí)具備了多進(jìn)程利用多核CPU斥扛、以及切換開銷低的兩個(gè)好處入问。
早期的一些網(wǎng)絡(luò)聊天室服務(wù),結(jié)合了多線程和多進(jìn)程使用的例子稀颁。一開始程序會(huì)啟動(dòng)多個(gè)廣播聊天的進(jìn)程队他,每個(gè)進(jìn)程都代表一個(gè)房間;每個(gè)用戶連接到聊天室峻村,就為他啟動(dòng)一個(gè)線程,這個(gè)線程會(huì)阻塞的讀取用戶的輸入流锡凝。這種模型在使用阻塞API的環(huán)境下粘昨,非常簡單,但也非常有效。
當(dāng)我們?cè)趶V泛使用多線程的時(shí)候张肾,我們發(fā)現(xiàn)芭析,盡管多線程有很多優(yōu)點(diǎn),但是依然會(huì)有明顯的兩個(gè)缺點(diǎn):一個(gè)內(nèi)存占用比較大且不太可控吞瞪;第二個(gè)是多個(gè)線程對(duì)于用一個(gè)數(shù)據(jù)使用時(shí)馁启,需要考慮復(fù)雜的“鎖”問題。由于多線程是基于對(duì)一個(gè)函數(shù)調(diào)用的并行運(yùn)行芍秆,這個(gè)函數(shù)里面可能會(huì)調(diào)用很多個(gè)子函數(shù)惯疙,每調(diào)用一層子函數(shù),就會(huì)要在棧上占用新的內(nèi)存妖啥,大量線程同時(shí)在運(yùn)行的時(shí)候霉颠,就會(huì)同時(shí)存在大量的棧,這些棧加在一起荆虱,可能會(huì)形成很大的內(nèi)存占用蒿偎。并且,我們編寫服務(wù)器端程序怀读,往往希望資源占用盡量可控诉位,而不是動(dòng)態(tài)變化太大,因?yàn)槟悴恢朗裁磿r(shí)候會(huì)因?yàn)閮?nèi)存用完而當(dāng)機(jī)菜枷,在多線程的程序中苍糠,由于程序運(yùn)行的內(nèi)容導(dǎo)致棧的伸縮幅度可能很大,有可能超出我們預(yù)期的內(nèi)存占用犁跪,導(dǎo)致服務(wù)的故障椿息。而對(duì)于內(nèi)存的“鎖”問題,一直是多線程中復(fù)雜的課題坷衍,很多多線程工具庫寝优,都推出了大量的“無鎖”容器,或者“線程安全”的容器枫耳,并且還大量設(shè)計(jì)了很多協(xié)調(diào)線程運(yùn)作的類庫乏矾。但是這些復(fù)雜的工具,無疑都是證明了多線程對(duì)于內(nèi)存使用上的問題迁杨。
![](http://mc.qcloudimg.com/static/img/787ffdb7f2a943aac51c8ea5e5ebc9e6/image.jpg)
同時(shí)排多條隊(duì)就是并行
由于多線程還是有一定的缺點(diǎn)钻心,所以很多程序員想到了一個(gè)釜底抽薪的方法:使用多線程往往是因?yàn)樽枞紸PI的存在,比如一個(gè)read()操作會(huì)一直停止當(dāng)前線程铅协,那么我們能不能讓這些操作變成不阻塞呢捷沸?——selector/epoll就是Linux退出的非阻塞式API。如果我們使用了非阻塞的操作函數(shù)狐史,那么我們也無需用多線程來并發(fā)的等待阻塞結(jié)果痒给。我們只需要用一個(gè)線程说墨,循環(huán)的檢查操作的狀態(tài),如果有結(jié)果就處理苍柏,無結(jié)果就繼續(xù)循環(huán)尼斧。這種程序的結(jié)果往往會(huì)有一個(gè)大的死循環(huán),稱為主循環(huán)试吁。在主循環(huán)體內(nèi)棺棵,程序員可以安排每個(gè)操作事件、每個(gè)邏輯狀態(tài)的處理邏輯熄捍。這樣CPU既無需在多線程間切換烛恤,也無需處理復(fù)雜的并行數(shù)據(jù)鎖的問題——因?yàn)橹挥幸粋€(gè)線程在運(yùn)行。這種就是被稱為“并發(fā)”的方案治唤。
![](http://mc.qcloudimg.com/static/img/8901a3acf9c89dd7571aec56a1d20d90/image.jpg)
服務(wù)員兼了點(diǎn)菜棒动、上菜就是并發(fā)
實(shí)際上計(jì)算機(jī)底層早就有使用并發(fā)的策略,我們知道計(jì)算機(jī)對(duì)于外部設(shè)備(比如磁盤宾添、網(wǎng)卡船惨、顯卡、聲卡缕陕、鍵盤粱锐、鼠標(biāo)),都使用了一種叫“中斷”的技術(shù)扛邑,早期的電腦使用者可能還被要求配置IRQ號(hào)怜浅。這個(gè)中斷技術(shù)的特點(diǎn),就是CPU不會(huì)阻塞的一直停在等待外部設(shè)備數(shù)據(jù)的狀態(tài)蔬崩,而是外部數(shù)據(jù)準(zhǔn)備好后恶座,給CPU發(fā)一個(gè)“中斷信號(hào)”,讓CPU轉(zhuǎn)去處理這些數(shù)據(jù)沥阳。非阻塞的編程實(shí)際上也是類似這種行為跨琳,CPU不會(huì)一直阻塞的等待某些I/O的API調(diào)用,而是先處理其他邏輯桐罕,然后每次主循環(huán)去主動(dòng)檢查一下這些I/O操作的狀態(tài)脉让。
多線程和異步的例子,最著名就是Web服務(wù)器領(lǐng)域的Apache和Nginx的模型功炮。Apache是多進(jìn)程/多線程模型的溅潜,它會(huì)在啟動(dòng)的時(shí)候啟動(dòng)一批進(jìn)程,作為進(jìn)程池薪伏,當(dāng)用戶請(qǐng)求到來的時(shí)候滚澜,從進(jìn)程池中分配處理進(jìn)程給具體的用戶請(qǐng)求,這樣可以節(jié)省多進(jìn)程/線程的創(chuàng)建和銷毀開銷嫁怀,但是如果同時(shí)有大量的請(qǐng)求過來博秫,還是需要消耗比較高的進(jìn)程/線程切換潦牛。而Nginx則是采用epoll技術(shù),這種非阻塞的做法挡育,可以讓一個(gè)進(jìn)程同時(shí)處理大量的并發(fā)請(qǐng)求,而無需反復(fù)切換朴爬。對(duì)于大量的用戶訪問場景下即寒,apache會(huì)存在大量的進(jìn)程,而nginx則可以僅用有限的進(jìn)程(比如按CPU核心數(shù)來啟動(dòng))召噩,這樣就會(huì)比apache節(jié)省了不少“進(jìn)程切換”的消耗母赵,所以其并發(fā)性能會(huì)更好。
![](http://mc.qcloudimg.com/static/img/fcb626d7db70e2c0e2ac56dc3de04508/image.gif)
Nginx的固定多進(jìn)程具滴,一個(gè)進(jìn)程異步處理多個(gè)客戶端
![](http://mc.qcloudimg.com/static/img/ae6258ef76f0162e6e12a084d039ed7c/image.gif)
Apache的多態(tài)多進(jìn)程凹嘲,一個(gè)進(jìn)程處理一個(gè)客戶
在現(xiàn)代服務(wù)器端軟件中,nginx這種模型的運(yùn)維管理會(huì)更簡單构韵,性能消耗也會(huì)稍微更小一點(diǎn)周蹭,所以成為最流行的進(jìn)程架構(gòu)。但是這種好處疲恢,會(huì)付出一些另外的代價(jià):非阻塞代碼在編程的復(fù)雜度變大凶朗。