Elasticsearch 避坑指南及面试常见问题

引言

本文主要讲述在生产环境中使用 Elasticserch 遇到的一些坑,以及如何避开这些坑。对于正在使用或即将使用 Elasticsearch 的用户来说可能可做参考,以下内容都是基于我个人的实践经验总结,如有任何错误或不足之处,请大家指正。

本文不做任何 Elasticsearch 概念和基础普及,需要读者对 Elasticsearch 的概念和名词有基础的认识,每个问题都会有最佳实践板块,方便快速查看。大家也可先收藏,等遇到问题后再来查看。

常见坑

  • Java 相关
  • 分布式相关
  • 集群健康度
  • 节点诊断
  • Shard 相关
  • 深度分页

Java 相关

由于 Elasticsearch 是 Java 开发,Java 相关的问题如 JVM 等都是不可避免的,这里先说说所有人肯定会遇到的,就是 Java 的虚拟机内存问题。

Elasticsearch 使用 ES_JAVA_OPTS 环境变量来配置 JVM。其中比较常用的配置是:

  • Xms:最小堆内存
  • Xmx:最大堆内存

这两个值建议设置成一样的,不超过物理机或虚拟机的 50 %,且不超过 32G。设置一样的是减轻堆伸缩带来堆压力。不超过 50 % 是因为必须要预留足够的内存给操作系统及其他进程,同时 JVM 本身和 Elasticsearch 的部分功能也需要。不超过 32G 是因为超过 32G JVM 会启用压缩普通对象指针(compressed object pointers)(compressed oops),部分操作系统可能在 26G 以上就会开启零基压缩(zero-based compressed oops)。

最佳实践

Elasticsearch 启动环境变量设置:ES_JAVA_OPTS="-Xms2g -Xmx2g",替换 2g 为使用的物理机或虚拟机内存的一半(最低 1G,最多 32G)。


分布式相关

分布式最大的问题就是脑裂,脑裂问题简单来说就是没人做主。比如我们多个人一起讨论问题可以使用少数服从多数来表决,但是如果是两个人,那互不相让就没法决定了。分布式系统也是这样,当集群有两个节点时,如果因为网络问题导致两个节点失联,那么它们都会认为是对方的问题(挂了),认为自己没问题,自己应该变成主节点,所以集群就变成两个主节点。

image

如果这时候网络恢复正常,而它们的数据不一致,就会变成互不相让的局面。

image

而如果我们的集群有三个节点(A,B,C),如果 A 和 B,C 失联,那么 B 和 C 会发现它们都连不上 A,就会标记 A 为失联,而它们可以选举新的主节点。所以分布式系统一般使用 2n + 1 (n > 0)个节点,生产系统中最少为 3 个节点。

最佳实践

分布式部署节点数量为奇数(3,5,7 ...),且在不同的网络拓扑上(例如公有云的不同区域或可用区,机房的不同机架),同时配置弹性伸缩,在节点出现问题时自动调节节点数量。这里的节点仅指可成为主节点的节点(配置 master: true),其他节点不影响。


集群健康度

Elasticsearch 的集群健康度是一个非常重要的监控指标,是 Elasticsearch 暴露的一个整体指标,如果你只能监控一个指标的话,那就是它了。健康度按层级分为分片健康度、索引健康度和集群健康度,指标分为绿、黄、红三个等级。

分片健康度

  • 红(red):至少一个主分片没有被分配
  • 黄(yellow):至少一个副本没有被分配
  • 绿(green):主副分片都正常分配

索引健康度

索引健康度就是此索引所有分片中最差的健康度,即只要有分片是红,索引就是红,只有分片都是绿,索引才是绿。

集群健康度

集群健康度是此集群上所有索引中最差度健康度,即只要有索引是红,集群就是红,只有索引都是绿,集群才是绿。

健康度相关 API

Elasticserch 提供了一系列的 API 供我们获取健康度。

GET /_cluster/health 获取集群的健康状态【文档

GET /_cluster/health?level=indices 获取所有索引的健康状态

GET /_cluster/health/<index> 获取单个索引的健康状态

GET /_cluster/allocation/explain 返回第一个未分配分片的原因【文档

集群不健康排查流程

Elasticsearch 集群出现红或者黄等不健康状态是很常见等问题,当集群出现不健康状态时(红或者黄),我们需要一步步找到问题的原因再修复。

首先,定位问题,可以通过上述的 API 查看不健康状态的原因。 比较常见的问题原因主要有:

  • 节点离线导致的分片无法分配
  • 索引配置、分片规则等问题导致的分片无法分配
  • 磁盘空间不足

