前言
我是[提前退休的java猿],一名7年java开发经验的开发组长,分享工作中的各种问题!
今天写这篇文章的背景就是在排查问题的时候,debug了 连接池和驱动的源码。然后就发现了几个坑儿。
😭这个坑儿让你的配置不生效,生效了但是只生效了一半,生效了但是为什么时间给我扩大了1000倍呢
比如:我现在需要调整数据库链接的两个参数一个是链接超时时间,一个是socket读取数据时间。
正常来说有两种配置方式:
-
- 跟到url后面, url: jdbc:kingbase8......&socket-timeout=300000&connect-timeout=300000
-
- 设置到连接池提供的参数上:
java
socket-timeout: 300000
connect-timeout: 300000
❔今天就通过源码的方式,看看这两种情况的配置究竟都存在什么问题
😱debug 环境:驱动是kingbase+连接池是durid+集群模式
国产驱动的配置玄学
connect-timeout、socket-timeout 读取设置过程
首先大家对这个参数的单位应该没有什么疑问吧,是毫秒对吧。那我们就开始分析问题吧
1. 创建连接池:读取线程池参数
创建链接池,读取连接池的配置并把配置set到连接池上
java
public DataSource createDataSource(DataSourceProperty dataSourceProperty) {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUsername(dataSourceProperty.getUsername());
dataSource.setPassword(dataSourceProperty.getPassword());
dataSource.setUrl(dataSourceProperty.getUrl());
dataSource.setName(dataSourceProperty.getPoolName());
String driverClassName = dataSourceProperty.getDriverClassName();
if (DsStrUtils.hasText(driverClassName)) {
dataSource.setDriverClassName(driverClassName);
}
DruidConfig config = dataSourceProperty.getDruid();
Properties properties = DruidConfigUtil.mergeConfig(gConfig, config);
List<Filter> proxyFilters = this.initFilters(dataSourceProperty, properties.getProperty("druid.filters"));
dataSource.setProxyFilters(proxyFilters);
try {
configMethod.invoke(dataSource, properties);
} catch (Exception ignore) {
}
//连接参数单独设置
dataSource.setConnectProperties(config.getConnectionProperties());
//设置druid内置properties不支持的的参数 : connectTimeout 和 socketTimeout 就包括在这个里面
for (String param : PARAMS) {
// 单个数据源的配置 会覆盖 全局的连接池配置
DruidConfigUtil.setValue(dataSource, param, gConfig, config);
}
if (Boolean.FALSE.equals(dataSourceProperty.getLazy())) {
try {
dataSource.init();
} catch (SQLException e) {
throw new ErrorCreateDataSourceException("druid create error", e);
}
}
return dataSource;
}
2. 连接池的初始化,设置默认配置
初始化过程,为0则使用线程池默认配置10_000 ms,伪代码如下DruidDataSource.init()
:
java
public void init() throws SQLException {
.............
// set 默认配置
if (connectTimeout == 0) {
connectTimeout = DEFAULT_TIME_CONNECT_TIMEOUT_MILLIS;
}
if (socketTimeout == 0) {
socketTimeout = DEFAULT_TIME_SOCKET_TIMEOUT_MILLIS;
}
..........
while(...){
// 创建链接
PhysicalConnectionInfo pyConnectInfo = createPhysicalConnection();
}
3.调用驱动创建链接:传递连接池的配置
判断驱动类型,根据具体的驱动进行参数类型的转换,传递 connectTimeout、socketTimeout 参数给驱动创建连接,创建连接之后,调用setNetworkTimeout
;
驱动源码:DruidAbstractDataSource.createPhysicalConnection()
java
public PhysicalConnectionInfo createPhysicalConnection() throws SQLException {
String url = this.getUrl();
Properties connectProperties = getConnectProperties();
。。。。。。。。。。。。。。。。。。。。
//1. 此时的connectTimeout、connectTimeout 是连接池的配置(默认)
if (connectTimeout > 0) {
if (isMySql) {
if (connectTimeoutStr == null) {
connectTimeoutStr = Integer.toString(connectTimeout);
}
physicalConnectProperties.put("connectTimeout", connectTimeoutStr);
} else if (isOracle) {
if (connectTimeoutStr == null) {
connectTimeoutStr = Integer.toString(connectTimeout);
}
physicalConnectProperties.put("oracle.net.CONNECT_TIMEOUT", connectTimeoutStr);
} else if (driver != null && POSTGRESQL_DRIVER.equals(driver.getClass().getName())) {
// see https://github.com/pgjdbc/pgjdbc/blob/2b90ad04696324d107b65b085df4b1db8f6c162d/README.md
physicalConnectProperties.put("loginTimeout", Long.toString(TimeUnit.MILLISECONDS.toSeconds(connectTimeout)));
physicalConnectProperties.put("connectTimeout", Long.toString(TimeUnit.MILLISECONDS.toSeconds(connectTimeout)));
if (socketTimeout > 0) {
physicalConnectProperties.put("socketTimeout", Long.toString(TimeUnit.MILLISECONDS.toSeconds(socketTimeout)));
}
} else if (DbType.sqlserver.name().equals(dbTypeName)) {
// see https://learn.microsoft.com/en-us/sql/connect/jdbc/setting-the-connection-properties?view=sql-server-ver16
physicalConnectProperties.put("loginTimeout", Long.toString(TimeUnit.MILLISECONDS.toSeconds(connectTimeout)));
if (socketTimeout > 0) {
// As SQLServer-jdbc-driver 6.1.2 can use this, see https://github.com/microsoft/mssql-jdbc/wiki/SocketTimeout
physicalConnectProperties.put("socketTimeout", Integer.toString(socketTimeout));
}
}
}
Connection conn = null;
................................
try {
// 上面的处理情况并没有对 kingbase 驱动做处理,所以 physicalConnectProperties 没有 connect/socket-timeout 两个值
conn = createPhysicalConnection(url, physicalConnectProperties);
initPhysicalConnection(conn, variables, globalVariables);
initedNanos = System.nanoTime();
......
// 这个地方也是重点,设置netWorkTimeout,socketTimeout 为连接池配置(默认)的参数
// As SQLServer-jdbc-driver 6.1.7 can use this, see https://github.com/microsoft/mssql-jdbc/wiki/SocketTimeout
conn.setNetworkTimeout(netTimeoutExecutor, socketTimeout); // here is milliseconds defined by JDBC
........
} catch (SQLException ex) {
..............
} finally {
........
}
return new PhysicalConnectionInfo(conn, connectStartNanos, connectedNanos, initedNanos, validatedNanos, variables, globalVariables);
}
3.1创建连接
创建连接: 省略中间的API,从上面我们得出了结论,就是durid在创建连接的时候,并没有把conn/socket-timeout参数传进来,下面infoProps
这个参数就是从url
解析出来的
- connectTimeout 从url读取(默认10)* 1000 ,这个参数的单位居然是秒😱(我们在url中设置了connectTimeout都挡在ms来使用的,那就嘿嘿了)
- socketTimeOut 从url读取(默认0),集群模式下面单位应该是按照ms来使用的,单点情况下面*1000应该按照s来设置的 驱动创建conn底层设置socket相关的代码如下
com.kingbase8.core.v3.ConnectionFactoryImpl.tryConnect()
:
java
private KBStream tryConnect(String user, String database, Properties infoProps, SocketFactory socketFactory, HostSpec _hostSpec, SslMode sslMode, int _version, Object cCMV2) throws SQLException, IOException {
TraceLogger.logLineInfo(Level.ALL, "lineInfo");
// ❗从infoProps中获取,没有就使用默认 10,可以看到这个地方的 connect-timeout 的单位是s😱
int connectTimeoutT = KBProperty.CONNECT_TIMEOUT.getInt(infoProps) * 1000;
// 会创建socket会话,connectTimeoutT 就是创建socket连接的超时时间
KBStream newStreamT = new KBStream(socketFactory, _hostSpec, connectTimeoutT, KBProperty.USEDISPATCH.getBoolean(infoProps) && infoProps.getProperty("isMonitor") == null, _version, cCMV2);
TraceLogger.logLineInfo(Level.ALL, "lineInfo");
// 从infoproprs 中获取,没有则使用默认值0 这个地方也没有 *1000 呢,(被当成ms来使用了吗)
int socketTimeoutT = KBProperty.SOCKET_TIMEOUT.getInt(infoProps);
if (KBProperty.USEDISPATCH.getBoolean(infoProps) && infoProps.getProperty("isMonitor") == null) {
TraceLogger.logLineInfo(Level.ALL, "lineInfo");
if (socketTimeoutT < 0) {
TraceLogger.logLineInfo(Level.ALL, "lineInfo");
socketTimeoutT = 0;
}
newStreamT.getSocket().setSoTimeout(1000);
newStreamT.setSocketTimeout(socketTimeoutT);
KBLOGGER.log(Level.INFO, "Dispatch : socketTimeout is " + socketTimeoutT * 1000, new Object[0]);
} else {
TraceLogger.logLineInfo(Level.ALL, "lineInfo");
if (socketTimeoutT > 0) {
TraceLogger.logLineInfo(Level.ALL, "lineInfo");
// 非集群模式下面 socketTimeout 又当成 秒来使用
newStreamT.getSocket().setSoTimeout(socketTimeoutT * 1000);
KBLOGGER.log(Level.INFO, "Single or Monitor : socketTimeout is " + socketTimeoutT * 1000, new Object[0]);
}
}
.................................
return newStreamT;
}
3.2 调用setNetworkTimeout
再次设置超时时间
创建conn之后(DruidAbstractDataSource.createPhysicalConnection()
),会调用conn的setNetworkTimeout 方法对超时时间重新设置,参数是从连接池传入的 ,我们看看Kingbase实现的具体逻辑com.kingbase8.core.KBStream.setNetworkTimeout()
:
java
public void setNetworkTimeout(int millisecs) throws IOException {
TraceLogger.logLineInfo(Level.ALL, "lineInfo");
//集群模式,再次写死 soTimeout 为1s
if (this.isUseDispatch()) {
TraceLogger.logLineInfo(Level.ALL, "lineInfo");
this.connectionSocket.setSoTimeout(1000);
//这个参数时间上是用来计算 重试次数的
this.setSocketTimeout(millisecs % 1000 == 0 ? millisecs / 1000 : millisecs / 1000 + 1);
KBLOGGER.log(Level.INFO, "Dispatch : socketTimeout is " + millisecs, new Object[0]);
} else {
TraceLogger.logLineInfo(Level.ALL, "lineInfo");
this.connectionSocket.setSoTimeout(millisecs);
KBLOGGER.log(Level.INFO, "Single or Monitor : socketTimeout is " + millisecs, new Object[0]);
}
}
- 再次写死Socket timeout 的时间为1000ms,就是1s
- 把连接池中的socketTimeout,设置this.socketTimeout的时候除以了1000,得到重试次数
看一下socketTimeout在哪用到了,VisibleBufferedInputStream.readMore
:
java
private boolean readMore(int wanted) throws IOException {
.................................
TraceLogger.logLineInfo(Level.ALL, "lineInfo");
int readT = true;
int i = 0;
int j = 0;
int readT;
while(true) {
try {
//----------------------------------------读取响应数据-----------------------------
readT = this.wrappedInputStream.read(this._buffer, this.endIndex, canFit);
break;
} catch (SocketTimeoutException var10) {
TraceLogger.logLineInfo(Level.ALL, "lineInfo");
if (this.useDispatch) {
if ((this.pCMV2.master_online_ip.equals(this._host) || this.pCMV2.slave_online_ip.contains(this._host + ",")) && (Integer)this.pCMV2._connVersion.get(this._host) == this._version) {
label78: {
// -------------------重试 socketTimeout 次数,------------
if (this.socketTimeout != 0) {
++i;
if (i >= this.socketTimeout) {
break label78;
}
}
++j;
if (j % 5 == 0) {
KBLOGGER.log(Level.INFO, "Online _host {0} has been waiting for {1} times, socketTimeout is {2}", new Object[]{this._host, j, this.socketTimeout});
}
continue;
}
}
............................
}
throw var10;
}
}
..................
}
总结
通过源码分析了connect-timeout
、socket-timeout
在集群模式下设置值的过程。我们可以得出以下结论
✔集群模式要配置这两个参数:
connect-timeout
: 跟到url后面并且单位为s:url: jdbc:kingbase8://172.xxx&connectTimeOut=30
,配置到线程池无效socket-timeout
:配置到连接池中,单位为ms:spring.datasource.dynamic.druid.connect-timeout= 30*1000
单位为ms,配置到url后无效
😭所以这就很坑啊,这两个参数都是控制socket超时的参数,正常逻辑单位或者是设置的地方应该保一致直才对,结果通过我们的源码去分析了,各有各的单位,各有各的配置方式。
这种情况,就会让很多程序员感到十分的懵,可能改了参数,发现没有生效。设置了时间发现和自己配置的不一样,所以一切的玄学背后皆有源头🤞
本篇文章,分析了driud+kingbase的环境,加载配置的流程,发现了同样的参数在 集群和单点环境加载的方式还不一样,连时间单位都不统一!其他驱动和连接池是否存在这个问题,欢迎大家留言评论