一次疑似数据库死锁问题的排查

Posted by Rimin on 2020-01-18

初次使用数据库连接相关的一个比较新的框架 Typeorm

TypeORM是一个ORM框架,它可以运行在NodeJS、浏览器、Cordova、PhoneGap、Ionic、React Native、Expo和Electron平台上,可以与TypeScript和JavaScript (ES5, ES6, ES7)一起使用。

但是在项目中初次使用typeorm时遇到的一个是疑似数据库锁死问题。部署之后一切运转正常,但是经过一定时间的运行之后会在所有存在数据库操作的地方卡住。服务重启之后可以恢复正常。

于是开始排查问题,难点在于,这个问题难以复现,也就是不知道什么时候应用会卡住,且对部分接口做过简单的压力测试也没有复现。因此只能根据"症状"及现有知识去逐个分析排查问题的根源所在。

排查过程

猜测一: 数据库连接数过大?

使用命令show full processlist

Alt 调试结果

可以看出当"卡住"时,连接数并不大,但是我们的服务应用有10条和数据库的连接(IP地址相同端口号不同)

接着使用 show variables like 'max_connections'

Alt 调试结果

实际上,数据库的最大连接数还有151,因此可以排除数据库连接数过多的问题。

猜测二: 数据库并发死锁?

由于发生"卡住"的问题时并发量并不大,并且使用命令 show engine innodb status \G; 也并没有发现有数据库死锁导致的错误的日志。

另外使用 SHOW STATUS LIKE 'Table%';

Alt 调试结果

通过检查 table_locks_waitedtable_locks_immediate 状态变量来分析系统上的表锁的争夺,如果 Table_locks_waited 的值比较高,则说明存在着较严重的表级锁争用情况。

由此也可以排除由于数据库发生死锁而导致的卡住的问题。

数据库端排查之后,确定问题应该是出在应用端,也就是发起数据库连接的服务应用。于是开始排查服务端哪里出了问题:

首先先来看看当 Node 服务端开启一个服务的时候发生了什么:

1
2
3
4
5
6
7
8
9
10
var http = require('http');
var server = http.createServer();
// 注册 request 请求事件
server.on('request', function(request, response){
console.log()
})
server.listen(8000, function(){
console.log('port 8000 is listening');
console.log(global);
})

通过一个简单的开启一个 Node 服务器来看一下全局 global 对象:

Alt 调试结果

实际上,就是一个 process 对象,pid33334, 即一个进程。

而实际上,当Node 开启一个服务时,它开启了:

  • 一个进程(One process)
  • 一个线程(One thread)
  • 一个事件循环(One event loop)
  • 一个JS引擎实例 ( One JS Engine Instance)
  • 一个NodeJs实例 ( One Node.js Instance)

Node 运行在单线程上,并且在事件循环中同一时刻只有一个进程的任务被执行,每次同一时刻只会执行一段代码(多段代码不会同时执行)。这是非常有效的,因为这样的机制足够简单,让你在使用 JavaScript 的时候无需担心并发编程的问题。

所以,如果一个 NodeJs 服务进程中有多个数据库连接(通过不同端口)(如第一张图片所示),只可能是在一个应用进程中创建了多个数据库连接。

于是回到 typeorm 本身,typeorm 的使用实际上很简单(只是文档有点杂乱,不够完善,比较坑 😷),而且开启了一个连接后,服务中需要使用数据库操作都可以复用服务启动时的连接而无需重新创建。

简单的一个使用 typeorm 连接数据库的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import "reflect-metadata";
import {createConnection} from "typeorm";
import {Photo} from "./entity/Photo";

createConnection({
type: "mysql",
host: "localhost",
port: 3306,
username: "root",
password: "admin",
database: "test",
entities: [
Photo
],
synchronize: true,
logging: false
}).then(connection => {
// here you can start to work with your entities
// 数据库连接后可以启动服务程序,connection 可设置在需要使用数据库的地方可复用
}).catch(error => console.log(error));

首先,从Node应用进程的角度以及对typeorm的使用,都是一次Node服务会创建一个可复用的链接,并且使用mysql时,默认使用数据库连接池连接,connection pooling

