springBoot 項目默認自動使用 HikariCP ,HikariCP 的性能比 alibaba/druid快。
一、背景
系統中多少個線程在進行與數據庫有關的工作?其中,而多少個線程正在執行 SQL 語句?這可以讓我們評估數據庫是不是系統瓶頸。
-
多少個線程在等待獲取數據庫連接?獲取數據庫連接需要的平均時長是多少?數據庫連接池是否已經不能滿足業務模塊需求?如果存在獲取數據庫連接較慢,如大于 100ms,則可能說明配置的數據庫連接數不足,或存在連接泄漏問題。
-
哪些線程正在執行 SQL 語句?執行了的 SQL 語句是什么?數據庫中是否存在系統瓶頸或已經產生鎖?如果個別 SQL 語句執行速度明顯比其它語句慢,則可能是數據庫查詢邏輯問題,或者已經存在了鎖表的情況,這些都應當在系統優化時解決。
-
最經常被執行的 SQL 語句是在哪段源代碼中被調用的?最耗時的 SQL 語句是在哪段源代碼中被調用的?在浩如煙海的源代碼中找到某條 SQL 并不是一件很容易的事。而當存在問題的 SQL 是在底層代碼中,我們就很難知道是哪段代碼調用了這個 SQL,并產生了這些系統問題。
在研究HikariCP的過程中,這些業務關注點我發現在連接池這層逐漸找到了答案。
JBDC背景
JABC是JAVA訪問關系型數據庫的標注API,它為各種關系型數據的訪問提供統一的接口標準,然后,各個關系型數據庫廠商按照JBDC的標準,提供能使JAVA訪問的驅動包。一般情況下,在JAVA中執行一條SQL語句,需要以下幾個步驟:
狀態JDBC驅動程序
-
建立數據庫連接
-
創建數據庫操作對象
-
訪問數據庫,執行SQL語句
-
處理返回結果集
-
斷開數據庫連接
其中第2步的連接需經歷一下步驟: -
與數據建立TCP連接的三次握手
-
數據庫賬號密碼認證的通信
-
sql執行與返回的通信
= 關閉TCP連接的4次握手
由此看出,執行一個sql的開銷是比較大的,因此,為了節省資源提高效率,使用數據庫連接池是很有必要的
二、配置
pom 引入
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-actuator</artifactId>
</dependency>
<dependency><groupId>io.dropwizard.metrics</groupId><artifactId>metrics-core</artifactId>
</dependency>
增加監控配置類
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.Slf4jReporter;
import com.zaxxer.hikari.HikariDataSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StringUtils;import java.util.concurrent.TimeUnit;@Configuration
public class DatasourceConfiguration {private final static Logger LOGGER = LoggerFactory.getLogger(DatasourceConfiguration.class);@Bean@ConfigurationProperties(prefix = "spring.datasource.hikari")HikariDataSource dataSource(DataSourceProperties properties) {HikariDataSource dataSource = properties.initializeDataSourceBuilder().type(HikariDataSource.class).build();if (StringUtils.hasText(properties.getName())) {dataSource.setPoolName(properties.getName());}dataSource.setMetricRegistry(initMetricRegistry(dataSource.getPoolName()));return dataSource;}/*** 配置指標監控* @param poolName* @return*/public MetricRegistry initMetricRegistry(String poolName) {MetricRegistry metricRegistry = new MetricRegistry();Slf4jReporter reporter = Slf4jReporter.forRegistry(metricRegistry).outputTo(LOGGER).convertRatesTo(TimeUnit.SECONDS).convertDurationsTo(TimeUnit.MILLISECONDS).build();reporter.start(30, TimeUnit.SECONDS);//30秒打印一次return metricRegistry;}
項目啟動后控制臺會打印監控日志
17:22:56.072 [metrics-logger-reporter-1-thread-1] [traceId= spanId=] INFO avicit.pdm.config.DataSourceConfig - type=GAUGE, name=pdmHikariCP.pool.ActiveConnections, value=0
17:22:56.072 [metrics-logger-reporter-1-thread-1] [traceId= spanId=] INFO avicit.pdm.config.DataSourceConfig - type=GAUGE, name=pdmHikariCP.pool.IdleConnections, value=5
17:22:56.072 [metrics-logger-reporter-1-thread-1] [traceId= spanId=] INFO avicit.pdm.config.DataSourceConfig - type=GAUGE, name=pdmHikariCP.pool.MaxConnections, value=40
17:22:56.072 [metrics-logger-reporter-1-thread-1] [traceId= spanId=] INFO avicit.pdm.config.DataSourceConfig - type=GAUGE, name=pdmHikariCP.pool.MinConnections, value=5
17:22:56.072 [metrics-logger-reporter-1-thread-1] [traceId= spanId=] INFO avicit.pdm.config.DataSourceConfig - type=GAUGE, name=pdmHikariCP.pool.PendingConnections, value=0
17:22:56.072 [metrics-logger-reporter-1-thread-1] [traceId= spanId=] INFO avicit.pdm.config.DataSourceConfig - type=GAUGE, name=pdmHikariCP.pool.TotalConnections, value=5
17:22:56.072 [metrics-logger-reporter-1-thread-1] [traceId= spanId=] INFO avicit.pdm.config.DataSourceConfig - type=HISTOGRAM, name=pdmHikariCP.pool.ConnectionCreation, count=84, min=752, max=10874, mean=825.1112278127991, stddev=106.00103981809646, median=793.0, p75=817.0, p95=1248.0, p98=1248.0, p99=1248.0, p999=1248.0
17:22:56.072 [metrics-logger-reporter-1-thread-1] [traceId= spanId=] INFO avicit.pdm.config.DataSourceConfig - type=HISTOGRAM, name=pdmHikariCP.pool.Usage, count=7, min=0, max=948, mean=148.27471786535676, stddev=322.1937983856062, median=3.0, p75=103.0, p95=948.0, p98=948.0, p99=948.0, p999=948.0
17:22:56.072 [metrics-logger-reporter-1-thread-1] [traceId= spanId=] INFO avicit.pdm.config.DataSourceConfig - type=METER, name=pdmHikariCP.pool.ConnectionTimeoutRate, count=0, mean_rate=0.0, m1=0.0, m5=0.0, m15=0.0, rate_unit=events/second
17:22:56.072 [metrics-logger-reporter-1-thread-1] [traceId= spanId=] INFO avicit.pdm.config.DataSourceConfig - type=TIMER, name=pdmHikariCP.pool.Wait, count=7, min=0.0128, max=78.9716, mean=55.49616178913166, stddev=34.76115035148255, median=76.7992, p75=77.636, p95=78.9716, p98=78.9716, p99=78.9716, p999=78.9716, mean_rate=0.006881493151999419, m1=3.2892511143081085E-8, m5=0.02117048948569106, m15=0.19678830181225304, rate_unit=events/second, duration_unit=milliseconds
springboot hikariCP 配置文件配置
spring:datasource:name: database-adriver-class-name: com.mysql.cj.jdbc.Driver # MySQL 驅動,這里根據引入的 mysql-connector-java 包版本選擇不同的 Driver, 8.x 需要用 cjurl: jdbc:mysql://mysql:3306/database-a?useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8&allowMultiQueries=trueusername: rootpassword: 1type: com.zaxxer.hikari.HikariDataSource # JDBC 連接池類型:HikariCPhikari:connection-timeout: 30000 # 等待連接池分配鏈接的最大時長(毫秒),超過這個時長還沒有可用的連接則發生 SQLException,默認:30 秒minimum-idle: 5 # 最小連接數maximum-pool-size: 20 # 最大連接數auto-commit: true # 自動提交idle-timeout: 600000 # 連接超時的最大時長(毫秒),超時則被釋放(retired),默認:10 分鐘pool-name: DataSourceHikariCP # 連接池名稱max-lifetime: 1800000 # 連接的生命時長(毫秒),超時而且沒被使用則被釋放(retired),默認: 30 分鐘connection-test-query: select 1```
hikariCP 配置參數
下面給出詳細的配置信息:
使用率較高
- autoCommit:用于控制從池中返回連接的默認自動提交行為,默認為true
connectionTimeout:客戶端等待池中連接的最大事件(毫秒),超時則會拋出 -SQLException,最低可接受時間為 250ms,默認值為30000ms - idleTimeout:池中連接保持空閑狀態的最長時間,只有在定義的minimumIdle 小于- – maximumPoolSize時生效,允許的最小時間為 10000ms。默認為 600000ms
- keepaliveTime:用于控制 HikariCP 中空閑線程的最大存活時間,該值必須小于- - - - maxLifetime,最小為 30000ms。默認為 0 (disabled)
- maxLifetime:控制連接池中連接的最長時間,正在使用的連接不會被刪除,只有當其關閉連接后才會被刪除,當設置為 0 時表示永不刪除,最小允許值為 30000ms。 默認值為 1800000ms
- connectionTestQuery:當使用的驅動為 JDBC4 時不建議設置該項。
- minimumIdle:控制 HikariCP 中維護的最小空閑連接數。當空閑連接數小于 - - - – - minimumIdle 并且池中的總連接數少于 maximumPoolSize 時,HikariCP 將添加其他連接直到 maximumPoolSize。為了獲得最佳性能和對峰值需求的響應能力建議不要設置此值。 默認值與 maximumPoolSize 相同
- maximumPoolSize:連接池中的最大連接數。默認為 10
- metricRegistry:該項僅在編程式配置或IoC容器中使用,允許指定一個 Codahale/Dropwizard MetricRegistry 的實例來記錄池中的各項指標
- healthCheckRegistry:同上,用于報告當前連接池的健康狀況
poolName:定義連接池的名稱,可以在日志或控制臺識別連接池
不常使用
- initializationFailTimeout:允許初始化失敗的次數。默認值為 1
- isolateInternalQueries:控制 HikariCP 是否在其自己的事務中隔離內部池查詢,僅在禁用 autoCommit 時適用。默認值為 false
- allowPoolSuspension:控制連接池是否可以通過JMX暫停和恢復,當連接池暫停時,對 getConnection() 的調用永不超時,直到連接池恢復。默認為 false
- readOnly:控制從池中獲取的連接是否默認為只讀。默認為 false
- registerMbeans:控制是否注冊JMX Management Bean (MBean)。默認值為 false
- catalog:為支持目錄概念的數據庫設置默認目錄。如果未指定此屬性,則使用 JDBC 驅動程序定義的默認目錄。默認值為 driver default
- connectionInitSql:設置一個 SQL 語句,該語句將在每次創建新連接后執行,然后再將其添加到池中。如果此 SQL 無效或引發異常,它將被視為連接失敗,并且將遵循標準的重試邏輯。
- driverClassName:HikariCP 將嘗試通過基于 jdbcUrl 的 DriverManager 解析驅動程序,但對于一些較舊的驅動程序,必須指定 driverClassName
- transactionIsolation:控制從池中返回連接的默認事務隔離級別。如果未指定此屬性,則使用 JDBC 驅動定義的默認事務隔離級別。默認值為 driver default
- validationTimeout:控制用于測試連接的最長存活時間,該值必須小于 - - - connectionTimeout,最短時間為 250ms。默認值為 5000ms
- leakDetectionThreshold:控制在log日志記錄可能發生連接泄漏的消息之前,連接可以離開池的時間。值為 0 表示禁用泄漏檢測。啟用泄漏檢測的最低時間為 2000ms。 默認值為 0
- dataSource:僅可通過編程式配置或IoC容器使用。通過此屬性可以直接設置 DataSource 要由池包裝的的實例,而不必讓 HikariCP 通過反射進行構造
- schema:為支持 schema 概念的數據庫設置默認的 schema,如果未指定此屬性,則使用 JDBC 驅動定義的默認模式。
- threadFactory:僅可通過編程式配置或IoC容器使用。此屬性允許通過 java.util.concurrent.ThreadFactory 創建池使用的所有線程
- scheduledExecutor:僅可通過編程式配置或IoC容器使用。此屬性允許通過 java.util.concurrent.ScheduledExecutorService 設置實將用于各種內部任務的調度。
三、HikariCP監控指標介紹和應用
輸出指標說明
打印指標的格式為{連接池名稱}.pool.{指標}
指標 | 解釋 | 在運維時的作用 |
---|---|---|
ActiveConnections | 活躍連接數 | 此數據長期保持最大連接數值的時候可以嘗試擴大連接數 |
IdleConnections | 空閑連接數 | 此數據過高的時候可以嘗試減少配置中的最小連接數 |
MaxConnections | 配置的最大連接數 | |
MinConnections | 配置的最小連接數 | |
PendingConnections | 排隊等待連接的線程數 | 如果此數據持續飆高,表示連接池中已經沒有空閑線程了 |
TotalConnections | 當前總連接數 | |
ConnectionCreation | 創建新連接的耗時 | 此數據主要反應當前服務到數據服務的網絡延遲 |
ConnectionTimeoutRate | 創建新連接的超時 | 如果經常創建連接超時這個時候需要排查數據服務或者網絡通訊是否異常 |
Usage | 連接被復用時長 | 此參數表示連接池中一個連接從返回連接池到再次被復用的時間間隔,表示數據訪問頻繁程度,對于使用較長的間隔可以嘗試減少連接數 |
Wait | 獲取連接的等待耗時 | 可以和PendingConnections結合分析連接池情況。 |
Wait和PendingConnections結合分析連接池情況
-
如果排隊多等待短:此時表示數據訪問頻繁可以嘗試擴大連接數;
-
如果排隊少等待長:此時連接中存在慢查詢或者比較大的事務;
-
如果排隊多等待長:此時可能是數據訪問壓力過大且存在大量慢查詢,但實際上如果頻繁出現慢查詢很有可能是程序或者業務上出現了問題,需要對業務和代碼進行排查。這種時刻也能網絡出現異常導致所有查詢都變得非常慢;
輸出度量說明
屬性 解釋
count | 指標記錄次數 |
---|---|
min | 最小記錄數 |
min | 最大記錄數 |
mean | 平均值 |
stddev | 標準差 |
median | 中位數 |
p75 | 75百分位數 |
p95 | 95百分位數 |
p98 | 98百分位數 |
p99 | 99百分位數 |
p999 | 99.9百分位數 |
mean_rate | 平均耗時 |
m1 | 1分鐘內記錄平均數 |
m5 | 5分鐘記錄平均數 |
m15 | 15分鐘記錄平均數 |
duration_unit | 統計單位 |
rate_unit | 記錄單位(events/second 為 事件次數/每秒鐘) |
四、 手動方式獲取連接池指標信息
有時候因為業務需要我們可以從DataSource中直接獲取指標數據進行處理
@AutowiredDataSource dataSource;@Value("${datasource.name:test}")String poolName;@RequestMapping("/getInfo")public String getInfo() throws SQLException {String indexName = poolName + ".pool.";MetricRegistry metricRegistry = (MetricRegistry) ((HikariDataSource) dataSource).getMetricRegistry();SortedMap<String, Gauge> gauges = metricRegistry.getGauges();Gauge activeConnections = gauges.get(indexName + "ActiveConnections");Object activeConnectionsV = activeConnections.getValue();log.info("activeConnections : " + activeConnectionsV);Gauge IdleConnections = gauges.get(indexName + "IdleConnections");Object IdleConnectionsV = IdleConnections.getValue();log.info("IdleConnections : " + IdleConnectionsV);Gauge MaxConnections = gauges.get(indexName + "MaxConnections");Object MaxConnectionsV = MaxConnections.getValue();log.info("MaxConnections : " + MaxConnectionsV);Gauge MinConnections = gauges.get(indexName + "MinConnections");Object MinConnectionsV = MinConnections.getValue();log.info("MinConnections : " + MinConnectionsV);Gauge PendingConnections = gauges.get(indexName + "PendingConnections");Object PendingConnectionsV = PendingConnections.getValue();log.info("PendingConnections : " + PendingConnectionsV);Gauge TotalConnections = gauges.get(indexName + "TotalConnections");Object TotalConnectionsV = TotalConnections.getValue();log.info("TotalConnections : " + TotalConnectionsV);SortedMap<String, Histogram> histograms = ((MetricRegistry) metricRegistry).getHistograms();Histogram ConnectionCreation = histograms.get(indexName + "ConnectionCreation");Object ConnectionCreationV = ConnectionCreation.getCount();Snapshot snapshot = ConnectionCreation.getSnapshot();log.info("ConnectionCreation : " + ConnectionCreationV);Histogram Usage = histograms.get(indexName + "Usage");Object UsageV = Usage.getCount();log.info("Usage : " + UsageV);SortedMap<String, Meter> meters = ((MetricRegistry) metricRegistry).getMeters();Meter meter = meters.get(indexName + "ConnectionTimeoutRate");long count = meter.getCount();log.info("ConnectionTimeoutRate : " + count);SortedMap<String, Timer> timers = ((MetricRegistry) metricRegistry).getTimers();Timer timer = timers.get(indexName + "Wait");long count1 = timer.getCount();log.info("Wait : " + count1);return "";}