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

Tags
池化设计
多进程编程
CreatedTime
Aug 23, 2022 09:19 AM
Slug
2020-10-12-pool
UpdatedTime
Last updated August 23, 2022

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

如果每次数据库请求都请求都开启一个连接,然后用完销毁,性能会有问题。
分析程序的日志之后,你发现系统慢的原因出现在和数据库的交互上。因为你们数据库的调用方式是先获取数据库的连接,然后依靠这条连接从数据库中查询数据,最后关闭连接释放数据库资源。
我们统计了一段时间的 SQL 执行时间,发现 SQL 的平均执行时间大概是 1ms,也就是说相比于 SQL 的执行,MySQL 建立连接的过程是比较耗时的。

连接池设计

数据库连接池有两个最重要的配置:最小连接数和最大连接数
  • 如果当前连接数小于最小连接数,则创建新的连接处理数据库请求;
  • 如果连接池中有空闲连接则复用空闲连接;
  • 如果空闲池中没有连接并且当前连接数小于最大连接数,则创建新的连接处理请求;
  • 如果当前连接数已经大于等于最大连接数,则按照配置中设定的时间(C3P0 的连接池配置是 checkoutTimeout)等待旧的连接可用;
  • 如果等待超过了这个设定时间则向用户抛出错误。
特别注意第一条,当所有连接小于最小连接时,优先创建连接,而不是先去空闲池中获取连接。
对于数据库连接池,根据我的经验,一般在线上我建议最小连接数控制在 10 左右,最大连接数控制在 20~30 左右即可。
除此之外,连接放在池中,分为空闲连接池和忙碌连接池:
  • 空闲连接池:其中的连接,可以直接取出,并返回给开发者
  • 忙碌连接池:从空闲连接池取出的就放入忙碌连接池
要检测mysql连接的状态,原因有是:MySQL 有个参数是“wait_timeout”,控制着当数据库连接闲置多长时间后,数据库会主动地关闭这条连接。这个机制对于数据库使用方是无感知的,所以当我们使用这个被关闭的连接时就会发生错误。
检测方法:启动一个线程来定期检测连接池中的连接是否可用,比如使用连接发送“select 1”的命令给数据库看是否会抛出异常,如果抛出异常则将这个连接从连接池中移除,并且尝试关闭。目前 C3P0 连接池可以采用这种方式来检测连接是否可用,也是我比较推荐的方式。

代码实现

类的定义:
class DbConnectPool { constructor() { this.minCount = 10; this.maxCount = 30; this.freePool = []; this.busyPool = []; this.checker = null; } }
获取连接和回收连接逻辑:
/** * 新建数据库连接(代码省略) */ 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); }
心跳策略检查连接可用性逻辑:
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; } }
销毁逻辑,防止内存泄漏:
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 时还有新的任务提交,那么我们就不得不将它们丢弃了。
notion image
 
为什么超过minCount(coreCount)后,先插入队列,而不是像连接池直接创建?
1. JDK 实现的这个线程池优先把任务放入队列暂存起来,而不是创建更多的线程,它比较适用于执行 CPU 密集型的任务,也就是需要执行大量 CPU 运算的任务。 因为执行 CPU 密集型的任务时 CPU 比较繁忙,因此只需要创建和 CPU 核数相当的线程就好了,多了反而会造成线程上下文切换,降低任务执行效率。所以当前线程数超过核心线程数时,线程池不会增加线程,而是放在队列里等待核心线程空闲下来。 2. 针对IO密集型操作,比如缓存查询、数据库查询,在执行 IO 操作的时候 CPU 就空闲了下来,这时如果增加执行任务的线程数而不是把任务暂存在队列中,就可以在单位时间内执行更多的任务,大大提高了任务执行的吞吐量。