數(shù)據(jù)庫(kù)連接池、線程池等管理的關(guān)鍵點(diǎn),你知道嗎?

在Java應(yīng)用開發(fā)中經(jīng)常會(huì)用到連接池、線程池等池化技術(shù)。池化(pool)技術(shù)的本質(zhì)是通過復(fù)用對(duì)象、連接等資源,減少創(chuàng)建對(duì)象/連接,降低垃圾回收(GC)的開銷,適當(dāng)使用池化相關(guān)技術(shù)能夠顯著提高系統(tǒng)效率,優(yōu)化性能

池化技術(shù)

概念

池化技術(shù):把一些能夠復(fù)用的東西(比如說數(shù)據(jù)庫(kù)連接、線程)放到池中,避免重復(fù)創(chuàng)建、銷毀的開銷,從而極大提高性能。

在開發(fā)過程中我們會(huì)用到很多的連接池,像是數(shù)據(jù)庫(kù)連接池、HTTP 連接池、Redis 連接池等等。而連接池的管理是連接池設(shè)計(jì)的核心,我就以數(shù)據(jù)庫(kù)連接池為例,來說明一下連接池管理的關(guān)鍵點(diǎn)。

數(shù)據(jù)庫(kù)連接池

數(shù)據(jù)庫(kù)連接池有兩個(gè)最重要的配置:最小連接數(shù)和最大連接數(shù),它們控制著從連接池中獲取連接的流程:

如果當(dāng)前連接數(shù)小于最小連接數(shù),則創(chuàng)建新的連接處理數(shù)據(jù)庫(kù)請(qǐng)求

如果線程池中有空閑連接,則使用空閑連接

如果沒有空閑連接,并且當(dāng)前連接數(shù)小于最大連接數(shù),則繼續(xù)創(chuàng)建新的連接

如果當(dāng)前連接數(shù)大于等于最大連接數(shù),并且沒有空閑連接了,則請(qǐng)求按照超時(shí)時(shí)間等待舊連接可用。、

超時(shí)之后,則獲取數(shù)據(jù)庫(kù)連接失敗

對(duì)于數(shù)據(jù)庫(kù)連接池,根據(jù)我的經(jīng)驗(yàn),一般在線上我建議最小連接數(shù)控制在 10 左右,最大連接數(shù)控制在 20~30 左右即可。

數(shù)據(jù)庫(kù)故障的原因可能有下面幾種:

數(shù)據(jù)庫(kù)的域名對(duì)應(yīng)的 IP 發(fā)生了變更,池子的連接還是使用舊的 IP,當(dāng)舊的 IP 下的數(shù)據(jù)庫(kù)服務(wù)關(guān)閉后,再使用這個(gè)連接查詢就會(huì)發(fā)生錯(cuò)誤;

MySQL 有個(gè)參數(shù)是“wait_timeout”,控制著當(dāng)數(shù)據(jù)庫(kù)連接閑置多長(zhǎng)時(shí)間后,數(shù)據(jù)庫(kù)會(huì)主動(dòng)的關(guān)閉這條連接。這個(gè)機(jī)制對(duì)于數(shù)據(jù)庫(kù)使用方是無感知的,所以當(dāng)我們使用這個(gè)被關(guān)閉的連接時(shí)就會(huì)發(fā)生錯(cuò)誤。

那么怎么去避免這種錯(cuò)誤呢?

啟動(dòng)一個(gè)線程來定期檢測(cè)連接池中的連接是否可用,比如使用連接發(fā)送“select 1”的命令給數(shù)據(jù)庫(kù)看是否會(huì)拋出異常,如果拋出異常則將這個(gè)連接從連接池中移除,并且嘗試關(guān)閉。目前 C3P0 連接池可以采用這種方式來檢測(cè)連接是否可用,也是我比較推薦的方式。

