记一次代码优化过程

1. 原始场景再现:
该模块主要是客户端负责上传一个包含手机号码的txt,其中一行一个手机号码。服务端读取并解析该文件,解析过程中需要做有效性验证。例如:号码位数,是有效数字及是否在有效号段之内。最后保存数据到DB。
该包含手机号码文件数据在20W到200W之间。

2. 问题所在
在客户端上传20W数据的时候,后台相应很慢,查看后台的CPU及内存
mpstat -P ALL 1   //查看LINUX系统内存及CPU的消耗情况
发现CPU一直处在100%状态下,而且消耗的时间很长。近十分钟也没有回复到客户端。

3. 问题详解
首先查看代码。列下原代码思路:
1) 使用apache的公用包来处理文件的上传,保存客户端文件到服务器
2) 打开读取文件IO,及写日志IO,读取文件信息,到一个LIST中。
3) 双重遍历LIST,进行查重操作。重复数据记录到日志文件中
4) 遍历LIST进行有效位数的校验。重复数据记录到日志文件中
5) 遍历LIST进行是否为有效数字验证。重复数据记录到日志文件中
6) 遍历该LIST,组装为数据库存储对象传递到DAO层,DAO层再次遍历对象容器,将插入对象添加到了批量提交的LIST中。最后将20W的数据一次性批量提交

4. 问题解决思路

第一次改动:仔细查看代码我们可以很清晰的看出,问题主要在两块:
一是数据的有效性检查,原代码采用了多次循环遍历的方式处理,很耗CPU。故首先将多次遍历的处理应该缩减为一次遍历即可,但是仔细想想其中存在一个查重的处理操作,故我们将原本用list存储的方式改为SET存储的方式,因为set不会存储重复的数据,这样可以达到查重的效果。

Key:HashSet底层使用hash数组实现的,其原理就是当保存一个对象的时候,首先调用该对象的hashCode方法,获得hash码与原数组及数组子链表中的数据进行比较,若是相同的话则不进行插入操作,再不存在的情况下,才进行存储。由于String 类型已经实现了hashCode方法,所以我们不需要实现该方法若是其他类型的对象我们则需要实现该方法。

二就是数组存储的地方了,原代码采用了几十万条数据的一次批量提交,当然很消耗资源,代码回复给客户端慢。这里我采用了ORACLE写一个存储过程,JAVA端传递一个数组给ORACLE,有存储过程来处理大数据量,这样就将服务器的压力转移到ORACLE安装的的那台服务器。
好的,第一次改动过完成。重启TOMCAT试下。结果当我们只上传20W数据的时候,发现还是很慢很慢,回头又仔细看代码,打断点。再调试的过程中,发现在我只采用一个遍历循环的时候,CPU就一直处理100%,原来在处理这20W数据的时候,CPU就一直处理很高的状态了,那该怎么办呢?这个时候就是第二次改动了
————————————这里是华丽的分割线————————————-
第二次改动:
个人经验,一般处理这种大数据量有两种方法,其实这两次方法的本质是一样的,就是为了降低CPU。
第一种是在我们在遍历循环的时候,在循环遍历到一定数量的时候,进行Thread.sleep(5)操作,带该线程睡眠片刻

For (int I =0 ;I < list.size();i++)
{
	//…业务处理
	If (3000 == i)
{
		Thread.sleep(5)
	}
}

强制CPU暂时不处理该线程。

第二种方法就是采用流控的方式,流控的原理就是我们有一个初始量,循环过程中我们累加这个量,当达到一定量的时候,该线程进行wait()操作,同时我们启动一个定时器,定时间周期对该变量进行清零操作,并唤醒该线程。其中可能会出现两种情况,一是我们定时器的清零操作还没到,累积量就已经到了,那么该线程就会处在等待状态,等待清零时间到,唤醒线程。二是累积量还没到,清零时间就到了,对线程进行清零,那么这个时候线程会一直处在一个运行状态,如果这个时候CPU使用率很高的话,就达不到我们需要的效果,所以该方式的控制就需要实际调整定时器的扫描周期、记累积量的值设定。实际过程中需要调整两参数。
因为我们是将流控写成了一个工具类,所以不好贴出来,具体也就是上面所所得思路。
比较两种方法我们明显可以看出,采用流控的方式更佳,因为它是不阻塞线程,wait的方式。第一次则是sleep,阻塞线程。
好,第二次改动已经完成了,我们就开工试试了。当你怀着美好的愿望时,老天总是不能让你如意。结果很失望,还是慢的一塌糊涂,然后不断的调整参数,结果还是没降下来。这个时候我就开始反思了,难道这种方式有问题,再去打断点调试的过程发现,采用了流控后,在处理数据的时候是CPU确实是降下来了,但是还是存在一个问题,再没处理完一条记录后,都会有一个日志的记录,这个时候用的是bufferRead,结果我们在最后的close()IO的时候,一次行将内存缓存中的数据库刷到文件,导致CPU及内存的过高。知道原因后我开始了第三次的改动。
————————————-分割线又出现了————————————–
第三次改动:
这个时候,我采用了流控相同的思路,就是在处理完一定数据量的数据后,我们进行一次flush操作,将内存中数据刷到文件中,免得一次性刷,同时因为我们一般是整数的W级数据,所以定义一个常量为3000(全局多个地方需要刷数据,好改动),作为flush标示。因为采用3000的话,最后一次我们刷的只有1000的数据量,同时关闭IO需要开销,所以能够在一定程度上降低CPU,同时发现在第一次flush的时候CPU会较高,越后CPU会越低,不知道什么原因。

If (…)
{
		if (3000 == i)
{
		read.flush()l
}
}

OK ,又可以开工进行测试了,我是费了九牛二虎之力开动了TOMCAT(机子太差),哈哈,终于成功了。CPU从原来的100%降到了10%左右,很有成就感,嘿嘿。这样就将给测试区测试了。但是,但是,测试告诉我一个很不幸的消息,CPU还是很高,这个是为什么呢?
仔细看看了,原来TMD应用的服务器跟DB在一台服务器上,气死老夫了。采用一次性传递数据的片刻,ORACLE会使CPU会飙到100%,而且处理这么多数据的时候也会持续5s左右的时间一直处在该位。这个时候咋办呢?这个就需要第四次改动了。
—————————–我是分割线,大家好!北边的朋友在哪里?————————–
第四次改动:
这个时候,首先改动的是存储过程,采用了动态变量绑定,同时调整commit;的次数,在1000,3000,5000,10000条记录的时候提交数据,结果调试后,没什么改观,没则,只好换个思路。
启用存储过程,在java端想办法,最简单的方法还是启用原来的批量提交方式,修改为一定数量数据后提交,因为这个提交,并在提交后sleep,方式CPU一直处在这个状态。
伪代码就是

If ()
{
	If (3000 == i)
{
		//提交数据到DB
	//清零标示,清空临时list
	Thread.sleep();
}
}

就这么简单。提交调试后,发现java端不会消耗很高的CPU,在第一次提交批量数据时会到30%左右,第二三次会降到15%左右,同时ORACLE消耗的CPU在第一次会100%,但是持续时间很短,就1s的时间,在达到这样的效果后,领导说可以了。总算结束了这段优化过程。
简单的说,我这种方式不过是用时间换CPU的性能,只使用与对于时间要求不是很高,更要求CPU的性能的场景。

作者: inter12

在这苦短的人生中,追求点自己的简单快乐

发表评论

电子邮件地址不会被公开。 必填项已用*标注