新聞中心
來(lái)想象這樣一個(gè)場(chǎng)景,一天,公司 CEO 把你叫到會(huì)議室,告訴你公司看到了一個(gè)新的商業(yè)機(jī)會(huì),希望你能帶領(lǐng)一位兄弟,迅速研發(fā)出一套面向某個(gè)垂直領(lǐng)域的電商系統(tǒng)。

在資源匱乏、時(shí)間緊迫的情況下,我迅速采用了一種極為簡(jiǎn)化的系統(tǒng)架構(gòu):一臺(tái)Web服務(wù)器負(fù)責(zé)運(yùn)行業(yè)務(wù)代碼,而一臺(tái)獨(dú)立的數(shù)據(jù)庫(kù)服務(wù)器則存儲(chǔ)所有業(yè)務(wù)數(shù)據(jù)。這種簡(jiǎn)單的架構(gòu)設(shè)計(jì)允許我們盡快啟動(dòng)項(xiàng)目并專注于核心功能的開(kāi)發(fā),以在有限的資源和時(shí)間內(nèi)完成任務(wù)。
圖片
當(dāng)垂直電商系統(tǒng)開(kāi)始吸引更多用戶流量時(shí),我們遇到了性能問(wèn)題,主要是因?yàn)槲覀兊臄?shù)據(jù)庫(kù)連接方式。在原始設(shè)計(jì)中,每次查詢都需要建立和關(guān)閉數(shù)據(jù)庫(kù)連接,這導(dǎo)致了性能下降。為了解決這個(gè)問(wèn)題,我們需要優(yōu)化數(shù)據(jù)庫(kù)連接管理,考慮使用連接池技術(shù)來(lái)減少連接的頻繁建立和關(guān)閉,以提高系統(tǒng)的響應(yīng)速度和性能。這將是一個(gè)重要的性能優(yōu)化步驟,以適應(yīng)不斷增長(zhǎng)的用戶流量。
那么為什么頻繁創(chuàng)建連接會(huì)造成響應(yīng)時(shí)間慢呢?來(lái)看一個(gè)實(shí)際的測(cè)試。
通過(guò)運(yùn)行"tcpdump -i bond0 -nn -tttt port 4490"命令來(lái)捕獲線上MySQL連接的網(wǎng)絡(luò)包后,我們可以將MySQL連接過(guò)程簡(jiǎn)化為兩個(gè)主要階段:
前三個(gè)數(shù)據(jù)包描繪了MySQL連接的起始階段。首先,客戶端發(fā)送了一個(gè)帶有SYN標(biāo)志的數(shù)據(jù)包,表示要與服務(wù)器建立連接。然后,服務(wù)器回應(yīng)客戶端,確認(rèn)收到連接請(qǐng)求,并且也發(fā)送一個(gè)帶有SYN標(biāo)志的數(shù)據(jù)包。最后,客戶端再次回應(yīng)服務(wù)器,確認(rèn)連接已建立。這個(gè)過(guò)程就是TCP協(xié)議中的三次握手,用于確??蛻舳撕头?wù)器之間的連接正常建立。
第二部分是 MySQL 服務(wù)端校驗(yàn)客戶端密碼的過(guò)程。其中第一個(gè)包是服務(wù)端發(fā)給客戶端要求認(rèn)證的報(bào)文,第二和第三個(gè)包是客戶端將加密后的密碼發(fā)送給服務(wù)端的包,最后兩個(gè)包是服務(wù)端回給客戶端認(rèn)證 OK 的報(bào)文。從圖中,你可以看到整個(gè)連接過(guò)程大概消耗了 4ms(969012-964904)。
圖片
根據(jù)我們的統(tǒng)計(jì)數(shù)據(jù),單條SQL執(zhí)行時(shí)間平均約為1毫秒,這意味著相對(duì)于SQL的執(zhí)行來(lái)說(shuō),MySQL建立連接的過(guò)程所花費(fèi)的時(shí)間較長(zhǎng)。當(dāng)請(qǐng)求量較小時(shí),這并不會(huì)產(chǎn)生顯著影響,因?yàn)椴还苁墙⑦B接還是執(zhí)行SQL,都在毫秒級(jí)別完成。然而,一旦請(qǐng)求量增加,如果每次都按照原始方式建立連接,然后只執(zhí)行一條SQL,那么每秒只能執(zhí)行大約200次數(shù)據(jù)庫(kù)查詢,其中有四分之三的時(shí)間都花在了建立連接上。
那這時(shí)你要怎么做呢?
在進(jìn)行了一番谷歌搜索后,我找到了一個(gè)相對(duì)簡(jiǎn)單的解決方案:使用連接池來(lái)預(yù)先建立數(shù)據(jù)庫(kù)連接。通過(guò)這個(gè)改進(jìn),我們不再需要在每次使用數(shù)據(jù)庫(kù)時(shí)都頻繁地創(chuàng)建連接。調(diào)整后,系統(tǒng)的性能顯著提高,現(xiàn)在每秒可以執(zhí)行1000次數(shù)據(jù)庫(kù)查詢,這是之前的5倍,從而更好地滿足了高請(qǐng)求量的需求。這種連接池的優(yōu)化方式有效地減少了連接建立的開(kāi)銷,提高了系統(tǒng)的響應(yīng)速度和性能。
用連接池預(yù)先建立數(shù)據(jù)庫(kù)連接
雖然短時(shí)間解決了問(wèn)題,不過(guò)你還是想徹底搞明白解決問(wèn)題的核心原理,于是又開(kāi)始補(bǔ)課。
在開(kāi)發(fā)過(guò)程中,我們經(jīng)常使用連接池,比如數(shù)據(jù)庫(kù)連接池、HTTP連接池和Redis連接池等。連接池的管理是連接池設(shè)計(jì)的核心,讓我以數(shù)據(jù)庫(kù)連接池為例,簡(jiǎn)要說(shuō)明一下關(guān)鍵點(diǎn)。
數(shù)據(jù)庫(kù)連接池有兩個(gè)關(guān)鍵配置參數(shù):最小連接數(shù)和最大連接數(shù)。這些參數(shù)控制著連接池如何管理連接的獲?。?/p>
- 如果當(dāng)前連接數(shù)小于最小連接數(shù),連接池會(huì)創(chuàng)建新的連接來(lái)處理數(shù)據(jù)庫(kù)請(qǐng)求。
- 如果連接池中有空閑連接,它將會(huì)被重用。
- 如果空閑連接池沒(méi)有可用連接,且當(dāng)前連接數(shù)小于最大連接數(shù),連接池會(huì)創(chuàng)建新的連接來(lái)處理請(qǐng)求。
- 如果當(dāng)前連接數(shù)已經(jīng)達(dá)到最大連接數(shù),連接池會(huì)根據(jù)設(shè)定的等待時(shí)間(比如C3P0連接池中的checkoutTimeout配置)等待已有連接變?yōu)榭捎谩?/li>
- 如果等待超過(guò)了設(shè)定的時(shí)間,連接池將向用戶拋出錯(cuò)誤。
為了方便你理解記憶這個(gè)流程,我來(lái)舉個(gè)例子。
假設(shè)你在機(jī)場(chǎng)里運(yùn)營(yíng)一家按摩椅的小店,店里總共有10臺(tái)按摩椅(類似于數(shù)據(jù)庫(kù)連接池的最大連接數(shù))。為了控制成本(按摩椅費(fèi)電),通常你會(huì)保持4臺(tái)按摩椅開(kāi)啟(最小連接數(shù)),其余6臺(tái)關(guān)閉。當(dāng)顧客到來(lái)時(shí),如果這4臺(tái)按摩椅中有空位,你可以直接為顧客提供服務(wù)。但如果所有4臺(tái)按摩椅都在使用中,那么你會(huì)開(kāi)啟一臺(tái)新的按摩椅,直到所有10臺(tái)都被占用。
當(dāng)所有10臺(tái)按摩椅都在使用中時(shí),你會(huì)告知等待的顧客:“請(qǐng)稍等5分鐘,我保證在這段時(shí)間內(nèi)會(huì)有按摩椅空出來(lái)。”然后第11位顧客開(kāi)始等待。這時(shí)有兩種可能性:如果在5分鐘內(nèi)有按摩椅空出來(lái),顧客可以立即使用;但如果等待了5分鐘還沒(méi)有按摩椅空出來(lái),你需要道歉并建議顧客去其他地方嘗試。
對(duì)于數(shù)據(jù)庫(kù)連接池,根據(jù)我的經(jīng)驗(yàn),一般在線上我建議最小連接數(shù)控制在 10 左右,最大連接數(shù)控制在 20~30 左右即可。
用線程池預(yù)先創(chuàng)建線程
JDK 1.5引入的ThreadPoolExecutor是一種線程池的實(shí)現(xiàn),它有兩個(gè)關(guān)鍵參數(shù):coreThreadCount和maxThreadCount。它的工作原理類似于之前描述的按摩椅店模式:
- 如果線程池中的線程數(shù)量小于coreThreadCount,新任務(wù)到來(lái)時(shí)會(huì)創(chuàng)建新線程來(lái)處理。
- 如果線程數(shù)量超過(guò)coreThreadCount,任務(wù)會(huì)被放入一個(gè)隊(duì)列中,等待當(dāng)前空閑的線程執(zhí)行。
- 當(dāng)隊(duì)列中的任務(wù)堆積滿了時(shí),線程池會(huì)繼續(xù)創(chuàng)建線程,直到達(dá)到maxThreadCount。
- 一旦線程數(shù)量達(dá)到maxThreadCount,并且仍有新任務(wù)提交,那么這些新任務(wù)可能會(huì)被丟棄。
圖片
這個(gè)任務(wù)處理流程看似簡(jiǎn)單,實(shí)際上有很多坑,你在使用的時(shí)候一定要注意。
JDK實(shí)現(xiàn)的ThreadPoolExecutor在處理任務(wù)時(shí),更傾向于將任務(wù)暫存于隊(duì)列中而不是過(guò)早地創(chuàng)建新線程。這種方式更適合執(zhí)行CPU密集型任務(wù),即需要大量的CPU計(jì)算任務(wù)。為什么呢?
這是因?yàn)樵趫?zhí)行CPU密集型任務(wù)時(shí),CPU非常繁忙,因此只需要?jiǎng)?chuàng)建與CPU核心數(shù)相近的線程即可。過(guò)多的線程反而會(huì)導(dǎo)致線程上下文切換,降低任務(wù)執(zhí)行效率。所以,當(dāng)當(dāng)前線程數(shù)超過(guò)核心線程數(shù)時(shí),線程池不會(huì)立即創(chuàng)建更多線程,而是將任務(wù)放入隊(duì)列中等待核心線程的空閑。
但是,在我們通常的Web系統(tǒng)中,存在大量的IO操作,例如數(shù)據(jù)庫(kù)查詢、緩存查詢等。當(dāng)任務(wù)執(zhí)行IO操作時(shí),CPU空閑下來(lái),此時(shí)如果增加執(zhí)行任務(wù)的線程而不是將任務(wù)暫存在隊(duì)列中,可以在單位時(shí)間內(nèi)執(zhí)行更多的任務(wù),從而大大提高任務(wù)執(zhí)行的吞吐量。這種情況下,Tomcat等Web服務(wù)器通常會(huì)對(duì)線程池進(jìn)行定制,當(dāng)線程數(shù)超過(guò)核心線程數(shù)時(shí),它們會(huì)優(yōu)先創(chuàng)建新線程,直到達(dá)到線程池的最大線程數(shù),以更好地適應(yīng)Web系統(tǒng)中的大量IO操作場(chǎng)景。你可以在實(shí)際應(yīng)用中考慮類似的線程池調(diào)整來(lái)提高性能。
此外,要監(jiān)控線程池中隊(duì)列的積壓情況也非常重要,特別是對(duì)于實(shí)時(shí)性要求較高的任務(wù)。我曾在實(shí)際項(xiàng)目中遇到過(guò)一個(gè)奇怪的問(wèn)題:任務(wù)被提交到線程池后,長(zhǎng)時(shí)間沒(méi)有被執(zhí)行。起初,我懷疑是代碼中的bug,但經(jīng)過(guò)排查發(fā)現(xiàn),問(wèn)題出在線程池的配置上,coreThreadCount和maxThreadCount設(shè)置得太小,導(dǎo)致任務(wù)在隊(duì)列中積壓。一旦我增大了這些參數(shù),問(wèn)題就得以解決。
從那以后,我將線程池隊(duì)列的任務(wù)積壓量視為系統(tǒng)監(jiān)控的重要指標(biāo),將其顯示在監(jiān)控大屏上。最后,我要強(qiáng)調(diào),如果使用線程池,請(qǐng)不要使用無(wú)界隊(duì)列(即沒(méi)有設(shè)置固定大小的隊(duì)列)。有人可能認(rèn)為無(wú)界隊(duì)列可以確保任務(wù)永遠(yuǎn)不會(huì)丟失,只要任務(wù)對(duì)實(shí)時(shí)性要求不高,遲早會(huì)被執(zhí)行完。然而,大量任務(wù)的堆積會(huì)占用大量?jī)?nèi)存空間。一旦內(nèi)存空間用盡,將頻繁觸發(fā)Full GC,導(dǎo)致服務(wù)不可用。我曾經(jīng)排查過(guò)一次因?yàn)镕ull GC引發(fā)的系統(tǒng)宕機(jī),而它的根本原因就是系統(tǒng)中的一個(gè)線程池使用了無(wú)界隊(duì)列。因此,理解線程池的關(guān)鍵要點(diǎn)后,我確保在系統(tǒng)中設(shè)置了合適的配置,保障了系統(tǒng)的穩(wěn)定性,成功完成了公司交給我的研發(fā)任務(wù)。
回顧連接池和線程池,它們都有一個(gè)共同特點(diǎn):它們管理的對(duì)象,無(wú)論是連接還是線程,都需要耗費(fèi)相對(duì)較多的時(shí)間和系統(tǒng)資源來(lái)創(chuàng)建。因此,我們將這些對(duì)象放入一個(gè)池中進(jìn)行統(tǒng)一管理,以提高性能和資源的重復(fù)利用。這是一種常見(jiàn)的軟件設(shè)計(jì)思想,稱為池化技術(shù)。其核心思想是通過(guò)提前創(chuàng)建對(duì)象來(lái)減少頻繁創(chuàng)建對(duì)象的性能開(kāi)銷,同時(shí)實(shí)現(xiàn)對(duì)象的統(tǒng)一管理,從而降低對(duì)象使用的成本。總之,池化技術(shù)帶來(lái)了許多好處。
然而,池化技術(shù)也存在一些缺點(diǎn)。例如,存儲(chǔ)池中的對(duì)象會(huì)占用額外的內(nèi)存,如果對(duì)象不經(jīng)常使用,可能會(huì)導(dǎo)致內(nèi)存浪費(fèi)。此外,池中的對(duì)象需要在系統(tǒng)啟動(dòng)時(shí)預(yù)先創(chuàng)建,這可能增加系統(tǒng)啟動(dòng)時(shí)間。然而,這些缺點(diǎn)相對(duì)于池化技術(shù)的優(yōu)勢(shì)來(lái)說(shuō)通常較小,只要我們明確需要頻繁創(chuàng)建和銷毀的對(duì)象在創(chuàng)建時(shí)確實(shí)具有高成本和資源消耗,并且這些對(duì)象確實(shí)會(huì)被頻繁使用,那么使用池化技術(shù)來(lái)優(yōu)化是值得的。
分享題目:池化技術(shù):如何減少頻繁創(chuàng)建數(shù)據(jù)庫(kù)連接的性能損耗?
文章鏈接:http://www.fisionsoft.com.cn/article/cdeoccc.html


咨詢
建站咨詢