在獲取到連接之后,先校驗(yàn)連接是否可用,如果可用才會(huì)執(zhí)行 SQL 語句。比如 DBCP 連接池的 testOnBorrow 配置項(xiàng),就是控制是否開啟這個(gè)驗(yàn)證。這種方式在獲取連接時(shí)會(huì)引入多余的開銷,在線上系統(tǒng)中還是盡量不要開啟,在測(cè)試服務(wù)上可以使用。

線程池

JDK 1.5 中引入的 ThreadPoolExecutor 就是一種線程池的實(shí)現(xiàn),它有兩個(gè)重要的參數(shù):coreThreadCount 和 maxThreadCount,這兩個(gè)參數(shù)控制著線程池的執(zhí)行過程。它的執(zhí)行原理類似如下:

如果線程池中的線程數(shù)少于 coreThreadCount 時(shí),處理新的任務(wù)時(shí)會(huì)創(chuàng)建新的線程

如果線程數(shù)大于 coreThreadCount 則把任務(wù)丟到一個(gè)隊(duì)列里面,由當(dāng)前空閑的線程執(zhí)行

當(dāng)隊(duì)列中的任務(wù)堆積滿了的時(shí)候,則繼續(xù)創(chuàng)建線程,直到達(dá)到 maxThreadCount

當(dāng)線程數(shù)達(dá)到 maxTheadCount 時(shí)還有新的任務(wù)提交,那么我們就不得不將它們丟棄了


這個(gè)任務(wù)處理流程看似簡(jiǎn)單,實(shí)際上有很多坑,你在使用的時(shí)候一定要注意。

首先, JDK 實(shí)現(xiàn)的這個(gè)線程池優(yōu)先把任務(wù)放入隊(duì)列暫存起來,而不是創(chuàng)建更多的線程,**它比較適用于執(zhí)行 CPU 密集型的任務(wù),也就是需要執(zhí)行大量 CPU 運(yùn)算的任務(wù)。**這是為什么呢?因?yàn)閳?zhí)行 CPU 密集型的任務(wù)時(shí) CPU 比較繁忙,因此只需要?jiǎng)?chuàng)建和 CPU 核數(shù)相當(dāng)?shù)木€程就好了,多了反而會(huì)造成線程上下文切換,降低任務(wù)執(zhí)行效率。所以當(dāng)當(dāng)前線程數(shù)超過核心線程數(shù)時(shí),線程池不會(huì)增加線程,而是放在隊(duì)列里等待核心線程空閑下來。

但是,我們平時(shí)開發(fā)的 Web 系統(tǒng)通常都有大量的 IO 操作,比方說查詢數(shù)據(jù)庫(kù)、查詢緩存等等。任務(wù)在執(zhí)行 IO 操作的時(shí)候 CPU 就空閑了下來,這時(shí)如果增加執(zhí)行任務(wù)的線程數(shù)而不是把任務(wù)暫存在隊(duì)列中,就可以在單位時(shí)間內(nèi)執(zhí)行更多的任務(wù),大大提高了任務(wù)執(zhí)行的吞吐量。所以你看 Tomcat 使用的線程池就不是 JDK 原生的線程池,而是做了一些改造,當(dāng)線程數(shù)超過 coreThreadCount 之后會(huì)優(yōu)先創(chuàng)建線程,直到線程數(shù)到達(dá) maxThreadCount,這樣就比較適合于 Web 系統(tǒng)大量 IO 操作的場(chǎng)景了,你在實(shí)際運(yùn)用過程中也可以參考借鑒。

其次,線程池中使用的隊(duì)列的堆積量也是我們需要監(jiān)控的重要指標(biāo),對(duì)于實(shí)時(shí)性要求比較高的任務(wù)來說,這個(gè)指標(biāo)尤為關(guān)鍵。

