因需求,公司SpringBoot
项目中需使用异步多线程进行FastDFS
的文件上传,单个上传耗时大概1秒左右,当并发操作超过8个时总是出现Unable to borrow buffer from pool
异常,小于8个时全部正常,因此怀疑是使用的组件的默认连接数限制导致的。上网翻查资料无果后决定分析一下所用的FastDFS组件的源码。
- 使用的FastDFS组件:
<dependency>
<groupId>com.github.tobato</groupId>
<artifactId>fastdfs-client</artifactId>
<version>1.26.2</version>
</dependency>
异常是在com.github.tobato.fastdfs.conn.ConnectionManager
类中的getConnection
方法执行到pool.borrowObject(address)
时抛出的:
/**
* 获取连接
*
* @param address
* @return
*/
protected Connection getConnection(InetSocketAddress address) {
Connection conn = null;
try {
// 获取连接
conn = pool.borrowObject(address);
} catch (FdfsException e) {
throw e;
} catch (Exception e) {
LOGGER.error("Unable to borrow buffer from pool", e);
throw new RuntimeException("Unable to borrow buffer from pool", e);
}
return conn;
}
参数address
即配置文件中配置的fdfs
的tracker
地址,查看borrowObject
方法得知,每个tracker
地址
都是作为key
被存到ConcurrentHashMap
中和该tracker
地址的连接池数据进行关联的。
回到getConnection
方法,这里的pool
是在ConnectionManager
类中使用@Autowire
注解得到的:
@Component
public class ConnectionManager {
/** 连接池 */
@Autowired
private FdfsConnectionPool pool;
进入FdfsConnectionPool
类,发现该类没有默认的无参构造方法,此时注解获取该对象时默认使用的是如下构造方法:
@Autowired
public FdfsConnectionPool(KeyedPooledObjectFactory<InetSocketAddress, Connection> factory,
GenericKeyedObjectPoolConfig config) {
super(factory, config);
}
这两个参数是通过@Autowire
注解获取的,找到第一个参数KeyedPooledObjectFactory
接口的实现类com.github.tobato.fastdfs.conn.PooledConnectionFactory
:
@Component
@ConfigurationProperties(prefix = FdfsClientConstants.ROOT_CONFIG_PREFIX)
public class PooledConnectionFactory extends BaseKeyedPooledObjectFactory<InetSocketAddress, Connection> {
/** 读取时间 */
private int soTimeout;
/** 连接超时时间 */
private int connectTimeout;
这里可以看出这两个参数设置的是fdfs
连接的读取时间
和连接超时时间
。
接着查看构造方法的另一个入参GenericKeyedObjectPoolConfig
类:
public class GenericKeyedObjectPoolConfig extends BaseObjectPoolConfig {
/**
* The default value for the {@code maxTotalPerKey} configuration attribute.
* @see GenericKeyedObjectPool#getMaxTotalPerKey()
*/
public static final int DEFAULT_MAX_TOTAL_PER_KEY = 8;
/**
* The default value for the {@code maxTotal} configuration attribute.
* @see GenericKeyedObjectPool#getMaxTotal()
*/
public static final int DEFAULT_MAX_TOTAL = -1;
/**
* The default value for the {@code minIdlePerKey} configuration attribute.
* @see GenericKeyedObjectPool#getMinIdlePerKey()
*/
public static final int DEFAULT_MIN_IDLE_PER_KEY = 0;
/**
* The default value for the {@code maxIdlePerKey} configuration attribute.
* @see GenericKeyedObjectPool#getMaxIdlePerKey()
*/
public static final int DEFAULT_MAX_IDLE_PER_KEY = 8;
private int minIdlePerKey = DEFAULT_MIN_IDLE_PER_KEY;
private int maxIdlePerKey = DEFAULT_MAX_IDLE_PER_KEY;
private int maxTotalPerKey = DEFAULT_MAX_TOTAL_PER_KEY;
private int maxTotal = DEFAULT_MAX_TOTAL;
发现该类的maxTotalPerKey
属性默认的值是8
,maxTotal
默认值为-1
,意为不限制总连接数。但是该类属于commons-pool2
组件,并不能通过@Autowire
注解获取实例,所以fastdfs
组件中肯定有一个类继承了这个类。
最终从fdfs
组件中找到com.github.tobato.fastdfs.conn.ConnectionPoolConfig
类,所以上面FdfsConnectionPool
类的默认构造方法参数注入时实际上用的是这个类。从这个类的构造方法可以看出该连接池配置类使用了一些默认的
参数。其中有以下三个静态常量:
/** 从池中借出的对象的最大数目 */
public static final int FDFS_MAX_TOTAL = 50;
/**
* 连接耗尽时是否阻塞(默认true)
* false报异常,ture阻塞直到超时
*/
public static final boolean FDFS_BLOCK_WHEN_EXHAUSTED = true;
/**
* 获取连接时的最大等待毫秒数(如果设置为阻塞时BlockWhenExhausted)
* 如果超时就抛异常,小于零:阻塞不确定的时间,默认-1
*/
public static final long FDFS_MAX_WAIT_MILLIS = 100;
可以看出,fdfs
组件并发最大连接数
是50
,连接耗尽时默认会阻塞直到超时,但是阻塞时间仅为0.1秒
,所以当连接耗尽且达到阻塞时间时就会抛出异常。该类最下面有默认构造方法:
public ConnectionPoolConfig() {
// 从池中借出的对象的最大数目
setMaxTotal(FDFS_MAX_TOTAL);
// 在空闲时检查有效性
setTestWhileIdle(FDFS_TEST_WHILE_IDLE);
// 连接耗尽时是否阻塞(默认true)
setBlockWhenExhausted(FDFS_BLOCK_WHEN_EXHAUSTED);
// 获取连接时的最大等待毫秒数100
setMaxWaitMillis(FDFS_MAX_WAIT_MILLIS);
// 视休眠时间超过了180秒的对象为过期
setMinEvictableIdleTimeMillis(FDFS_MIN_EVICTABLE_IDLETIME_MILLIS);
// 每过60秒进行一次后台对象清理的行动
setTimeBetweenEvictionRunsMillis(FDFS_TIME_BETWEEN_EVICTION_RUNS_MILLIS);
// 清理时候检查所有线程
setNumTestsPerEvictionRun(FDFS_NUM_TESTS_PEREVICTION_RUN);
// 配置jmx
this.setJmxNameBase(FDFS_JMX_NAME_BASE);
this.setJmxNamePrefix(FDFS_JMX_NAME_PREFIX);
}
即在依赖注入ConnectionPoolConfig
类的对象时对commons-pool2
对象池进行的一些初始设置。
该类上使用了@ConfigurationProperties(prefix = FdfsClientConstants.POOL_CONFIG_PREFIX)注解,意思是从SpringBoot
的配置文件中读取配置信息通过对象属性的set方法
赋值给对象。所以该类的父类org.apache.commons.pool2.impl.GenericKeyedObjectPoolConfig
中的属性是可以通过配置文件中的配置指定初始化值的。
查看GenericKeyedObjectPoolConfig
类和它的父类BaseObjectPoolConfig
中的属性所拥有的公共set方法
,可以针
对fdfs
组件的总连接数(maxTotal)
、每个tracker地址(fdfs可以配置多个tracker地址)的连接数(maxTotalPerKey)
和阻塞时的最大等待时间(maxWaitMillis)
这个三参数在配置文件中进行如下配置(具体数值结合项目实际情况配置):
## 连接池最大数量
fdfs.pool.max-total=200
## 每个tracker地址的最大连接数
fdfs.pool.max-total-per-key=50
## 连接耗尽时等待获取连接的最大毫秒数
fdfs.pool.max-wait-millis=5000
至此,标题中的异常再也没有出现。