在生产环境运行Elasticsearch深度指南

在本文,我不是来告诉你 Elasticsearch 强大,快速并且几乎可以正常运行。

在本文,我也不是来告诉你 Elasticsearch 可能是不透明的,让人困扰,并且似乎无缘无故地出问题。

在本文,我想分享我的经验和技巧,了解如何正确配置 Elasticsearch 并避免常见的陷阱。

我写文章目的也不是为了赢利,所以我会将全部内容放到这一篇文章中,而不是将它分解成一系列。你可以随意跳过不感兴趣的章节。

基础知识:集群,节点,索引和分片

如果你是 Elasticsearch(ES)新手,我想先解释一些基本概念。本节完全不涉及最佳实践,主要侧重于解释术语。大多数人可以直接跳过本节。

Elasticsearch 是用于运行 Apache Lucene(基于 Java 的搜索引擎)分布式管理框架。Lucene 是实际保存数据并进行索引和搜索的地方。ES 位于它之上,让你可以并行运行数千个 Lucene 实例。

ES 的最高级别单元是集群(cluster)。集群是 ES 节点 和索引的集合。

节点 (Node) 是 ES 的实例。它可以是单个服务器,也可以是服务器上运行的 一个 ES 进程。服务器和节点不同,一个 VM 或物理服务器可以包含许多 ES 进程,每个 ES 进程是一个节点。节点只可以加入一个集群。节点有不同类型(type),其中最值得关注的两个类型是数据节点(data node)主候选节点( Master-Eligible node)。一个节点可以同时具有多种类型。数据节点运行所有数据操作,即存储、索引和检索数据。主候选节点具有投票 master 的权限,用于管理集群和索引。

索引(Index)是对数据的高级抽象,索引本身不保存数据,它们只是实际存储数据的另一种抽象。对数据执行的任何操作(例如插入,删除,建立索引和搜索)都会对索引产生影响。索引可以完全属于一个集群,并且由分片( shard) 组成。

分片(Shard)是 Apache Lucene 的实例。一个分片可以容纳许多文档。分片是数据存储,索引和搜索的实际对象。分片只属于一个节点和索引。分片有两种类型:primary 和 replica,它们基本上是完全相同的,拥有相同的数据,并且搜索并行运行在所有分片。在拥有相同数据的所有分片中,其中一个属于 primary。这是唯一可以接受索引请求的分片。如果节点中的主分片挂了,副本将接管并成为主分片。然后,ES 将创建一个新的副本并复制数据。

总结一下,我们整理得到下图:

更深入了解 Elasticsearch

如果你想运行一个系统,相信你需要了解该系统。在本节中,我将解释 Elasticsearch 的各个部分,如果想在生产中进行管理,我相信你需要理解它。本节也不牵涉到具体建议,后文会介绍。本节目的只是为了介绍必要的背景。

Quorum

理解 Elasticsearch 是一个(有缺陷的)选举体系非常重要。节点投票决定谁应该管理它们,即主节点。主节点运行大量集群管理进程,并且在许多事务方面拥有最终决策权。ES 选举是有缺陷的,是因为只有一小部分节点,即主候选(master-eligible)节点才具有投票权。主候选节点是通过以下配置启用:

node.master: true

在集群启动或主节点离开群集时,所有符合主选举条件的节点都会开始选举新的主节点。为此,你需要具有 2n + 1 个主候选节点。否则,可能会出现脑裂情况,比如同时两个节点获得 50% 的选票,将会导致两个分区之一中的所有数据丢失。为了不发生这种情况。你需要 2n + 1 个符合主候选的节点。

节点如何加入集群

当 ES 节点启动时,它在广阔世界中独自存在。它怎么知道它属于哪个集群?有不同的方法可以完成此操作,如今大多使用种子主机(Seed Host)的方法来实现。

基本上,Elasticsearch 节点会不断地就他们所见过的所有其他节点进行通讯。因此一个节点最初只需要了解几个其他节点即可了解整个集群。让我们来看一个三节点集群的示例:

初始状态

最初,节点 A 和 C 只知道 B。B 是种子主机。种子主机要么以配置文件的形式提供给 ES,要么直接放入 elasticsearch.yml 中。

节点 A 与 B 连接并交换信息

一旦节点 A 连接到 B,B 就知道 A 的存在。对于 A 而言,没有任何变化。

节点 C 连接并与 B 共享信息

现在,C 连上来。一旦发生这种情况,B 就会告诉 C 有 A 的存在。C 和 B 现在知道群集中的所有节点。一旦 A 重新连接到 B,它也将了解 C 的存在。

段和段合并

上面我说过数据存储分片中,这只是部分正确。最终数据是以文件的形式存储在文件系统中。在 Lucene 和 Elasticsearch 中,这些文件称为段(Segment)。一个分片将具有一到数千个段。

同样,段是实际的真实文件,你可以在 Elasticsearch 安装的 data 目录中查看它。这意味着使用段存在开销。如果要查看段,则必须找到文件并打开它。这意味着需要打开许多文件,并且将会有很多开销。Lucene 中的段是不可变的,这是个问题。它们只能写一次,然后就不能改了。反过来,这意味着你放入 ES 中的每个文档都将创建一个仅包含这个文档的段。显然,一个拥有十亿个文档的集群具有十亿个段,这意味着文件系统上确实有十亿个文件,这样理解对吗?非也。

在后台,Lucene 进行持续的段合并,它不能更改段,但是可以使用两个较小段的数据合并创建新的段。

这样,lucene 会不断尝试使段数(文件数,即开销)保持较小。你也可以使用强制合并。

消息路由

在 Elasticsearch 中,你可以对集群中的任何节点运行任何命令,返回结果是相同的。有趣的是,文档最终将只存在于一个主分片及其副本中,而 ES 不知道它在哪里。没有一个映射来记录某个文档位于哪个分片中。

当执行搜索时,获取请求的 ES 节点会将其广播到索引中的所有分片。即主分片及所有副本。这些分片然后会在包含该文档的所有段中进行查找。

当执行插入时,ES 节点将随机选择一个主分片并将文档放在其中。然后将其写入该主分片及其所有副本。

如何在生产环境运行 Elasticsearch?

本节是实践部分。我前面提到,我管理 ES 的主要目的是为了记录日志,本文将尽力避免这种倾向的影响,但有可能会失败。

大小

需要提出并随后回答自己的第一个问题是关于大小调整。你需要多少规模的 ES 集群?

内存

我首先说的是 RAM,因为 RAM 将限制所有其他资源。

ES 用 Java 编写,Java 使用堆,你可以将其视为 Java 保留的内存。如果将所有堆的重要因素都列出,会使这个文档的大小增加三倍,所以我将介绍最重要的部分,即堆大小。


尽量使用更多内存,但堆大小不得超过 30G。

有一个很多人都不知道的关于堆的秘密:堆中的每个对象都需要一个唯一的地址,即一个对象指针。该地址的长度是固定的,这意味着可以寻址的对象数量是有限的。简单一点来描述就是,超出某个范围时,Java 将开始使用压缩的对象指针而不是未压缩的对象指针。这意味着每个内存访问都将涉及其他步骤,这会严重拖慢速度。因此你 100% 不需要设置超过此阈值(大约 32G)。

我曾经整整一个星期都呆在一个黑暗的房间里,没做别的,只是使用 esrally 基准化测试 Elasticsearch 在不同文件系统、堆大小、文件和 BIOS 设置组合。长话短说,下面就是关于堆大小如何设置的内容:

添加索引延迟,越低越好

命名约定为 fs_heapsize_biosflags。如你所见,从 32G 的堆大小开始,性能突然开始变差。吞吐量也同样情况:

索引附加中值吞吐量。越高越好。

长话短说:如果想幸运一点,请使用 29G 或 30G 的 RAM,并使用 XFS,并尽可能启用 hardwareprefetch 和 llc-prefetch。

文件缓存

大多数人在 Linux 上运行 Elasticsearch,Linux 使用内存作为文件系统缓存。常见的建议是 ES 服务器使用 64G 内存,这样的想法是一半用于缓存,一半用于堆。我尚未测试过文件缓存。但是不难看出,大型 ES 集群(如用于日志记录)可以从配置大文件缓存中受益匪浅。如果你所有的索引都适合放入内存堆,则不会那么多好处。

CPU

这取决于对集群执行的操作。如果进行大量索引,与仅执行日志记录相比,你需要更多更快的 CPU。对于日志记录,我发现 8 个 CPU 核绰绰有余,但是发现很多人使用更大的配置,但是对他的使用场景并没有什么好处。

磁盘

这块也没有想像那么直接。首先,如果索引能放入 RAM,则磁盘仅在节点冷启动时才重要。其次,实际可以存储的数据量取决于索引布局。每个分片都是一个 Lucene 实例,它们都有内存需求。这意味着你可以在堆中容纳最大数量分片是有限的。我将在索引布局部分中详细讨论这一点。

通常,你可以将所有数据磁盘放入 RAID0。你需要在 Elasticsearch 级别进行复制,因此丢失一个节点无关紧要。请勿将 LVM 与多个磁盘一起使用,因为 LVM 一次只能写入一个磁盘,根本就不会带来多个磁盘的好处。

