记一次服务优化(涉及到多线程)

最近刚跳槽,来新公司不久接到一个服务优化的任务,现在优化工作基本完成,觉得中间有一些值得记录的地方,毕竟好记性不如烂笔头。

首先说说这个服务的大致情况:服务主要是用来将文件中的数据进行处理之后同步到redis,服务提供一个rpc接口用来接收任务请求,根据请求到文件服务器上下载对应的文件到本地解压(每个处理文件都在200万行以上,大小在700M以上,每个任务中的文件数量不等,一般情况下同一时间只会运行一个任务,偶尔存在同时运行两个任务的情况);生产者线程逐个读取文件,每1000行作为一块放入队列中(这里的队列不是用的MQ,只是用了一个ConcurrentLinkedQueue放在内存中,队列大小为10,长度超过10时入队列需要等待),创建100个线程消费这个队列,处理数据,写入redis,生产者线程在处理完当前文件后会阻塞,等待消费者100个线程执行完毕之后再进行下一个文件的处理,当前文件处理完毕之后会销毁创建的100个消费者线程、以及redis链接等等。

再说说这个服务当前存在的问题:服务在运行任务时,出现了几次假死的情况,其他人初步排查之后发现cms gc非常频繁,而且经常出现gc失败的情况。

之后这个任务就交到了我手上,下面说一下排查过程。

其实老鸟看了上面的介绍之后应该很容易就能看出线程的问题,100个线程,太多了,如果处理的数据量比较大,很容易就会把内存撑爆,而这个服务正好就是属于这种情况。不过优化之前我还不懂这些,也是后知后觉。

首先在测试环境模拟线上,看能不能复现问题,无奈测试环境太垃圾,服务器内存、磁盘大小、文件服务器权限、redis大小都达不到要求,在升级了服务器内存和磁盘,开通了文件服务器权限之后,redis这块初步估计下来得要一个200G的集群,我估摸着运维应该不会给我弄,就硬着头皮在测试redis上开始了测试,于是就开始了与测试和其他开发抢用redis的剧情,因为我这边跑任务的时候会往redis里面插入大量的数据,redis在内存不足的时候会进行key的淘汰,导致他们要用的key都被我的任务给顶掉了。。。当然,这个淘汰机制是可以设置的,具体这边就不细说了。

这样模拟之后发现测试环境跑起来一切正常,用jvisualvm观测服务的gc和内存使用情况也是非常漂亮规律的峰线,于是深入代码,发现由于测试数据与线上不一致,导致测试环境的消费者处理数据特别快,创建的100个线程实际上只有两三个线程在真正运行任务,于是为了模拟线上处理数据的时间,在每个线程中添加了sleep,让100个消费者线程都运行起来,再测试,果然很快就出现了类似于线上的情况,在几个cms gc失败之后,服务直接假死了。

这个时候我怀疑是队列中堆积的数据导致内存被撑爆(并没有注意到队列长度是10,尴尬),拉出线上OOM之后的hprof文件进行分析,发现任务队列中并没有数据,反而有一个类的数量特别多,占用内存也特别大,战且称它为类T,然后从T入手,代码中顺藤摸瓜,发现这个类是从redis中取出的数据经过处理之后生成的,由于队列中的数据都是1000个key进行批量处理,每个key又对应着若干个T对象,导致T对象的数量和占用内存直接爆炸。

问题的根源找出来之后就要想怎么去优化,一开始想着要么减少批量处理的个数,从10000下降到5000,测试了一下,任务可以正常运行;或者减少线程的数量,从100减少到50,测试了一下,也可以正常运行。对比了一下,觉得批量处理本来就是为了快,减少批量处理个数肯定会导致处理速度降低,减少线程数量也会导致处理速度降低,但是线程数量减半的影响应该更大,所以选择减少批量处理的个数。后来跟同事讨论,对方说到处理器核心个数的问题,我才恍然大悟,想到减少线程数量说不定还能提升任务执行速度,所以重点转到找出最适合的线程个数。有一个公式可以推出任务适合使用多少个线程,网上有,自己搜。

由于测试环境redis的问题,所以无法模拟线上处理的耗时,直接使用sleep来模拟,但是结果出来,50个线程跑了80多分钟,100个线程跑了40多分钟,这让我怀疑起理论的正确性。后来发现线程sleep时是不占用cpu时间片的,也就是说100个线sleep10s和50个线程sleep10s所使用的cpu时间是一样的,这就是为什么测试耗时增加了大概一倍。后来使用

long mills = System.currentTimeMillis();
while(System.currentTimeMillis() - mills < 30000){
    flag = 1;
}

来模拟占用cpu时间片,后来发现这样不行,用相同的时间并不能模拟相同的任务,所以改用

for (int i = 0; i < 5; i++) {
    for (int j = 0; j < Integer.MAX_VALUE; j++) {
        for (int k = 0; k < Integer.MAX_VALUE; k++) {
            int m = 1;
        }
    }
}

但是修改之后发现一个现象,当线程数在40或者40一下时,程序会卡住,消费者运行上面代码块的时间会增加很多,但是在40线程以上时就运行得非常顺畅,添加了一些日志信息后发现,线程数在40以上时,前面很多线程都并没有真正运行完上面的代码块,处于正在运行的状态,而后面一些线程跑这个代码块的时间是0ms,感觉应该是底层对这种完全相同或者没有结果的任务有优化(这个问题上我也没有查到任何资料,可能是缓存的原因),线程数量超过一定阈值就会优化,所以继续对程序就行修改

for (int i = 0; i < 5; i++) {
    Random r = new Random();
    int m = r.nextInt(2);
    total += ((double)m / (i + 1));
}

这样运行起来就与理论相符合了,我的机器cpu数量为4,程序跑起来也的确是4个线程运行的平均时间最快。

线程数量优化完成之后进行代码逻辑优化。老版本里面会对文件逐个进行处理,会为每个文件生成100个线程去处理,并且等待100个线程处理完毕之后才会进行下一个文件的解压、读取,所以我的优化策略是使读文件的生产者线程和处理文件的消费者线程尽量的互不干扰,将消费者优化成一个线程池,生产者产生的所有待运行任务都由这个线程池进行消费,这样就减少了很多线程的创建和销毁的时间,并且生产者线程也不需要等待文件处理完在读下一个文件。

老版本程序是在所有任务运行完毕之后再进行文件删除,但是任务文件解压之前大概10G,运行过程中会被解压,完全解压之后会达到数百G,对磁盘空间造成很大影响,所以我在优化过程中对这一块进行了优化,生产者每读完一个文件,就把文件扔给一个删除文件的线程进行删除,之所以单独起一个线程是因为删除文件是一个比较耗时的操作,如果包含在生产者中会影响生产者的生产速度。

这样一顿操作下来,测试得到任务运行时间缩短了20%左右。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值