在定位问题原因之后,我们就可以根据情况确定解决方案。在官网的这个页面https://www.elastic.co/guide/en/elasticsearch/reference/current/cat-shards.html)可以看到 Elasticsearch 分片无法分配的错误,下面我们来讲讲一些错误的解决办法。

  • 节点离线后,由于主副分片不能位于同一节点上,可能导致分片分配失败,解决办法就是增加节点,一般对于生产集群,我们都会根据情况保留一定的节点数量,在节点离线时及时启动新的节点加入集群。
  • 对于索引配置,分配规则等问题导致的无法分配,如没有满足规则的节点,只有一个测试节点,但是默认配置来一个主副分片等。我们就要根据实际情况来修改配置或规则,对于测试环境,删除重建是最快的方法,而对于生产环境,就需要结合实际情况来考虑了。
  • 对于磁盘空间不足的问题,需要扩充磁盘或者将索引迁移到其他节点上。
  • DANGLING_INDEX_IMPORTED:当集群中的某个节点离线后进行了索引的删除,然后这个节点又回到集群,就会发生这个错误,主要是索引的数据不一致导致的,可以重新在删除已被删除的索引即可。
  • EXISTING_INDEX_RESTORED:当集群中的某个索引被关闭(关闭不等于删除),又恢复到这个索引就会引发这个问题,需要把索引删除后再恢复。

同时,需要注意的是,在分片重新分配的过程中,会出现短暂的不健康状态,在监控时需要设置合理的告警阈值,避免过于敏感。


节点诊断

节点是从集群的物理机或者虚拟机层面来看,虽然 Elasticsearch 认为节点都是不可靠的,当节点出现问题后,我们会用一些方法保证集群的可靠。但是我们不可能在节点出现问题后,只是简单粗暴地更换节点,还是需要一些方法来确保节点基本没有问题。

节点诊断相关 API

GET /_cat/nodes?v 查看节点到基本信息即负载情况【文档

GET /_nodes/stats/indices 查看节点的索引详情【文档

POST /_cache/clear 清除节点缓存【文档

节点内存问题

Elasticsearch 集群节点最有可能发生的就是内存问题,Java 的长时间 GC 等可能导致的集群变慢,产生 OOM(Out Of Memory),甚至是节点离线。下面我们来讲讲这些问题的产生原因及解决办法。

首先,我们也需要通过上述的 API 及节点的监控找到问题的原因,比较常见的内存问题主要有:

