如何打造高性能的數據庫鏈接池框架,能夠從哪些角度進行優化,鏈接池的大量優化實踐如何爲你的系統保駕護航,本專題將帶你走進鏈接池的世界,爲你一一揭曉。java
你們可能會有這樣疑問:鏈接池相似於線程池或者對象池,就是一個放鏈接的池子,使用的時候從裏面拿一個,用完了再歸還,功能很是簡單,有什麼可講的。數據庫
可能還會有這樣的疑問:高性能這麼高大上,一個小小的鏈接池,如何跟高大上靠上邊的。緩存
本主題將會全面介紹鏈接池原理,高性能的設計,優化實踐,現有鏈接池的瓶頸及解決方案。同時也會介紹惟品會自研數據庫鏈接池產品(代號:Caelus)性能優化
先看一下鏈接池所處的位置:服務器
應用框架的業務實現通常都會訪問數據庫,緩存或者HTTP服務。爲何要在訪問的地方加上一個鏈接池呢?網絡
下面以訪問MySQL爲例,執行一個SQL命令,若是不使用鏈接池,須要通過哪些流程。併發
1:TCP創建鏈接的三次握手框架
2:MySQL認證的三次握手異步
3:真正的SQL執行分佈式
4:MySQL的關閉
5:TCP的四次握手關閉
能夠看到,爲了執行一條SQL,卻多了很是多咱們不關心的網絡交互。
優勢:實現簡單。
缺點:
1:網絡IO較多
2:數據庫的負載較高
3:響應時間較長及QPS較低
4:應用頻繁的建立鏈接和關閉鏈接,致使臨時對象較多,GC頻繁
5:在關閉鏈接後,會出現大量TIME_WAIT 的TCP狀態(在2個MSL以後關閉)
第一次訪問的時候,須要創建鏈接。 可是以後的訪問,均會複用以前建立的鏈接。
優勢:
1:較少了網絡開銷
2:系統的性能會有一個實質的提高
3:沒了麻煩的TIME_WAIT狀態
固然,現實每每是殘酷的,當咱們解決了一個問題的時候,同時伴隨着另一個問題的產生。
使用鏈接池面臨的最大挑戰: 鏈接池的性能
分庫DB部署結構:
假設有128個分庫:32個服務器,每一個服務器有4個schema。按照128個分庫的設計,便會新建128個獨立數據庫鏈接池。
特色:
1:128個鏈接池徹底獨立,不一樣的schema也對應不一樣的鏈接池
2:先經過拆庫,讀寫等策略選擇對應的鏈接池,再從鏈接池獲取一個鏈接進行操做
3:操做完後,再將鏈接歸還到對應的鏈接池中。
優勢:
結構簡單,分散競爭
面臨的問題:
1:線程數過多
先看一下新建一個鏈接池,須要新建的線程數的個數。
鏈接池 | 線程數 | 描述 | 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上下文切換開銷。
Tips:因爲stack和heap申請爲虛地址空間,可是一旦使用就不會釋放。(線程也不必定會佔用1M的空間)
2:鏈接數過多
數據庫的鏈接資源比較重,而且隨着鏈接的增長,數據庫的性能會有明顯的降低。DBA通常會限制每一個DB創建鏈接的個數,好比限制爲3K 。假設數據庫單臺限制3K,32臺則容量爲3K*32=96K。若是應用最大,最小鏈接數均爲10,則每一個應用總計須要128*10=1.28K個鏈接。那麼數據庫理論上支持的應用個數爲96K/1.28K= 80 臺
3:不能鏈接複用
同一個物理機下面不一樣的schema徹底獨立,鏈接不能複用
特色:
1:只有一個鏈接池,全部節點共享線程 (解決了線程數過多的問題)
2:每一個物理機對應一個host, host裏面維護多個schema,schema存放鏈接。
3:同一個host下面的不一樣schema 能夠進行鏈接複用(解決鏈接數過多的問題)
獲取鏈接流程:
1:獲取鏈接須要帶上 ip,port和schema信息:好比獲取的是host31的schema1
2:先到host31的schema1中獲取空閒鏈接,可是schema1無空閒鏈接,便會從schema2中獲取空閒鏈接。
3:從schema2中獲取的鏈接執行useschema1,該鏈接便切換到schema1上面。
4:執行對應的SQL操做,執行完成後,歸還鏈接到schema1的池子裏面。
優勢:
1:鏈接複用:有效減小鏈接數。
2:提高性能:避免頻繁的新建鏈接。新建鏈接的開銷比較大,而使用use schema開銷很是小
3:有效減小線程數。按現有方案大概只須要4個線程便可。而優化前須要512個線程
缺點:
1:管理較爲複雜
2:不符合JDBC接口規範。DataSource只有簡單的getConnection()接口,沒有針對獲取對應schema的鏈接的接口。須要繼承DataSouce,實現特定接口。
事務語句性能優化
從鏈接池裏面獲取到鏈接,默認是自動提交。爲了開啓事務,須要執行setautocommit=false 操做,而後再執行具體的SQL,歸還鏈接的時候,還須要將鏈接設置爲自動提交(須要執行set autocommit=true) 。能夠看到開啓事務,須要額外執行兩條事務的語句。
每一個schema裏面全部的鏈接會按照autocommit進行分組。 分爲自動提交(autocommit=true) 和非自動提交(autocommit=false)。獲取鏈接時優先獲取相同autocommit的分組裏的鏈接,若是沒有可用鏈接則從另一個分組中獲取鏈接,業務操做執行完後,再歸還到對應的分組裏面。該種機制避免了開啓事務多執行的兩條事務語句。
鏈接池的通用功能:
鏈接池主要包含五部分:獲取鏈接,歸還鏈接,定時任務,維護組件及資源池
獲取鏈接:
1:獲取超時:若是超過規定時間未獲取到鏈接,則會拋出異常
2:有效性檢查:當從資源池裏面獲取到資源,須要檢查該資源的有效性,若是失效,再次獲取鏈接。避免執行業務的時候報錯。
3:建立鏈接:能夠同步建立,也能夠異步建立。
歸還鏈接:
1:歸還鏈接:好比須要檢查最大空閒數,肯定是物理關閉仍是歸還到鏈接池
2:銷燬鏈接: 可同步銷燬也可異步銷燬
定時任務:
1:空閒檢查:主要是檢查空閒鏈接,鏈接空閒超過必定時間,則會關閉鏈接。
2:最小鏈接數控制:通常會設置最小鏈接數。保證當前系統裏面最小的鏈接數。若是不夠,則會新建鏈接。
組件維護:
1:鏈接狀態控制:空閒,使用,刪除等狀態控制
2:異常處理:對JDBC訪問的異常統一處理,若是異常與鏈接相關,則會將該鏈接銷燬掉。
3:緩存:避免對SQL重複解析,PrepareStatement機制下,會對SQL解析的對象進行緩存。
4:JDBC封裝:對JDBC進行了實現,真正的實現是底層的driver,好比MySQL-connector-java 。
資源池:
1:資源池是存放鏈接的地方,也是鏈接池最核心的地方。
2:全部的組件基本上都與資源池進行交互,對鏈接資源的競爭很是激烈。該處的性能將決定了整個鏈接池的性能。
3:通常資源池的實現是使用JDK提供的BlockingQueue。那麼是否有方案能夠進行無鎖的設計,來避免競爭。
獲取鏈接大概流程:
1:從ThreadLocal裏面獲取鏈接,若是沒有空閒鏈接,則從全局鏈接池(CopyOnWriteArrayList)中獲取。
2:若是全局鏈接池中沒有空閒鏈接,則會異步新建鏈接。
3:斷定超時時間是否大於閾值,若是小於閾值,則進行自旋。不然進行park休眠。
4:鏈接創建成功後,會對park的線程進行喚醒
主要從四個方面實現了無鎖的設計:ThreadLocal,CopyOnWriteArrayList,異步創建鏈接及自旋。
1:每一個線程均有一個鏈接隊列。該隊列是全局隊列的引用。
2:獲取鏈接時先從ThreadLocal裏面拿鏈接,若是鏈接是空閒狀態,則使用。不然移除掉,再拿下一個,直到拿不到鏈接爲止。
3:歸還鏈接時,只須要歸還到Threadlocal的隊列裏面,同時設置鏈接爲空閒狀態
4:若是使用BlockQueue,獲取鏈接時調用poll,歸還鏈接時調用offer,存在兩次鎖的競爭。優化後經過CAS避免了兩次鎖的開銷(獲取鏈接時,使用CAS置鏈接爲非空閒狀態;歸還時,使用CAS置鏈接爲空閒狀態)
1:該隊列使用場景是:大量讀,少許寫的操做,而且存儲的數據比較有限。而鏈接池的場景很是適合採用CopyOnWriteArrayList。
2:在獲取鏈接或者歸還鏈接時,只會經過CAS更改鏈接的狀態,不會對鏈接池進行添加或者刪除的操做。
3:通常鏈接池鏈接的個數比較可控,CopyOnWriteArrayList在寫操做時會對全部鏈接進行拷貝,對內存影響不大。
獲取到鏈接後,判斷一下是否有併發正在等待獲取鏈接,若是有,則異步創建鏈接。避免下一個鏈接的等待。若是CopyOnWriteArrayList沒有空閒鏈接,則異步創建鏈接。
該自旋比較相似於JDK對synchronized的自旋機制。若是發現超時時間大於設定的閾值(好比10微秒),則會進行線程掛起。若是小於設定的閾值,則從新獲取鏈接,進行自選,避免線程的上下文切換帶來的性能開銷。。
方法內聯優化
1:每調用一次方法,線程便會新建一個棧幀,新建棧幀開銷相對比較大
2:JIT在運行時會進行內聯優化,多個方法使用一個棧幀,避免棧幀新建過多
3:JIT方法內聯優化默認的字節碼個數閾值是35個字節,低於35個字節,纔會進行優化。(可經過-XX:MaxInlineSize=35進行設置)
經過修改上述代碼,編譯後字節碼修改到34個字節,則能夠知足內聯的條件。
MySQL driver默認是client模式,若是須要開啓server模式,須要設置 useServerPrepStmts=true 。PrepareStatement默認的client模式和Statement對於DB端沒有區別。你們廣泛理解PrepareStatement和Statement的區別是PrepareStatement能夠避免SQL注入。可是避免SQL注入是如何作到的?
使用PrepareStatement設置參數的時候,好比調用setString(int parameterIndex, String x),本地會對設置的參數進行轉義來避免SQL注入。
執行SQL的時候,會將SQL的?替換成轉義後的字符,發送到數據庫執行。
MySQLdriver 默認不開啓,可經過設置 cachePrepStmts = true 進行開啓
以前也遇到由於開啓了queryTimeout,致使鏈接泄露的問題。
Caelus是惟品會自研的高性能的分佈式的數據庫鏈接池。
高性能:基於無鎖的鏈接池設計模型來提高鏈接池性能;
在分庫較多的場景下,減小線程數。 假若有128個分庫,現有鏈接池模型下則須要使用128個獨立的鏈接池,每一個鏈接池都須要線程(1-4個,不一樣的鏈接池不一樣)處理任務。則總共須要維護128到128*4個線程,開銷巨大。而Caelus鏈接池會大大減小線程數。
鏈接複用。 對於 一個MySQL 的instance上面有多個schema場景下。現有鏈接池不一樣的schema的鏈接不可複用。而Caelus能夠複用不一樣schema的鏈接,提高性能。
過多的事務指令。若是是事務語句,則從鏈接池拿到鏈接後,須要先開啓事務(setautocommit=false),歸還時須要再設置(set autocommit=true)。每使用一次鏈接,均須要額外執行兩條事務指令。Caelus能有效減小事務指令。
配置規範的統一。結合MySQL的設置,提供規範統一,最優的配置。