關(guān)于高性能高并發(fā)服務(wù)這個(gè)概念大家應(yīng)該也都比較熟悉了,今天為大家?guī)?lái)如何做一個(gè)高性能高并發(fā)服務(wù)架構(gòu)的實(shí)踐和思考。
本次分享主要包括三個(gè)部分:
?服務(wù)的瓶頸有哪些
?如何提升整體服務(wù)的性能及并發(fā)?
如何提升單機(jī)服務(wù)的性能及并發(fā)?
服務(wù)的瓶頸有哪些
?通常來(lái)說(shuō)程序的定義是算法+數(shù)據(jù)結(jié)構(gòu)+數(shù)據(jù),算法簡(jiǎn)單的理解就是一種計(jì)算方式,數(shù)據(jù)結(jié)構(gòu)顧名思義是一種存儲(chǔ)組織數(shù)據(jù)的結(jié)構(gòu)。
?這兩者體現(xiàn)了程序需要用到的計(jì)算機(jī)資源,涉及到 CPU 資源、內(nèi)存資源,而數(shù)據(jù)部分除了內(nèi)存資源,往往還可能涉及到硬盤(pán)資源,甚至是彼此之間傳輸數(shù)據(jù)時(shí)會(huì)消耗網(wǎng)絡(luò)(網(wǎng)卡)資源。
?當(dāng)我們搞清楚程序運(yùn)行起來(lái)時(shí)涉及哪些資源后,就可以更好地分析我們的服務(wù)中哪些可能是臨界資源。
?所謂臨界資源就是多個(gè)進(jìn)程(線程)并發(fā)訪問(wèn)某個(gè)資源時(shí),該資源同時(shí)只能服務(wù)某個(gè)或者某些進(jìn)程(線程)。
服務(wù)的瓶頸主要就是在這些臨界資源上,還有一些資源原本并不是臨界資源。
?比如內(nèi)存在一開(kāi)始是夠的,但是因?yàn)檫B接數(shù)或者線程數(shù)不斷地增多,最終導(dǎo)致其成為臨界資源,其他的 CPU、磁盤(pán)、網(wǎng)卡其實(shí)和內(nèi)存一樣,在訪問(wèn)量增大以后一樣都可能會(huì)成為瓶頸。
所以怎么做到高性能高并發(fā)的服務(wù),簡(jiǎn)單地說(shuō)就是找到服務(wù)的瓶頸,在合理的范圍內(nèi)盡可能的消除瓶頸或者降低瓶頸帶來(lái)的影響。
?再通俗一點(diǎn)的說(shuō)就是資源總量不夠就加資源,什么資源不夠就加什么資源,同時(shí)盡量降低單次訪問(wèn)的資源消耗,做到在資源總量一定的情況下有能力支撐更多的訪問(wèn)。
?如何提升整體服務(wù)的性能及并發(fā)
數(shù)據(jù)拆分
最典型的一個(gè)臨界資源就是數(shù)據(jù)庫(kù),數(shù)據(jù)庫(kù)在一個(gè)大訪問(wèn)量的系統(tǒng)中往往是最薄弱的一環(huán),因?yàn)閿?shù)據(jù)庫(kù)本身的服務(wù)能力是有限的。
?以 MySQL 為例,MySQL 可以支持的并發(fā)連接數(shù)可能也就幾千個(gè),假設(shè)是 3000 個(gè),一個(gè)服務(wù)對(duì)其數(shù)據(jù)庫(kù)的并發(fā)訪問(wèn)如果超過(guò)了 3000 個(gè),有部分訪問(wèn)可能在建立連接的時(shí)候就失敗了。
在這種情況下,需要考慮的是如何將數(shù)據(jù)進(jìn)行分片,引入多個(gè) MySQL 實(shí)例,增加資源,如圖 1 所示。

