这里分享的是一个分布式分析系统的
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设计中,在多线程处理任务中,尽量让工作者的逻辑无差别话,任务都自包含描述,工作者逻辑是通用逻辑引擎,这样对于与线程池或者不同机器进程来说,任务调度将是很简单也是容易扩展的。
优化其实看似都是很简单的内容,但是如何去观察问题,分析问题,解决问题,总结问题都是有很多技巧的,也只有会做好这几步,才可能去做优化,否则就是空谈。
分享到:
相关推荐
在StackOverflow的最近...第二篇主要是一些实际开发中的典型应用案例,通过这些案例读者可以看到C++11的这些新特性是如何综合运用于实际开发中的,具有实践的指导作用。相信本书会成为读者学习和应用C++11的良师益友。
某些信息由基准测试提供,但大多数内存基准仅限于简单的访问模式,这些模式不能代表实际应用程序中的模式。 本论文介绍了AdaptMemBench,这是一个可配置的基准框架,旨在探索从应用程序中提取的计算内核的性能能力...
DB2数据库性能调整和优化(第2版)侧重于介绍DB2数据库的性能调优。性能调优是一个系统工程...《DB2数据库性能调整和优化(第2版)》覆盖了进行DB2数据库性能调优所需的全部知识和工具,并提供了大量的性能调优的实际案例。
在课程内容上几乎不用过多的介绍,单是查阅目录就会发现非常的强悍,课程从思路和实际案例的角度出发,非常全面的像同学们诠释了JVM与GC调优的思路和策略,对实际企业级应用是有巨大的提升价值。 〖课程目录〗: (1)\...
针对数据库的启动和关闭、参数及参数文件、数据字典、内存管理、Buffer Cache与Shared Pool原理、重做、回滚与撤销、等待事件、性能诊断与SQL优化等几大Oracle热点主题,本书从基础知识入手,深入研究相关技术,并...
3,结合工作实践及分析应用,培养解决实际问题的能力。 4,使用综合案例来加强重点知识,用切实的应用场景提升编程能力,充分巩固各个知识点的应用。 5,整个课程的讲解思路是先提出问题,然后分析问题,并编程解决...
针对数据库的启动和关闭、控制文件与数据库初始化、参数及参数文件、数据字典、内存管理、Buffer Cache与Shared Pool原理、重做、回滚与撤销、等待事件、性能诊断与SQL优化等几大Oracle热点主题,本书从基础知识入手...
象、产生原因和相关的原理进行了深入浅出的讲解,更主要的是,结合实际应用环境,提供了一系列解决 问题的思路和方法,包括详细的操作步骤,具有很强的实战性和可操作性,满足面向实际应用的读者需求 。... ...
本书针对数据库的启动和关闭、能数及参数文件、数据字典、内存管理、Buffer Cache与Shared Pool原理、重做、回滚与撤销、等待事件、性能诊断与SQL优化等几大Oracle热点主题从基础知识入手,深入研究相关技术,并结合...
针对数据库的启动和关闭、参数及参数文件、数据字典、内存管理、Buffer Cache与Shared Pool原理、重做、回滚与撤销、等待事件、性能诊断与SQL优化等几大Oracle热点主题,本书从基础知识入手,深入研究相关技术,并...
本书针对数据库的启动和关闭、能数及参数文件、数据字典、内存管理、Buffer Cache与Shared Pool原理、重做、回滚与撤销、等待事件、性能诊断与SQL优化等几大Oracle热点主题从基础知识入手,深入研究相关技术,并结合...
本书针对数据库的启动和关闭、能数及参数文件、数据字典、内存管理、Buffer Cache与Shared Pool原理、重做、回滚与撤销、等待事件、性能诊断与SQL优化等几大Oracle热点主题从基础知识入手,深入研究相关技术,并结合...
针对数据库的启动和关闭、控制文件与数据库初始化、参数及参数文件、数据字典、内存管理、Buffer Cache与Shared Pool原理、重做、回滚与撤销、等待事件、性能诊断与SQL优化等几大Oracle热点主题,本书从基础知识入手...
本书给出了大量取自实际工作现场的实例,在分析实例的过程中,兼顾深度与广度,不仅对实际问题的现象、产生原因和相关的原理进行了深入浅出的讲解,更主要的是,结合实际应用环境,提供了一系列解决问题的思路和...
本书给出了大量取自实际工作现场的实例,在分析实例的过程中,兼顾深度与广度,不仅对实际问题的现象、产生原因和相关的原理进行了深入浅出的讲解,更主要的是,结合实际应用环境,提供了一系列解决问题的思路和...
本书给出了大量取自实际工作现场的实例,在分析实例的过程中,兼顾深度与广度,不仅对实际问题的现象、产生原因和相关的原理进行了深入浅出的讲解,更主要的是,结合实际应用环境,提供了一系列解决问题的思路和...
针对数据库的启动和关闭、控制文件与数据库初始化、参数及参数文件、数据字典、内存管理、Buffer Cache与Shared Pool原理、重做、回滚与撤销、等待事件、性能诊断与SQL优化等几大Oracle热点主题,本书从基础知识入手...
针对数据库的启动和关闭、控制文件与数据库初始化、参数及参数文件、数据字典、内存管理、Buffer Cache与Shared Pool原理、重做、回滚与撤销、等待事件、性能诊断与SQL优化等几大Oracle热点主题,本书从基础知识入手...
针对数据库的启动和关闭、控制文件与数据库初始化、参数及参数文件、数据字典、内存管理、buffer cache与shared pool原理、重做、回滚与撤销、等待事件、性能诊断与sql优化等几大oracle热点主题,本书从基础知识入手...