乐趣区

给你总结几个ES下最容易踩的坑

我本人接触 Elasticsearch(一下简称 ES)有挺长一段时间了,本文结合自己的一些项目经验,给你总结几个实际项目中比较容易踩到的坑。希望读者能够避免犯这样的错误。

坑一,时区问题

在我们的项目中,索引下一般都会存在一个时间的字段,这个字段可以用来排序,或者做时间范围查询,或者聚合的场景等都会用到。

ES 底层默认采用 UTC 时间格式,而中国的时间 (CST) 是

CST=UTC+8

所以实际的项目中经常遇到查询结果和自己期望的不一致。关于时区的问题以及如何解决,我之前专门写了一篇文章,感兴趣的可以看看:

ES 系列之一文带你避开日期类型存在的坑

坑二,使用默认的 mappings

ES 本身支持我们在写入一个索引的时候,可以不为该索引设置任何的 mappings。这种情况下,ES 会为索引根据写入的字段值,” 推断 ” 该字段的类型。

看起来似乎不错,但是根据我的经验,还是建议应该明确的为自己的索引定义 mappings。

因为有时候 ES “ 推断 ” 出来的结果并不一定是我们想要的,然后给我们带来一些头疼的问题。

我还是给你举个例子:

写入一个名为 message 的索引,

PUT /message/_doc/1
{
  "head":   "message001",
  "body": "2020-05-30"
}

然后继续再插入一条,

PUT /message/_doc/2
{
  "head":   "message002",
  "body": "this is an emergency"
}

这里就报错了,错误的内容是提示我们,body 这个字段无法被解析。产生这个问题的原因是当我们写入第一条文档的时候,ES “ 擅自做主 ” 把 body 这个字段标记成日期类型了,然后写入文档 2 的时候不是日期字符串,所以无法解析。

可以用下面的命令看下索引的 mapping,证实我们的猜想:

GET message/_mapping

在这个示例中,我们如果明确的定义 body 为 text 类型就不会有这样的问题了。

坑三,没有规划好分片

我们一般都需要为索引设置分片的数量,具体设置成多少需要根据你的项目实际情况来定。

一般来说,单个 Shard 的建议最大大小是 20G 左右,最大不要超过 50G。

单个 shard 过大,或者 shard 过小导致 shard 数量太多,都会影响查询的效率。

举个例子,比如你预估索引的大小是 100G,这个时候分片是 3~5 比较好。

如果你的索引是每天增量比较大的场景,比如日志类,订单类的索引,可能你首先要把根据日期来新建不同的索引,根据时间的数据规模选择按天,周,甚至月来建索引。然后这些索引使用相同的分片设置。

坑四,过多依赖 ES 聚合的结果

ES 某些场景下的聚合结果是不准确的,计算的结果只是告诉你一个大概的分布情况,并不是精确的。

如果你不了解这个情况,可能会在实际的项目中犯错误。

我曾经写过一篇文章,对这个坑有过详细的分析以及闭坑指南,有兴趣可以看看这篇文章:

ES 系列之原来 ES 的聚合统计不准确啊

坑五,分桶聚合查询的内存爆炸

在分桶聚合的场景下,大多数时候对单个字段的聚合查询非常快的,如果是多个字段嵌套聚合。有可能撑爆内存,引发 OOM。看下面一个例子。

假设我们有个很多电影数据的索引,有个字段是数组,保存演员的名字。

{
  "actors" : [
    "Fred Jones",
    "Mary Jane",
    "Elizabeth Worthing"
  ]
}

然后,我们希望查询出演影片最多的 10 个演员,以及他们合作最多的 5 位演员,可以使用下面这个聚合,

{
  "aggs" : {
    "actors" : {
      "terms" : {
         "field" : "actors",
         "size" :  10
      },
      "aggs" : {
        "costars" : {
          "terms" : {
            "field" : "actors",
            "size" :  5
          }
        }
      }
    }
  }
}

结果返回前 10 位演员,以及与他们合作最多的 5 位演员。但是就是这样一个简单的查询,可能导致 OOM。

我们可以想象下在内存中构建一个树来表示这个 嵌套 terms 分桶聚合。首先 actors 聚合会构建树的第一层,每个演员都有一个桶。

然后,第一层的每个节点之下,costar 聚合会构建第二层,每个联合演员一个桶,如果你学过排列组合,应该知道这实际上要构建 n 的平方个分桶。(n 是每个影片中演员的数量)

假如平均每部影片(文档)有 10 名演员,每部影片就会生成

10^2

100 个桶。如果总共有 20000 部影片,粗率计算就会生成 2000000 个桶。这个引起内存爆炸就不奇怪了。

上面产生问题的根源在于 ES 对于这种嵌套聚合默认使用了 深度优先 规则,即先构建完整的树,再筛选符合条件的结果。

ES 允许我们使用一种 广度优先 的模式来进行这种场景的聚合,这种策略的工作方式有些不同,它先执行第一层聚合,再继续下一层聚合之前会先做修剪。

要使用广度优先,只需简单 的通过参数 collect 开启,

{
  "aggs" : {
    "actors" : {
      "terms" : {
         "field" :        "actors",
         "size" :         10,
         "collect_mode" : "breadth_first" 
      },
      "aggs" : {
        "costars" : {
          "terms" : {
            "field" : "actors",
            "size" :  5
          }
        }
      }
    }
  }
}

广度优先仅仅适用于每个组的聚合数量远远小于当前总组数的情况下,因为广度优先会在内存中缓存裁剪后的仅仅需要缓存的每个组的所有数据,以便于它的子聚合分组查询可以复用上级聚合的数据。

坑六,mapping 包含的字段过多

我们给索引建模时,要尽量避免 mapping 中的包含的字段过多。

过多的字段一个是难以维护,当存在成千上百个字段时,很难有人真正明确每个字段的含义。另外一个导致的问题是,当我们需要更新文档的时候,ES 会在不同的节点同步这些更新。过多的字段意味着更新变慢。

如果我们的业务场景确实需要很多字段,应该充分利用 ES 的 dynamic templates 机制,提前定义好字段映射的规则,这样一些字段就没有必要在 mapping 里定义好。

不过无论如何,都应该尽量保持你的 mapping 字段足够小。

总结

Elasticsearch 是一个分布式可扩展的实时搜索和分析引擎。这样的神器如果用好了让你的工作事半功倍,但是如果没用好可能又会给你带来不少的困扰。

先写这么多吧,后续如果工作中踩到新的坑再跟大家分享。


参考:

https://www.elastic.co/guide/…

退出移动版