1 请求pending场景引出数据库连接池问题
当你的node项目中出现请求一直被pending住可能是什么原因呢?当然是一直没有返回。那为什么没有返回呢,肯定是哪里最后没有调用返回。 看一个最核心的例子, 理解一下
const http = require('http')
http.createServer(function(req, res) {
setTimeout(()=>{
res.end('End')
},5000)
}).listen(8040)
这个例子就是5秒后调用end 所以接口pending了5秒,如果一直不调用end则会一直pending直到超时失败。
有人说缩短超时时间, 那样只会让请求一直失败,没有从根本上解决问题。当然我们出问题的时候不会这么简单,因为我们用了各种node框架,连接了各种其他的服务。如果是单一的接口出问题还比较容易定位,就怕的是所有的接口都出问题。
下面我就说一个case,当服务使用了一段时间之后所有接口都会pending的排查过程:
- 首先是重启服务之后就会好,证明是服务本身的问题。
- 猜测是中间件没有调用next导致pending,检查代码,发现没问题。
- 查看下服务的监控,发现CPU,内存都没问题。
- 查看日志都是请求进入之后就没有返回了,也没有报错。
- 本地进行复现,然后就可以进行调试。
- 本地调试发现所有方法都是卡到了TypeORM的方法上就没有再执行了。
- 那就继续深入调试TypeORM的方法,把它的日志都加上,发现到最后就是不执行SQL。
- 研究
TypeORM
的源码,内部建立连接使用的还是mysql
, 继续看mysql
连接池的参数, 关键的参数如下:
- `waitForConnections`:确定没有可用连接且已达到限制时池的操作。如果`true`,池将连接请求排队并在一个可用时调用它。如果`false`,池将立即回调并返回错误。(默认: `true`)
- `connectionLimit`:连接池中最大的连接数量。(默认: `10`)
- `queueLimit`: 排队等待连接的最大队列数。如果设置为`0`,则对排队的连接请求数没有限制。(默认: `0`)
这三个默认参数非常重要,waitForConnections
默认值是true,如果没有可用的连接 就会一直排队等着,也不会抛出错误。queueLimit
默认值是0,就代表队列长度没有限制。综合这三个参数就可以说明 如果有连接被使用不释放并且一直不放回连接池,那么之后的所有请求都会排队等待。
其次连接池还有一些事件:
获取到连接
pool.on('acquire', function (connection) {
console.log('Connection %d acquired', connection.threadId);
});
创建新的连接
pool.on('connection', function (connection) {
connection.query('SET SESSION auto_increment_increment=1')
});
有回调进入获取连接的队列
pool.on('enqueue', function () {
console.log('Waiting for available connection slot');
});
连接被释放回连接池
pool.on('release', function (connection) {
console.log('Connection %d released', connection.threadId);
});
我们把这些事件加上,然后调整参数, 只要有等待的情况就报错,而且最大连接数设置为了2。
{
waitForConnections: false,
connectionLimit: 2,
}
设置了之后,再进行测试,发现正常的TypeORM方法调用之后都进入release事件,只有在一个接口的时候没有进入release事件,然后之后的调用都触发了ERROR。
接下来就是看那个接口中的代码,定位到问题,手动创建了QueryRunner
但是没有调用释放方法。官网文档的提示如下:
2 用TypeORM还需要手动释放数据库连接吗
通过上面排查到问题之后,我们可能就会有疑问,用TypeORM还需要手动释放数据库连接吗? 这么不智能吗? 我都什么情况下需要手动释放,什么情况下不用? 回答以上问题 我们需要看TypeORM中的一些关键概念,连接建立的过程,连接池是如何工作的,TypeORM中是如何封装的。
2.1 TypeORM中创建连接池的过程
DataSource
DataSource
数据源,连接一个数据库的对象,是最根本的一个对象, 创建它的选项是DataSourceOptions
,它包含两部分的选项,第一是通用参数,因为TypeORM支持多种数据库,这部分参数是所有数据库都支持,另一部分只针对指定数据库的参数。
const options: DataSourceOptions = {
...
}
const dataSource = new DataSource(options)
dataSource.initialize().then(
async (dataSource) => {
// 这里可以利用dataSource的API进行数据库操作
},
(error) => console.log("Cannot connect: ", error),
)
Driver
是操作不同数据库的驱动器,因为TypeORM支持多种数据库,所以是通过DriverFactory
的方式创建。
export class DataSource {
constructor(options: DataSourceOptions) {
this.driver = new DriverFactory().create(this)
}
}
DriverFactory
中根据类型创建相应的Driver
对象
export class DriverFactory {
/**
* Creates a new driver depend on a given connection's driver type.
*/
create(connection: DataSource): Driver {
const { type } = connection.options
switch (type) {
case "mysql":
return new MysqlDriver(connection)
case "postgres":
return new PostgresDriver(connection)
...
}
}
把options参数继续直接传递给了MysqlDriver
建立连接
initialize 方法
dataSource.initialize().then(
async (dataSource) => {
// 这里可以利用dataSource的API进行数据库操作
},
(error) => console.log("Cannot connect: ", error),
)
内部调用Driver
的connect
方法
async initialize(): Promise<this> {
...
await this.driver.connect()
...
}
connect
方法中建立的连接池
async connect(): Promise<void> {
...
this.pool = await this.createPool(
this.createConnectionOptions(this.options, this.options),
)
...
}
构造createPool的参数, 最关键的是其中merge了extra的参数。
所以在第一节中说给连接池设置参数是需要放到options.extra
中。
createPool 中继续调用的是createPool
protected createPool(connectionOptions: any): Promise<any> {
// create a connection pool
const pool = this.mysql.createPool(connectionOptions)
// make sure connection is working fine
return new Promise<void>((ok, fail) => {
// (issue #610) we make first connection to database to make sure if connection credentials are wrong
// we give error before calling any other method that creates actual query runner
pool.getConnection((err: any, connection: any) => {
if (err) return pool.end(() => fail(err))
connection.release()
ok(pool)
})
})
}
最后到了mysql的createPool方法,
exports.createPool = function createPool(config) {
var Pool = loadClass('Pool');
var PoolConfig = loadClass('PoolConfig');
return new Pool({config: new PoolConfig(config)});
};
之后TypeORM中都是使用这个连接池中的连接
this.pool.getConnection((err: any, dbConnection: any) => {
err ? fail(err) : ok(this.prepareDbConnection(dbConnection))
})
2.2 连接池的工作原理
连接池的工作原理就需要查看mysql的源码了。
有四个队列
function Pool(options) {
...
this._acquiringConnections = []; // 正在获取中的连接
this._allConnections = []; // 所有的连接
this._freeConnections = []; // 当前连接池中空闲的连接
this._connectionQueue = []; // 等待连接的回调
}
获取连接方法,已经添加了注释。逻辑是 有可用的直接返回,超过限制则报错或者加入等待队列,否则就创建新的连接。
Pool.prototype.getConnection = function (cb) {
...
var connection;
var pool = this;
//_freeConnections 可用的connect队列
if (this._freeConnections.length > 0) {
connection = this._freeConnections.shift();
// ping之后 算获取成功
this.acquireConnection(connection, cb);
return;
}
// 没有限制 或者小于限制 则创建新的connection
if (this.config.connectionLimit === 0 || this._allConnections.length < this.config.connectionLimit) {
connection = new PoolConnection(this, { config: this.config.newConnectionConfig() });
// 获取中
this._acquiringConnections.push(connection);
// 加入所有链接队列
this._allConnections.push(connection);
// 发起链接
connection.connect({timeout: this.config.acquireTimeout}, function onConnect(err) {
// 获取中删除
spliceConnection(pool._acquiringConnections, connection);
if (err) {
// 出错的connect 竟然也放到了 free队列中 可见只存的connect的对象 每次用的时候会重新连接
// 可能连上了就不用重新连 没连上就需要重新建立 这个需要看 connection.connect的实现方式
pool._purgeConnection(connection);
cb(err);
return;
}
// 成功触发
pool.emit('connection', connection);
pool.emit('acquire', connection);
cb(null, connection);
});
return;
}
// 如果不等 则直接抛错
if (!this.config.waitForConnections) {
process.nextTick(function(){
var err = new Error('No connections available.');
err.code = 'POOL_CONNLIMIT';
cb(err);
});
return;
}
// 否则加入队列中
this._enqueueCallback(cb);
};
释放连接方法,主干逻辑就是 放入_freeConnections
队列,如果有等待则执行等待回调。
Pool.prototype.releaseConnection = function releaseConnection(connection) {
...
// release方法会将其重新放入 free的队列中
this._freeConnections.push(connection);
this.emit('release', connection);
if(this._connectionQueue.length) {
// get connection with next waiting callback
this.getConnection(this._connectionQueue.shift());
}
...
};
2.3 TypeORM中执行语句的方法
TypeORM中有多少种执行SQL的方法,他们之间是什么关系,哪些需要释放连接,哪些不需要呢?
QueryRunner
每一个QueryRunner实例从一个连接池中分配一个独立的连接,可以进行操作,常用的方法
- connect 建立连接
- release 释放连接
- startTransaction 开始事物
- commitTransaction 提交事物
- rollbackTransaction 回滚事物
- query 执行查询
它属于TypeORM中最底层的操作对象,如果使用它的方法则需要自己手动的释放连接,否则就会一直占用连接池中的数量。
用DataSource对象可以创建它,可以看到还给QueryRunner添加了一个manager属性,后边会讲到。
createQueryRunner(mode: ReplicationMode = "master"): QueryRunner {
const queryRunner = this.driver.createQueryRunner(mode)
const manager = this.createEntityManager(queryRunner)
Object.assign(queryRunner, { manager: manager })
return queryRunner
}
DataSource
DataSource也提供了一个query
方法直接执行SQL,是不需要手动释放连接,内部已经进行了释放
async query(
query: string,
parameters?: any[],
queryRunner?: QueryRunner,
): Promise<any> {
...
const usedQueryRunner = queryRunner || this.createQueryRunner()
try {
return await usedQueryRunner.query(query, parameters) // await is needed here because we are using finally
} finally {
// 不是用户自己传递的则释放
if (!queryRunner) await usedQueryRunner.release()
}
}
QueryBuilder
QueryBuilder
是 TypeORM 最强大的功能之一,它允许你使用优雅方便的语法构建 SQL 查询,执行它们并获得自动转换的实体。 它的内部也是自动的创建QueryRunner 然后释放。
finally {
if (queryRunner !== this.queryRunner) {
// means we created our own query runner
// 是内部自己创建的 则进行释放
await queryRunner.release()
}
}
DataSource 和EntityManager都提供了创建的方法 createQueryBuilder,但是最终都是调用到DataSource的方法上。
createQueryBuilder<Entity>(
entityOrRunner?: EntityTarget<Entity> | QueryRunner,
alias?: string,
queryRunner?: QueryRunner,
)
EntityMangaer
封装了所有Entity常用的CRUD方法,里边最后执行也是同样有释放的逻辑
finally {
// release query runner only if its created by us
if (!this.queryRunner) await queryRunner.release()
}
DataSouce自身有一个默认的manger,也提供了createEntityManager
可以自己创建。
createEntityManager(queryRunner?: QueryRunner): EntityManager {
return new EntityManagerFactory().create(this, queryRunner)
}
Reposotity
是针对某一个实体进行CRUD操作的对象,和EntityManager相比就是每一个方法可以少传一个Entity类型。它里边的方法完全是调用EntityManager的方法,例如:
save<T extends DeepPartial<Entity>>(
entityOrEntities: T | T[],
options?: SaveOptions,
): Promise<T | T[]> {
return this.manager.save<Entity, T>(
this.metadata.target as any,
entityOrEntities as any,
options,
)
}
获取它的方法是EntityManager的getRepository
getRepository<Entity extends ObjectLiteral>(
target: EntityTarget<Entity>,
): Repository<Entity> {
// 缓存
const repository = this.repositories.find(
(repository) => repository.target === target,
)
if (repository) return repository
...
// 创建
const newRepository = new Repository<any>(
target,
this,
this.queryRunner,
)
this.repositories.push(newRepository)
return newRepository
}
DataSource也提供了方法。
getRepository<Entity extends ObjectLiteral>(
target: EntityTarget<Entity>,
): Repository<Entity> {
return this.manager.getRepository(target)
}
有一个关键的点,上面没有提,其实EntityManager、QueryBuilder在创建的时候是可以指定QueryRunner的,而且如果是指定QueryRunner,则不会进行释放,需要用户手动释放。 所以需要手动释放的场景都是自己创建了QueryRunner, 非必要不需要创建QueryRunner对象。
3 总结
本文从TypeORM中未释放数据库连接导致的问题为入口,讲解了排查问题的思路,然后深入源码,分析了mysql连接池的原理,发生问题的原因。最后讲解了TypeORM中关键的几个对象,他们之间的关系,使用他们执行命令时是否需要手动释放连接。
- 如果觉得有用请帮忙点个赞🙏。
- 我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿。