springBoot 项目默认自动使用 HikariCP ,HikariCP 的性能比 alibaba/druid快。
系统中多少个线程在进行与数据库有关的工作?其中,而多少个线程正在执行 SQL 语句?这可以让我们评估数据库是不是系统瓶颈。
多少个线程在等待获取数据库连接?获取数据库连接需要的平均时长是多少?数据库连接池是否已经不能满足业务模块需求?如果存在获取数据库连接较慢,如大于 100ms,则可能说明配置的数据库连接数不足,或存在连接泄漏问题。
哪些线程正在执行 SQL 语句?执行了的 SQL 语句是什么?数据库中是否存在系统瓶颈或已经产生锁?如果个别 SQL 语句执行速度明显比其它语句慢,则可能是数据库查询逻辑问题,或者已经存在了锁表的情况,这些都应当在系统优化时解决。
最经常被执行的 SQL 语句是在哪段源代码中被调用的?最耗时的 SQL 语句是在哪段源代码中被调用的?在浩如烟海的源代码中找到某条 SQL 并不是一件很容易的事。而当存在问题的 SQL 是在底层代码中,我们就很难知道是哪段代码调用了这个 SQL,并产生了这些系统问题。
在研究HikariCP的过程中,这些业务关注点我发现在连接池这层逐渐找到了答案。
JABC是JAVA访问关系型数据库的标注API,它为各种关系型数据的访问提供统一的接口标准,然后,各个关系型数据库厂商按照JBDC的标准,提供能使JAVA访问的驱动包。一般情况下,在JAVA中执行一条SQL语句,需要以下几个步骤:
状态JDBC驱动程序
建立数据库连接
创建数据库操作对象
访问数据库,执行SQL语句
处理返回结果集
断开数据库连接
其中第2步的连接需经历一下步骤:
与数据建立TCP连接的三次握手
数据库账号密码认证的通信
sql执行与返回的通信
= 关闭TCP连接的4次握手
由此看出,执行一个sql的开销是比较大的,因此,为了节省资源提高效率,使用数据库连接池是很有必要的
<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
spring:
datasource:
name: database-a
driver-class-name: com.mysql.cj.jdbc.Driver # MySQL 驱动,这里根据引入的 mysql-connector-java 包版本选择不同的 Driver, 8.x 需要用 cj
url: jdbc:mysql://mysql:3306/database-a?useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8&allowMultiQueries=true
username: root
password: 1
type: com.zaxxer.hikari.HikariDataSource # JDBC 连接池类型:HikariCP
hikari:
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```
下面给出详细的配置信息:
输出指标说明
打印指标的格式为{连接池名称}.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中直接获取指标数据进行处理
@Autowired
DataSource 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 "";
}