我在實(shí)際項(xiàng)目中就曾經(jīng)遇到過任務(wù)被丟給線程池之后,長(zhǎng)時(shí)間都沒有被執(zhí)行的詭異問題。最初,我認(rèn)為這是代碼的 Bug 導(dǎo)致的,后來經(jīng)過排查發(fā)現(xiàn),是因?yàn)榫€程池的 coreThreadCount 和 maxThreadCount 設(shè)置的比較小,導(dǎo)致任務(wù)在線程池里面大量的堆積,在調(diào)大了這兩個(gè)參數(shù)之后問題就解決了(任務(wù)早早被放到隊(duì)列中堆積著,并且由于coreThreadCount比較小,導(dǎo)致工作線程比較少,處理速度較慢)。跳出這個(gè)坑之后,我就把重要線程池的隊(duì)列任務(wù)堆積量,作為一個(gè)重要的監(jiān)控指標(biāo)放到了系統(tǒng)監(jiān)控大屏上。

最后,如果你使用線程池請(qǐng)一定記住不要使用無界隊(duì)列(即沒有設(shè)置固定大小的隊(duì)列)。也許你會(huì)覺得使用了無界隊(duì)列后,任務(wù)就永遠(yuǎn)不會(huì)被丟棄,只要任務(wù)對(duì)實(shí)時(shí)性要求不高,反正早晚有消費(fèi)完的一天。但是,大量的任務(wù)堆積會(huì)占用大量的內(nèi)存空間,一旦內(nèi)存空間被占滿就會(huì)頻繁地觸發(fā) Full GC,造成服務(wù)不可用,我之前排查過的一次 GC 引起的宕機(jī),起因就是系統(tǒng)中的一個(gè)線程池使用了無界隊(duì)列。

這時(shí),你回顧一下這兩種技術(shù),會(huì)發(fā)現(xiàn)它們都有一個(gè)共同點(diǎn):它們所管理的對(duì)象,無論是連接還是線程,它們的創(chuàng)建過程都比較耗時(shí),也比較消耗系統(tǒng)資源。所以,我們把它們放在一個(gè)池子里統(tǒng)一管理起來,以達(dá)到提升性能和資源復(fù)用的目的。

這是一種常見的軟件設(shè)計(jì)思想,叫做池化技術(shù),它的核心思想是空間換時(shí)間,期望使用預(yù)先創(chuàng)建好的對(duì)象來減少頻繁創(chuàng)建對(duì)象的性能開銷,同時(shí)還可以對(duì)對(duì)象進(jìn)行統(tǒng)一的管理,降低了對(duì)象的使用的成本,總之是好處多多。

不過,池化技術(shù)也存在一些缺陷,比方說存儲(chǔ)池子中的對(duì)象肯定需要消耗多余的內(nèi)存,如果對(duì)象沒有被頻繁使用,就會(huì)造成內(nèi)存上的浪費(fèi)。再比方說,池子中的對(duì)象需要在系統(tǒng)啟動(dòng)的時(shí)候就預(yù)先創(chuàng)建完成,這在一定程度上增加了系統(tǒng)啟動(dòng)時(shí)間。

可這些缺陷相比池化技術(shù)的優(yōu)勢(shì)來說就比較微不足道了,只要我們確認(rèn)要使用的對(duì)象在創(chuàng)建時(shí)確實(shí)比較耗時(shí)或者消耗資源,并且這些對(duì)象也確實(shí)會(huì)被頻繁地創(chuàng)建和銷毀,我們就可以使用池化技術(shù)來優(yōu)化。

java性能優(yōu)化,通常要考慮GC, 線程上下文切換,網(wǎng)絡(luò)IO操作的影響;池化技術(shù)可在一定場(chǎng)景下很好的規(guī)避這些問題,如對(duì)象(內(nèi)存)池,線程池,連接池等; 本文講幾個(gè)典型案例;

一. 規(guī)避GC--對(duì)象池

apache common-pool對(duì)象池,對(duì)象復(fù)用,完整的狀態(tài)管理;

二. 規(guī)避線程上下文切換損失---線程池

1 線程池主要類型:

