关于缓存

在本章前面(过滤器的内部操作)中,我们已经简单介绍了过滤器是如何计算的。 其核心实际是采用一个 bitset 记录与过滤器匹配的文档。Elasticsearch 积极地把这些 bitset 缓存起来以备随后使用。一旦缓存成功,bitset 可以复用 任何 已使用过的相同过滤器,而无需再次计算整个过滤器。

这些 bitsets 缓存是“智能”的:它们以增量方式更新。当我们索引新文档时,只需将那些新文档加入已有 bitset,而不是对整个缓存一遍又一遍的重复计算。和系统其他部分一样,过滤器是实时的,我们无需担心缓存过期问题。

独立的过滤器缓存

属于一个查询组件的 bitsets 是独立于它所属搜索请求其他部分的。这就意味着,一旦被缓存,一个查询可以被用作多个搜索请求。bitsets 并不依赖于它所存在的查询上下文。这样使得缓存可以加速查询中经常使用的部分,从而降低较少、易变的部分所带来的消耗。

同样,如果单个请求重用相同的非评分查询,它缓存的 bitset 可以被单个搜索里的所有实例所重用。

让我们看看下面例子中的查询,它查找满足以下任意一个条件的电子邮件:

  • 在收件箱中,且没有被读过的
  • 不在 收件箱中,但被标注重要的
GET /inbox/emails/_search
{
  "query": {
      "constant_score": {
          "filter": {
              "bool": {
                 "should": [
                    { "bool": {
                          "must": [
                             { "term": { "folder": "inbox" }}, 
                             { "term": { "read": false }}
                          ]
                    }},
                    { "bool": {
                          "must_not": {
                             "term": { "folder": "inbox" } 
                          },
                          "must": {
                             "term": { "important": true }
                          }
                    }}
                 ]
              }
            }
        }
    }
}

两个过滤器是相同的,所以会使用同一 bitset 。

尽管其中一个收件箱的条件是 must 语句,另一个是 must_not 语句,但他们两者是完全相同的。这意味着在第一个语句执行后, bitset 就会被计算然后缓存起来供另一个使用。当再次执行这个查询时,收件箱的这个过滤器已经被缓存了,所以两个语句都会使用已缓存的 bitset 。

这点与查询表达式(query DSL)的可组合性结合得很好。它易被移动到表达式的任何地方,或者在同一查询中的多个位置复用。这不仅能方便开发者,而且对提升性能有直接的益处。

自动缓存行为

在 Elasticsearch 的较早版本中,默认的行为是缓存一切可以缓存的对象。这也通常意味着系统缓存 bitsets 太富侵略性,从而因为清理缓存带来性能压力。不仅如此,尽管很多过滤器都很容易被评价,但本质上是慢于缓存的(以及从缓存中复用)。缓存这些过滤器的意义不大,因为可以简单地再次执行过滤器。

检查一个倒排是非常快的,然后绝大多数查询组件却很少使用它。例如 term 过滤字段 "user_id" :如果有上百万的用户,每个具体的用户 ID 出现的概率都很小。那么为这个过滤器缓存 bitsets 就不是很合算,因为缓存的结果很可能在重用之前就被剔除了。

这种缓存的扰动对性能有着严重的影响。更严重的是,它让开发者难以区分有良好表现的缓存以及无用缓存。

为了解决问题,Elasticsearch 会基于使用频次自动缓存查询。如果一个非评分查询在最近的 256 词查询中被使用过(次数取决于查询类型),那么这个查询就会作为缓存的候选。但是,并不是所有的片段都能保证缓存 bitset 。只有那些文档数量超过 10,000 (或超过总文档数量的 3% )才会缓存 bitset 。因为小的片段可以很快的进行搜索和合并,这里缓存的意义不大。

一旦缓存了,非评分计算的 bitset 会一直驻留在缓存中直到它被剔除。剔除规则是基于 LRU 的:一旦缓存满了,最近最少使用的过滤器会被剔除。