背景:
晚上收到报警,说线上的一个solr的collection挂掉了,赶紧打开远程服务起查看服务器的状态,果然业务方查询全部超时,增量更新也宕机了,从异常信息上来看是集群中没有可用的节点可以使用,看到这样的问题,第一想到的是要重启一下服务器。悲剧的是重启完服务器,服务只正常了15秒钟,转而又全部宕机。
判断是VM的堆内存溢出了,看了一下虚拟机启动参数 -Xmx4400m -Xms4400m(服务器是8G内存)
,临时的解决方案是增加内存,设置为-Xmx6400m -Xms6400m,设置完成,迅速重启服务器之后,观察了一会,core节点果然是不会挂了,但,通过命令工具 jstat -gcutil 命令查看VM fullgc的状态,观察到Full GC的频率是比较高,只是服务器勉强不会挂罢了。
同事告诉我他们团队刚刚上了一个新的查询需求,这个提示我是不是因为的查询里面设置了什么查询条件的原因,随即仔细查看了一下日志,果然在查询日志中观察到了有两个查询中带sort的参数,需要排序的字段在在schema field节点只是设置了indexed=true而没有设置docValues=true ,瞬间明白了为什么会OOM。
原因分析:
究其原因是客户端查询请求中需要sort的字段在schema field定义中,开启了indexed=true ,但是没有开启docValue=true,这样solr在对命中结果集进行排序时候,会将docid对应的value预先加载内存中,如果一个core中文档数有2000w条的话,试想一个long字段类型的field,在内存中就需要2000w*8个字节的内存,大概152兆内存,而且这个内存块需要随着文档内容更新,频繁刷新,内存频繁OOM也可想而知了。
在solr5.0开始,框架中引入了docvalue机制,按照我现在的理解,这种存贮格式有以下三个特点:
- 这始终列存储,所以通过docid取单列中的内容比基于行存储的document中的内容要要快上好多倍
- 存储中的内容在物理上是按序排列的,利用这个特性,在文档排序时,只需要通过docid取对应的所在存储上的偏移量offset,通过这个offset偏移量就能判断两个值的大小,这样就能省去额外的IO开销,具体可以查看org.apache.solr.response.SortingResponseWriter这个类。基于此,solr框架可以衍生出很多非常酷的功能,比如基于"/export"的流式导出功能,和基于"/export"的stream expression功能。
- 存储内容不是依赖于内存的,这个和老版本的fieldcache机制有本质区别。
以下是查询中使用了sort字段,solr的执行栈路径视图:
从调用路径来看,最终会调用FieldCacheImpl的getNumerics的方法,如下:
@Override public NumericDocValues getNumerics(LeafReader reader, String field, Parser parser, boolean setDocsWithField) throws IOException { if (parser == null) { throw new NullPointerException(); } // schema field 上是否开启了docValue=true final NumericDocValues valuesIn = reader.getNumericDocValues(field); if (valuesIn != null) { // Not cached here by FieldCacheImpl (cached instead // per-thread by SegmentReader): return valuesIn; } else { final FieldInfo info = reader.getFieldInfos().fieldInfo(field); if (info == null) { return DocValues.emptyNumeric(); } else if (info.getDocValuesType() != DocValuesType.NONE) { throw new IllegalStateException("Type mismatch: " + field + " was indexed as " + info.getDocValuesType()); } else if (info.getIndexOptions() == IndexOptions.NONE) { return DocValues.emptyNumeric(); } return (NumericDocValues) caches.get(Long.TYPE).get(reader, new CacheKey(field, parser), setDocsWithField); } }
通过代码了解到,先调用LeafReader的getNumericDocValues的方法,结果是否为空取决于schema中的field定义是否设置docValue=true设置,如果开启了docvalue这里就能直接取得docValue对象,不然的话就通过预先在cache中准备的五种基于索引term的,field加载策略:
private Map我就纳闷了,solr5.0中既然已经有docvalue机制,为什么还要在框架中保留这些通过term预加载到内存的fieldcache机制,因为一旦用户需要使用排序,功能而又在schema中忘记定义docvalue为true,一旦文档数量多,很有可能导致OOM的,也许solr的开发者为了版本向下兼容的原因吧。,Cache> caches; FieldCacheImpl() { init(); } private synchronized void init() { caches = new HashMap<>(6); caches.put(Long.TYPE, new LongCache(this)); caches.put(BinaryDocValues.class, new BinaryDocValuesCache(this)); caches.put(SortedDocValues.class, new SortedDocValuesCache(this)); caches.put(DocTermOrds.class, new DocTermOrdsCache(this)); caches.put(DocsWithFieldCache.class, new DocsWithFieldCache(this)); }
问题解决:
那如何解决用户索引结构不当使用导致的OOM问题呢,是通过在wiki中标注,应该如何小心的配置schema,来防止出现类似的问题。这就像在市区的马路上,通过在马路当中画上双黄线,来明令告知驾驶者不要越过双黄线逆向行驶,但事实是在高峰期,只要没有监控的地方总有胆子大的驾驶者要越过双黄线,解决办法就是,要像高速公路上在马路中间建造隔离带,强行防止开到逆向车道上去,但是这个成本确实是有点高,但是非常有效。我们在做平台式的产品中也需要借鉴类似经验,需要在平台产品中构筑起一个个轨道,保证用户在既定的轨道上操作,如果用户试图跳出既定轨道,我们就要通过友好的反馈消息告知他已经偏离了轨道需要及时纠正。这样一种办法,可定比在wiki中写开发规约之类的东西有效,友好得多。
所以我在solr容器启动的时,执行了一个将solr框架预先准备的cache清空的操作,后续如果有操作试图不通过docvalue机制来执行sort之类的操作就一律报错,这样在开发过程中就避免的因为不合理设置schema导致的错误,代码如下:
RemoveFieldCacheListener:
public class RemoveFieldCacheListener implements ServletContextListener { @Override public void contextInitialized(ServletContextEvent sce) { RemoveFieldCacheStrategy.removeFieldCache(); } @Override public void contextDestroyed(ServletContextEvent sce) { } }RemoveFieldCacheStrategy:
import java.io.IOException; import java.lang.reflect.Field; import java.util.Map; import org.apache.lucene.index.BinaryDocValues; import org.apache.lucene.index.LeafReader; import org.apache.lucene.index.SortedDocValues; import org.apache.lucene.uninverting.FieldCacheImpl.Cache; import org.apache.lucene.uninverting.FieldCacheImpl.CacheKey; import org.apache.lucene.uninverting.FieldCacheImpl.DocsWithFieldCache; import org.apache.lucene.util.Accountable; public class RemoveFieldCacheStrategy { @SuppressWarnings("all") public static void removeFieldCache() { try { FieldCacheImpl fieldCacheManager = (FieldCacheImpl) FieldCache.DEFAULT; Field cacheField = FieldCacheImpl.class.getDeclaredField("caches"); cacheField.setAccessible(true); // 防止启动的时候在schema中没有设置 docvalue属性的时候,字段设置了indexed=true // 将doc的term的值预先加载到内存中,防止業務方不適當設置query對象導致服務端oom Map, Cache> caches = (Map , Cache>) cacheField.get(fieldCacheManager); FieldCacheImpl.Cache disable = new FieldCacheImpl.Cache(null) { @Override public Object get(LeafReader reader, CacheKey key, boolean setDocsWithField) throws IOException { throw new IllegalStateException( "you are intending to use sorting,facet,group or other statistic feature,please set field:[" + key.field + "] docValue property 'true'"); } @Override protected Accountable createValue(LeafReader reader, CacheKey key, boolean setDocsWithField) throws IOException { return null; } }; caches.clear(); caches.put(Long.TYPE, disable); caches.put(BinaryDocValues.class, disable); caches.put(SortedDocValues.class, disable); caches.put(DocTermOrds.class, disable); caches.put(DocsWithFieldCache.class, disable); } catch (Exception e) { throw new RuntimeException(e); } } }
完!