???????????????????????????????? 圖1:?jiǎn)螖?shù)據(jù)實(shí)例改成數(shù)據(jù)庫(kù)集群
數(shù)據(jù)庫(kù)這個(gè)臨界資源通過(guò)數(shù)據(jù)拆分的方式,由原來(lái)的一個(gè) MySQL 實(shí)例變成了多個(gè) MySQL 實(shí)例。
這種情況下數(shù)據(jù)庫(kù)資源的整體并發(fā)服務(wù)能力自然提升了,同時(shí)由于服務(wù)壓力被分散,整個(gè)數(shù)據(jù)庫(kù)集群表現(xiàn)出來(lái)的性能也會(huì)比單個(gè)數(shù)據(jù)庫(kù)實(shí)例高很多。
存儲(chǔ)類(lèi)的解決思路基本是類(lèi)似的,都是將數(shù)據(jù)拆分,通過(guò)引入多個(gè)存儲(chǔ)服務(wù)實(shí)例提升整體存儲(chǔ)服務(wù)的能力,不管對(duì)于 SQL 類(lèi)的還是 NoSQL 類(lèi)的或文件存儲(chǔ)系統(tǒng)等都可以采用這個(gè)思路。
?服務(wù)拆分
?應(yīng)用程序自身的服務(wù)需要根據(jù)業(yè)務(wù)情況進(jìn)行合理的細(xì)化,讓每個(gè)服務(wù)只負(fù)責(zé)某一類(lèi)功能,這個(gè)思想和微服務(wù)思想類(lèi)似。
?一句話就是盡量合理地將服務(wù)拆分,同時(shí)有一個(gè)非常重要的原則是讓拆分以后的同類(lèi)服務(wù)盡量是無(wú)狀態(tài)或弱關(guān)聯(lián),這樣就可以很容易進(jìn)行水平擴(kuò)展。
如果拆分以后的同類(lèi)服務(wù)的不同實(shí)例之間本身是有一些狀態(tài)引起彼此非常強(qiáng)的依賴(lài),比如彼此要共享一些信息這些信息又會(huì)彼此影響,那這種拆分可能就未必非常的合理,需要結(jié)合業(yè)務(wù)重新進(jìn)行審視。
?當(dāng)然生產(chǎn)環(huán)節(jié)上下游拆分以后不同的服務(wù)彼此之間的關(guān)聯(lián)又是另外一種情形,因?yàn)橥粋€(gè)生產(chǎn)環(huán)節(jié)上往往是走完一個(gè)服務(wù)環(huán)節(jié)才能進(jìn)入下一個(gè)服務(wù)環(huán)節(jié)。
?相當(dāng)于有多個(gè)串行的服務(wù),任何一個(gè)環(huán)節(jié)的服務(wù)都有可能出現(xiàn)瓶頸,所以需要拆分以后針對(duì)相應(yīng)的服務(wù)進(jìn)行單獨(dú)優(yōu)化,這是拆分以后服務(wù)與服務(wù)之間的關(guān)系 。
假設(shè)各個(gè)同類(lèi)服務(wù)本身是無(wú)狀態(tài)或者弱依賴(lài)的情況下,針對(duì)應(yīng)用服務(wù)進(jìn)行分析,不同的應(yīng)用服務(wù)不太一樣,但是通常都會(huì)涉及到內(nèi)存資源以及計(jì)算資源。
以受內(nèi)存資源限制為例,一個(gè)應(yīng)用服務(wù)能承受的連接數(shù)是有限的(連接數(shù)受限),另外如果涉及上傳下載等大量數(shù)據(jù)傳輸?shù)那闆r,網(wǎng)絡(luò)資源很快就會(huì)成為瓶頸(網(wǎng)卡打滿(mǎn))。
?這種情況下最簡(jiǎn)單的方式就是同樣的應(yīng)用服務(wù)實(shí)例部署多份,達(dá)到水平擴(kuò)展,如圖 2 所示。

