错误看法:
之前我认为数据库连接池尽量设置的大些,越大数据库的性能越高,吞吐量越高。
现在来看是错误的。
性能测试:
先看一个连接池越大反而性能越低的例子:
Oracle性能小组发布的连接池大小性能测试,假设并发量为1万。模拟了9600个并发线程来操作数据库,没两次数据库操作之间sleep 550ms,测试用例及结果为:
1):连接池数为2048,结果每个请求要在连接池队列中等待33ms,获得连接之后,执行SQL耗时77ms,CPU消耗在95%左右。
2):连接池数为1024,结果每个请求要在连接池队列中等待38ms,获得连接之后,执行SQL耗时30ms,耗时减少很多。
两次比较结果为吞吐量基本没变,但是连接池数减半之后wait事件也减少了一半。
3):连接池数为96,结果每个请求在连接池队列中平均等待时间为1ms,SQL执行耗时为2ms。吞吐量大大提高。
这是因为一核的CPU同一时刻只能执行一个线程,多个线程并发执行的话操作系统为每个线程分配时间片,然后快速切换时间片,执行其他线程,不停反复,给我们造成所有线程同时运行的假象。
因此单核CPU顺序执行AB两个线程永远比并发切换时间片执行AB要快!
一旦线程的数量超过了 CPU 核心的数量,再增加线程数系统就只会更慢,而不是更快,因为这里涉及到上下文切换耗费的额外的性能。
其他影响性能的因素
1)CPU
2)磁盘IO
3)网络IO
CPU:
暂不考虑磁盘IO和网络IO,只看CPU的话,在一个8核的服务器上,数据库连接数&线程数设置为8(与核心相同)能够提供最优的性能,如果再增加连接数,反而会因为上下文切换导致性能下降。
磁盘IO:
数据库通常把数据存储在磁盘上,磁盘读写寻址的时候,线程需要阻塞等待着磁盘(IO等待),此时操作系统可以将那个空闲的CPU核心用于服务其他线程。所以,由于线程总是在IO上阻塞,我们可以让线程/连接数比CPU核心多一些,这样能够在同样的时间内完成更多的工作。
较新型的SSD不需要寻址,也没有旋转的碟片。但是别认为应该增加更多的线程数,因为无需寻址和没有旋回耗时意味着更少的阻塞(CPU不会因阻塞而空闲),所以更少的线程【更接近于CPU核心数】会发挥出更高的性能。只有当阻塞创造了更多的执行机会时(CPU因阻塞而空闲),更多的线程数才能发挥出更好的性能。
网络IO:
网络和磁盘类似,通常以太网接口读写数据时也会形成阻塞,10G带宽会比1G带宽的阻塞少一些,1G带宽又会比100M带宽的阻塞少一些。不过网络通常是放在第三位考虑的,有些人会在性能计算中忽略它们。
总结
寻找最合适的连接数可以参考下面这个公式:
连接数 =(核心数*2)+ 有效磁盘数
一般服务器的磁盘个数都是1,因此CPU为4核的数据库服务器的连接池大小应该为(4*2)+1=9,取整为10。
根据性能压力测试,这个值能轻松搞定3000用户以6000TPS的速率并发执行查询的场景,如果连接数超过10,就会看到响应时长开始增加,TPS开始下降。
即连接池中的连接数量应该等于你的数据库能够有效同时进行的查询任务数(通常不会高于2*CPU核心数)
注:这一公式其实不仅适用于数据库连接池的计算,大部分涉及计算和I/O的程序,线程数的设置都可以参考这一公式。
我们需要的是一个小连接池和一个大的饱和等待连接的线程的队列
如果并发数为10000,我们需要一个大小为10的连接池,然后让剩下的业务线程在队列里等待就可以了。
附:数据源配置
@Bean public BasicDataSource globalDataSource() { BasicDataSource dataSource = new BasicDataSource(); dataSource.setDriverClassName("com.mysql.jdbc.Driver"); dataSource.setUrl(jdbc:mysql://xxx:3306/xxx); dataSource.setUsername(xxx); dataSource.setPassword(xxx); // 分配的最大连接数(推荐2N,N为数据库服务器cpu核心数),负值表示无限制 dataSource.setMaxActive(10); dataSource.setMaxIdle(2); dataSource.setMinIdle(1); dataSource.setMaxWait(6000); // DBA推荐连接池配置 // 用来验证连接是否生效的sql语句 dataSource.setValidationQuery("SELECT 1"); // 从池中获取连接前进行验证 dataSource.setTestOnBorrow(false); // 向池中还回连接前进行验证 dataSource.setTestOnReturn(false); // 连接空闲时验证 dataSource.setTestWhileIdle(true); // 运行判断连接超时任务(evictor)的时间间隔,单位为毫秒,默认为-1,即不执行任务 dataSource.setTimeBetweenEvictionRunsMillis(60000); // 连接的超时时间,默认为半小时 dataSource.setMinEvictableIdleTimeMillis(60000); return dataSource; }
参考:https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing