从Solr卡顿到G1垃圾回收

开源搜索引擎Solr是一款非常优秀的搜素引擎,只要一些简单的配置就能进行使用,大大减少了开发时间。

在我工作的环境中,整站的商品搜索业务都是依托于Solr,在Solr的使用上沉淀了不少宝贵的开发经验。随着公司商品数据规模不断的扩大,针对Solr的二次开发难度也在不断的增大,在过去的几年时间内,我把大量的数据放在索引构建上,从之前的DB模式24小时都无法完成全量的构建,到现在使用hive + avro + mapreduce把全量构建压缩到3小时以内,从之前的DB增量模式经常出现更新失败,更新延迟,到现在使用kafka+redis的模式保证更新数据不丢实,更新的延迟在1分钟以内。整个全量增量的索引架构都是可以横向扩展。

在索引构建告一段落后,查询又出现了问题,经常可以看到Solr每隔20分钟,会有timeout情况的出现。查看了下日志,在那段报错的时间段,正好是slave从master上获取增量数据的时间(replication配置了每隔20分钟slave replication一次数据到master),而Solr中许多内置的缓存都开的比较大。当数据更新后,Solr会对Cache进行重新的预热,在这个时候,有大量的内存对象会被换入换出,可能在这个点触发了full gc。

为了验证是否是full gc的可能,首先第一步获取full的信息,命令:

使用lsof命令根据端口找出pid(当然linux有很多其他方式,条条大路通罗马),然后使用jstat -gc命令获取信息。图中该Solr程序已经运行了196hrs35min,可以看出FGC一共执行了356次,一共花费6331s,平均一次停顿18s,这种停顿在java中有个专有名词——stop the world(STW),顾名思义在这18秒内,所有的业务代码都会stop给GC让出资源。结合程序启动的时间,平均每30分钟有一次18秒的卡顿,无论solr本身的性能再优越18秒足以让client time,问题也就这样终于被发现了。

由于Solr进程没有进行full gc的制定,所以都是java6的默认配置,java6默认的配置对于Solr这种低延迟的场景显然是不适用的,所以需要选择一款合适的GC。GC的两大标准:吞吐量 & 响应时间。就service来说,响应时间优先级要高于吞吐量,对于上述的垃圾回收而言,串行垃圾回收和并行垃圾回收不适合service的场景,对于CMS和G1,虽然都是以响应时间优先,但是在内部实现上差异很大,G1是oracle唯一还在继续开发优化的垃圾回收器,支持并发并行操作,在保证响应时间的前提下,尽可能的提高了吞吐量,所以更建议对于大型service服务使用G1作为默认的垃圾回收。由于G1在jdk6上还是个不稳定的版本,所以需要将JDK升到7.4以上。

关于G1的介绍有很多,就不介绍了,主要介绍下调优的一些思路

1. 请打开gc log日志

     gc log日志在gc调优的过程中非常关键,很多参数的调整都需要以gc log日志的反馈作为根据,从而判断参数是否生效。
     参数:-XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintAdaptiveSizePolicy -Xloggc:./gc.log

2. 根据gc日志的返回,找到full gc的原因
     

    从上述的GC信息中可以看到,是因为无法分配内存导致FGC的触发。在这里先简答介绍下G1的模型,G1和其他GC算法不同的是,G1使用region把内存分为多块,每个region的大小可以根据-XX:G1HeapGegionSize进行设置,最大不超过32m,每个region有唯一一个角色(unused,young,old),由于Solr需要大量的内存作为cache,所以我将该值设置为最大,即:-XX:G1HeapGegionSize=32m,G1当要为对象申请内存空间的时候,回去region列表中找合适的region进行分配,但是这里有个特殊的场景,当一个内存对象很大,大到一个region无法满足之后,G1会查找一片连续的region用于存放该对象,并且这些region标记为old,跳过young gc。在G1算法内,如果一个对象超过了region的1/2,就认为是一个大对象。再回到上面的日志可以发现,Solr需要申请1个G的内存空间,而由于region的大小为32M,即需要32个连续的region。但是我从内存的使用上来看,申请了100G的内存,实际只使用了30G就出现了这种场景,原因是g1内存分配不适连续分配,导致内存碎片,所以即使还有很多余量,但是无法找到32个连续的region,导致强制执行了fgc。找到了根本原因后就简单了。

3. 参数调整

     G1对内存的压缩只在两种情况下进行,第一个中情况是fgc,这种事我们应该尽力避免的,第二种是在初始话标记阶段,所以调优的目标是希望能在适当的时间触发该阶段。在补充了G1的整个执行周期,G1首先上来进行young gc,会清理一部分内存数据,把旧的数据迁移到old region,随着内存不断的增加,每次做完一次young gc后,会根据-XX:InitiatingHeapOccupancyPercent判断是否需要进入初始化标记阶段,所以这个参数在G1大对象调优上非常关键,我最后调整为:-XX:InitiatingHeapOccupancyPercent=30,即到达最大内存的30%开始执行初始化标记。当完成标记后,会进行clean up在clean up阶段old区域的内存会被回收压缩,达到目的。在后面会进入mixed gc,反复执行多次满足条件后再返回到young gc,一个周期就这么完成了。

当然还有很多参数的调整没有给出,但是我觉得还是希望大家自己去研究,很多参数的调整需要针对特殊的业务,千万不可生搬硬套。最后的调整也起到了非常好的效果,从一天上万的报错降低到100个以内,也得到了CTO和其他老大们的肯定。

参考资料

https://blogs.oracle.com/g1gc/entry/g1gc_logs_how_to_print 

http://www.oracle.com/webfolder/technetwork/tutorials/obe/java/G1GettingStarted/index.html

https://software.intel.com/en-us/blogs/2014/06/18/part-1-tuning-java-garbage-collection-for-hbase

http://www.oracle.com/technetwork/cn/community/developer-day/2-jdk7-g1-2083344-zhs.pdf

    

你可能感兴趣的:(从Solr卡顿到G1垃圾回收)