???????????????????????????????????????????????????????圖 2:服務(wù)拆分
?實(shí)際在真正拆分的時(shí)候需要考慮具體的業(yè)務(wù)特點(diǎn),比如像京東主站這種類(lèi)型的網(wǎng)站,用戶(hù)在訪問(wèn)的時(shí)候除了加載基本信息以外,還有商品圖片信息、價(jià)格信息、庫(kù)存信息、購(gòu)物車(chē)信息以及訂單信息、發(fā)票信息等。
?以及下單完成以后對(duì)應(yīng)的分揀配送等配套的物流服務(wù),這些都可以拆成單獨(dú)的服務(wù),拆分以后各個(gè)服務(wù)各司其職也能做更好的優(yōu)化。
?服務(wù)拆分這件事情,打個(gè)不是特別恰當(dāng)?shù)谋确?,就好比上學(xué)時(shí)都是學(xué)習(xí),但是分了很多的科目,高考的時(shí)候要看總分,有些同學(xué)會(huì)有偏科的現(xiàn)象,有些科成績(jī)好有些科成績(jī)差一點(diǎn)。
因?yàn)榉趾芏嗫颇克院苋菀字雷约耗目剖潜容^強(qiáng)的、哪科是比較弱的,為了保證總體分?jǐn)?shù)最優(yōu),一般在弱的科目上都需要多花點(diǎn)精力努力提高一下分?jǐn)?shù),不然總體分?jǐn)?shù)不會(huì)太高。
?服務(wù)拆分也是同樣的道理,拆分以后可以很容易知道哪個(gè)服務(wù)是整體服務(wù)的瓶頸,針對(duì)瓶頸服務(wù)再進(jìn)行重點(diǎn)優(yōu)化就可以比較容易的提升整體服務(wù)的能力。
想要更多的Java視頻資料的同學(xué)請(qǐng)加?? 731661047
?增長(zhǎng)服務(wù)鏈路
?在大型的網(wǎng)站服務(wù)方案上,在各種合理拆分以后,數(shù)據(jù)拆分以及服務(wù)拆分支持?jǐn)U展只是其中的一部分工作,之后還要根據(jù)需求看看是否需要引入緩存 CDN 之類(lèi)的服務(wù)。
?我把這個(gè)叫做增長(zhǎng)服務(wù)鏈路,原來(lái)直接打到數(shù)據(jù)庫(kù)的請(qǐng)求,現(xiàn)在可能變成了先打到緩存再打到數(shù)據(jù)庫(kù),對(duì)整個(gè)服務(wù)鏈路長(zhǎng)度來(lái)說(shuō)是變長(zhǎng)的。
?增長(zhǎng)服務(wù)鏈路的原則主要是將越脆弱或者說(shuō)越容易成為瓶頸的資源(比如數(shù)據(jù)庫(kù))放置在鏈路的越末端。
?在增長(zhǎng)完服務(wù)鏈路之后,還要盡量的縮短訪問(wèn)鏈路,比如可以在 CDN 層面就返回的就盡量不要繼續(xù)往下走了。
如果可以在緩存層面返回的就不要去訪問(wèn)數(shù)據(jù)庫(kù)了,盡可能地讓每次的訪問(wèn)鏈路變短。
可以一步解決的事情就一步解決,可以?xún)刹浇鉀Q的事情就不要走第三步,本質(zhì)上是降低每次訪問(wèn)的資源消耗,尤其是越到鏈路的末端訪問(wèn)資源的消耗會(huì)越大。
比如獲取一些產(chǎn)品的圖片信息可以在訪問(wèn)鏈路的最前端使用 CDN,將訪問(wèn)盡量擋住。
如果 CDN 上沒(méi)有命中,就繼續(xù)往后端訪問(wèn),利用 Nginx 等反向代理將訪問(wèn)打到相應(yīng)的圖片服務(wù)器上,而圖片服務(wù)器本身又可以針對(duì)性的做一些訪問(wèn)優(yōu)化等。
?比如像價(jià)格等信息比較敏感,如果有更改可能需要立即生效,需要直接訪問(wèn)最新的數(shù)據(jù),但是如果讓訪問(wèn)直接打到數(shù)據(jù)庫(kù)中,數(shù)據(jù)庫(kù)往往直接就打掛了。
?所以可以考慮在數(shù)據(jù)庫(kù)之前引入 Redis 等緩存服務(wù),將訪問(wèn)打到緩存上,價(jià)格服務(wù)系統(tǒng)本身保證數(shù)據(jù)庫(kù)和緩存的強(qiáng)一致,降低對(duì)數(shù)據(jù)庫(kù)的訪問(wèn)壓力。
在極端情況下,數(shù)據(jù)量雖然不是特別大,幾十臺(tái)緩存機(jī)器就可以抗住,但訪問(wèn)量可能會(huì)非常大,可以將所有的數(shù)據(jù)都放在緩存中,如果緩存有異常甚至都不用去訪問(wèn)數(shù)據(jù)庫(kù)直接返回訪問(wèn)失敗即可。
?因?yàn)樵谠L問(wèn)量非常大的情況下,如果緩存掛了,訪問(wèn)直接打到數(shù)據(jù)庫(kù)上,可能瞬間就把數(shù)據(jù)庫(kù)打趴下了。
所以在特定場(chǎng)景下可以考慮將緩存和數(shù)據(jù)庫(kù)切開(kāi),服務(wù)只訪問(wèn)緩存,緩存失效重新從數(shù)據(jù)庫(kù)中加載數(shù)據(jù)到緩存中再對(duì)外服務(wù)也是可以的,所以在實(shí)踐中是可以靈活變通的。
小結(jié)
如何提升整體服務(wù)的性能及并發(fā),一句話概括就是:在合理范圍內(nèi)盡可能的拆分,拆分以后同類(lèi)服務(wù)可以通過(guò)水平擴(kuò)展達(dá)到整體的高性能高并發(fā)。
?同時(shí)將越脆弱的資源放置在鏈路的越末端,訪問(wèn)的時(shí)候盡量將訪問(wèn)鏈接縮短,降低每次訪問(wèn)的資源消耗。
?如何提升單機(jī)服務(wù)的性能及并發(fā) 前面說(shuō)的這些情況可以解決大訪問(wèn)量情況下的高并發(fā)問(wèn)題,但是高性能最終還是要依賴(lài)單臺(tái)應(yīng)用的性能。
?如果單臺(tái)應(yīng)用性能在低訪問(wèn)量情況下性能已經(jīng)成渣了,那部署再多機(jī)器也解決不了問(wèn)題,所以接下來(lái)聊一下單臺(tái)服務(wù)本身如果支持高性能高并發(fā)。
多線程/線程池方式
以 TCP server 為例來(lái)展開(kāi)說(shuō)明,最簡(jiǎn)單的一個(gè) TCP server 代碼,版本一示例如圖 3 所示。

