`
buliedian
  • 浏览: 1186816 次
  • 性别: Icon_minigender_2
  • 来自: 北京
文章分类
社区版块
存档分类
最新评论

耗内存应用优化实际案例

阅读更多

这里分享的是一个分布式分析系统的 Master内存消耗状况的优化,有些比较特定的优化未必适用于其他系统,但是从这一系列优化过程中,应该能带给其他系统在做设计时提前考虑一点优化点。

         下面先描述一下背景,看了背景可以对后续的优化点可以比较清楚一些,注意,部分设计仅适用于大量计算中,会牺牲可维护性来换取性能提升。最后一点优化应该是比较有通用性意义的。

背景:

         开放平台每天产生大量的调用日志,希望能够从海量日志中即时的去分析业务指标和系统运行状况。当前实现的是类似 MapReduce的设计,不过 Master Slave之间是松耦合的关系,比传统的 MapReduce更利于扩展和即时分析(当然和 Hadoop的目标是不一致的,规模量及作用也不同,主要用于统计规则易变,需要即时分析,数据量在 T以内),同时内嵌了统计规则引擎,使得统计逻辑只需要通过配置即可实现分析定义。具体的部署图和流程图如下:

                                                 

流程如下:


                                                       

Master Slave之间没有注册和管理的关系, Slave连接到 Master请求任务,从任务中获取数据来源,分析规则配置,获取数据块大小的信息,然后将数据块拉下来分析,最后返回结果给 Master。至此, Master Slave的交互完成。

Master自身主要负责:

1.              任务列表创建和重置(根据配置信息创建任务列表,由于是增量对应用服务器分析,则定期将任务全部重置,可以让 Slave增量的对应用服务器去拖取数据分析)。

2.              重置一些分配出去但是较久都没有执行的任务,防止 Slave任务执行失败没有反馈而出现死等的现象。

3.              合并 Slave传递过来已完成的任务结果集到主干结果集上。

4.              将主干结果集定时输出提供给第三方使用(告警,图形化等),导出中间结果,提供给 Master异常重启后回复现场使用。

问题:

         由于报表配置增多,单报表的结果数据量大, Master合并多个结果集内存吃紧,不断的 GC,最后导致恶性循环。因此优化原先认为任务不重的 Master迫在眉睫。

优化过程:

1. 合并过程中,主干结果和 Slave结果都比较大,在操作后是否可以通过主动 clear set null来更快的清理释放资源。(基本没有效果, GC 已经做了很多优化

2. 分析器是基于定义去分析出 <key,value>结果集合,然后根据配置将 key相同的结果串联成为 key,value1,value2,value3(这就是传统的报表结果)。发现有一些配置的 <key,value>规则在实际输出报表的时候没有被使用,因此在构建分析规则的时候直接过滤这部分配置。(也就是在实现很多系统的时候,有些结果是中间结果,中间结果是否需要如果在系统启动时就能判断,就将这些中间结果计算的逻辑过滤掉,节省计算资源和内存资源,同时可以有一些提示,可能是系统配置中的错误导致这部分数据没有被用

3. 系统中很多地方都用到 Calendar来处理一些日期相关的内容,比如说想获取年月日时分秒的数据来做 Action,比如通过格式化内容然后作为输出归类。由于 Calendar是线程不安全的,因此不得不大规模的去构造和使用,其实内存消耗较大。

改造方式:能够用 long的时候全部用 long来处理, System.currentTimeMillis有消耗,但很小。如果要计算年月日时分秒可以用除法取余来做(注意计算天的时候要考虑中国时差 8个 小时)。同时如果是中间结果然后后续也要输出,由于输出需要便于用户查看,所以希望格式化,建议系统内部还是保持数字型,直到输出时做一次格式化处 理。(不过这点取决于场景中是中间结果被输出和内部使用的频率,如果内部使用较少,有大量多次复用输出,则可以内部处理好,避免多次格式化)

4. 观察了一段时间,发现 Slave处理结果在高峰期每次返回还有 5-6M甚至更高,这样对于 Master在并发处理多个 Slave时开销很大(接收缓存区随着 Slave的增多和内容返回的增多而不断地增大),因此出于优化网络和接收发送缓存,都要求将 Map后的数据作压缩。

改造方式:考虑使用 QuickLZ这个简单的开源类来做压缩,但是由于用到了对象的 Outputstream,则直接使用了 Output的管道化方式,后来比较了一下,压缩效果两者不相上下,速度到没有再去比较,因为 Output管道化效果较好,代码如下:

ByteArrayOutputStream bout = new ByteArrayOutputStream();

Deflater def = new Deflater(Deflater. BEST_COMPRESSION , false );

DeflaterOutputStream deflaterOutputStream = new DeflaterOutputStream(bout,def);

ObjectOutputStream objOutputStream = new ObjectOutputStream(deflaterOutputStream);

      最后的ByteArrayOutputStream将会成为ByteBuffer的数据源。 (压缩后,网络传输和接收缓冲消耗将降低,但当时没有是考虑一来数据原来不大,二来压缩消耗CPU,但现在的场景发生变化,因此不得不消耗CPU来节省内存。所以大家根据不同场景来优化,得失自己权衡)

5. 下面是一段 NIO接收业务数据后的代码,平时看来很干净正规,但是在高并发大量数据的情况下就是一段恶魔代码。

byte[] content = new byte[receivePacket.getByteBuffer().remaining()];

receivePacket.getByteBuffer().get(content);

log.error("package content size :" + content.length);

ByteArrayInputStream bin = new ByteArrayInputStream(content);

         修改后:

         ByteArrayInputStream bin = new ByteArrayInputStream(

                            receivePacket.getByteBuffer().array(),receivePacket.getByteBuffer().position()

                                                                                                                ,receivePacket.getByteBuffer().remaining());

         让输入流直接基于 ByteBuffer来处理数据,而不是重新申请内存来拷贝出数据。其实在 NIO Buffer Channel出来以后,由于和 Steam的操作没有桥接的方式,因此很多时候都倾向于自己申请内存去读取然后再作为 Stream的输入输出。 Buffer 内部的很多方法是支持做镜像,子集等操作来最大限度复用内部数据流,因此需要仔细的去权衡是否可以复用,但是要注意的是复用的模式需要考虑仔细,否则读取和写入数据的游标就会相互影响)

6. Merge Reduce)的压力分散。当前如果有 50 Job,那么 50 job的所有结果都需要 Master来合并,其压力和内存消耗肯定很大,如果可以将多个 job的结果在 Slave上合并,那么就可以缓解 Master的压力。因此给每个 Slave配置了一个系统级参数,每次请求 Master分配的最大 Job个数。修改了 Master Slave直接的获取任务协议,可以申请要求多个 job Master根据任务完成情况返回小于等于请求个数的任务。 Slave这边并行执行然后合并结果的机制其实一早就有,只是从当年分析大文件转向基于 Http数据流增量分析后,没有充分利用 Slave的并行处理能力。(这种设计很多,其实在 SD 会议上我说了几个简单的场景, TOP 需要将业务返回的对象在格式化为标准的 xml 或者 json 方式,一种是 TOP 自身包揽处理,一种是将部分业务逻辑外移,将计算和内存消耗分担到更多的应用节点上,带来的问题是,升级外移的逻辑成本较高。集中处理的好处在于逻辑维护方便,一次处理多次使用。分担处理的好处在于充分利用更多资源来解决规模化问题)

 

7. 通过 jstat gcutil观察,发现 Heap增长除了 merge以外在报表输出时也有不小的波动,发现为了保证系统异常退出时能够在再次启动继续增量统计,每次重置任务列表并输出报表时就会导出内存数据对象,便于下次载入。现在每隔 3分钟是任务重置期,也就是每隔 3分钟都会导出中间结果一次,这个频率过高,因此将导出动作设置扩大,毕竟异常退出不是经常发生,同时支持命令主动导出。另一方面也采用压缩的方式对输出内容作处理,减少内存消耗和导出时间。(很多时候,我们会设计一些异常保护的策略和检查,但是不要让这样的工作成为系统的负担,通过放大尺度和接受主动即时处理,可以得到一样的效果)

 

8. 这点优化看起来很傻,但是效果却是明显的,其实说明了一样问题,就是一点细节可以让你的程序有很大的改观。

当前 Map后的结果集格式为: Map<EntryId,Map<key,value>> Entry就代表了一个 <key,value>计算的定义。那多个结果集合并的处理方式为(下面是伪代码)

Map<EntryId,Map<key,value>>[] needMergeResult;// 这是外部传入的需要合并的结果数组

Map<EntryId,Map<key,value>> result = new Map<EntryId,Map<key,value>>; // 构建一个合并后的结果集

         for ( j = 0 ; j < needMergeResult.size; j++) // 遍历所有的结果集

         {

                   Map<EntryId,Map<key,value>> node = needMergeResult[j];

                  

                   Loop :遍历 node 所有的 EntryId

                   {

                            Loop :遍历 EntryId 对应的 Map

                            {

根据规则将 key 对应的 value needMergeResult[j+1].get(EntryId).get(key) needMergeResult[needMergeResult.size-1].get(EntryId).get(key) value 做合并计算,然后移除 needMergeResult[j+1].get(EntryId).get(key) needMergeResult[needMergeResult.size-1].get(EntryId).get(key) 对应的数据,避免后续外部循环重复计算

}

                   }

         }

         写了一大堆,其实没有优化算法(大家觉得合并算法如果有更好的可以告知我),做的优化就是红色那句被去掉了,也就是原来是构建一个新的结果集作为基础结果集,现在的做法是合并前先选择 Base最大的结果集作为基础结果集,然后后续处理同样,这样其实省略了内存申请,合理的利用了已有的内存空间,同时这步也为最后的并行合并结果作了优化。

 

9. 除了线上运行期 GC的观察以外,本地数据量小,但是也跑了 master slave jprofiler观察了一下,发现程序中有大量的对 ConcurrentMap size的检查,来做保护,来做一些行为判断,对于一个长期高并发处理的系统来说,也是有不少损耗的( ConcurrentMap内部为了提高效率是分片存储的,因此 size不是一个简单的计数器),因此采用 Atomic类型的原子计数器来替代,代价是程序复杂度增加。(这点优化其实也是依据场景而定,如果程序在这方面操作不频繁,简单的使用 size 方法更靠谱。需要说明的就是 Java 很多并发组件的内部实现并不是简单的处理,因此如果调用次数很多很频繁,可以考虑其他方式去实现)

 

10.              Master的主线程阻塞式合并结果集的改变。从最上面的设计图中可以看出,起始的设计我就考虑用单线程阻塞模式来处理结果集的合并,原因很简单,所有的结果最终都是要合并到“主干”,因此无论如何合并的动作都会加锁,也就是串行化,与其这样,还不如简单的用单线程阻塞式处理。

现象: Slave处理并合并后的多个结果会不定期的到来,由于 Slave分析后的数据量呈几个数量级的增长,原先的 Master阻塞式合并时间也变长,此时挂在合并列表上的结果集也会增多(中间结果的生命周期增加,直接导致内存消耗增加),需要做的就是尽可能的减少由于处理满导致内存堆积消耗,提高 Master内存利用率。

下面是考虑优化的过程:

1.              主线程只负责需要合并结果的分发,执行合并的为外部线程池。(带来的问题:多个线程如何并发的去合并到主干?采用锁的方式那么依然只有一个线程可以执行合并,依然是串行化操作)

2.              设计采用两类合并的设计。设计如下图:

                                          

1. Master主线程负责获取需要合并的结果集(包括原始 Slave提交的结果集和后面会提到的被合并过的结果集)

2. Master主线程分发合并任务给线程池。

3. 线程池的执行线程执行前尝试获取主干锁。

4. 如果获得主干锁,则将所有结果集合并到主干结果集上。

5. 如果没有获得主干锁,则将结果集内部合并,并且将合并后的结果集放到队列中,等待再次合并。

首 先,依托于上面谈起过的合并方式是基于某一个被合并的结果集来做,因此多组合并在资源消耗上可以接受(只是在计算的时候有所消耗),大量原始结果的并存可 以被少量中间结果并存替代。其次,任何一次合并都不在等待与主干合并,可以实现并行化,与主干合并的动作没有做太多特殊处理,工作线程逻辑统一,无差别对 待,提升线程池利用率。

带来的问题:由于 Slave原始结果很难预期到来时间, Master的频繁小规模合并反而会带来负面效果,同时也出现了中间结果被反复的多次合并,浪费计算资源。

3.              根据 2提出的问题,做了一些改进。首先给 Master增加了两个系统参数,任务批量执行的最小数目和任务堆积等待最大时间,当在任务堆积最大等待时间之内,必须达到批量执行最小数目才可以提交线程池执行。(当发现当前的合并结果集 +已经合并的结果集 =所有结果总数,此条件无效)。其次,中间结果不参与到非主干合并的计算中,除非中间结果是最后需要合并的结果。修改后的流程图如下:

                                     

线上做了初步配置测试后,效果明显,内存利用率提高,释放速度加快,整体执行时间缩短。

其实这个优化点总结一下背后的实质性特点:当所有操作最后还是需要锁在一个瓶颈点上来做串行化操作时,最简单的方式就是串行化处理。(简单即高效)但是,某些场景下可以做部分优化:

1.              节省资源。如果在串行化操作前,并行处理能够减少资源消耗,那么在整体事务处理时间不变的情况下,资源可以得到充分利用(反向资源充足时也许可以提速系统处理能力,间接帮助提高事务处理时间)。

2.              节省预处理计算时间。简单来说就是磨刀不误砍材功。在前一阵写着关于任务切割,然后事件驱动的模式里面说到,任务切割后,最大的好处就是可以加速不同阶段消耗的资源释放,即并行化可以并行的操作。如果有 10本电话簿, 1个电话厅,那么打电话的人可以在电话亭外面先查好电话,然后进入电话亭直接打电话,因为查电话簿是可以小规模并行的,可以提升串行化处理的效率。

补充最后一点,在并行计算中很重要,在分析器的 Slave设计中,在多线程处理任务中,尽量让工作者的逻辑无差别话,任务都自包含描述,工作者逻辑是通用逻辑引擎,这样对于与线程池或者不同机器进程来说,任务调度将是很简单也是容易扩展的。

         优化其实看似都是很简单的内容,但是如何去观察问题,分析问题,解决问题,总结问题都是有很多技巧的,也只有会做好这几步,才可能去做优化,否则就是空谈。

2
3
分享到:
评论

相关推荐

    深入应用C++11:代码优化与工程级应用

    在StackOverflow的最近...第二篇主要是一些实际开发中的典型应用案例,通过这些案例读者可以看到C++11的这些新特性是如何综合运用于实际开发中的,具有实践的指导作用。相信本书会成为读者学习和应用C++11的良师益友。

    特定应用的内存子系统基准测试(计算机硕士毕业论文英文参考资料).pdf

    某些信息由基准测试提供,但大多数内存基准仅限于简单的访问模式,这些模式不能代表实际应用程序中的模式。 本论文介绍了AdaptMemBench,这是一个可配置的基准框架,旨在探索从应用程序中提取的计算内核的性能能力...

    DB2数据库性能调整和优化 牛新庄 PDF

    DB2数据库性能调整和优化(第2版)侧重于介绍DB2数据库的性能调优。性能调优是一个系统工程...《DB2数据库性能调整和优化(第2版)》覆盖了进行DB2数据库性能调优所需的全部知识和工具,并提供了大量的性能调优的实际案例。

    大厂架构师-日均百万订单量的JVM优化与高级GC调优策略实战(5.8G)

    在课程内容上几乎不用过多的介绍,单是查阅目录就会发现非常的强悍,课程从思路和实际案例的角度出发,非常全面的像同学们诠释了JVM与GC调优的思路和策略,对实际企业级应用是有巨大的提升价值。 〖课程目录〗: (1)\...

    深入浅出Oracle-DBA入门、进阶与诊断案例

    针对数据库的启动和关闭、参数及参数文件、数据字典、内存管理、Buffer Cache与Shared Pool原理、重做、回滚与撤销、等待事件、性能诊断与SQL优化等几大Oracle热点主题,本书从基础知识入手,深入研究相关技术,并...

    Spark分布式内存计算框架视频教程

    3,结合工作实践及分析应用,培养解决实际问题的能力。 4,使用综合案例来加强重点知识,用切实的应用场景提升编程能力,充分巩固各个知识点的应用。 5,整个课程的讲解思路是先提出问题,然后分析问题,并编程解决...

    [深入解析Oracle.DBA入门进阶与诊断案例].盖国强.扫描版.part2

    针对数据库的启动和关闭、控制文件与数据库初始化、参数及参数文件、数据字典、内存管理、Buffer Cache与Shared Pool原理、重做、回滚与撤销、等待事件、性能诊断与SQL优化等几大Oracle热点主题,本书从基础知识入手...

    深入浅出Oracle:DBA入门,进阶与诊断案例(盖国强编)

    象、产生原因和相关的原理进行了深入浅出的讲解,更主要的是,结合实际应用环境,提供了一系列解决 问题的思路和方法,包括详细的操作步骤,具有很强的实战性和可操作性,满足面向实际应用的读者需求 。... ...

    深入浅出Oracle:DBA入门、进阶与诊断案例

    本书针对数据库的启动和关闭、能数及参数文件、数据字典、内存管理、Buffer Cache与Shared Pool原理、重做、回滚与撤销、等待事件、性能诊断与SQL优化等几大Oracle热点主题从基础知识入手,深入研究相关技术,并结合...

    深入浅出oracle dba入门.进阶与诊断案例

    针对数据库的启动和关闭、参数及参数文件、数据字典、内存管理、Buffer Cache与Shared Pool原理、重做、回滚与撤销、等待事件、性能诊断与SQL优化等几大Oracle热点主题,本书从基础知识入手,深入研究相关技术,并...

    深入浅出Oracle: DBA入门、进阶与诊断案例.zip.001

    本书针对数据库的启动和关闭、能数及参数文件、数据字典、内存管理、Buffer Cache与Shared Pool原理、重做、回滚与撤销、等待事件、性能诊断与SQL优化等几大Oracle热点主题从基础知识入手,深入研究相关技术,并结合...

    深入浅出Oracle: DBA入门、进阶与诊断案例.zip.002

    本书针对数据库的启动和关闭、能数及参数文件、数据字典、内存管理、Buffer Cache与Shared Pool原理、重做、回滚与撤销、等待事件、性能诊断与SQL优化等几大Oracle热点主题从基础知识入手,深入研究相关技术,并结合...

    深入解析Oracle.DBA入门进阶与诊断案例 part2

    针对数据库的启动和关闭、控制文件与数据库初始化、参数及参数文件、数据字典、内存管理、Buffer Cache与Shared Pool原理、重做、回滚与撤销、等待事件、性能诊断与SQL优化等几大Oracle热点主题,本书从基础知识入手...

    深入解析OracleDBA入门进阶与诊断案例 3/4

     本书给出了大量取自实际工作现场的实例,在分析实例的过程中,兼顾深度与广度,不仅对实际问题的现象、产生原因和相关的原理进行了深入浅出的讲解,更主要的是,结合实际应用环境,提供了一系列解决问题的思路和...

    深入解析OracleDBA入门进阶与诊断案例 2/4

     本书给出了大量取自实际工作现场的实例,在分析实例的过程中,兼顾深度与广度,不仅对实际问题的现象、产生原因和相关的原理进行了深入浅出的讲解,更主要的是,结合实际应用环境,提供了一系列解决问题的思路和...

    深入解析OracleDBA入门进阶与诊断案例 4/4

     本书给出了大量取自实际工作现场的实例,在分析实例的过程中,兼顾深度与广度,不仅对实际问题的现象、产生原因和相关的原理进行了深入浅出的讲解,更主要的是,结合实际应用环境,提供了一系列解决问题的思路和...

    深入浅出Oracle—DBA入门、进阶与诊断案例

    针对数据库的启动和关闭、控制文件与数据库初始化、参数及参数文件、数据字典、内存管理、Buffer Cache与Shared Pool原理、重做、回滚与撤销、等待事件、性能诊断与SQL优化等几大Oracle热点主题,本书从基础知识入手...

    深入解析Oracle.DBA入门进阶与诊断案例 part1

    针对数据库的启动和关闭、控制文件与数据库初始化、参数及参数文件、数据字典、内存管理、Buffer Cache与Shared Pool原理、重做、回滚与撤销、等待事件、性能诊断与SQL优化等几大Oracle热点主题,本书从基础知识入手...

    Oracle DBA入门、进阶与诊断案例 中文版

    针对数据库的启动和关闭、控制文件与数据库初始化、参数及参数文件、数据字典、内存管理、buffer cache与shared pool原理、重做、回滚与撤销、等待事件、性能诊断与sql优化等几大oracle热点主题,本书从基础知识入手...

Global site tag (gtag.js) - Google Analytics