对SolrCloud中的事务日志(Transaction Logs)、软提交(SoftCommit)和提交(Commit)的理解

Solr中的“硬提交”、“软提交”及事务日志
在Solr 4.0中多了一个叫做“软提交”(soft commit)的新功能,在原有的hard commit中也增加了一个新的参数,叫做openSearcher。这样,你可能就会对软提交和硬提交两者的作用产生疑惑了,主要是对事务日志的影响。在solr自带d的例子文件中,配置文件solrconfig.xml解释了其中的这些参数,但毕竟是例子文档说明,不可能将每件事情说得清楚,而如果想要把每个细节都涉及到,那么实例中的说明文档可能会有10M左右,这样就没人愿意看例子了。相关的正式文档能够在Solr Reference Guide中找到,但也是相当简洁的概述了一遍。我(原作者)已经从solr的代码贡献者中足够的细节,可以肯定的是我得到了足够完整而细致的内容,如果我在这里表述的内容不准确,那只能是因为我表达不精确的原因。(作者相当谦虚,认为代码的作者已经给够了他精准的信息,可能会造成转述的错误,认为他将为这写错误负责。我在此翻译也可能误解作者的意思,所以造成这些错误那便也都是我的错了)。
重要的事情说三遍
请记住:“硬提交有关持久化,软提交有关可见性”(原句:Hard commits are about durability, soft commits are about visibility”)。硬提交和软提交两者概念有关系,但是为不同的目的而服务。在这个简单的表述中,隐藏着许许多多的细节,我会试着一点点解释清楚。首先我们来看相关的定义:
事务日志(Transaction log, tlog):该文件主要用于恢复数据,原始数据文档(raw document)首先将写入到该文件中。在SolrCloud中,每个节点(node)中都有它的tlog,在更新的时候,一个完整的文档将被写入到tlog中。对于原子更新(Atomic Update),它仍然是整个文档,因为他包含了原文档的数据。也就是说,写入tlog的不是增量数据,而是整个文档的数据。对于tlog来说,数据的一致性是至关重要的,所以,当segments在JVM停止而没有被及时关闭时,这些数据也能被更新到索引中去。(也就是说当JVM发生意外时,solr还没来得及将数据更新到索引中,而tlog如果存在未索引的数据那么这些数据也还是能恢复到索引中去的)
注意:如果服务器没有被正常地关闭,那么事务日志将会在服务器重启的时候进行回放。如果事务日志文件很大时(现实中也存在有上G大小的),重启服务器将会变得很慢很慢,可能会达到几个小时。
硬提交(Hard commit):该功能可以通过solrconfig.xml中<autoCommit>的配置项来控制,或者也可以由客户端(SolrJ或浏览器、cURL或其他发送的Http)来显示地调用。索引文件中,硬提交会结束当前的segment,打开另一个新的segment。
openSearcher:<autoCommit>中的子属性,为布尔类型,控制是否将新提交的文档出现在搜索中。
软提交(Soft Commit):能够让文档立刻被搜索到,但比硬提交(openSearcher=true)开销小。但是软提交没有使用事务日志。
重要事项:虽然软提交的系统开销比较小,但这并不是说他们不要开销。为了保证良好的性能,应将软提交的时间间隔设置得越长越好。
fsynch:底层的I/O读写,当调用fsynch返回时,字节数据已经写入到磁盘中了。这跟调用Java中的写数据方法有点区别,Java层面的写数据只是保证将新的数据提交给操作系统,提交之后函数便返回了,由操作系统来决定何时将数据写到磁盘上。(也就是说fsynch完成了数据的全部写过程,直到数据写入到磁盘它才返回,而Java层面的写只是将数据转交给操作系统的时候就返回,它本身不管数据的具体写入)
flush:Java运行的时候将数据转交给操作系统,在返回的时候,磁盘上的字节并没有发生改变,此时如果操作系统发生崩溃,这些数据将会丢失。
值得注意的是,在SolrCloud中,每个shard可能不止一个replica,此种情况下丢失数据需要两个node在同一时刻都没有将数据写入到磁盘中,这种情况下丢失数据的可能性不太大。
操作系统能在flush之后在几毫秒内(10-50毫秒)将比特数据写入磁盘中,如果JVM崩溃了,操作系统在flush之后仍会改变磁盘上的数据。在Java写文件之后,同时在I/O子系统将数据写入到磁盘之前,在这个短暂的时间段内,操作系统此时发生崩溃,数据才有可能发生丢失。所以,一般情况下都不用担心数据的丢失,重要的是你应该记住数据总是完整的。
事务日志(Transaction Logs)
事务日志在solr4中是保证数据完整性的重要组件之一,但使用起来有点不太简单,我们来深入了解一下他们。为了更好的了解事务日志,我们要了解整个索引的流程:
首先,在SolrCloud中的某个node接受到了新的文档,得到文档之后便转交给相应的leader。
然后leader将这些文档发送给所有相关shard中的replicas。
接着replicas向leader做出接收到文档了的响应。
等所有的leaders都接收到了响应,最开始接收文档的node向客户端返回结果。此时,这些文档都已经写入到了cluster中所有node的tlog中了。
如果JVM崩溃了,这些文档还是能够保存下来的;而如果操作系统崩溃了,他们就可能无法保存下来了。
如果JVM崩溃(或被kill -9杀掉),当重启的时候,tlog会进行回放恢复数据。
你可以更改solrconfig.xml中的设置,将flush更改为fsynch才返回,但通常没有必要这么做。因为由4若干个leader和replicas组成的SolrCloud不太可能发生同一时候都崩溃的情况。在实际情况下,可能仍然无法容忍这种小概率事件的发生,所以会选择牺牲掉一部分性能来保证数据的完整性。
注意:tlogs在硬提交(不管openSearcher是设置true还是false)中是自动“翻转”(rolled over)的,关闭旧的事务日志时,会有新的事务日志打开。为了保证一个时刻内事务日志中包含100个左右的文档,新的tlog可能会不断的打开,与此同时,旧的tlogs就将被删除。假设有若干文档需要索引到solr中,这些文档以25个一批进行索引,每一批文档索引后便进行硬提交(不应该提交这么频繁,但这里只是假设)。这样,在每个时刻总是会有5个tlogs,其中包含四个旧文档(已被关闭,每个都包含25个文档)、一个当前还没有关闭的tlog,总共有100个文档。如果当前的文档关闭时,最旧的那个tlog将会被删除,另一个新的tlog会被打开。请注意,solr不会自己动态保留这100个文档,事务日志只会在你硬提交的时候(或者在solrconfig.xml中设置autoCommit)才会去“翻转”。当大批量建索引时,比如每秒钟1000个文档,如果你在一个小时候都没有做硬提交,那么你的单个tlog将包含3600 000个文档。当不幸来临时,你只能整个回放事务日志才能让solr重新运行起来,可能需要花费几个小时的时间。你很可能等不了那么长时间,在想是不是哪里出错了,最简单的做法是再次重启solr,但这还是会重头开始回放事务日志。然后,就没有然后了。所以,当你在建索引时会发生很大的tlog时,你最好设置一下你的硬提交,不然会有很大的麻烦。在3.x版本的时候,因为没有openSearch=false这个设置选项,每当硬提交时会占用比较多的系统资源,所以一般会将时间间隔设置得比较长,在这种情况就可能会发生一些比较棘手的情况了。
软提交(Soft commit)
还是那句话,“软提交有关可见性,硬提交有关持久化”。很容易理解软提交的作用是让文档能立即被搜索到,以消耗一些系统资源为代价。首先是你在solrconfig.xml中设置的诸如filterCache、queryResultCache等一些顶层缓存,都将变成无效的了。然后,自动预热(Atuowarming)开始在顶层缓存(也就是filterCache、queryResultCache)中运行,执行每一个newSearcher查询。另外,FiledValueCache也将失效,所以facet只能等缓存重新建立之后才能被查询到。在软提交非常频繁的情况下,顶层缓存的作用大大减小,或者根本就不起作用了。相对而言,segment层的缓存(包括函数查询、排序查询等)是每一个segment,所以在软提交时他们仍然有效。
那么问题来了:软提交的机制是怎么样子的呢?
我们一起来看软提交执行完后发生了哪些事情:
事务日志并没有被关闭,仍然会继续存放原始文档。
文档能够被立刻被搜索到。
部分缓存将重新缓存
顶层缓存失效。
自动预热将被执行。
注意,在软体交中,索引的segments并没有多大的作用,因为那是给硬提交使用的。这里需要再次说明一下,软提交比硬提交(openSearcher=true的时候)较少消耗系统资源,但不意味着它们不耗费资源。 正如科幻小说《The Moon Is a Harsh Mistress》(Robert Heinlein著)中月球殖民地上的一句格言:天下没有免费的午餐(TANSTAAFL,There Ain’t No Such Thing As A Free Lunch)。虽然软提交能真正做到近实时(Near Real Time)索引,但它仍会消耗系统资源,所以为了更好的性能,尽量将软提交的间隔时间设置得尽量长些。
硬提交
还是那句话:“硬提交有关持久化,软提交有关可见性”。硬提交有两种模式,openSearcher=true和openSearcher=false。首先我们来谈谈这两种模式有什么区别。以下是两种模式下都会发生的事情:
旧事务日志将被关闭,而新的事务日志开始被打开,另外,当新打开的日志的文档数大于100的时候,旧的、被关闭了的事务日志将被删除。
当前的索引segment将被关闭,同时写入到磁盘中。
后台的合并segment的线程可能会启动。
那么不同的openSearcher设置又会怎么样:
openSearcher=true:Solr/Lucene的searcher会重新打开,所有的缓存都会失效,同时执行自动预热等。在以前的版本中,这曾经是能让刚被加进来的文档搜索到的唯一方法。
openSearcher=false:其实,它除了上面所提及的三点之外,其他什么没有做。所以,为了能够实时搜索到新加入的文档,软提交就出现了。
恢复
我在上文中已经涉及到了有关持久化的相关的内容了,现在我们再补充一些。在系统发生崩溃、JVM将强制退出过程中, cluster状态是这样子的:
在cluster中的最后一次成功的更新都能够保证文档写入到tlog中,默认情况下,成功调用返回时tlog是被flush的,而不是fsync,如有必要可以将默认设置改为fsync,但并不推荐这么做。
当崩溃的系统重启时,solr将跟leader进行通信,同时执行下列情况中的一种任务
如果tlog中最新更新的文档小于100篇,文档将重新回放。注意,在回放过程中,如果有其他新的文档需要添加进来时,会被加入到tlog中的末尾,他们也将被顺带回放。
从leader中接受了大于100篇的更新文档时,solr会先将其离线更新,进行一种旧的复制模式以保持同步。
恢复会花费一段时间,所以很多人会将solr配置成SolrCloud。这样他们会经常启动关闭服务,用kill -9或其他方法杀掉solr进程。一方面是因为这样杀掉进程很顺手,同时也测试了SolrCloud的恢复机制。另一方面,因为它是纯手工操作让人感到高大上。如果你经常在一天内将某些node干掉,你的问题不仅仅是启动的时候会变慢,还可能产生另一些在启动的时候发生的错误。
建议
我其实是有点怀疑自己给出的建议是否合理的,因为实际遇到的情况远远超出我假设的情况。我的第一个建议是不要对我提出的建议纠结太多。另外,一些大牛可以配置得让solr运行得很稳定。首先从最简单的开始入手,然后慢慢按需求调整。特别注意一下你的事务日志大小,调整硬提交的时间间隔以保证事务日志的文件大小在合理的范围内。记住这个大小与系统崩溃时重启回放的时间挂钩。能容忍15秒的时间间隔吗?为什么要设置更小的时间间隔呢?在大批量的导入数据时,往往将硬提交的时间间隔设置得远小于软提交的时间间隔。详情见下面解释。
请温柔的关闭solr服务。也就是说千万别在索引的时候用kill -9杀掉进程,不然是自寻烦恼。
那怎么关闭呢?
首先,停止导入文档。
接着,发送硬提交命令,或等到它自动提交完成。
最后,关闭solr服务
下面是大批量导入的设置,请根据你的情况酌情调整设置。
大批量导入
假设你要将一批很大的数据以最快的速度导入到索引中,以便今后能够方便搜索。然后,数据是从数据源(或者其他)导入进来的。
请将软提交的时间间隔设置得大点,比如10分钟或者更多(当设置-1时,软提交将不再起作用)。“软提交有关可见性”,这里假设建立索引并不需要及时被搜索到,所以并不需要打开任何searcher等多余的任务。
将硬提交间隔时间设置为15秒,并设置openSearcher=false。还是认为我们现在想做的只是将数据导入到solr中。这样设置保证你最多只要15秒左右的时间从事务日志中回放数据。如果你的系统经常会启动关闭,考虑一下设置这个参数。
在你把上面的配置完成之后,我们来更加完善配置,这些配置一般都用在特殊的环境下,其中包含下面这些内容:
在大批量导入时完全关闭事务日志。
用map-reduce离线建索引。
在刚开始加载的时候,只允许每个shard拥有一个leader,没有replica加载完后再启动replica,以传统的复制方式使得replica的数据保持同步。注意这个操作是自动的:如果node发现它的数据跟leader的没有同步,它会启动传统的复制方式。当数据完成同步之后,它就可以接受新的文档生成自己的事务日志和建索引了。
其他
重度索引,轻度查询
这种情况相对应的例子有:建立日志索引。日志会不停的产生大量的数据,然而查询的负载却很小很小,经常只是用在遇到问题时或者分析日志信息。以下是配置建议:
软提交的时间可以设置得很长,长到你能忍受在如此长的时间内都搜不到这些文档,可以设置为几分钟或者更长。根据执行硬提交(openSearcher=true)或软提交的性能来自己决定这个时间间隔,或者甚至将时间间隔设置成几个小时。
将硬提交的时间间隔设置成15秒,并设置openSearcher=false。
轻度索引,轻度或重度索引
这种情况的前提是索引一般来说比较稳定,只是偶尔(比如每隔5-10分钟或更久)会需要更新索引
除非需要NTR(近实时)功能,那么软提交的设置就可以不需要考虑,并将硬提交(设置openSearcher=true)设置为间隔5-10分钟。这种情况下,如果你只有一个外部进程来控制进行索引的话,用客户端来发出硬提交命令也是可以行的。
重度索引,重度查询
这种情况下一般是要求近实时查询的(Near Real Time,NRT),这种情况下是最复杂的。一般需要多次试验,但我还是会给出些自己的经验:
在你能够容忍的范围内,将软提交的时间间隔设置得越长越好。不要听你产品经理的,他们会说他们1秒钟都忍受不了。你要关注的是否给用户提供了最好的服务、用户是否察觉到了这种差异。然后,再次提醒一下,软提交和NRT确实设计得很牛逼,但是他们并不是不需要消耗资源。
将硬提交的时间间隔设置为15秒。
Solrj和HTTP,及客户端索引
一般情况下,所有配置选项都能通过SolrJ或HTTP这些客户端进行控制。HTTP远程调用命令的文档在这里(没错,这也是英文的)。SolrJ的接口文档在Java文档中的SolrServer类中,最新更新时间为2014年7月。(请不要在意这些细节,因为最近更新时间肯定不是这个,而且客户端的类在5.x版本以上都已经更新为SolrClient了)。请注意:从客户端进行提交(commit)时要非常非常的小心,实际应用中,最好不要从客户端发送提交命令。总的来说,都不要从任何客户端发出带提交命令的索引请求。特别是在多个客户端发出索引的命令的时候,并发提交是件非常糟糕的事情。多客户端运行时,可能会出现众多”提交“一并发送过来,而每个提交会产生上文所说的一系列动作,结果就是,你可能在你的日志中看到“预热searcher过多”(too many warming searchers)这样的警告。或许你还能看到很多小文件segment。又或者其他诸多不好的事情…所以请尽量使用自动提交,因为自动提交是一个时刻仅提交一次。事实上,即使只有一个客户端,我也会使用自动提交。否则,我会等到所有的的进程都完成了,然后再手动进行提交,比如发送这样的url请求:
http://host:port/solr/collection/update?commit=true
如果真要这么做,可以用cURL工具或从浏览器中输入上面的地址。也可以使用SolrJ,把文档添加到solr中,同时指定commitWithin的时间间隔(单位为毫秒),这是SolrJ客户端解决并发提交较为完美合理的方案,这样产生的效果跟直接在solr中所作的一样的。当接受到第一个更新请求时,服务器启动一个commitWithin(这是一个lon类型的数值)毫秒的计时器。经过commitWithin毫秒后才开始对从其他客户端接受到的文档进行索引,不管有没有设置commitWithin(这是一个设置项)。下一个指定commitWithin的更新将启动另一个新的计时器。顺便一提,优化操作(optimizing)一般很少使用的!最后,祝你索引快乐。

工程师必备
- 项目客服
- 培训客服
- 平台客服
TOP
