大家可能會有這樣疑問:連接配接池類似于線程池或者對象池,就是一個放連接配接的池子,使用的時候從裡面拿一個,用完了再歸還,功能非常簡單,有什麼可講的。
可能還會有這樣的疑問:高性能這麼高大上,一個小小的連接配接池,如何跟高大上靠上邊的。
本主題将會全面介紹連接配接池原理,高性能的設計,優化實踐,現有連接配接池的瓶頸及解決方案。同時也會介紹唯品會自研資料庫連接配接池産品(代号:caelus)
先看一下連接配接池所處的位置:

應用架構的業務實作一般都會通路資料庫,緩存或者http服務。為什麼要在通路的地方加上一個連接配接池呢?
下面以通路mysql為例,執行一個sql指令,如果不使用連接配接池,需要經過哪些流程。
tcp建立連接配接的三次握手
mysql認證的三次握手
真正的sql執行
mysql的關閉
tcp的四次握手關閉
可以看到,為了執行一條sql,卻多了非常多我們不關心的網絡互動。
<b>優點:</b>實作簡單。
<b>缺點:</b>
網絡io較多
資料庫的負載較高
響應時間較長及qps較低
應用頻繁的建立連接配接和關閉連接配接,導緻臨時對象較多,gc頻繁
在關閉連接配接後,會出現大量time_wait 的tcp狀态(在2個msl之後關閉)
第一次通路的時候,需要建立連接配接。 但是之後的通路,均會複用之前建立的連接配接。
<b>優點:</b>
較少了網絡開銷
系統的性能會有一個實質的提升
沒了麻煩的time_wait狀态
當然,現實往往是殘酷的,當我們解決了一個問題的時候,同時伴随着另外一個問題的産生。
使用連接配接池面臨的最大挑戰: <b>連接配接池的性能</b>
<b>分庫db部署結構:</b>
假設有128個分庫:32個伺服器,每個伺服器有4個schema。按照128個分庫的設計,便會建立128個獨立資料庫連接配接池。
<b>資料庫連接配接池的模型</b>
<b>特點:</b>
128個連接配接池完全獨立,不同的schema也對應不同的連接配接池。
先通過拆庫,讀寫等政策選擇對應的連接配接池,再從連接配接池擷取一個連接配接進行操作。
操作完後,再将連接配接歸還到對應的連接配接池中。
結構簡單,分散競争
<b>面臨的問題:</b>
<b>1:線程數過多</b>
先看一下建立一個連接配接池,需要建立的線程數的個數。
連接配接池
線程數
128個分庫需要的線程數
c3p0
4
3個helperthread (pollerthread),1個定時任務admintasktimer(deadlockdetector)
4*128=512
dbcp
1
負責心跳,最小連接配接數維持,最大空閑時間和防連接配接洩露
1*128=128
druid
2
一個異步建立連接配接。一個異步關閉連接配接。
2*128=256
可以看到随着分庫的增加,不管選用哪個連接配接池,線程的個數均會線性增長。線程數過多将會導緻記憶體占用較大: 預設1個線程會占用1m的空間,如果是512個線程,則會占用1m*512=512m上下文切換開銷。
<b>tips</b>:由于stack和heap申請為虛位址空間,但是一旦使用就不會釋放。(線程也不一定會占用1m的空間)
<b>2:連接配接數過多</b>
資料庫的連接配接資源比較重,并且随着連接配接的增加,資料庫的性能會有明顯的下降。dba一般會限制每個db建立連接配接的個數,比如限制為3k 。假設資料庫單台限制3k,32台則容量為3k*32=96k。如果應用最大,最小連接配接數均為10,則每個應用總計需要128*10=1.28k個連接配接。那麼資料庫理論上支援的應用個數為96k/1.28k= 80 台
<b>3:不能連接配接複用</b>
同一個實體機下面不同的schema完全獨立,連接配接不能複用
<b>優化後的資料庫連接配接池模型</b>
隻有一個連接配接池,所有節點共享線程 (解決了線程數過多的問題)
每個實體機對應一個host, host裡面維護多個schema,schema存放連接配接。
同一個host下面的不同schema 可以進行連接配接複用(解決連接配接數過多的問題)
<b>擷取連接配接流程:</b>
擷取連接配接需要帶上 ip,port和schema資訊:比如擷取的是host31的schema1
先到host31的schema1中擷取空閑連接配接,但是schema1無空閑連接配接,便會從schema2中擷取空閑連接配接。
從schema2中擷取的連接配接執行useschema1,該連接配接便切換到schema1上面。
執行對應的sql操作,執行完成後,歸還連接配接到schema1的池子裡面。
連接配接複用:有效減少連接配接數。
提升性能:避免頻繁的建立連接配接。建立連接配接的開銷比較大,而使用use schema開銷非常小
有效減少線程數。按現有方案大概隻需要4個線程即可。而優化前需要512個線程
管理較為複雜
不符合jdbc接口規範。datasource隻有簡單的getconnection()接口,沒有針對擷取對應schema的連接配接的接口。需要繼承datasouce,實作特定接口。
<b>優化前執行事務的模型</b>
從連接配接池裡面擷取到連接配接,預設是自動送出。為了開啟事務,需要執行setautocommit=false 操作,然後再執行具體的sql,歸還連接配接的時候,還需要将連接配接設定為自動送出(需要執行set autocommit=true) 。可以看到開啟事務,需要額外執行兩條事務的語句。
<b>優化後執行事務的模型</b>
每個schema裡面所有的連接配接會按照autocommit進行分組。 分為自動送出(autocommit=true) 和非自動送出(autocommit=false)。擷取連接配接時優先擷取相同autocommit的分組裡的連接配接,如果沒有可用連接配接則從另外一個分組中擷取連接配接,業務操作執行完後,再歸還到對應的分組裡面。該種機制避免了開啟事務多執行的兩條事務語句。
連接配接池主要包含五部分:擷取連接配接,歸還連接配接,定時任務,維護元件及資源池
<b>擷取連接配接:</b>
擷取逾時:如果超過規定時間未擷取到連接配接,則會抛出異常
有效性檢查:當從資源池裡面擷取到資源,需要檢查該資源的有效性,如果失效,再次擷取連接配接。避免執行業務的時候報錯。
建立連接配接:可以同步建立,也可以異步建立。
<b>歸還連接配接:</b>
歸還連接配接:比如需要檢查最大空閑數,确定是實體關閉還是歸還到連接配接池
銷毀連接配接: 可同步銷毀也可異步銷毀
<b>定時任務:</b>
空閑檢查:主要是檢查空閑連接配接,連接配接空閑超過一定時間,則會關閉連接配接。
最小連接配接數控制:一般會設定最小連接配接數。保證目前系統裡面最小的連接配接數。如果不夠,則會建立連接配接。
<b>元件維護:</b>
<b>連接配接狀态控制</b>:空閑,使用,删除等狀态控制
<b>異常處理</b>:對jdbc通路的異常統一處理,如果異常與連接配接相關,則會将該連接配接銷毀掉。
<b>緩存</b>:避免對sql重複解析,preparestatement機制下,會對sql解析的對象進行緩存。
<b>jdbc封裝</b>:對jdbc進行了實作,真正的實作是底層的driver,比如mysql-connector-java 。
<b>資源池:</b>
資源池是存放連接配接的地方,也是連接配接池最核心的地方。
所有的元件基本上都與資源池進行互動,對連接配接資源的競争非常激烈。該處的性能将決定了整個連接配接池的性能。
一般資源池的實作是使用jdk提供的blockingqueue。那麼是否有方案可以進行無鎖的設計,來避免競争。
<b>擷取連接配接大概流程:</b>
從threadlocal裡面擷取連接配接,如果沒有空閑連接配接,則從全局連接配接池(copyonwritearraylist)中擷取。
如果全局連接配接池中沒有空閑連接配接,則會異步建立連接配接。
判定逾時時間是否大于門檻值,如果小于門檻值,則進行自旋。否則進行park休眠。
連接配接建立成功後,會對park的線程進行喚醒
主要從四個方面實作了無鎖的設計:threadlocal,copyonwritearraylist,異步建立連接配接及自旋。
<b>threadlocal</b>
每個線程均有一個連接配接隊列。該隊列是全局隊列的引用。
擷取連接配接時先從threadlocal裡面拿連接配接,如果連接配接是空閑狀态,則使用。否則移除掉,再拿下一個,直到拿不到連接配接為止。
歸還連接配接時,隻需要歸還到threadlocal的隊列裡面,同時設定連接配接為空閑狀态
如果使用blockqueue,擷取連接配接時調用poll,歸還連接配接時調用offer,存在兩次鎖的競争。優化後通過cas避免了兩次鎖的開銷(擷取連接配接時,使用cas置連接配接為非空閑狀态;歸還時,使用cas置連接配接為空閑狀态)
<b>copyonwritearraylist</b>
該隊列使用場景是:大量讀,少量寫的操作,并且存儲的資料比較有限。而連接配接池的場景非常适合采用copyonwritearraylist。
在擷取連接配接或者歸還連接配接時,隻會通過cas更改連接配接的狀态,不會對連接配接池進行添加或者删除的操作。
一般連接配接池連接配接的個數比較可控,copyonwritearraylist在寫操作時會對所有連接配接進行拷貝,對記憶體影響不大。
<b>異步建立連接配接</b>
擷取到連接配接後,判斷一下是否有并發正在等待擷取連接配接,如果有,則異步建立連接配接。避免下一個連接配接的等待。如果copyonwritearraylist沒有空閑連接配接,則異步建立連接配接。
<b>自旋</b>
該自旋比較類似于jdk對synchronized的自旋機制。如果發現逾時時間大于設定的門檻值(比如10微秒),則會進行線程挂起。如果小于設定的門檻值,則重新擷取連接配接,進行自選,避免線程的上下文切換帶來的性能開銷。
<b>方法内聯優化</b>
每調用一次方法,線程便會建立一個棧幀,建立棧幀開銷相對比較大
jit在運作時會進行内聯優化,多個方法使用一個棧幀,避免棧幀建立過多
jit方法内聯優化預設的位元組碼個數門檻值是35個位元組,低于35個位元組,才會進行優化。(可通過-xx:maxinlinesize=35進行設定)
通過修改上述代碼,編譯後位元組碼修改到34個位元組,則可以滿足内聯的條件。
<b>心跳語句選擇</b>
<b>preparestatement模式選擇</b>
mysql driver預設是client模式,如果需要開啟server模式,需要設定 useserverprepstmts=true 。preparestatement預設的client模式和statement對于db端沒有差別。大家普遍了解preparestatement和statement的差別是preparestatement可以避免sql注入。但是避免sql注入是如何做到的?
使用preparestatement設定參數的時候,比如調用setstring(int parameterindex, string x),本地會對設定的參數進行轉義來避免sql注入。
執行sql的時候,會将sql的?替換成轉義後的字元,發送到資料庫執行。
<b>pscache</b>
<b> </b>
mysqldriver 預設不開啟,可通過設定 cacheprepstmts = true 進行開啟
<b>querytimeout</b>
之前也遇到因為開啟了querytimeout,導緻連接配接洩露的問題。
具體可參考:
mysql driver的bug發現之旅:
<a target="_blank" href="http://blog.csdn.net/hetaohappy/article/details/52091005">http://blog.csdn.net/hetaohappy/article/details/52091005</a>
關于連接配接池的最優配置之前做過整理,可參考:
<a target="_blank" href="http://blog.csdn.net/hetaohappy/article/details/51861015">http://blog.csdn.net/hetaohappy/article/details/51861015</a>
caelus是唯品會自研的高性能的分布式的資料庫連接配接池。
高性能:基于無鎖的連接配接池設計模型來提升連接配接池性能;
在分庫較多的場景下,減少線程數。 假如有128個分庫,現有連接配接池模型下則需要使用128個獨立的連接配接池,每個連接配接池都需要線程(1-4個,不同的連接配接池不同)處理任務。則總共需要維護128到128*4個線程,開銷巨大。而caelus連接配接池會大大減少線程數。
連接配接複用。 對于 一個mysql 的instance上面有多個schema場景下。現有連接配接池不同的schema的連接配接不可複用。而caelus可以複用不同schema的連接配接,提升性能。
過多的事務指令。如果是事務語句,則從連接配接池拿到連接配接後,需要先開啟事務(setautocommit=false),歸還時需要再設定(set autocommit=true)。每使用一次連接配接,均需要額外執行兩條事務指令。caelus能有效減少事務指令。
配置規範的統一。結合mysql的設定,提供規範統一,最優的配置。
本主題講解了資料庫連接配接池的性能設計優化。 關于緩存(memcache,redis)
和http連接配接池的優化實踐,後面有機會再和大家一塊溝通學習。
關于netty的純異步http連接配接池設計,可參考:
<a target="_blank" href="http://blog.csdn.net/hetaohappy/article/details/51867059">http://blog.csdn.net/hetaohappy/article/details/51867059</a>
<b>作者簡介:</b>
<b>何濤,</b>現任職于唯品會平台架構部,要負責資料通路層,網關,資料庫中間件,平台架構等開發設計工作。在資料庫性能優化,架構設計等方面有着大量的經驗積累。熱衷于高可用,高并發及高性能的架構研究。<b></b>
<b>本文轉載自微信公衆号 中生代技術 freshmantechnology</b>