确定分片大小编辑

Elasticsearch 中的每个索引都分为一个或多个分片,每个分片都可以跨多个节点复制以防止硬件故障。如果您使用的是数据流,则每个数据流都由一系列索引支持。单个节点上可以存储的数据量有限,因此您可以通过添加节点并增加索引和分片的数量来提高集群的容量。但是,每个索引和分片都有一定的开销,如果您将数据分散到过多的分片中,则开销可能会变得非常大。索引或分片过多的集群被称为过度分片。过度分片的集群对搜索的响应效率较低,在极端情况下甚至可能变得不稳定。

创建分片策略编辑

防止过度分片和其他与分片相关的问题的最佳方法是创建分片策略。分片策略可帮助您确定和维护集群的最佳分片数量,同时限制这些分片的大小。

遗憾的是,没有一种通用的分片策略。在一个环境中有效的策略在另一个环境中可能无法扩展。良好的分片策略必须考虑您的基础架构、用例和性能预期。

创建分片策略的最佳方法是在生产硬件上使用您在生产中看到的相同查询和索引负载对生产数据进行基准测试。有关我们推荐的方法,请观看定量集群大小调整视频。在测试不同的分片配置时,请使用 Kibana 的Elasticsearch 监控工具来跟踪集群的稳定性和性能。

Elasticsearch 节点的性能通常受到底层存储性能的限制。查看我们关于优化存储以提高索引搜索速度的建议。

以下部分提供了一些在设计分片策略时应考虑的注意事项和准则。如果您的集群已经过度分片,请参阅减少集群的分片数量

大小调整注意事项编辑

构建分片策略时,请牢记以下几点。

搜索在每个分片上运行一个线程编辑

大多数搜索会命中多个分片。每个分片都在单个 CPU 线程上运行搜索。虽然一个分片可以运行多个并发搜索,但跨大量分片的搜索可能会耗尽节点的搜索线程池。这会导致吞吐量低和搜索速度慢。

每个索引、分片、段和字段都有一定的开销编辑

每个索引和每个分片都需要一些内存和 CPU 资源。在大多数情况下,一小组大型分片比许多小型分片使用的资源更少。

段在分片的资源使用中起着重要作用。大多数分片包含多个段,这些段存储其索引数据。Elasticsearch 将一些段元数据保存在堆内存中,以便可以快速检索以进行搜索。随着分片的增长,其段会合并为更少、更大的段。这会减少段的数量,这意味着堆内存中保留的元数据更少。

每个映射字段在内存使用和磁盘空间方面也有一定的开销。默认情况下,Elasticsearch 会为其索引的每个文档中的每个字段自动创建映射,但您可以关闭此行为以控制您的映射

此外,每个段的每个映射字段都需要少量堆内存。这种每个段每个字段的堆开销包括字段名称的副本,如果适用,则使用 ISO-8859-1 编码,否则使用 UTF-16 编码。通常,这并不明显,但如果您的分片具有很高的段计数,并且相应的映射包含很高的字段计数和/或非常长的字段名称,则您可能需要考虑此开销。

Elasticsearch 会自动平衡数据层内的分片编辑

集群的节点被分组到数据层中。在每个层内,Elasticsearch 尝试在尽可能多的节点上分布索引的分片。当您添加新节点或节点发生故障时,Elasticsearch 会自动在层的剩余节点之间重新平衡索引的分片。

最佳实践编辑

如果适用,请使用以下最佳实践作为分片策略的起点。

删除索引,而不是文档编辑

删除的文档不会立即从 Elasticsearch 的文件系统中删除。相反,Elasticsearch 会在每个相关分片上将文档标记为已删除。标记的文档将继续使用资源,直到在定期段合并期间将其删除。

如果可能,请删除整个索引。Elasticsearch 可以立即从文件系统中删除已删除的索引并释放资源。

对时序数据使用数据流和 ILM编辑

数据流允许您跨多个基于时间的支持索引存储时序数据。您可以使用索引生命周期管理 (ILM)来自动管理这些支持索引。

此设置的一个优点是自动滚动,当当前索引满足定义的 max_primary_shard_sizemax_agemax_docsmax_size 阈值时,它会创建一个新的写入索引。当不再需要某个索引时,您可以使用 ILM 自动删除它并释放资源。

