資料庫連線池初探
資料庫連線池初探
為什麼需要連線池
MySQL連線原理
所謂的資料庫連線操作實際上是MySQL客戶端與MySQL服務端進行通訊,再細化一點便是連線程序與MySQL服務程序之間的程序通訊。常用的程序通訊方式有管道
、共享記憶體
、TCP socket
、unix domain socket
,而MySQL提供的連線方式也大抵如此。
-
TCP socket
這應該是使用最普遍的一種MySQL連線方式,也是我們日常開發過程中使用的,隨著微服務化的流行,資料庫RDS通常與應用伺服器分離,資料庫連線幾乎都採用這種在TCP連線上建立一個基於網路的連線請求來完成任務。這種連線方式的連線過程如下:
-
應用資料層向DataSource請求資料庫連線
- DataSource使用資料庫Driver開啟資料庫連線
- 建立資料庫連線,內部可能建立執行緒,開啟TCP socket
- 應用讀/寫資料庫
- 如果該連線不再需要就關閉連線
-
關閉socket
-
其它連線方式
當客戶端和服務端都在同一臺伺服器上的時候,還可以使用
命名管道
、共享記憶體
和Unix域套接字
來進行連線。這些方式都是本地通訊,不經過網絡卡,不需要進行TCP握手與揮手,效能自然也比TCP Socket的方式要快的多,但我們用不上。
連線的資源佔用
在使用TCP Socket的連線方式下,我們來看看MySQL連線需要佔用的資源有哪些。
-
TCP連線
tcp連線和斷開資源的消耗,如三次握手四次揮手所消耗的時間、協議棧的記憶體分配等等。
-
執行緒分配
MySQL內部會為每個連線建立一個執行緒,為每個連線分配連線緩衝區和結果緩衝區。雖然內部維護了一個非常類似執行緒池的
Threads_catched
來避免頻繁的建立執行緒和銷燬執行緒,但它僅僅只是將用過的空閒連線給快取下來放到池中,在連線不夠的時候仍然存在建立連線消耗資源的操作。
資源池化
我們根據MySQL連線過程可以看到,如果每一個請求都需要建立新的資料庫連線的話,那麼每次DataSource都要通過驅動程式去建立一個新的MySQL連線。這麼做的話將非常消耗資源。如果我們提前建立好這些連線,並把連線放在一個集合容器內,然後需要用去取連線,這樣不同的請求便可以複用已經建立好的連線。這便是資料庫連線池的基本思想,池化資源複用來節省系統資源消耗和降低請求時間。
連線池初探
從Class.forName到DataSource
在說連線池之前,先來了解一下JDBC驅動中一個非常重要的API。在比較老式的JDBC程式設計中,通常使用下面的模板程式碼來獲得一個數據庫連線:
/** * 連線資料庫 * @return */ public static Connection getConnection(){ Connection connection = null; String url = "jdbc:mysql://localhost:3306/chartroom"; String user = "root"; String password = "root"; try { Class.forName("com.mysql.jdbc.Driver"); connection = DriverManager.getConnection(url, user, password); } catch (SQLException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } return connection; }
在老式的JDBC程式設計中,都是通過DriverManager
類來連線到資料來源並提供Connection類用以操作資料庫的。但在JDBC 2.0
的 API 中新增了DataSource
介面,它提供了連線到資料來源的另一種方法。使用DataSource
物件現在已經是連線到資料來源的首選方法。
而眾多資料庫連線池的實現,無論是tomcat
連線池還是hikariCP
,其實都是DataSource
的一種具體實現,可以看作是一個代理。只不過各個連線池在對資料庫連線管理的實現層面上有不同的邏輯。
連線池最佳實踐之HikariCP
HikariCP
可謂是效能極致的資料庫連線池,目前在spring-boot 2.0
中已被當作是預設連線池來使用。本文的目的在於分享對連線池的瞭解以及統一團隊內的連線池選擇,因此主要來說說HikariCP
如何配置和如何監控,不探討HikariCP內部實現細節,有時間的話可以再細說。
配置項
一些不常用的配置,比如isAllowPoolSuspension
、isIsolateInternalQueries
之類的這裡不再提及。只說說幾個經常使用以及爭議比較大的配置。
| name| 構造器預設值| 預設配置validate之後的值 | |-------------------|--------------------------------|--------------------------| | minIdle| -1| 10| | maxPoolSize| -1| 10| | maxLifetime| MINUTES.toMillis(30) = 1800000 | 1800000| | connectionTimeout | SECONDS.toMillis(30) = 30000| 30000| | idleTimeout| MINUTES.toMillis(10) = 600000| 600000|
- 關於pool-size
首先給出結論,連線池的大小並不是越大越好的。作者在專案wiki中有一篇關於連線池大小配置的文章,有興趣可以讀一讀,About Pool Sizing 。大意便是儘可能在CPU執行緒切換的消耗與io阻塞等待消耗中尋找平衡。其中有一點很讓人思考,隨著SSD硬碟的普及,磁碟IO的時間將大大縮短,當然也意味著更頻繁的切換執行緒。文章中還給了一些測試報告,包括Oracle的實驗和某PG專案的測試,結論是前者將連線池大小從2048逐漸減少到96時響應時間從100ms降到了2ms,而作者認為大小為96的連線池仍然太大了。後者則用一個大小為9的連線池執行在一臺小型四核i7伺服器上可以輕鬆處理3000個前端使用者在6000 TPS下的簡單查詢,
那麼連線池大小究竟該設定為多少呢?作者給出了一個"經驗性"的結論是connections = ((core_count * 2) + effective_spindle_count)
,corecount為CPU的核心數、effective
spindle_count為資料庫伺服器磁碟列陣中的硬碟數。我覺得可以在公式的基礎上做壓測進行實驗,然後根據實驗結果的情況進行調整,但怎麼看都不要過度配置連線池。
- 關於fixed-pool-design
hikari作者比較傾向於連線池是固定大小的,一方面是因為如果將minIdle與maxPoolSize設定成不同的話,在流量激增的時候,請求會阻塞在getConnection()
方法上會造成效能損失;另一方面作者認為即便設定成一樣空閒的連線對整體的效能並不會有多大的影響,覺得設定min和max在必要的時候可以釋放連線以釋放記憶體給其他功能用的邏輯不成立,因為在高峰時仍然會達到maxPoolSize的量級消耗掉這部分記憶體,既然高峰時都能犧牲掉省下的這部分記憶體為何在空閒時又會需要。
我是比較贊同作者的觀點的,尤其是在當下微服務盛行,每個服務連的資料庫都是不同的,不會有多個服務的多個連線加起來導致資料庫連線有壓力。把min和max設定成一樣當作固定大小的連線池來使用我覺得是十分合理的,不會在流量激增的時候出現阻塞導致拿不到連線丟擲異常的問題,同時也提高了系統性能。
-
其它配置
-
hikari多了一個
maxLifetime
的配置,該配置用來指定所有連線的存活時間,即所有的連線在maxLifetime之後都得重連一次,保證連線池的活性。 -
idleTimeout僅僅在連線池不是fixed的時候才生效,用來消除高峰流量激增時生成的多餘空閒連線。
監控項
HikariCP本身設計以效能為主,也經常在網上被用來和阿里的Druid
進行比較,後者號稱為監控而生。但實際上HikariCP
在擁有極佳效能的前提下,對監控的支援也並不算弱。HikariCP
自身提供了IMetricsTracker
介面,並給了micrometer
、dropwizard
、prometheus
三種度量統計收集器的實現,並在執行時將連線池的各個狀態值上傳到初始化連線池時設定的MeterRegistry
(可以看作是一個度量資料收集中心),想要監控只需要起個定時任務定時的從MeterRegistry
把上傳進去的值給取出來寫到influxDB裡結合監控系統就行了。
以下是hikari
提供的監控指標:
| 監控指標| meter型別 | 描述| |-------------------------------|-----------|--------------------------------------------| | hikaricp.connections.min| Gauge| minIdle,最小空閒數量| | hikaricp.connections| Gauge| 連線池當前所有連線數| | hikaricp.connections.idle| Gauge| 當前空閒連線數量| | hikaricp.connections.max| Gauge| maxPoolSize,連線池最大連線數| | hikaricp.connections.creation | Timer| 建立一個新的連線所需的時間| | hikaricp.connections.active| Gauge| 活躍連線數| | hikaricp.connections.pending| Gauge| 當前排隊獲取連線的執行緒數| | hikaricp.connections.acquire| Timer| 獲取連線時間| | hikaricp.connections.usage| Timer| 一個事務執行耗時。即連線被租用到歸還的耗時 | | hikaricp.connections.timeout| Counter| 從池中獲取連線超時的數量|
這些指標均會隨著程式執行而產生不同的值。這裡先對各個meter型別稍作解釋:
- Gauge 可以理解為上下浮動的度量,但存在上下界。
- Counter 無限自增的度量,不存在上屆,只要資料沒被清除就會一直增加。
- Timer 與時間有關的度量,比如請求的耗時之類。
這些指標中,我們尤其要關注當前排隊獲取連線的執行緒數,因為我們鼓勵將連線池設定成fixed的,那麼我們就要做好等待執行緒數的監控,以免真的連線池容量配的太少導致等待執行緒數過多而發生請求超時。
下面是監控打點程式碼:
/** * 從meterRegistry中取出上傳的merter數量寫到influxdb中去 */ public void doMonitor() { for (Meter m : meterRegistry.getMeters()) { if (m instanceof Timer) { writeTimer((Timer) m); } if (m instanceof Gauge) { writeGauge(m.getId(), ((Gauge) m).value()); } if (m instanceof Counter) { writeCounter(m.getId(), ((Counter) m).count()); } } }