文章

IO/CPU密集型优化思路—池化技术

IO/CPU密集型优化思路—池化技术

场景1:数据库连接池(IO密集型)

如果每次数据库请求都请求都开启一个连接,然后用完销毁,性能会有问题。

分析程序的日志之后,你发现系统慢的原因出现在和数据库的交互上。因为你们数据库的调用方式是先获取数据库的连接,然后依靠这条连接从数据库中查询数据,最后关闭连接释放数据库资源。

我们统计了一段时间的 SQL 执行时间,发现 SQL 的平均执行时间大概是 1ms,也就是说相比于 SQL 的执行,MySQL 建立连接的过程是比较耗时的。

连接池设计

数据库连接池有两个最重要的配置:最小连接数和最大连接数

  • 如果当前连接数小于最小连接数,则创建新的连接处理数据库请求;
  • 如果连接池中有空闲连接则复用空闲连接;
  • 如果空闲池中没有连接并且当前连接数小于最大连接数,则创建新的连接处理请求;
  • 如果当前连接数已经大于等于最大连接数,则按照配置中设定的时间(C3P0 的连接池配置是 checkoutTimeout)等待旧的连接可用;
  • 如果等待超过了这个设定时间则向用户抛出错误。

特别注意第一条,当所有连接小于最小连接时,优先创建连接,而不是先去空闲池中获取连接。

对于数据库连接池,根据我的经验,一般在线上我建议最小连接数控制在 10 左右,最大连接数控制在 20~30 左右即可。

除此之外,连接放在池中,分为空闲连接池和忙碌连接池:

  • 空闲连接池:其中的连接,可以直接取出,并返回给开发者
  • 忙碌连接池:从空闲连接池取出的就放入忙碌连接池

要检测mysql连接的状态,原因有是:MySQL 有个参数是“wait_timeout”,控制着当数据库连接闲置多长时间后,数据库会主动地关闭这条连接。这个机制对于数据库使用方是无感知的,所以当我们使用这个被关闭的连接时就会发生错误。

检测方法:启动一个线程来定期检测连接池中的连接是否可用,比如使用连接发送“select 1”的命令给数据库看是否会抛出异常,如果抛出异常则将这个连接从连接池中移除,并且尝试关闭。目前 C3P0 连接池可以采用这种方式来检测连接是否可用,也是我比较推荐的方式。

代码实现

类的定义:

1
2
3
4
5
6
7
8
9
10
11
class DbConnectPool {
    constructor() {
        this.minCount = 10;
        this.maxCount = 30;

        this.freePool = [];
        this.busyPool = [];

        this.checker = null;
    }
 }

获取连接和回收连接逻辑:

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
    /**
     * 新建数据库连接(代码省略)
     */
    async createConnect() {}

    /**
     * 获取可用的数据库连接
     */
    async getConnect(retryTimes = 0) {
        if (retryTimes > 2) {
            throw new Error('暂无可用数据库连接')
        }

        if (this.freePool.length + this.busyPool.length < this.minCount) {
            const n = await this.createConnect();
            this.busyPool.push(n);
            return n;
        }

        if (this.freePool.length > 0) {
            const top = this.pool.pop();
            this.busyPool.push(top);
            return top;
        }

        if (this.freePool.length + this.busyPool.length < this.maxCount) {
            const n = await this.createConnect();
            this.busyPool.push(n);
            return n;
        }

        await sleep(10);
        return await this.getConnect(retryTimes + 1)
    }
    /**
     * 回收连接
     */
    recycleConnect(connect) {
        const index = this.busyPool.findIndex(item => item === connect)
        if (index === -1) {
            return;
        }
        this.busyPool.splice(index, 1);
        this.freePool.push(connect);
    }

心跳策略检查连接可用性逻辑:

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
    startCheck() {
        if (this.checker) {
            return;
        }

        this.checker = setInterval(() => {
            this.checkConnect(this.busyPool, 'busy');
            this.checkConnect(this.freePool, 'free')
        }, 100)
    }

    async checkConnect(allConnect, mode = '') {
        const validConnect = []
        for (let i = 0; i < allConnect; ++i) {
            try {
                await connect.send('SELECT 1');
                validConnect.push(allConnect[i])
            } catch (error) {
                console.log('......')
            }
        }
        if (mode === 'busy') {
            this.busyPool = validConnect;
        } else if (mode === 'free') {
            this.freePool = validConnect;
        }
    }

销毁逻辑,防止内存泄漏:

1
2
3
4
5
6
7
8
9
10
    async destory() {
        const allConnect = [...this.busyPool, ...this.freePool];
        for (const connect of allConnect) {
            await connect.close();
        }
        this.busyPool.length = 0;
        this.freePool.length = 0;

        clearInterval(this.checker);
    }

场景2:计算线程池(计算密集型)

设计

JDK 1.5 中引入的 ThreadPoolExecutor 就是一种线程池的实现。

逻辑:

  • 如果线程池中的线程数少于 coreThreadCount 时,处理新的任务时会创建新的线程;
  • 如果线程数大于 coreThreadCount 则把任务丢到一个队列里面,由当前空闲的线程执行;
  • 当队列中的任务堆积满了的时候,则继续创建线程,直到达到 maxThreadCount;
  • 当线程数达到 maxTheadCount 时还有新的任务提交,那么我们就不得不将它们丢弃了。

Untitled.png

为什么超过minCount(coreCount)后,先插入队列,而不是像连接池直接创建?

  1. JDK 实现的这个线程池优先把任务放入队列暂存起来,而不是创建更多的线程,它比较适用于执行 CPU 密集型的任务,也就是需要执行大量 CPU 运算的任务。 因为执行 CPU 密集型的任务时 CPU 比较繁忙,因此只需要创建和 CPU 核数相当的线程就好了,多了反而会造成线程上下文切换,降低任务执行效率。所以当前线程数超过核心线程数时,线程池不会增加线程,而是放在队列里等待核心线程空闲下来。
  2. 针对IO密集型操作,比如缓存查询、数据库查询,在执行 IO 操作的时候 CPU 就空闲了下来,这时如果增加执行任务的线程数而不是把任务暂存在队列中,就可以在单位时间内执行更多的任务,大大提高了任务执行的吞吐量。
本文由作者按照 CC BY 4.0 进行授权