今天在公司对系统进行压测,由于我的sit和dev和uat环境都是用的用的是一个数据库服务器,我让用户在的sit进行压测,分别是单线程测试,并发测试,但是用户一开始测,我的三个环境就都挂掉了。 用户一下子全部找上门来,我就跑去看容器是不是cpu内存或者io被耗尽了,结果一看40%!还差的远呢!那就究竟是什么导致我的环境都访问不了呢?我打开Navicat去看看数据库,结果发现连接数据库居然耗时特别久,我就明白了原来是数据库连接拿不到。那么应该的因素有哪些呢?一个是数据库的最大连接数;一个是配置文件中数据源连接池的配置。
通常,mysql的最大连接数默认是100, 最大可以达到16384
show variables like 'max_connections';(查可以看当前的最大连接数)
set global max_connections=1000;(设置最大连接数为1000,可以再次查看是否设置成功,这个只是暂时的,如果mysql服务重启,就会失效,真正要改还是要在my.ini文件中修改)
在修改最大连接数的时候会有这样一个疑问—这个值是不是越大越好,或者设置为多大才合适?这个参数的大小要综合很多因素来考虑,比如使用的平台所支持的线程库数量(windows只能支持到2048)、服务器的配置(特别是内存大小)、每个连接占用资源(内存和负载)的多少、系统需要的响应时间等。可以在global或session范围内修改这个参数。连接数的增加会带来很多连锁反应,需要在实际中避免由此引发的负面影响。
首先看一下MySQL的状态:
show status;
Open tables:560,即当前数据库打开表的数量是560个,注意这个560并不是实际的560个表,因为MySQL是多线程的系统,几个不同的并发连接可能打开同一个表,这就需要为不同的连接session分配独立的内存空间来存储这些信息以避免冲突。因此连接数的增加会导致MySQL需要的文件描述符数目的增加。另外对于MyISAM表,还会建立一个共享的索引文件描述符。
在MySQL数据库层面,有几个系统参数决定了可同时打开的表的数量和要使用的文件描述符,那就是table_open_cache、max_tmp_tables和open_files_limit
table_open_cache:2000,这就是说所有的MySQL线程一共能同时打开2000个表,我们可以搜集系统的打开表的数量的历史记录和这个参数来对比,决定是否要增加这个参数的大小。查看当前的打开表的数目(Open tables)可用上边提到过的status命令,另外可以直接查询这个系统变量的值:
Open_tables就是当前打开表的数目,通过flush tables命令可以关闭当前打开的表。 这个值如果过大,并且如果没有经常的执行flush tables命令,可以考虑增加table_open_cache参数的大小。
接下来看max_tmp_tables:
show variables like 'max_tmp%';
max_tmp_tables:32即单个客户端连接能打开的临时表数目。
查看当前已打开的临时表的信息:
show global status like '%tmp%table%';
根据这两个值可以判断临时表的创建位置,一般选取BLOB和TEXT列、Group by 和 Distinct语句的数据量超过512 bytes,或者union的时候select某列的数据超过512 bytes的时候,就直接在磁盘上创建临时表了,另外内存中的临时表变大的时候,也可能被MySQL自动转移到磁盘上(由tmp_table_size和max_heap_table_size参数决定)。
增加table_open_cache或max_tmp_tables 参数的大小后,从操作系统的角度看,mysqld进程需要使用的文件描述符的个数就要相应的增加,这个是由open_files_limit参数控制的。
show variables like 'open_files%';
但是这个参数是OS限制的,所以我们设定的值并不一定总是生效。如果OS限制MySQL不能修改这个值,那么置为0。如果是专用的MySQL服务器上,这个值一般要设置的尽量大,就是设为没有报Too many open files错误的最大值,这样就能一劳永逸了。当操作系统无法分配足够的文件描述符的时候,mysqld进程会在错误日志里记录警告信息。
相应的,有两个状态变量记录了当前和历史的文件打开信息:
MySQL为每个连接分配线程来处理,可以通过threads_connected参数查看当前分配的线程数量:
show status like '%thread%';
比较threads_connected参数和前面提到的max_connections参数,也可以作为目前的系统负载的参照,决定是否需要修改连接数。
查看每个线程的详细信息:mysql>show processlist;对影响系统运行的线程:kill connection|query threadid的命令杀死。
Java程序很大一部分要操作数据库,操作数据库需要不断建立连接释放连接,为了高性能操作数据库,数据库连接池横空出世。阿里开源在 github 上面的数据库连接池是其中十分优秀的,国内大量的公式都在使用。
这里主要讲高并发下的数据源连接池配置,druid的其它优秀的公共包括监控、安全机制等都不作拓展。
配置参数:
和其它连接池一样DRUID的DataSource类为:com.alibaba.druid.pool.DruidDataSource,基本配置参数如下:
配置 | 缺省值 | 说明 |
name | 配置这个属性的意义在于,如果存在多个数据源,监控的时候可以通过名字来区分开来。 如果没有配置,将会生成一个名字,格式是:"DataSource-" + System.identityHashCode(this) |
|
jdbcUrl | 连接数据库的url,不同数据库不一样。例如: mysql : jdbc:mysql://10.20.153.104:3306/druid2 oracle : jdbc:oracle:thin:@10.20.149.85:1521:ocnauto |
|
username | 连接数据库的用户名 | |
password | 连接数据库的密码。如果你不希望密码直接写在配置文件中,可以使用ConfigFilter。详细看这里:https://github.com/alibaba/druid/wiki/%E4%BD%BF%E7%94%A8ConfigFilter | |
driverClassName | 根据url自动识别 | 这一项可配可不配,如果不配置druid会根据url自动识别dbType,然后选择相应的driverClassName(建议配置下) |
initialSize | 0 | 初始化时建立物理连接的个数。初始化发生在显示调用init方法,或者第一次getConnection时 |
maxActive | 8 | 最大连接池数量(不是越大越好) |
maxIdle | 8 | 已经不再使用,配置了也没效果 |
minIdle | 最小连接池数量 | |
maxWait | 获取连接时最大等待时间,单位毫秒。配置了maxWait之后,缺省启用公平锁,并发效率会有所下降,如果需要可以通过配置useUnfairLock属性为true使用非公平锁。 | |
poolPreparedStatements | false | 是否缓存preparedStatement,也就是PSCache。PSCache对支持游标的数据库性能提升巨大,比如说oracle。在mysql下建议关闭。 |
maxOpenPreparedStatements | -1 | 要启用PSCache,必须配置大于0,当大于0时,poolPreparedStatements自动触发修改为true。在Druid中,不会存在Oracle下PSCache占用内存过多的问题,可以把这个数值配置大一些,比如说100 |
validationQuery | 用来检测连接是否有效的sql,要求是一个查询语句。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会其作用。 | |
testOnBorrow | true | 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 |
testOnReturn | false | 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能 |
testWhileIdle | false | 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。 |
timeBetweenEvictionRunsMillis | 有两个含义: 1) Destroy线程会检测连接的间隔时间2) testWhileIdle的判断依据,详细看testWhileIdle属性的说明 |
|
numTestsPerEvictionRun | 不再使用,一个DruidDataSource只支持一个EvictionRun | |
minEvictableIdleTimeMillis | ||
connectionInitSqls | 物理连接初始化的时候执行的sql | |
exceptionSorter | 根据dbType自动识别 | 当数据库抛出一些不可恢复的异常时,抛弃连接 |
filters | 属性类型是字符串,通过别名的方式配置扩展插件,常用的插件有: 监控统计用的filter:stat日志用的filter:log4j防御sql注入的filter:wall |
|
proxyFilters | 类型是List |
数据库连接池的设置分析——测试数据
该测试数据是Oracle 性能小组测试&发布的,视频内容在https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing
连接池设置数量越小、总耗时越少?
联想到为啥Nginx内部仅仅使用了4个线程,其性能就大大超越了100个进程的Apache HTTPD呢?好像更加坚定了这一观点。
这取决于你的资源:要知道,即使是单核 CPU 的计算机也能“同时”运行着数百个线程。但我们其实都知道,这只不过是操作系统快速切换时间片,跟我们玩的一个小把戏罢了。一核 CPU同一时刻只能执行一个线程,然后操作系统切换上下文,CPU 核心快速调度,执行另一个线程的代码,不停反复,给我们造成了所有进程同时运行假象。其实,在一核 CPU 的机器上,顺序执行A和B永远比通过时间分片切换“同时”执行A和B要快,其中原因,学过操作系统这门课程的童鞋应该很清楚。一旦线程的数量超过了 CPU 核心的数量,再增加线程数系统就只会更慢,而不是更快,因为这里涉及到上下文切换耗费的额外的性能。
时间片的概念是什么?
时间片即CPU分配给各个程序的时间,每个线程被分配一个时间段, 称作它的时间片,即该进程允许运行的时间,使各个程序从表面上看是同时进行的。
如果在时间片结束时进程还在运行,则CPU将被剥夺并分配给另一个进程。如果进程在时间片结束前阻塞或结束,则CPU当即进行切换。而不会造成CPU资源浪费。
在宏观上:我们可以同时打开多个应用程序,每个程序并行不悖,同时运行。
但在微观上:由于只有一个CPU,一次只能处理程序运行的一部分,如何处理公平,一种方法就是引入时间片,每个程序轮流执行。
结论:当线程数大于CPU的核心数,需要通过算法及时间分片切换该哪个线程获得CPU的使用权,这个是需要耗时的。因为这里涉及到上下文切换耗费的额外的性能。
如果计算机是4核,机器值运行四个线程,处理速度相当快。但这只是理想状态。
实际上,限制线程发挥的,不止有CPU,还有磁盘IO和网络IO。
下面分别来看CPU 磁盘IO 网络IO:
只考虑CPU
结论1:如果只考虑CPU,不考虑磁盘I/O和网络I/O,在一个8核的服务器上,数据库连接数/线程数设置为8能够提供最优的性能,如果再增加连接数,反而会因为上下文切换导致性能下降。(这是理想情况)
数据库连接数/线程数= CPU的核心数
耗时因素:寻址的耗时+旋转耗时;
当你的线程处理的是IO密集型业务时,便可以让 线程/连接数 设置的比CPU核心数大一些,这样就能够在同样的时间内,完成更多的工作,提升吞吐量。因为磁盘寻址的时候,CPU是空闲的,可以让CPU做点其他的。
问题:设置成多大合适呢?
取决于磁盘:SSD固态硬盘、普通机械硬盘
SSD固态硬盘,它不需要寻址,也不需要旋转碟片!是不是可以设置更大些?
结论正好相反! 无需寻址和没有旋回耗时的确意味着更少的阻塞,所以更少的线程( 更接近于CPU核心数)会发挥出更高的性能。只有当阻塞密集时,更多的线程数才能发挥出更好的性能。
考虑网络IO
网络是我们应用程序最难把控了,因为网络拥塞、超时 由不得我们控制;通常网络瓶颈业务我们系统优化放在最后的。
如图,随着客户端连接数的增加,每秒传输速度影响还是很大的。
该图是 PostgreSQL 的基准性能测试数据,从图中我们可以看到,TPS 在连接数达到 50 时开始变缓。回过头来想下,在上面 Oracle 的性能测试视频中,测试人员们将连接数从 2048 降到了 96,实际上 96 还是太高了,除非你的服务器 CPU 核心数有 16 或 32。
数据库连接池计算公式
下面公式由PostgreSQL 提供,不过底层原理是不变的,它适用于市面上绝大部分数据库产品。还有,你应该模拟预期的访问量,并通过下面的公式先设置一个偏合理的值,然后在实际的测试中,通过微调,来寻找最合适的连接数大小。
连接数=((CPU核心数*2)+有效磁盘数)
注意:核心数不应包含超线程(hyper thread),即使打开了超线程也是如此,如果热点数据全被缓存了,那么有效磁盘数实际是0,随着缓存命中率的下降,有效磁盘数也逐渐趋近于实际的磁盘数。另外需要注意,这一公式作用于SSD的效果如何,尚未明了
举个栗子
按照这个公式,如果说你的服务器CPU是4核i7的,连接池大小应该为((4*2)+1)=9。
取个整,我们就设置为10吧。你这个行不行啊? 10 也太小了吧!
测试结果表明:这个设置能轻松支撑3000用户以6000 TPS的速率并发执行简单查询的场景。你还可以将连接池大小超过10,那时,你会看到响应时长开始增加,TPS开始下降。
结论:你需要的是一个小连接池,和一个等待连接的线程队列!
补充:连接池中的连接数量大小应该设置成:数据库能够有效同时进行的查询任务数(通常情况下来说不会高于2*CPU核心数)。
回顾数据库连接池的实现原理
JDBC访问数据库的问题总结
(1)数据库连接资源很稀缺,每次创建和关闭都非常耗时。
(2)每次数据库连接使用完后要及时关闭回收,如果程序运行中间出现异常,导致连接没有及时关闭,将有可能导致内存泄漏,服务器崩溃。
(3)这种方式,不能控制被创建的连接对象的数量,也就是说,你可以创建无数个连接对象。没有谁去约束限制,对象太多,导致内存泄漏,服务器崩溃。
由网络资料收集整理而成