Web应用单点压力测试调优-第6季-阶段性总结
阶段性总结
<!--[if !supportLists]-->1) <!--[endif]-->应用的测试用例类型其实是简单的随机get几条json信息。并没有复杂的业务逻辑处理(权限验证等等),所以在业务上基本没有可优化的余地,转成异步的队列请求操作也是得不偿失。像注册用户、提交评论、上传图片等等这些耗时、与下一个操作无关联的、又是POST的HTTP请求就可以使用生产者与消费者模式进行(story待改变)。
<!--[if !supportLists]-->2) <!--[endif]-->首先保证http的连接都能够正确的进行接入,tomcat的连接器模块,默认的bio+默认的连接池个数,连接代价过大,并且操作系统需要创建过多的线程(连接)进行维护。至于http的长短连接,基本上随着并发量的增加,可以使用默认的keep-alive。因为采用NIO本身就是为了复用多路通道达到长连接的目的,而随着并发量的增大,原先的看似比较短暂的连接也会变成长连接(时间耗费在:缓存穿透、JVM GC回收、缓存Hash表的扩容、读写锁的阻塞、网络以及磁盘的IO耗时、网络数据的解码-编码、Java程序的运行、Web容器的生命周期流转等等环节)
<!--[if !supportLists]-->3) <!--[endif]-->应用程序日志,为了避免过多的磁盘IO,内存交换、CPU二级缓存的读写,只能将其调整为ERROR模式或者再升一级INFO模式,ERROR为了排查问题、INFO模式基本上是为了分析运行日志,预测网站下一个阶段在哪些功能上有比较高的访问以及使用需求。将运行日志(http访问日志、应用业务日志、耗时日志、MySql操作日志、Linux系统运行日志等等)放到Hadoop中进行建模->分析->出结果。可以预测网站下一步要做哪些事情,才能预防”雪崩”的事件发生。
保证了http的请求后,下面就是根据业务场景进行相关优化。
主要是代码优化,代码是指从Spring MVC的控制层开始一直到mysql server之前。1-从MVC的控制层进行优化,首先将grid上面访问两次数据库(一次是分页查询数据,一次是查询记录总个数)的业务改为一次。因为互联网对于数据一致性(CAP理论)的要求不是很高。所以,启动Web容器的时候,将所有业务表的记录先select count(*)一遍,将结果存入到一个memory表中,索引类型为hash类型。之后再将其键值对儿(表名:记录个数)缓存到本地内存。每20分钟(可根据后期实际情况变更设置-主要参考点是:数据的时效性)一次查询。只要数据库服务没关闭或者宕掉,memory就会存储这一时段的记录数。
如此做有两个目的:第一,减少一次db连接访问数;第二,如果记录结果过大(比如表记录的数量级超过了100w左右)每次的count操作很慢,不如就记录一定时间内的记录个数,得出的记录虽然不准确,但是,也不会影响业务查询处理。
2-之后进入到持久层,将JDBC要执行的SQL写成预编译语句,这样mysql底层引擎不必每次都编译类似,甚至相同的sql语句,节省了中间过程,不过要保证mysql的欲编译空间要够。
3-下面就是最关键的20%解决了80%的本地内存缓存了。将热点数据缓存到内存中,本次压力测试,假设了新闻记录是热点数据(经常访问的vo list),将其缓存到了本地内存中。配合上JDK的读写锁——ReentrantReadWriteLock。
4-持久层循环,在持久层循环传入预编译参数的时候,可以跳跃性的传递,将循环步长值+2,减少循环的次数。
Connection connection = null; PreparedStatement statement = null; ResultSet resultSet = null; int length = parms.length; try { connection = dataSource.getConnection(); statement = connection.prepareStatement(sql); for (int i = 0; i < length; i=i+2) { if(i!=length-1){ statement.setObject(i + 1, parms[i]); statement.setObject(i + 2, parms[i+1]); }else{ statement.setObject(i + 1, parms[i]); } } resultSet = statement.executeQuery(); List list = AnalystResultSet.getBeans(sql, clazz, resultSet); return list; }
这样做的好处不仅仅是减少循环,还让connection对象可以快速“还给”资源池。
5-内部orm框架缓存表结构,字段结构以及实体分析结果。获取ResultSet的元信息,一次即可,存储到本地缓存中,下次访问直接从缓存里面去取(表名:Bean结构、SQL:字段数组)。将反射后的method对象也要进行缓存,方便后续直接拿出来就用。
6-返回json数据替换原始的jackson为fastjson。网上有测试,证明阿里巴巴的fastjson性能高于jackson(http://www.iteye.com/topic/1113183)
7-其他代码级别优化,初始化map构造函数、初始化stringbuilder初始化函数、初始化List构造函数,缓存反射实体等等。
<!--[if !supportLists]-->1) <!--[endif]-->JVM相关优化
GC方面:采用UseAdaptiveSizePolicy,将自动调整新生代中eden和sub1、sub2的比例关系,SurvivorRatio=XXXX失效。-server配置后,年轻代采用标记-复制算法(parallel scavenge)、老年代采用标记-整理算法(serial old),保证吞吐量优先,也是-server的默认配置,策略为新生代使用并行清除,年老代使用单线程Mark-Sweep-Compact的垃圾收集器(因为虚拟机是单核CPU)。MaxTenuringThreshold=20,年轻代的年龄为20次,20次未能回收,直接放到老年代。年轻代是并行收集,老年代是串行。因此后续的配置将年轻代配的比较大。(根据此应用需求,大多数对象数据,朝生夕灭)。
内存大小:-XX:PermSize=82M -XX:MaxPermSize=82M -Xss256k -Xms480m -Xmx480m -Xmn320m,根据上面GC考虑以及JMX观察,持久带不需要过多的内存,90mb左右足够,堆内存最大480m内存,年轻代占了将近66.7%左右。每个线程只占用256k的栈内存,调小此项可以建立更多的线程。因为此栈调用无递归等复杂操作,所以256k栈深度已经足够了。
锁优化:因为JDK6已经默认启用了很多锁的优化,所以未进行干扰(http://kenwublog.com/docs/java6-jvm-options-chinese-edition.htm),如果修改也是进行自旋次数的设置-XX:PreBlockSpin=10,默认10次检测。根据业务调整以及竞争度进行调整。
其他方面:+UseFastAccessorMethods:优化原始类型的getter方法性能;在非调试环境下去除各种gc log配置;-Xverify:none,因为暂时没有不可控的代码,默认去除类加载中的类合法性的验证环节。-Djava.nio.channels.spi.SelectorProvider强指定NIO的提供者是oracle的实现——sun.nio.ch.EPollSelectorProvider;若系统稳定了,可以去除JMX监控,看性能指标是否略有提升。
(JVM的调优还可进一步进行,主要关注点还是,GC组合算法、锁、分代年龄、若内存减少数据库方面的消耗的话,还可以进一步剥削,堆内存可调大,栈内存继续略调小)
<!--[if !supportLists]-->2) <!--[endif]-->数据库优化
最难的当属数据库优化,因为它既牵扯着业务(数据结构、数据记录内容)也牵扯着非功能(表引擎、连接数、mysql相关配置)。
在应用层修改JDBC连接池的url配置,是为了让JDBC Connection连接都能开启相应的优化特性(仅在应用程序与mysql connection之间这座连接桥)比如:useServerPrepStmts=true是为了让连接支持长久的预编译statement。对于mysql以及应用程序,更能快速的互相通讯、请求-吞吐数据。
Mysql优化后的server方面的配置再次附上,附解说
#update start #客户端最大连接数 max_connections=1500 #查询缓存,因为在应用内存中。所以此项调制较小(http://banu.blog.163.com/blog/static/2314648201077735322/) query_cache_size=16M #默认表引擎是带事务的INNODB default-storage-engine=INNODB #表缓存大小,同以上连接介绍。为所有线程打开表的数量。增加该值能增加mysqld要求的文件描述符的数量。MySQL对每个唯一打开的表需要2个文件描述符还是因为表中热点数据已经缓存到应用本地内存,等价于----->table_open_cache #table_cache=256 #此设置是查询中如果出现了临时虚表,虚表的大小,每个查询线程都要占用内存,因为应用中使用了group by语句建立了临时表,所以需要设置此项,在业务中尽量避免group by(麻烦) tmp_table_size=8M #这个值表示可以重新利用保存在缓存中线程的数量,当断开 连接时如果缓存中还有空间,那么客户端的线程将被放到缓存中,如果线程重新被请求,那么请求将从缓存中读取,如果缓存中是空的或者是新的请求,那么这个线 程将被重新创建,如果有很多新的线程,这里整个操作系统的内存才1GB,那么设为8其实有点大。 thread_cache_size=8 #因为用例是连续扫描news表的前10页(每页15条)数据记录,所以为了避免连续扫描过慢,设置此读取缓存 read_buffer_size=64K #类似于read_buffer_size选项,MySql的随机读(查询操作)缓冲区大小。当按任意顺序读取行时(例如,按照排序顺序),将分配一个随机读缓存区。进行排序查询时,MySql会首先扫描一遍该缓冲,以避免磁盘搜索,提高查询速度,如果需要排序大量数据,可适当调高该值。但MySql会为每个客户连接发放该缓冲空间,所以应尽量适当设置该值,以避免内存开销过大。因为查询语句有个随机页数,非顺序读取。所以排序缓存应当设置上 read_rnd_buffer_size = 256K #消息缓冲区被初始化为net_buffer_length字节,但是可在需要时增加到max_allowed_packet个字节。缺省地,该值太小必能捕捉大的(可能错误)包。如果你正在使用大的BLOB列,你必须增加该值。用例查询并未涉及到大字段的列,都是简单短型json字符数据,所以可以预估缓冲的大小。每个JDBC反馈的数据包缓冲大小8k差不多可以承载15条数据记录 net_buffer_length = 8K #为所有线程打开表的数量。增加该值能增加mysqld要求的文件描述符的数量。MySQL对每个唯一打开的表需要2个文件描述符。表缓存大小,同以上连接介绍,还是因为表中热点数据已经缓存到应用本地内存---->等价于table_cache table_open_cache = 16 #每个线程排序所需的缓冲 sort_buffer_size = 256K #这个参数用来设置 InnoDB 存储的数据目录信息和其它内部数据结构的内存池大小,类似于Oracle的library cache。因为表的结构不复杂,索引信息也不复杂。所以2m有点大 innodb_additional_mem_pool_size=2M #如果将此参数设置为1,将在每次提交事务后将日志写入磁盘。为提供性能,可以设置为0或2,但要承担在发生故障时丢失数据的风险。设置为0表示事务日志写入日志文件,而日志文件每秒刷新到磁盘一次。设置为2表示事务日志将在提交时写入日志,但日志文件每次刷新到磁盘一次 innodb_flush_log_at_trx_commit=0 #innodb的日志缓冲,同log4j innodb_log_buffer_size=4M # innodb_buffer_pool_size 定义了 InnoDB 存储引擎的表数据和索引数据的最大内存缓冲区大小。和 MyISAM 存储引擎不同, MyISAM 的 key_buffer_size 只能缓存索引键,而 innodb_buffer_pool_size 却可以缓存数据块和索引键。适当的增加这个参数的大小,可以有效的减少 InnoDB 类型的表的磁盘 I/O 。在一个以 InnoDB 为主的专用数据库服务器上,可以考虑把该参数设置为物理内存大小的 60%-80% (http://blog.zol.com.cn/2413/article_2412509.html),在此还是仅仅设置16m,因为热点数据缓存在了应用内存,除非缓存穿透了。 innodb_buffer_pool_size=16M #每个log日志的大小,增大此数目可以减少日志个数,避免过多创建日志文件 innodb_log_file_size=128M #服务器CPU有几个就设置为几,默认为多少,这是允许mysql同时处理的会话线程个数 innodb_thread_concurrency=1 #update over
单点还可以采取哪些手段?
1):增加像nginx这样的http服务器。开启静态缓存,用于缓存静态文件。不过此用例暂时不涉及页面,仅仅返回json data。如果和页面联调压力测试,需要加上此手段。
2):JDK6不支持AIO连接,不过tomcat有本地Native APR连接器,若前端安装Nginx配合Native APR连接器,节省NIO开销,将连接压力踢给了操作系统。
3):Mysql数据库,还有进一步配置优化的余地,而且使用Mysql的变种mariadb以及Percona可以使用他们变种的优化特性。
4):操作系统,最后就是底层操作系统相关配置的优化。因为使用的是Centos操作系统,修改配置(IO、内存页、系统日志、关闭无关服务),升级磁盘系统(EXT3-EXT4),升级Linux内核。都可以达到优化操作系统的目的——待测试。
分享一个linux优化连接----http://www.myhack58.com/Article/sort099/sort0102/2011/29507.htm
第一阶段到此为止,后续有内容,笔者会持续更新。。。。。