数据库连接池负责分配、管理和释放数据库连接,它允许应用程序重复使用一个现有的数据库连接,即数据库连接池在初始化的过程中已经创建了若干数据库连接置于池内备用。因此对于需要频繁创建,释放连接引起的操作,能避免由此引起的大量的性能开销。

数据库连接池的作用主要在于

  • 资源的重用
  • 更快的响应速度
  • 统一的连接管理(如根据预先的连接占用超时时的设定,强制回收被占用的连接)

但是对于不需要频繁创建连接及关闭的应用,一般一个服务程序应该只有一个连接。但是从 show full processlist 来看,当时创建了十个数据库连接,所以提出猜测。

猜测三:在使用 typeorm 的过程中创建了连接却没有释放超过连接池的最大连接数

通过仔细查阅文档,发现是在应用中使用了 querryRunner 但是却没有在使用之后释放 。

使用querryRunner 的简单示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
const queryRunner = connection.createQueryRunner();

// you can use its methods only after you call connect
// which performs real database connection
await queryRunner.connect();

// .. now you can work with query runner and call its methods

await queryRunner.query('select * from test_table');

// very important do not forget to release query runner once you finished working with it

await queryRunner.release();

一般来说,由于typeorm使用的是orm语法:

面向对象编程把所有实体看成对象(object),关系型数据库则是采用实体之间的关系(relation)连接数据。很早就有人提出,关系也可以用对象表达,这样的话,就能使用面向对象编程,来操作关系型数据库。
ORM 就是通过实例对象的语法,完成关系型数据库的操作的技术,是"对象-关系映射"(Object/Relational Mapping) 的缩写。

ORM 语法支持的sql操作主要是CRUD, 即主要为增删查改。

但是对于比较复杂的操作,没有对应的语法,就需要使用sql 语句,queryRunner 模块就提供了这样一个功能。

但是官方文档并没有说明为何在使用之后需要释放,于是简单地从源码去验证是创建了新的数据库连接:

MysqlQueryRunner.ts#L68MysqlDriver.ts#L661 可以看出 queryRunner 会从已创建的连接池中获取一条新的连接:

部分源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

/**
* Creates/uses database connection from the connection pool to perform further operations.
* Returns obtained database connection.
*/
// mysql driverQuerry connet 方法
connect(): Promise<any> {
if (this.databaseConnection)
return Promise.resolve(this.databaseConnection);

if (this.databaseConnectionPromise)
return this.databaseConnectionPromise;

if (this.mode === "slave" && this.driver.isReplicated) {

this.databaseConnectionPromise = this.driver.obtainSlaveConnection().then(connection => {
this.databaseConnection = connection;
return this.databaseConnection;
});

} else { // master
this.databaseConnectionPromise = this.driver.obtainMasterConnection().then(connection => {
this.databaseConnection = connection;
return this.databaseConnection;
});
}

return this.databaseConnectionPromise;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* Obtains a new database connection to a master server.
* Used for replication.
* If replication is not setup then returns default connection's database connection.
*/
obtainMasterConnection(): Promise<any> {
return new Promise<any>((ok, fail) => {
if (this.poolCluster) {
this.poolCluster.getConnection("MASTER", (err: any, dbConnection: any) => {
err ? fail(err) : ok(this.prepareDbConnection(dbConnection));
});

} else if (this.pool) {
this.pool.getConnection((err: any, dbConnection: any) => {
err ? fail(err) : ok(this.prepareDbConnection(dbConnection));
});
} else {
fail(new Error(`Connection is not established with mysql database`));
}
});
}

每使用一次就创建了一次数据库连接,而 typeorm 默认的数据库连接池最大连接池就是10, 如果没有释放,就会使得连接数一直增加没有释放直到连接数最大值 10,从而出现卡死的现象(但是没有报错,个人认为应该加上报错的机制)。于是问题得到解决。

总结

  • typeorm 作为一个使用 typescript 开发的数据库连接操作封装库比较新,有可能有坑,文档不够完善也需要一定的学习成本,但是语法便捷易用,本身轻量,性能也有一定的保证。

  • 问题的排查
    有很多bug是线上的问题,生产环境出的问题实际上难以复现, 因此可以在保证线上运行正常的情况下先根据表象做出合理的推理,尽可能多地收集线索,分析整理,逐一排查攻克。