newCachedThreadPool如果線程池長(zhǎng)度超過處理需要,可靈活回收空閑線程,若無可回收,則新建線程;newFixedThreadPool創(chuàng)建一個(gè)指定工作線程數(shù)量的線程池,不能處理任務(wù)暫時(shí)放如隊(duì)列;newSingleThreadExecutor創(chuàng)建一個(gè)單線程,發(fā)生異常,新建一個(gè)來替代,保證所有任務(wù)按照指定順序(FIFO, LIFO,優(yōu)先級(jí))執(zhí)行newScheduleThreadPool創(chuàng)建一個(gè)定長(zhǎng)的線程池,而且支持定時(shí)的以及周期性的任務(wù)執(zhí)行

2 線程池使用注意事項(xiàng)

線程安全選擇合適的構(gòu)造參數(shù),如區(qū)分是計(jì)算cpu密集型還是io密集型,前者選擇核心線程數(shù)可根據(jù)cpu cores;不要將異常直接拋給線程;合理關(guān)閉,防止拖垮操作系統(tǒng)資源

三. 規(guī)避io網(wǎng)絡(luò)連接 ---鏈接池

1 druid數(shù)據(jù)源 :

優(yōu)點(diǎn)--維持到指定數(shù)據(jù)庫(kù)的多個(gè)可用連接池;包含調(diào)用統(tǒng)計(jì)、慢查詢統(tǒng)計(jì)、斷路重連;曾遇到問題:拿不到鏈接;? 排查原因:慢查詢太多,maxActive設(shè)置太低;解決方法,臨時(shí)增加maxActive值,后續(xù)解決掉慢查詢;

2 httpClient:

? 優(yōu)點(diǎn):維持到指定路由的http連接池,省去了tcp的3次握手和4次揮手的時(shí)間,極大降低請(qǐng)求響應(yīng)的時(shí)間

? 適合場(chǎng)景:服務(wù)之間的互相調(diào)用,且調(diào)用比較頻繁;

? 注意項(xiàng):? 不要直接關(guān)閉http鏈接,而是交給連接池來處理;

3 lettuce

? ? 每個(gè)節(jié)點(diǎn)保持一個(gè)鏈接,不存在線程安全問題,基于netty維持鏈接池;多路復(fù)用,事件驅(qū)動(dòng);

四. 規(guī)避native內(nèi)存申請(qǐng)和GC---本地內(nèi)存池

1 . JVM直接內(nèi)存的特征

a.申請(qǐng):相對(duì)于堆內(nèi)存,申請(qǐng)效率較低b.GC:JVM中的直接內(nèi)存,通常在老年區(qū)滿了觸發(fā)FullGC時(shí),才會(huì)觸發(fā)回收;JVM中的直接內(nèi)存,存在堆內(nèi)存中其實(shí)就是DirectByteBuffer類,和實(shí)際內(nèi)存就是引用關(guān)系;DirectByteBuffer熬過了幾次younggc之后,會(huì)進(jìn)入老年代。當(dāng)老年代滿了之后,會(huì)觸發(fā)FullGC。因?yàn)楸旧砗苄。茈y占滿老年代,因此基本不會(huì)觸發(fā)FullGC,帶來的后果是大量堆外內(nèi)存一直占著不放,無法進(jìn)行內(nèi)存回收;? 每次申請(qǐng)直接內(nèi)存,都先看看是否超限 —— 直接內(nèi)存的限額默認(rèn)(可用-XX:MaxDirectMemorySize重新設(shè)定)。如果超過限額,就會(huì)主動(dòng)執(zhí)行System.gc(),這樣會(huì)帶來一個(gè)影響,系統(tǒng)會(huì)中斷100ms。如果沒有成功回收直接內(nèi)存,并且還是超過直接內(nèi)存的限額,就會(huì)拋出OOM——內(nèi)存溢出。c.好處: 減少內(nèi)次copy

2. JVM直接內(nèi)容使用優(yōu)化方案---直接內(nèi)存池

3. netty內(nèi)存池

a.內(nèi)存分級(jí)從上到下主要分為:Arena,ChunkList,Chunk,Page,SubPage五級(jí)