????????????????????????????????????????????????????????圖 3:版本一
?這種方式純粹是一個(gè)示例,因?yàn)檫@個(gè) server 啟動(dòng)以后只能接受一條連接,也就是只能跟一個(gè)客戶(hù)端互動(dòng),且該連接斷開(kāi)以后,后續(xù)就連不上了,也就是這個(gè) server 只能服務(wù)一次。
這個(gè)當(dāng)然是不行的,于是就有了版本二,如圖 4 所示,版本二可以一次接受一條連接,并進(jìn)行一些交互處理,當(dāng)這條連接全部處理完以后才能繼續(xù)下一條連接。

???????????????????????????????????????????????????? ?圖 4:版本二
這個(gè) server 相當(dāng)于是串行的,沒(méi)有并發(fā)可言,所以在版本二的基礎(chǔ)上又演化出了版本三,如圖 5 所示

???????????????????????????????????????????????????? ?圖 5:版本三
這其實(shí)是我們經(jīng)常會(huì)接觸到的一種模型,這種模型的特點(diǎn)是每連接每線程,MySQL 5.5 以前用的就是這種模型,這種模型的特點(diǎn)是當(dāng)有大量連接的時(shí)候會(huì)創(chuàng)建大量的線程。
?所以往往需要限制連接總數(shù),如果不做限制可能會(huì)出現(xiàn)創(chuàng)建了大量的線程,很快就會(huì)將內(nèi)存等資源耗干。
?另一個(gè)是當(dāng)出現(xiàn)了大量的線程的時(shí)候,操作系統(tǒng)會(huì)有大量的 CPU 資源花費(fèi)在線程間的上下文切換上,導(dǎo)致真正給業(yè)務(wù)提供服務(wù)的 CPU 資源比例反倒很小。
同時(shí),考慮到大多數(shù)時(shí)候即使有很多連接也并不代表所有的連接在同一個(gè)時(shí)刻都是活躍的,所以版本三又演化出了版本四,如圖 6 所示。

