问题场景:
索引数据达到上亿的量,搜索并发量太大,出现heap size的内存溢出。而后把heap size加到4GB也没用。
这个没用的意思是:刚开始一两小时内存的使用量还行,而后就开始用满内存程序卡死,再后来因为内存溢出程序调用什么函数都是nullpointer,所以过亿量的数据搜索加大内存不是根本的解决办法。
我这还是使用单服务器的自造轮子的Lucene,没用到ES的分布式,所以下面所说的解决办法是针对使用Lucene的同学来说的,当然ES基于Lucene的,ES也可以参考下!
我的Lucene一开始使用的版本是7.3.0,然后搜索过程中Lucene给出的totalHits总是能达到四五千万的数量。得出这个totalHits的数量后,Lucene会进行排序和得分的计算,所以你有些句子搜索时间过长,不妨定位到totalHits的数量是不是很大。然后我们就能知道这个计算得分是很耗CPU和内存的,因为此过程不只是要计算得分,还会创建了数千万个对象进行计算。由于并发量过大,搜索过程中也会把totalHits的计算结果存放于缓存中,导致GC认为这些对象还不能清除,然后堆积起来就内存溢出了。
定位到这我们也知道内存溢出的主要原因了。
Linux查看Java的对象在内存中的情况:
jmap -histo (java的)pid | head -20
Java项目的pid搜索:ps -ef|grep java
初始解决办法:
①参考外网的方法,有个外国友人研究了下google的搜索策略,大致是把totalHits的那段代码修改成为:如果totalHits超过一定数量就给那个totalHits的源码中的while循环break掉,让原本totalHits能达到两千万的修改成只能达到一千万,这样是个办法,但是损失掉好多结果。不推荐
②还有就是参考百度的搜索策略,只能搜索前38个字。我试了下,还挺不错的,因为这个38字还是有些根据的,有兴趣的同学可以去查查论文(自然语言方面的),修改完这个前38个字搜索策略后,程序内存溢出的情况少见了。注意是少见,因为还是会出现,你不跟不能拒绝掉有些前38个字的搜索的totalHits还是会出现数千万级的量级,所以内存溢出还是会出现。
中期解决办法:
参考了https://elasticsearch.cn/article/361这篇文章吴哥大佬的思路,查看了下Lucene7.3.0和8.5.1的源码,8版本后totalHits不再提供完全精确的数量,仅仅提供最大万级的数量返回,这时搜索速度提升不止3倍,而且内存溢出问题暂时不出现了。
所以数据过大并且频繁出现内存溢出的同学可以考虑把Lucene更新到最新版本!
咱们也看到上面标注的是中期解决办法,因为更高的并发量还未到来,所以我不能将此说为终极的,后续如果还有更加更加好的解决办法,我会继续写出来的。
当然,其实索引超过5G后是该考虑分布式搜索了,但是我们这边暂时不需要去做成分布式,单机的配置很高了,在代码层面优化还是可以用的。