ILM 还使您能够轻松地随着时间的推移更改分片策略

  • 想要减少新索引的分片数量?
    更改数据流的匹配索引模板中的index.number_of_shards设置。
  • 想要更大的分片或更少的后备索引?
    提高您的 ILM 策略的滚动阈值
  • 需要跨越更短时间间隔的索引?
    通过更快地删除旧索引来抵消增加的分片数量。您可以通过降低策略的删除阶段min_age 阈值来做到这一点。

每个新的后备索引都是进一步调整策略的机会。

目标是最多包含 2 亿个文档或大小在 10GB 到 50GB 之间的分片编辑

每个分片都有一定的开销,包括集群管理和搜索性能。搜索一千个 50MB 的分片比搜索包含相同数据的单个 50GB 分片的成本要高得多。但是,非常大的分片也会导致搜索速度变慢,并且在故障后需要更长时间才能恢复。

分片的物理大小没有硬性限制,理论上每个分片最多可以包含 20 多亿个文档。但是,经验表明,10GB 到 50GB 之间的分片通常适用于许多用例,只要每个分片的文档计数保持在 2 亿以下。

根据您的网络和用例,您可能可以使用更大的分片,而较小的分片可能适用于企业搜索和类似用例。

如果您使用 ILM,请将滚动操作max_primary_shard_size 阈值设置为 50gb,以避免分片大于 50GB。

要查看分片的当前大小,请使用cat shards API

response = client.cat.shards(
  v: true,
  h: 'index,prirep,shard,store',
  s: 'prirep,store',
  bytes: 'gb'
)
puts response
GET _cat/shards?v=true&h=index,prirep,shard,store&s=prirep,store&bytes=gb

pri.store.size 值显示索引的所有主分片的总大小。

index                                 prirep shard store
.ds-my-data-stream-2099.05.06-000001  p      0      50gb
...

可作为主节点的节点每 3000 个索引至少应有 1GB 堆内存编辑

主节点可以管理的索引数量与其堆大小成正比。每个索引所需的精确堆内存量取决于各种因素,例如映射的大小和每个索引的分片数量。

作为一般经验法则,主节点上每 GB 堆内存应少于 3000 个索引。例如,如果您的集群具有 4GB 堆内存的专用主节点,则您应该少于 12000 个索引。如果您的主节点不是专用主节点,则适用相同的规模指南:您应该在每个可作为主节点的节点上为集群中的每 3000 个索引至少保留 1GB 堆内存。

请注意,此规则定义了主节点可以管理的最大索引数,但不能保证涉及这么多索引的搜索或索引的性能。您还必须确保您的数据节点拥有足够的资源来处理您的工作负载,并且您的整体分片策略满足您的所有性能要求。另请参阅每个分片上的搜索都在单个线程上运行每个索引、分片、段和字段都有开销

要检查每个节点配置的堆大小,请使用cat nodes API

response = client.cat.nodes(
  v: true,
  h: 'heap.max'
)
puts response
GET _cat/nodes?v=true&h=heap.max

您可以使用cat shards API来检查每个节点的分片数。

response = client.cat.shards(
  v: true
)
puts response
GET _cat/shards?v=true

添加足够的节点以保持在集群分片限制内编辑

集群分片限制防止每个节点创建超过 1000 个非冻结分片,以及每个专用冻结节点创建 3000 个冻结分片。确保您的集群中每种类型的节点都足够多,以处理您需要的分片数量。

为字段映射器和开销留出足够的堆内存编辑

映射字段会在每个节点上消耗一些堆内存,并且在数据节点上需要额外的堆内存。确保每个节点都有足够的堆内存用于映射,并为与其工作负载相关的开销留出额外的空间。以下部分将展示如何确定这些堆内存需求。

集群状态中的映射元数据编辑

集群中的每个节点都有一个集群状态的副本。集群状态包含有关每个索引的字段映射的信息。此信息会产生堆内存开销。您可以使用集群统计信息 API来获取去重和压缩后所有映射的总大小的堆内存开销。

