原文来自:Understanding and tuning your Solr caches


为了获得最大的查询性能,Solr 将一些不同的信息存储在内存中。结果集、过滤器和文档字段全部被缓存起来,后续相似的请求可以快速得到处理。

缓存不是万能的,但是理解缓存并且让缓存适合你的搜索是非常重要的。这篇文章介绍不同的缓存,研究缓存所使用的内存量并探讨怎么知道你设置的缓存大小是否合理。

通过 velvet pants 去搜索你的索引 ,你将在 10100 毫秒以内得到结果。重新请求一次,你将会在几毫秒内得到同样的结果。你在这里看到的结果就是缓存:包括系统级别的缓存(因为操作系统缓存了你刚刚搜索时 Solr 的索引块)与 Solr 本身的缓存

缓存的类型

Solr 缓存不同类型的信息去确保相似的查询不会重复不必要的工作。有如下三种主要的缓存:

  • 查询缓存 - 存储通过查询返回的文档 id 集合。如果你通过 velvet pants 去查询,返回了 1000 个结果,那么这 1000 个文档 id 集将会被存储在该查询字符串的查询缓存中。
  • 过滤器缓存 - 存储 Solr 构建的过滤器并作为响应添加到查询的过滤器中。如果你在搜索 velvet pants 的同时加上过滤参数 fq=category:apparelSolr 将会为 apparel 这个 category 构建一个过滤器,并将这个过滤器添加到它的缓存中去。
  • 文档缓存 - 存储显示查询结果时请求的文档字段。当你通过 Solr 去搜索的时候,一般会要求返回一个或者多个存储字段在你的结果中(通过使用 fl 字段)。Solr 也会储存文档字段为了减少后续请求同一个文档的时间。

在接下来的部分,我们将会研究这些不同的缓存以及它们可能需要使用的内存大小。之后我们将通过一些工具去研究一下我们确定的这些缓存大小是否合理。

查询缓存

Solr 通过如下方式来处理对 velvet pants 的搜索:

  • 它会通过查询索引去寻找所有的文档中是否包含 velvet 这个词。然后产生一个包含这个词的内部文档 id 列表
  • pants 这个词也是一样,也会产生另一个 id 列表
  • 最终,它会包含两个列表。如果你用的是默认的 OR 操作符,Solr 将会合并这两个文档 id 集。如果你用的是 AND 操作符,将会得到这两个文档 id 集的交集(同时包含这两个词的文档)

当查询变得更复杂,Solr 会做更多的工作。当你通过 velvet pants propellerheads -category:appare 来进行搜索时,Solr 将会找到 4 个文档 id 集,合并前三个为一个集合,然后与第四个集合做差集。

感谢齐夫定律,通常情况下,某些查询一次又一次的执行,但是长尾词的查询将只会出现一次。在一个流行的网站上研究它的搜索日志,你会发现最常见的搜索查询频率将会是第二常见搜索查询频率的两倍。通过观察查询列表,你会发现大部分的查询仅仅只出现了一次。

因为这些重复查询,Solr 的查询缓存会有很大不同。Solr 每次会计算出匹配到的文档 id 集并把它存到查询缓存中。如果随后进行另一个查询 velvet pants propellerheads -category:apparelSolr 可以从查询缓存中拿到结果,几乎不用做任何工作。

查询缓存并不只是用于经常出现查询。分页接口的搜索结果也得益于查询缓存:从 Solr 的角度看,当用户反复的点击下一页就像用户反复的执行同一条查询语句,查询缓存能够减少获取后续页面的工作。

Solr 通过整型数组来存储查询缓存,所以每个缓存条目大致为查询字符串的字节数,加上结果集中每个文档的 8 个字节。给一个简单的例子:

  • 如果查询的字符串平均为 50 个字符(也就是 100 个字节)
  • 查询返回的结果平均为 1000 个(一个整数占 8 个字节,总共占 80,000 个字节)
  • 那么每个查询条目的大小可能会比 80KB 大一点

如果你配置了 Solr 的查询缓存能够保存 1000 个缓存条目,那么大概需要 80MB 的内存去存储这些缓存。

过滤器缓存

过滤器可以限制文档的大小。例如:以下查询:

“fq” stands for “filter query”, and tells Solr to build a filter. /select?q=velvet+pants&fq=category:apparel

返回的文档会包含查询字符串 velvet pants,并且 category 的值为 apparel

你可能认为下面这种查询方式也可以:

/select?q=(velvet+pants)+AND+category:apparel

从返回的结果来说你也是对的,但是使用过滤器有一下的好处:

  • 使用过滤器不会影响返回结果的相关性排名,因为包含 apparel 的文档权重不会提高
  • 从查询语句中剔除多余的词,意味着我们可以利用 velvet pants 的查询缓存,尽可能的节省整个搜索的时间
  • 过滤器可以被保存和重复使用,如果你执行的查询中都包含同一个过滤语句,那么它们就非常的快