关于文件系统和 RAID 设置,我整理了以下几点:

  • Scheduler:cfq 和 deadline 优于 noop。如果你有 nvme,Kyber 可能会很好,但我还没有测试过
  • QueueDepth:尽可能高
  • 预读:请打开
  • Raid chunk size:无影响
  • FS 块大小:无影响
  • FS 类型:XFS > ext4

索引布局

这在很大程度上取决于你的用例。我只能从日志场景(尤其是使用 Graylog)讨论一下。

分片

精简版:

  • 对于写入繁重的工作负载,主分片 = 节点数
  • 对于读取繁重的工作负载,主分片 * 副本数 = 节点数
  • 更多副本 = 更高的搜索性能

可以通过以下公式给出最大写入性能:

node_throughput * number_of_primary_shards
节点吞吐量 * 主分片数量

原因很简单:如果只有一个主分片,那么写入速度只类似于单节点,因为一个分片只能位于一个节点上。如果确实想优化写入性能,则应确保每个节点上只有一个分片(主节点或副本),因为在此情况下副本可以获得与主节点相同的写入速度,并且写入很大程度上取决于磁盘 IO。注意:如果有很多索引,那么上述的策略可能有问题,性能瓶颈可能是其他原因。

如果要优化搜索性能,可以通过以下公式给出:

node_throughput * (number_of_primary_shards + number_of_replicas)
节点吞吐量 *(主分片数量 + 副本数)

对于搜索,主分片和副本基本相同。因此,如果想提高搜索性能,只需增加副本的数量。

大小

关于索引大小,我已经多次讨论过。以下是我的经验:

30G of heap = 140 shards maximum per node
30G 堆内存 = 可以在一个节点最多启动 140 个分片

使用 140 个以上的分片,Elasticsearch 就会进程崩溃并出现内存不足错误。这是因为每个分片都是 Lucene 实例,并且每个实例都需要一定数量的内存。这意味着每个节点可以拥有的分片数量是有限制的。

如果你有大量节点,分片和索引大小,则可以容纳多少个索引可以由以下公式计算:

number_of_indices = (140 * number_of_nodes) / (number_of_primary_shards * replication_factor)
索引数 = (140*节点数)/(主分片数 * 复制因子)

根据磁盘大小,可以很容易地计算出索引的大小:

index_size = (number_of_nodes * disk_size) / number_of_indices
索引大小 = (节点数量 * 磁盘大小)/ 索引数量

然而索引越大搜索越慢。对于日志记录来说,慢一点问题不大,但是对于真正的搜索量大的应用程序,应该根据 RAM 大小来调整索引大小。

段合并

每个段都是文件系统中的一个实际文件,更多的段意味着更大的读取开销。基本上,对于每个搜索查询,它都会转到索引中的所有分片,再转到分片中的所有段。多个段极大地增加集群读取 IOPS,直到它变得不可用。因此,需要尽可能减少段的数量。

force_merge API 允许你将段合并到某个数量,比如 1。如果你执行索引滚动(例如使用 Elasticsearch 做日志记录),那么在集群未使用时执行常规强制合并是一个好建议。强制合并会占用大量资源,并且会显著降低集群的运行速度。因此,最好不要让 Graylog 帮你来做,而是选择在较少使用集群时间自己完成。如果你有很多索引的话,你必须定期执行段合并。否则,集群运行速度将会非常慢并最终挂掉。

集群布局

对于除最小设置外的所有设置,最好使用专用的候选主节点。主要原因是确保始终有 2n + 1 个候选主节点来确保仲裁。但是对于数据节点,可以在任何时候添加新节点,而不必担心这个需求。另外,我们不希望数据节点上的高负载影响主节点。

最后,主节点是种子节点的理想候选节点。请记住,种子节点是在 Elasticsearch 中进行节点发现的最简单方法。由于主节点很少更改,因此它是最佳选择,因为它很可能已经知道集群中的所有其他节点。

主节点可以非常小,一个 CPU 核,4G 的 RAM 已经足够大多数集群使用。当然也需要关注实际使用情况,并进行相应调整。

监控

ES 为你提供了大量的指标,并且以 JSON 的形式提供所有指标,这使得传递给监控工具非常容易。以下是一些有用的监控指标:

  • 段数
  • 堆使用
  • 堆 GC 时间
  • 平均 搜索,索引,合并时间
  • IOPS
  • 磁盘利用率

结论

本文花了我大约 5 个小时,包含了我对 ES 的所有了解,希望能让你碰到问题的时候不那么头疼。

资源

原文链接:

facinating.tech/2020/02

发布于 02-27

文章被以下专栏收录