??????????????????????????????????????????????????? ?圖 6:版本四
版本四的時(shí)候是很多的連接共享一個(gè)線程池,這些線程池里的線程數(shù)是固定的,這樣就可以做到線程池里的一個(gè)線程同時(shí)服務(wù)多條連接了,MySQL 5.6 之后采用的就是這種方式。
在絕大多數(shù)的開(kāi)發(fā)中,線程池技術(shù)就已經(jīng)足夠了,但是線程池在充分榨干 CPU 計(jì)算資源或者說(shuō)提供有效計(jì)算資源方面并不是最完美的。
以一核的計(jì)算資源為例,線程池里假設(shè)有 x 個(gè)線程,這 x 個(gè)線程會(huì)被操作系統(tǒng)依據(jù)具體調(diào)度策略進(jìn)行調(diào)度,但是線程上下文切換本身是會(huì)消耗一定的 CPU 資源的。
假設(shè)這部分消耗代價(jià)是 w,而實(shí)際有效服務(wù)的能力是 c,那么理論上來(lái)說(shuō) w+c 就是總的 CPU 實(shí)際提供的計(jì)算資源,同時(shí)假設(shè)一核 CPU 理論上提供計(jì)算資源假設(shè)為 t,這個(gè)是固定的。
所以就會(huì)出現(xiàn)一種情況:當(dāng)線程池中線程數(shù)量較少的時(shí)候并發(fā)度較低,w 雖然小了,但是 c 也是比較小的,也就是 w+c < t,甚至是遠(yuǎn)遠(yuǎn)小于 t,如果線程數(shù)很多,又會(huì)出現(xiàn)上下文切換代價(jià)太大,即 w 變大了。
雖然 c 也隨之提升了一些,但因?yàn)?t 是固定的,所以 c 的上限值一定是小于 t-w 的,而且隨著 w 越大,c 的上限值反倒降低了,因此使用線程池的時(shí)候,線程數(shù)的設(shè)置需要根據(jù)實(shí)際情況進(jìn)行調(diào)整。
基于事件驅(qū)動(dòng)的模式
多線程(線程池)的方式可以較為方便地進(jìn)行并發(fā)編程,但是多線程的方式對(duì) CPU 的有效利用率并不是最高的,真正能夠充分利用 CPU 的編程方式是盡量讓 CPU 一直在工作,同時(shí)又盡量避免線程的上下文切換等開(kāi)銷(xiāo)。
基于事件驅(qū)動(dòng)的模式(也稱(chēng) I/O 多路復(fù)用)在充分利用 CPU 有效計(jì)算能力這件事件上是非常出色的。
比較典型的有 select/poll/epoll/kevent(這些機(jī)制本身之間的優(yōu)劣今天先不展開(kāi)說(shuō)明,后續(xù)以 epoll 為例說(shuō)明)。
這種模式的特點(diǎn)是將要監(jiān)聽(tīng)的 socket fd 注冊(cè)在 epoll 上,等這個(gè)描述符可讀事件或者可寫(xiě)事件就緒了,那么就會(huì)觸發(fā)相應(yīng)的讀操作或者寫(xiě)操作。
?可以簡(jiǎn)單地理解為需要 CPU 干活的時(shí)候就會(huì)告知 CPU 需要做什么事情,實(shí)際使用時(shí)示例,如圖 7 所示。