缓存占用过多内存,如 Segment 占用过多内存可能是由于频繁写入导致产生了很多零散的 Segment,可以使用 Force Merge API 将它们合并为一个。如果使用了 FieldData 且没有限制 FieldData 缓存的上限,可能因为消耗多大导致频繁 GC。FieldData 是针对 Text 类型做排序和聚合使用的(像 Keyword 一样),一般不推荐使用(https://www.elastic.co/guide/en/elasticsearch/reference/7.5/modules-fielddata.html)。

大量复杂的嵌套聚合也可能引发频繁 GC,因为嵌套聚合会在内存中生成大量的 Bucket 对象,生产环境中应该尽可能避免复杂的嵌套聚合查询。

如果无法完全限制请求的查询,Elasticsearch 提供了断路器的功能,用户可以根据需求配置相应规则,只要满足规则,请求就会熔断,从而保护集群,避免生产 OOM 问题。配置也很简单,这里就不展开,详细可以查看官方文档https://www.elastic.co/guide/en/elasticsearch/reference/7.5/circuit-breaker.html)。

最佳实践

对于频繁写入的索引,需要定期监控节点内存,必要时使用 Force Merge。 对于复杂查询与聚合,要在业务端控制查询请求,如无法限制,则可使用断路器。断路器只是最后对集群的保护,如果用户的查询请求总是消耗过多的资源而被断路器中断,用户可能频繁尝试且获取不到结果,对用户体验不好,后台压力也不小,还是需要业务端及时调整和控制。


Shard 相关

Elasticsearch 把一个索引切分成多个 Shard 来存储,将一份大的数据切分成多个部分来存储可以提高查询效率,也便于将数据分布式存储。不过每个索引的 Shard 数量是固定的,必须在创建时指定,后期修改必须要重建索引。分片的数量和大小都需要根据实际情况调整,一个分片实际上是一个 Lucene 索引,过多的分片会导致额外的性能开销,同时过多的分片也会导致聚合不准的问题(https://www.elastic.co/guide/en/elasticsearch/reference/7.5/search-aggregations-bucket-terms-aggregation.html)。一般来说,单个 Shard 的建议最大大小是 20G 到 50G,对于普通搜索类数据,最好控制在 20G,而对于时间序列类数据(如日志)最好控制在 50G。如果每天的日志量无法预估,可以使用 Rollover API 进行自动的索引生命周期管理(https://www.elastic.co/guide/en/elasticsearch/reference/7.5/using-policies-rollover.html)。而单个节点的总数据量最好在 2 TB 以内。

因为 Shard 直接关乎集群健康度,所以对于生产集群来说,这个设置尤为重要,我们也不希望因为设置的问题导致集群变黄或红。

Shard 操作相关 API

GET /_cat/shards/<index> 查看 Shard 信息【文档

POST /<alias>/_rollover/<target-index> 当索引满足某些条件时(如数据量太大)自动切到新的索引,非常适合无法预估大小的时间序列类索引【文档

POST /<index>/_forcemerge 强制合并索引数据【文档】

POST /<index>/_shrink/<target-index> 新建索引并减少主分片数量【文档

POST /<index>/_split/<target-index> 新建索引并增加主分片数量【文档

POST /_reindex 重建索引【文档

这些 API 的使用需要有很多限制条件,具体大家可以查看各个 API 的文档,大家可以提前了解下避免出现使用时用不了的尴尬。

最佳实践

分片的数量和大小都需要根据实际情况调整,一般来说,单个 Shard 的建议最大大小是 20G 到 50G,对于普通搜索类数据,最好控制在 20G,而对于时间序列类数据(如日志)最好控制在 50G。如果每天的日志量无法预估,可以使用 Rollover API 进行自动的索引生命周期管理。而单个节点的总数据量最好在 2 TB 以内。


深度分页

Elasticsearch 有三种分页方式。

from + size 参数

from 定义数据偏移量,size 定义获取数据量,from 和 size 的方式类似 SQL 的 offset 和 limit,很好理解,但是在分页量大时(深度分页)效率很差。因为这会从每个分片获取适量的数据,在查询节点上缓存数据。例如 from=1000,size=10,每个分片会排序上报 1001 * 10 条数据,查询节点会归并排序所有结果,并返回第 1001 到 1010 条数据。由于这个原因,当取的页码很大时,缓存节点的效率会很差,所以 Elasticsearch 会限制最大的排序数据(配置 index.max_result_window,默认是 10000)。这种方式适合测试与小范围的分页。

from & size 分页

优点:简单好用

缺点:不适用深度分页

总结:适用于简单分页场景,不适用于大量数据的遍历

Scroll API

Scroll 类似 SQL 数据库的服务端游标,通过查询时指定 ?scroll=<时间> 参数使用,返回的结果会带上 scroll_id,下次获取时只需要用 scroll_id 查询即可,同时延长缓存时间。

POST /<index>/_search?scroll=1m
{
    "size": 100,
    "query": {
        "match" : {
            "title" : "elasticsearch"
        }
    }
}

POST /_search/scroll 
{
    "scroll" : "1m", 
    "scroll_id" : "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAD4WYm9laVYtZndUQlNsdDcwakFMNjU1QQ==" 
}

其实这就在第一次查询后建立一个查询快照(Search Context),接下来就通过 scroll_id 获取之前的查询快照,而无需再次查询。一看这个原理我们也可以知道,每个快照必须占用一定的内存,且快照创建后无法感知新的数据变更。同时,由于 Elasticsearch 在数据合并时会删除小的 segments,创建大的 segments,而 Scroll 的快照如果在使用这些旧的 segments,它们会避免被删除,这也会造成资源耗费。

Scroll API

优点:适合深度分页,在一定时间内缓存结果,效率高

缺点:只能顺序单向翻页,超过缓存时间后失效,如果开启了多个翻页会耗费较多内存,需要主动关闭,快照创建后无法感知变更

总结:不适用于用户深度分页,适合批量数据遍历与处理

Search After API

Search After API 用前一次的结果作为下一次的查询条件,在查询体中使用 search_after 参数,同时 from 参数必须是 0 或者 -1。这种方式必须提供 sort 排序且排序唯一(如排序最后加入 _id),如 size=10,那么每个分片只要根据排序规则获取 10 条数据返回给查询节点处理即可。和 Scroll API 一样可以适用于深度分页,但 Search after 方式是无状态的,无需保存查询快照。

GET <index>/_search
{
    "size": 10,
    "query": {
        "match" : {
            "title" : "elasticsearch"
        }
    },
    "search_after": [1463538857, "654323"],
    "sort": [
        {"date": "asc"},
        {"_id": "asc"}
    ]
}

Search After API​

优点:适用于深度分页,且分页无状态

缺点:只能顺序单向翻页,且需要唯一的 sort 排序

总结:适用于用户的单向深度分页

最佳实践

对于普通用户查询场景,使用 from + size 方式,对于用户顺序遍历(载入更多数据,无页码),使用 Search After API,对于内部数据处理或导出,使用 Scroll API


实用利器

最后分享一些我经常使用的工具,用好了绝对事半功倍。

Kibana:这个相信大家都用过,Elasticsearch 最好的 UI 和可视化,功能强大,开发测试使用的 Dev Tools 尤其好用。

Cerebro:这个是 Elasticsearch 集群的可视化监控工具,优点是将集群健康状态,分片分布,节点状态等可视化展示,便于操作。缺点是没有认证管理,生产环境如果没有其他访问控制手段很危险。下图盗自百度。

image

Elasticsearch Curator:Elastic 官方出品的命令行运维管理工具,主要是将一些常用的管理 API 包装成命令,便于使用和自动化运维,如定期删除过期的日志索引等(需要结合定时器触发)。

这些工具的使用这里就不展开了,如果大家想了解的话就评论告诉我,我再具体展开。

先写这些,以后再补充...

参考

推荐阅读更多精彩内容