为了处理过滤查询,Solr 执行查询语句得到一个文档 id 集,然后与属于过滤器的文档 id 集做交集,只留下属于过滤器的文档 id 集。

一旦你通过 category:apparel 得到了一个文档 id 集,你可以一直重复使用这个文档集,如果查询语句都包含同一个过滤查询。如果一个用户执行了如下三次查询,都包含了 category:apparel

/select?q=velvet+pants&fq=category:apparel
/select?q=shoes&fq=category:apparel
/select?q=top+hat+with+monocle&fq=category:apparel

后面两个查询语句可以利用这个过滤器几乎不用做多余的工作。如果你知道用户会在什么场景下进行搜索,非常适合使用过滤器。例如:

  • 文档在 dinosaurs 这个集合中
  • 我是否有权限去看某些文档
  • 某些文档在 1995 被添加

过滤器缓存可以重复使用某个过滤器:一旦 Solr 通过过滤器构建了一个文档 id 集,就会在需要的地方去重复使用它。

跟查询缓存类似,使用过滤器缓存,内存的使用量可能会非常大。Solr 通过一位一个文档的比特串来表示过滤器中的文档 id 集。如果每个过滤器包含一百万个文档,那么需要一百万个比特的内存 —— 大约 125KB。如果过滤器缓存能够包含 1000 缓存条目,那么需要大约 120MB 的内存。

文档缓存

当你请求 Solr 时,你不仅仅想要文档 id,你还想要 titlesproduct namesauthorsdescriptions 或者其它的字段,在索引期间,我们要求 Solr 将这些信息作为 stored fields 保存在文档中,然后在查询结果中获取这些字段信息。

Solr 返回搜索结果时,会将请求的字段在文档中一起返回。获取这些字段,必须单独从索引中去获取,通过查询磁盘上的数据结果,获取每个文档 id 所对应的字段。相对于从内存中获取数据比较慢。

如果某些文档被频繁请求,Solr 将这些文档的字段存储在内存中能够省去很多麻烦。文档缓存的任务就是缓存这些被经常请求的字段。一般来说,文档缓存在提升性能方面没有其它两种缓存那么明显。多个文档被请求多次,这种情况不常见。这也是文档缓存的命中率非常低的原因。

如果你有许多的字段或者大值字段需要存储,你应该把你的文档缓存设置的相对小,因为这种数据会消耗大量内存。

Solr 缓存调优
注意

Solr 的新手常常希望将缓存的值设置的远远大于他们实际所需要的值。毕竟,如果你的机器内存非常大,为什么不多分配点内存给 Solr 让缓存也变大一点呢?这种做法实际上对性能有损伤,而不是提高性能:

  • 分配系统的内存给 Solr操作系统一般能够很好的利用系统的空闲内存,并缓存数据块用来提高 I/O 性能。
  • 大缓存只是垃圾的另一个名称:如果你把对象累积在缓存中,JVM 的垃圾收集器最终需要去清理它。大量的垃圾会增加垃圾收集器收集的时间,并损害系统的响应性能。

如果你有疑问的话,减少缓存而不是增加缓存,仅仅在你真正需要的时候才增加缓存。

衡量缓存的有效性

诀窍就是弄清楚缓存是否为你所用。最好的地方去找这些缓存信息就是在 Solrweb 界面,那里有到目前为止我们所讨论的缓存的详细信息。不同版本的 Solr 可能有所差异,但是在 Solr 4 中,你可以找到这些统计信息通过访问 Solr Url,然后通过点击 Core Selector[core name]Plugins / Stats

每组统计数据都有不同的指标,如下的几组指标能够决定缓存是否有效:

  • 累计命中率(cumulative_hitratio):有多大的查询比例命中了缓存(它的值在 0 ~ 1 之间,理想情况是 1
  • 累计插入次数(cumulative_inserts):在其生命周期内添加到缓存的条目数
  • 累计淘汰数(cumulative_evictions):在其生命周期内被移除出缓存的条目数

最根本衡量缓存性能的指标是它的命中率。你需要通过实验去找到最理想的缓存大小,关注缓存命中率确保你能把事情做好而不是更糟。 一些建议:

  • 相对于插入有大量的移除时,尝试增加缓存的大小并关注对命中率的影响。这可能是你的搜索请求太频繁导致的有些条目被频繁的移除
  • 如果缓存有高命中率但是移除率非常低时,那可能是缓存太大了。尝试减少缓存的太小,并关注命中率是否发生相应的变化。
  • 如果你某些缓存一直有很低的命中率,不要气馁。如果你的查询通常是不重复的,那么没有那么多缓存去让这个数字增加。因此你该选择小的缓存大小。
  • 不要因为命中率低,就有把缓存完全关掉的冲动。某些查询对单个查询还是有益的,因此小缓存还是值得尝试的。
  • 当改变缓存大小时,通过之前讨论的方法去大致估算下在最坏的情况下内存使用的大小,确保没有因为创建过大的内存而导致内存泄漏。

这不是一门精湛的科学,但是一些实验和对细节的关注能让性能得到全面的提升。