b.內(nèi)存池內(nèi)存分配入口是PoolByteBufAllocator類,該類最終將內(nèi)存分配委托給PoolArena進(jìn)行;為了減少高并發(fā)下多線程內(nèi)存分配碰撞帶來的性能影響,PoolByteBufAllocator維護(hù)著一個(gè)PoolArena數(shù)組,線程通過輪詢獲取其中一個(gè)進(jìn)行內(nèi)存分配,進(jìn)而實(shí)現(xiàn)鎖分離;

c.內(nèi)存分配的基本單元是PoolChunk,從PoolArena中分配獲取一個(gè)PoolChunk,一個(gè)PoolChunk包含多個(gè)Page內(nèi)存頁(yè),通過完全二叉樹維護(hù)多個(gè)內(nèi)存頁(yè)用于內(nèi)存分配--請(qǐng)參照slab分配,Buddy(伙伴)分配;

4. netty可回收對(duì)象池

Netty自己實(shí)現(xiàn)了一套輕量級(jí)的對(duì)象池。在Netty中,通常會(huì)有多個(gè)IO線程獨(dú)立工作,基于NioEventLoop的實(shí)現(xiàn),每個(gè)IO線程輪詢單獨(dú)的Selector實(shí)例來檢索IO事件,并在IO來臨時(shí)開始處理。最常見的IO操作就是讀寫,具體到NIO就是從內(nèi)核緩沖區(qū)拷貝數(shù)據(jù)到用戶緩沖區(qū)或者從用戶緩沖區(qū)拷貝數(shù)據(jù)到內(nèi)核緩沖區(qū)。這里會(huì)涉及到大量的創(chuàng)建和回收Buffer,Netty對(duì)Buffer進(jìn)行了池化從而降低系統(tǒng)開銷。

a.借用:首先判斷池中是否存在對(duì)象,如果由則優(yōu)先從本地線程stack中獲取Object,如果stack為空時(shí),再將其他線程queue集合的所有對(duì)象根據(jù)一定的規(guī)則轉(zhuǎn)移到本地線程stack中(1/7規(guī)則),然后再?gòu)膕tack獲取Object并返回.如果池中不存在對(duì)象,創(chuàng)建對(duì)象并返回。

b.回收:如果當(dāng)前回收的線程是原始線程(創(chuàng)建對(duì)象的線程),如果超過Stack的容量(默認(rèn)4*1024)或者1/7規(guī)則 就drop(由jvm回收釋放)

總結(jié)

連接池和線程池你并不陌生,不過你可能對(duì)它們的原理和使用方式上還存在困惑或者誤區(qū),我在面試時(shí),就發(fā)現(xiàn)有很多的同學(xué)對(duì)線程池的基本使用方式都不了解。借用這節(jié)課,我想再次強(qiáng)調(diào)的重點(diǎn)是:

池子的最大值和最小值的設(shè)置很重要,初期可以依據(jù)經(jīng)驗(yàn)來設(shè)置,后面還是需要根據(jù)實(shí)際運(yùn)行情況做調(diào)整。

池子中的對(duì)象需要在使用之前預(yù)先初始化完成,這叫做池子的預(yù)熱,比方說使用線程池時(shí)就需要預(yù)先初始化所有的核心線程。如果池子未經(jīng)過預(yù)熱可能會(huì)導(dǎo)致系統(tǒng)重啟后產(chǎn)生比較多的慢請(qǐng)求。

池化技術(shù)核心是一種空間換時(shí)間優(yōu)化方法的實(shí)踐,所以要關(guān)注空間占用情況,避免出現(xiàn)空間過度使用出現(xiàn)內(nèi)存泄露或者頻繁垃圾回收等問題。

最后希望大家能從文章中得到幫助獲得收獲,也可以評(píng)論出你想看哪方面的技術(shù)。文章會(huì)持續(xù)更新,希望能幫助到大家,哪怕是讓你靈光一現(xiàn)。喜歡的朋友可以點(diǎn)點(diǎn)贊和關(guān)注,也可以分享出去讓更多的人看見,一起努力一起進(jìn)步!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容