response = client.cluster.stats(
  human: true,
  filter_path: 'indices.mappings.total_deduplicated_mapping_size*'
)
puts response
GET _cluster/stats?human&filter_path=indices.mappings.total_deduplicated_mapping_size*

这将向您显示类似于以下示例输出的信息

{
  "indices": {
    "mappings": {
      "total_deduplicated_mapping_size": "1gb",
      "total_deduplicated_mapping_size_in_bytes": 1073741824
    }
  }
}
检索堆大小和字段映射器开销编辑

您可以使用节点统计信息 API来获取每个节点的两个相关指标

  • 每个节点上的堆大小。
  • 每个节点每个字段的任何其他估计的堆内存开销。这特定于数据节点,除了上面提到的集群状态字段信息之外,数据节点持有的索引的每个映射字段还有额外的堆内存开销。对于不是数据节点的节点,此字段可能为零。
response = client.nodes.stats(
  human: true,
  filter_path: 'nodes.*.name,nodes.*.indices.mappings.total_estimated_overhead*,nodes.*.jvm.mem.heap_max*'
)
puts response
GET _nodes/stats?human&filter_path=nodes.*.name,nodes.*.indices.mappings.total_estimated_overhead*,nodes.*.jvm.mem.heap_max*

对于每个节点,这将向您显示类似于以下示例输出的信息

{
  "nodes": {
    "USpTGYaBSIKbgSUJR2Z9lg": {
      "name": "node-0",
      "indices": {
        "mappings": {
          "total_estimated_overhead": "1gb",
          "total_estimated_overhead_in_bytes": 1073741824
        }
      },
      "jvm": {
        "mem": {
          "heap_max": "4gb",
          "heap_max_in_bytes": 4294967296
        }
      }
    }
  }
}
考虑额外的堆内存开销编辑

除了上述两个字段开销指标之外,您还必须为 Elasticsearch 的基线使用以及您的工作负载(例如索引、搜索和聚合)留出足够的堆内存。对于许多合理的工作负载,0.5GB 的额外堆内存就足够了,如果您的工作负载非常轻,您可能需要更少的内存,而繁重的工作负载可能需要更多内存。

示例编辑

例如,考虑上面数据节点的输出。节点的堆内存至少需要

  • 1 GB 用于集群状态字段信息。
  • 1 GB 用于数据节点字段的额外估计堆内存开销。
  • 0.5 GB 的额外堆内存用于其他开销。

由于在示例中,节点的堆内存最大大小为 4GB,因此足以满足 2.5GB 的总堆内存需求。

如果节点的堆内存最大大小不足,请考虑避免不必要的字段,或扩展集群,或重新分配索引分片。

请注意,上述规则不一定能保证涉及大量索引的搜索或索引的性能。您还必须确保您的数据节点拥有足够的资源来处理您的工作负载,并且您的整体分片策略满足您的所有性能要求。另请参阅每个分片上的搜索都在单个线程上运行每个索引、分片、段和字段都有开销

避免节点热点编辑

如果分配给特定节点的分片过多,则该节点可能会成为热点。例如,如果单个节点包含索引的过多分片,而该索引的索引量很大,则该节点可能会出现问题。

为防止出现热点,请使用index.routing.allocation.total_shards_per_node索引设置来明确限制单个节点上的分片数量。您可以使用更新索引设置 API来配置index.routing.allocation.total_shards_per_node

response = client.indices.put_settings(
  index: 'my-index-000001',
  body: {
    index: {
      'routing.allocation.total_shards_per_node' => 5
    }
  }
)
puts response
PUT my-index-000001/_settings
{
  "index" : {
    "routing.allocation.total_shards_per_node" : 5
  }
}

避免不必要的映射字段编辑

默认情况下,Elasticsearch 会自动为其索引的每个文档中的每个字段创建映射。每个映射字段都对应于磁盘上的一些数据结构,这些数据结构对于对该字段进行高效搜索、检索和聚合是必需的。每个映射字段的详细信息也保存在内存中。在许多情况下,这种开销是不必要的,因为在任何搜索或聚合中都不会使用某个字段。使用显式映射而不是动态映射来避免创建从未使用的字段。如果通常一起使用一组字段,请考虑使用copy_to在索引时合并它们。如果一个字段很少使用,那么最好将其设为运行时字段

