用TypeORM还需要手动释放数据库连接吗?

2,806 阅读7分钟

1 请求pending场景引出数据库连接池问题

当你的node项目中出现请求一直被pending住可能是什么原因呢?当然是一直没有返回。那为什么没有返回呢,肯定是哪里最后没有调用返回。 看一个最核心的例子, 理解一下

const http = require('http')

http.createServer(function(req, res) {
  setTimeout(()=>{
    res.end('End')
  },5000)
}).listen(8040)

image.png 这个例子就是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但是没有调用释放方法。官网文档的提示如下:

image.png

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),
)

内部调用Driverconnect方法

 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的参数。

image.png

所以在第一节中说给连接池设置参数是需要放到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中关键的几个对象,他们之间的关系,使用他们执行命令时是否需要手动释放连接。

  • 如果觉得有用请帮忙点个赞🙏。
  • 我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