1. solrHome/corename/instanceDir/dataDir
solrHome下面挂多个coreName,每个coreName对应一个instanceDir,相对solrHome的。dataDir是任意的,默认是相对instanceDir。dataDir的送耦合,意味着多磁盘时候,IO可以均衡。 每个SolrCore管理一个instanceDir、对应一个dataDir。在权限上都是final,并且没有set方法。也就是说,solrcore一旦确定,就不允许动态调整dataDir等参数,new一个新的solrCore,配置新的dataDir。这种绑定,意味着索引的安全!dataDir的篡改是不安全的!一种扩展方法就是委托coreDecriptor来间接的调整dataDir,并且依赖reader来改变真实路径。这需要依赖getSearcher来打开。这明显违背了solrcore的初衷。
2. SolrCore成员变量全部 private 或者private final
注意final修饰的变量。这些变量意味着运行时不推荐调整。
SolrResourceLoader resourceLoader 这里有个坑,那就classload问题。第三方的loader的加载和潜入需要注意。
UpdateHandler updateHandler 这是solrconfig中配置的,意味着变更能自动reload。 RequestHandlers reqHandlers 注意这是多个handlers对象,不同reqHandler 关联他自己的Map searchComponents。尽管有多个path,每个path对应自己的reqhandler和component,但是,底层solrIndexSearcher是共享的,由SolrCore分配出来。也就是说,不同类型的query,可有自己的queryparse、进入lucenereader之前的各种不同的处理逻辑,但是索引是一份、reader是一份、SolrCore协调管理向外提供的SolrIndexSearcher也是一个共享的,计数来维护资源释放。
Map updateProcessorChains;这是链式加载,意味着扩展的方便上面都是读相关,下面
private IndexDeletionPolicyWrapper solrDelPolicy;
private DirectoryFactory directoryFactory;
private IndexReaderFactory indexReaderFactory;
针对reader、merger、directory,直接影响性能、索引文件如何提高服务的。这个直接连接lucene的API。这3个点的扩展是高级活,意味着reader管理、性能优化。 当然,如果需要扩展,那么,可以通过solrconfig配置updateHandler、components等直接的对象,通过这个对象间接的 插入其他本地逻辑。
这样由SolrCore严格接管updateHandler、resourceLoader、reqhandler目的:使得任何对索引的操作,访问最小单元限制在SolrCore层,统一对SolrCore做计数和引用,从而SolrCore下面的reader、searcher共享的生命周期和线程安全问题,统一在SolrCore层,从高层屏蔽底层细节,使得扩展在SolrCore之上的话,更加安全、更加符合solrCore设计目标。 所以,不管是es、lucid等,各种在lucene基础上搭建的分布式,一定少不了一个类似SolrCore的模块,以不同实现方式,来完成和扮演SolrCore相关的角色和功能。
3. 关于计数
SolrCore open的时候 计数++,close的时候--,在-- 结果是0的时候关闭。从CoreContainer get SolrCore务必在使用完毕之后记得close一下。否则,计数不对称。
SolrIndexSearcher是共享的,每次获取new SolrIndexSearcher对象,优先共享当前对象。new之前优先获取已经已经打开但没有注册的对象。如果是强制new searcher是一定是构建一个新的。这个在每次commit、optimizer、mergern等索引发生变更的地方需要重新打开searcher对象,间接打开reader。不变的部分直接引用,变的部分reopen,这样reopen速度更快。reader打开慢,是因为要加载indexreader对于index的 倒排信息。
整个solr管理的最小完备单元(add\update\request\cache...)就是SolrCore,所有对solrCore的操作、同步,间接体现在SolrCore的计数上。
从getSearcher计数开始,往地下reader走,屏蔽细节。往上走,要求对象每次请求,每次new。也就是response和queryparse都是一次一new的。
kafka是solr lucene的活跃用户和使用、研究者,属于这方面的专家啊。
4. Listeners
在solrCore提供的listener 应该说是listener 链,有两条链,firstSearcherListeners 和newSearcherListeners。也就是说可以firstsearcher这一事件,可以通知消息给多个listener,listener的配置顺序,决定了回调顺序。从而潜在表明:listener之间的前后顺序,最好不影响业务逻辑,或者说listener之间不共享数据,或者说顺序化本身能确保数据的不冲突。 从listener被回调看,是串行通知的。也就是说是挨个挨个触发消息。这个不适合并发通知。或者需要并发通知,那么在自己的listener中完成并发转发通知。
listener的一个巧用:通过自定义的listener,来改变searcher行为。也就是说,当listern事件触发了,那么在这个listener事件处理中,完成SolrIndexSearcher的获取,进而获取reader信息,进而可以直接基于reader层,来完整某些特殊业务需求。例如:term list query,批量and or的优化等。
5. 各种plugin
updatehandler request handler、event listener、deletePolicy、index参数、qparse。。。核心入口就是 构造函数public SolrCore(String name, String dataDir, SolrConfig config, IndexSchema schema, CoreDescriptor cd)
这个构造函数,完成了最小、最完备、自包含的管理单元的构建。core的reload也是基于这个构造函数来实现的。 而对SolrCore的管理:reload、swap、remove、create、register都是在CoreContainer中完成。也就是说SolrCore他自己内部,不存在对自己的reload,需要在外部控制下reload。
也就是说在SolrCore在具体使用时,是工作在 CoreContainer的管理下面的。SolrCore只是直接执行任务,而应用层面对SolrCore的Core层面的操作是在CoreContainer完成。
有一个缺点就是 CoreContainer不能对SolrCore执行资源隔离,尽管不同SolrCore实例是隔离的。如果在同一个jvm进程内,多个solrCore部署在一个CoreContaner中,当某个solrCore资源消耗大的时候,就会影响其他SolrCore的运行。
在整个plugin体系中,除了solrconfig.xml支持的灵活的配置,从查询业务角度看,能适应非常多的场景需求。另外一个非常头疼、也必须认真对待的就是classload了。搞不好就会发生加载自定义bean冲突或者加载失败。
配置load是 SolrResourceLoader,这个load的构造函数,生成了一个parent= null的 classloader。
如果配置了共享的lib,也就是solrhome实例层面的,而不是具体core层面的lib的时候,需要重新生成libLoader。这个生成parent 是当前线程的ClassLoader,也就是调用new SolrCore的classload。
注意了,如果不是通过corecontainer来创建SolrCore,而是自己主动的new SolrCore,有可能 当前new SolrCore的线程所在classLoad与系统默认其他的不一样,这样后面创建的SolrCore与 之前默认启动的SolrCore之间就是不同上下文了。 URLClassLoader.newInstance(new URL[0],parent) 生成一个新的classload对象,然后replace SolrResourceer的classloader,这个classloader用来加载lib下面的jar。也就是传入lib下面的jars,并结合之前classloader的 父load,构建新的classloader
而其他的非lib 下面jar的 配置在solrconfig里面的默认支持的class 加载,是SolrResourceLoader 里,直接Class newIntance 生成的。
到此基本上一个SolrCore的初始化完事了。接下来就是具体如何update 新数据、如何query索引了。这两块一个是writer、一个reader,对于writer需要加锁,对于reader是只读、共享,并且在writer完毕需要fresh
reader。
6. update流程梳理
在solrCore里面,有初始化updateHanler、updateProcessorChains。更新或者add、delete的操作,一种理解是request的一种,那么,统一在SolrRequestHandler 的handleRequestBody 里面执行正在操作。这里面顺便提下:solr是基于http协议来查询的是主流,全部请求都与path相关,而每个path都对应注册了自己的SolrRequestHandler,包括了查询的业务、add、delete、update等业务都是封装为SolrRequestHandler.同时,solr也提供了embeded操作,那就是直接架SolrCore上,直接调用SolrCore提供的方法接口。
在具体调用线上,有三条线
线1:直接调用SolrCore的方法,updatehandler 处理各个UpdateCommand的时候,直接调用addDoc、delte等操作。
线2:通过EmbededSolrServer,的request,间接执行core.execute(handler,req,rsp),这里面handler的handleRequest 间接调用抽象方法handleRequestBody。具体的handler实现,会有自己的handlerRequestBody内容。对于XmlUpdateRequestHandler 就是用来处理增删改的。XmlUpdateRequestHandler extends ContentStreamHandlerBase。 ContentStreamHandlerBase的 handleRequestBody 调用了UpdateRequestProcessorChain来处理相关请求。
线3:DirectSolrConnection和 SolrServlet,直接从 core.execute(handler, solrReq, solrRsp ); 入口线3已经不推荐,线2是solrj的默认实现了,线1是定制实现。线1对于默认实现来说,丢失了事务性。
参照ContentStreamHandlerBase .handleRequestBody,其中处理update是两阶段提交的,实现了事务特性。
回到ContentStreamHandlerBase.handleRequestBody中,注意流程一个是documentLoader.load,一个是 handleCommit。后者是事务的操作。
这里在真正提交时候,commit调用了UpdateRequestProcessor的processCommit方法,processor会链式处理下一个processoor的processCommit方法。其中UpdateRequestProcessorChain 关联不同path,对应一个该path的UpdateRequestProcessor链,注createProcessor方式,加载的是List哦,获取其实是get第一个的。public UpdateRequestProcessor createProcessor(SolrQueryRequest req, SolrQueryResponse rsp)
{
UpdateRequestProcessor processor = null;
UpdateRequestProcessor last = null;
for (int i = chain.length-1; i>=0; i--) {
processor = chain[i].getInstance(req, rsp, last);
last = processor == null ? last : processor;
}
return last;
}
通常默认的时候,没有配置chain,此时默认加载LogUpdateProcessorFactory 和 RunUpdateProcessorFactory
在LogUpdateProcessorFactory中,只是log信息变更和转发,没有执行其他操作。在RunUpdateProcessorFactory中,首先调用updateHandler.commit(cmd),然后执行 next操作。
关注ContentStreamLoader.load,回到XMLLoader.load ,进入processUpdate逻辑,其中adding doc 进入processor的 processAdd逻辑,也就回到RunUpdateProcessorFactory中,其中调用了updateHandler.add(AddUpdateCommand)
至此,整个update的流程基本有一个全貌的认识了。
7. 关于queryquery
也是通过handler处理的。基于EmbeddedSever流程,进入SolrServer的query方法。其中QueryRequest.process方法处理请求。process又调用SolrServer的request,回到EmbeddedServer的request方法中来了。最后进入 core.execute( handler, req, rsp );回到与add的类似处理流程。只是,这次的handler是默认的StandardRequestHandler 到SearchHandler,到RequestHandlerBase,其中searcherHandler的handleRequestBody 处理真正的请求查询计算。 这里分两个分支流程走向,一种是非shard query,一种是shard query。
非shard query,会遍历执行SearchComponent的prpare、process过程。其中 SearchComponent都是在solrcore初始化的时候注入的。多个searchComponent从配置看,没有关联关系,并且代码页没有对顺序做限制,只是将debugQueryComponent放置最后。其实QueryComponent必须先执行,当有facetcomponent的时候。
注意,如果isDebug打开了,会对各部分执行计时。
prepare 阶段,非shard和shard是共享的,
processor部分针对shard 与非shard有分支执行。 非shard部分,核心就是QueryComponent了,其中获取结果、排序、以及各种group、facet、hl的结果都在里面。分布式的逻辑也有这部分。本部分细节在
(9)再细说。下面讲解分布式shard的这部分的并发流程。
(8)shard搜索部分的处理逻辑。
(9)QueryComponent的process部分详解
(10)getSearcher()分析