您可以使用字段使用情况统计信息API 获取有关正在使用哪些字段的信息,并且可以使用分析索引磁盘使用情况API 分析映射字段的磁盘使用情况。但是请注意,不必要的映射字段也会占用一些内存开销以及它们的磁盘使用量。

减少集群的分片数量编辑

如果您的集群已经分片过多,您可以使用以下一种或多种方法来减少其分片数量。

创建涵盖更长时间段的索引编辑

如果您使用 ILM 并且您的保留策略允许,请避免对滚动操作使用max_age阈值。而是使用max_primary_shard_size来避免创建空索引或许多小分片。

如果您的保留策略需要max_age阈值,请增加它以创建涵盖更长时间间隔的索引。例如,您可以每周或每月创建一次索引,而不是每天创建一次索引。

删除空索引或不需要的索引编辑

如果您正在使用 ILM 并根据max_age阈值滚动索引,则可能会无意中创建没有文档的索引。这些空索引没有任何好处,但仍然会消耗资源。

您可以使用cat count API找到这些空索引。

response = client.cat.count(
  index: 'my-index-000001',
  v: true
)
puts response
GET _cat/count/my-index-000001?v=true

获得空索引列表后,您可以使用删除索引 API删除它们。您还可以删除任何其他不需要的索引。

response = client.indices.delete(
  index: 'my-index-000001'
)
puts response
DELETE my-index-000001

在非高峰时段强制合并编辑

如果您不再写入索引,则可以使用强制合并 API将较小的段合并为较大的段。这可以减少分片开销并提高搜索速度。但是,强制合并会占用大量资源。如果可能,请在非高峰时段运行强制合并。

response = client.indices.forcemerge(
  index: 'my-index-000001'
)
puts response
POST my-index-000001/_forcemerge

将现有索引缩减为更少的分片编辑

如果您不再写入索引,则可以使用缩减索引 API来减少其分片数量。

ILM 还有一个用于处于温阶段的索引的缩减操作

合并较小的索引编辑

您还可以使用重新索引 API将具有相似映射的索引合并为一个大索引。对于时间序列数据,您可以将短时间段的索引重新索引到涵盖更长时间段的新索引中。例如,您可以将 10 月份的每日索引(使用共享索引模式,例如my-index-2099.10.11)重新索引到每月my-index-2099.10索引中。重新索引后,删除较小的索引。

response = client.reindex(
  body: {
    source: {
      index: 'my-index-2099.10.*'
    },
    dest: {
      index: 'my-index-2099.10'
    }
  }
)
puts response
POST _reindex
{
  "source": {
    "index": "my-index-2099.10.*"
  },
  "dest": {
    "index": "my-index-2099.10"
  }
}

排查与分片相关的错误编辑

以下是解决常见的分片相关错误的方法。

此操作将添加 [x] 个总分片,但此集群当前最多打开了 [y]/[z] 个分片;编辑

cluster.max_shards_per_node集群设置限制了集群的最大打开分片数。此错误表示某个操作将超过此限制。

如果您确信您的更改不会破坏集群的稳定性,则可以使用集群更新设置 API临时增加限制并重试该操作。

response = client.cluster.put_settings(
  body: {
    persistent: {
      'cluster.max_shards_per_node' => 1200
    }
  }
)
puts response
PUT _cluster/settings
{
  "persistent" : {
    "cluster.max_shards_per_node": 1200
  }
}

这种增加应该是暂时的。作为长期解决方案,我们建议您向过度分片的 data tier 添加节点,或者 减少集群的分片数量。如需在进行更改后获取集群的当前分片数量,请使用 集群统计信息 API

response = client.cluster.stats(
  filter_path: 'indices.shards.total'
)
puts response
GET _cluster/stats?filter_path=indices.shards.total

制定长期解决方案后,我们建议您重置 cluster.max_shards_per_node 限制。

response = client.cluster.put_settings(
  body: {
    persistent: {
      'cluster.max_shards_per_node' => nil
    }
  }
)
puts response
PUT _cluster/settings
{
  "persistent" : {
    "cluster.max_shards_per_node": null
  }
}