?圖 7:epoll 示例
這個(gè)事情拿一個(gè)經(jīng)典的例子來(lái)說(shuō)明:假如在餐廳就餐,餐廳里有很多顧客(訪問(wèn)),每連接每線程的方式相當(dāng)于每個(gè)客戶(hù)一個(gè)服務(wù)員(線程相當(dāng)于一個(gè)服務(wù)員)。
服務(wù)的過(guò)程中一個(gè)服務(wù)員一直為一個(gè)客戶(hù)服務(wù),那就會(huì)出現(xiàn)這個(gè)服務(wù)員除了真正提供服務(wù)以外有很大一段時(shí)間可能是空閑的,且隨著客戶(hù)數(shù)越多服務(wù)員數(shù)量也會(huì)越多,可餐廳的容量是有限的。
因?yàn)橐瑫r(shí)容納相同數(shù)量的服務(wù)員和顧客,所以餐廳服務(wù)顧客的數(shù)量將變成理論容量的 50%。 那這件事件對(duì)于老板(老板相當(dāng)于開(kāi)發(fā)人員,希望可以充分利用 CPU 的計(jì)算能力,也就是在 CPU 計(jì)算能力<成本>一定的情況下希望盡量的多做一些事情)來(lái)說(shuō)代價(jià)就會(huì)很大。
線程池的方式是雇傭固定數(shù)量的服務(wù)員,服務(wù)的時(shí)候一個(gè)服務(wù)員服務(wù)好幾個(gè)客戶(hù),可以理解為一個(gè)服務(wù)員在客戶(hù) A 面前站 1 分鐘,看看 A 客戶(hù)是否需要服務(wù)。
如果不需要就到 B 客戶(hù)那邊站 1 分鐘,看看 B 客戶(hù)是否需要服務(wù),以此類(lèi)推。
這種情況會(huì)比之前每個(gè)客戶(hù)一個(gè)服務(wù)員的情況節(jié)省一些成本,但是還是會(huì)出現(xiàn)一些成本上的浪費(fèi)。
還有一種模式也就是 epoll 的方式,相當(dāng)于服務(wù)員就在總臺(tái)等著,客戶(hù)有需要的時(shí)候就會(huì)在桌上的呼叫器上按一下按鈕表示自己需要服務(wù),服務(wù)員每次看一下總臺(tái)顯示的信息。
比如一共有 100 個(gè)客戶(hù),一次可能有 10 個(gè)客戶(hù)呼叫,這個(gè)服務(wù)員就會(huì)過(guò)去為這 10 個(gè)客戶(hù)服務(wù)(假設(shè)服務(wù)每個(gè)客戶(hù)的時(shí)候不會(huì)出現(xiàn)停頓且可以在較短的時(shí)間內(nèi)處理完)。
等這個(gè)服務(wù)員為這 10 個(gè)客戶(hù)服務(wù)員完以后再重新回到總臺(tái)查看哪些客戶(hù)需要服務(wù),依此類(lèi)推。在這種情況下,可能只需要一個(gè)服務(wù)員,而餐廳剩余的空間可以全部給客戶(hù)使用。
Nginx 服務(wù)器性能非常好,也能支撐非常多的連接,其網(wǎng)絡(luò)模型使用的就是 epoll 的方式,且在實(shí)現(xiàn)的時(shí)候采用了多個(gè)子進(jìn)程的方式。
相當(dāng)于同時(shí)有多個(gè) epoll 在工作,充分利用了 CPU 多核的特性,所以并發(fā)及性能都會(huì)比單個(gè) epoll 的方式會(huì)有更大的提升。
另外 Redis 緩存服務(wù)器大家應(yīng)該也非常熟悉,用的也是 epoll 的方式,性能也是非常好。
?通過(guò)這些現(xiàn)成的經(jīng)典開(kāi)源項(xiàng)目,大家就可以直觀地理解基于事件驅(qū)動(dòng)這一方式在實(shí)際生產(chǎn)環(huán)境中的性能是非常高的,性能提升以后并發(fā)效果一般都會(huì)隨之提升。 但是這種方式在實(shí)現(xiàn)的時(shí)候是非常考驗(yàn)編程功底以及邏輯嚴(yán)謹(jǐn)性,換句話編程友好性是非常差的。
因?yàn)橐粋€(gè)完整的上下文邏輯會(huì)被切成很多片段,比如“客戶(hù)端發(fā)送一個(gè)命令-服務(wù)器端接收命令進(jìn)行操作-然后返回結(jié)果”這個(gè)過(guò)程。
這個(gè)過(guò)程至少會(huì)包括一個(gè)可讀事件、一個(gè)可寫(xiě)事件??勺x事件,簡(jiǎn)單地理解就是指這條命令已經(jīng)發(fā)送到服務(wù)器端的 tcp 緩存區(qū)了,服務(wù)器去讀取命令(假設(shè)一次讀取完,如果一次讀取的命令不完整,可能會(huì)觸發(fā)多次讀事件)。
服務(wù)器再根據(jù)命令進(jìn)行操作獲取到結(jié)果,同時(shí)注冊(cè)一個(gè)可寫(xiě)事件到 epoll 上,等待下一次可寫(xiě)事件觸發(fā)以后再將結(jié)果發(fā)送出去。
想象一下當(dāng)有很多客戶(hù)端同時(shí)來(lái)訪問(wèn)時(shí),服務(wù)器就會(huì)出現(xiàn)一種情況——一會(huì)兒在處理某個(gè)客戶(hù)端的讀事件,一會(huì)兒在處理另外的客戶(hù)端的寫(xiě)事件。
?總之都是在做一個(gè)完整訪問(wèn)的上下文中的一個(gè)片段,其中任何一個(gè)片段有等待或者卡頓都將引起整個(gè)程序的阻塞。
?當(dāng)然這個(gè)問(wèn)題在多線程編程時(shí)也是同樣是存在的,只不過(guò)有時(shí)候大家習(xí)慣將線程設(shè)置成多個(gè),有些線程阻塞了,但可能其他線程并沒(méi)有在同一時(shí)刻阻塞。
?所以問(wèn)題不是特別嚴(yán)重,更嚴(yán)謹(jǐn)?shù)淖龇ㄊ窃诙嗑€程編程時(shí),將線程池的數(shù)量調(diào)整到最小進(jìn)行測(cè)試。
如果確實(shí)有卡頓,可以確保程序在最快的時(shí)間內(nèi)出現(xiàn)卡頓,從而快速確認(rèn)邏輯上是否有不足或者缺陷,確認(rèn)這種卡頓本身是否是正常現(xiàn)象。
語(yǔ)言層提供協(xié)程支持
多線程編程的方式明顯是支持了高并發(fā),但因?yàn)檎麄€(gè)程序線程間上下文調(diào)度可能造成 CPU 的利用率不是那么高,而基于事件驅(qū)動(dòng)的編程方式效果是非常好的。
但對(duì)編程功底要求非常高,而且在實(shí)現(xiàn)的時(shí)候需要花費(fèi)的時(shí)間也是最多的,所以一種比較折中的方式是考慮采用提供協(xié)程支持的語(yǔ)言比如 golang 這種的。
?簡(jiǎn)單說(shuō)就是語(yǔ)言層面抽象出了一種更輕量級(jí)的線程,一般稱(chēng)為協(xié)程,在 golang 里又叫 goroutine。
這些底層最終也是需要用操作系統(tǒng)的線程去跑,在 golang 的 runtime 實(shí)現(xiàn)時(shí)底層用到的操作系統(tǒng)的線程數(shù)量相對(duì)會(huì)少一點(diǎn)。
?而上層程序里可以跑很多的 goroutine,這些 goroutine 會(huì)在語(yǔ)言層面進(jìn)行調(diào)度,看該由哪個(gè)線程來(lái)最終執(zhí)行這個(gè) goroutine。
因?yàn)?goroutine 之間的切換代價(jià)是遠(yuǎn)小于操作系統(tǒng)線程之間的切換代價(jià),而底層用到的操作系統(tǒng)數(shù)量又較少,線程間的上下文切換代價(jià)也會(huì)大大降低。
這類(lèi)語(yǔ)言能比其他語(yǔ)言的多線程方式提供更好的并發(fā),因?yàn)樗鼘⒉僮飨到y(tǒng)的線程間切換的代價(jià)在語(yǔ)言層面盡可能擠壓到最小,同時(shí)編程復(fù)雜度大大降低,在這類(lèi)語(yǔ)言中上下文邏輯可以保持連貫。
因?yàn)榻档土司€程間上下文切換的代價(jià),而 goroutine 之間的切換成本相對(duì)來(lái)說(shuō)是遠(yuǎn)遠(yuǎn)小于線程間切換成本。
?所以 CPU 的有效計(jì)算能力相對(duì)來(lái)說(shuō)也不會(huì)太低,可以比較容易的獲得了一個(gè)高并發(fā)且性能還可以的服務(wù)。
小結(jié)
?如何提升單機(jī)服務(wù)的性能及并發(fā),如果對(duì)性能或者高并發(fā)的要求沒(méi)有達(dá)到非??量痰囊?,選型的時(shí)候基于事件驅(qū)動(dòng)的方式可以?xún)?yōu)先級(jí)降低一點(diǎn),選擇普通的多線程編程即可(其實(shí)多數(shù)場(chǎng)景都可以滿(mǎn)足了)。
?如果想單機(jī)的并發(fā)程度更好一點(diǎn),可以考慮選擇有協(xié)程支持的語(yǔ)言,如果還嫌不夠,那就將邏輯理順,考慮采用基于事件驅(qū)動(dòng)的模式,這個(gè)在 C/C++ 里直接用 select/epoll/kevent 等就可以了。
在 Java 里可以考慮采用 NIO 的方式,而從這點(diǎn)上來(lái)說(shuō)像 golang 這種提供協(xié)程支持的語(yǔ)言一般是不支持在程序?qū)用孀约簩?shí)現(xiàn)基于事件驅(qū)動(dòng)的編程方式的。
總結(jié)
其實(shí)并沒(méi)有一刀切的萬(wàn)能法則,大體原則是根據(jù)實(shí)際情況具體問(wèn)題具體分析,找到服務(wù)瓶頸,資源不夠加資源,盡可能降低每次訪問(wèn)的資源消耗,整體服務(wù)每個(gè)環(huán)節(jié)盡量做到可以水平擴(kuò)展。
?同時(shí)盡量提高單機(jī)的有效利用率,從而確保在扛住整個(gè)服務(wù)的同時(shí)盡量降低資源消耗成本。
原文作者:作者:張成遠(yuǎn)來(lái)源:51CTO技術(shù)棧
【責(zé)任編輯:武曉燕 TEL:(010)68476606】
分享一張架構(gòu)圖片
