分片大小

编辑

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 亿个文档。但是,经验表明,只要每个分片的文档计数保持在 2 亿以下,10GB 到 50GB 之间的分片通常适用于许多用例。

您可以使用更大的分片,具体取决于您的网络和用例,较小的分片可能适用于企业搜索和类似用例。

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

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

resp = client.cat.shards(
    v=True,
    h="index,prirep,shard,store",
    s="prirep,store",
    bytes="gb",
)
print(resp)
response = client.cat.shards(
  v: true,
  h: 'index,prirep,shard,store',
  s: 'prirep,store',
  bytes: 'gb'
)
puts response
const response = await client.cat.shards({
  v: "true",
  h: "index,prirep,shard,store",
  s: "prirep,store",
  bytes: "gb",
});
console.log(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
...

如果索引的分片由于超过建议的 50GB 大小而导致性能下降,您可能需要考虑修复索引的分片大小。分片是不可变的,因此它们的大小是固定的,因此必须使用更正的设置复制索引。这首先需要确保有足够的磁盘来复制数据。之后,您可以使用以下选项之一使用更正的设置复制索引的数据

  • 运行拆分索引以增加主分片的数量
  • 使用更正的设置创建目标索引,然后运行Reindex

请注意,执行恢复快照和/或克隆索引不足以解决分片的大小调整问题。

一旦将源索引的数据复制到其目标索引中,就可以删除源索引。然后,您可以考虑对目标索引设置创建别名,以便源索引的名称指向它以保持连续性。

符合主节点条件的节点应为每个 3000 个索引至少具有 1GB 的堆

编辑

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

作为一般经验法则,您应该在主节点上每 GB 堆的索引少于 3000 个。例如,如果您的集群具有每个 4GB 堆的专用主节点,那么您应该有少于 12000 个索引。如果您的主节点不是专用主节点,则适用相同的调整大小指南:您应该在每个符合主节点条件的节点上为集群中的每 3000 个索引保留至少 1GB 的堆。

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

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

resp = client.cat.nodes(
    v=True,
    h="heap.max",
)
print(resp)
response = client.cat.nodes(
  v: true,
  h: 'heap.max'
)
puts response
const response = await client.cat.nodes({
  v: "true",
  h: "heap.max",
});
console.log(response);
GET _cat/nodes?v=true&h=heap.max

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

resp = client.cat.shards(
    v=True,
)
print(resp)
response = client.cat.shards(
  v: true
)
puts response
const response = await client.cat.shards({
  v: "true",
});
console.log(response);
GET _cat/shards?v=true

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

编辑

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

为字段映射器和开销预留足够的堆空间

编辑

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

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

集群中的每个节点都有一个集群状态的副本。集群状态包括每个索引的字段映射信息。此信息具有堆开销。您可以使用Cluster stats API来获取去重和压缩后所有映射的总大小的堆开销。

resp = client.cluster.stats(
    human=True,
    filter_path="indices.mappings.total_deduplicated_mapping_size*",
)
print(resp)
response = client.cluster.stats(
  human: true,
  filter_path: 'indices.mappings.total_deduplicated_mapping_size*'
)
puts response
const response = await client.cluster.stats({
  human: "true",
  filter_path: "indices.mappings.total_deduplicated_mapping_size*",
});
console.log(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
    }
  }
}
检索堆大小和字段映射器开销
编辑

您可以使用Nodes stats API来获取每个节点的两个相关指标

  • 每个节点上的堆大小。
  • 每个节点字段的任何额外估计堆开销。这特定于数据节点,除了上面提到的集群状态字段信息外,数据节点持有的索引的每个映射字段还有额外的堆开销。对于非数据节点,此字段可能为零。
resp = client.nodes.stats(
    human=True,
    filter_path="nodes.*.name,nodes.*.indices.mappings.total_estimated_overhead*,nodes.*.jvm.mem.heap_max*",
)
print(resp)
response = client.nodes.stats(
  human: true,
  filter_path: 'nodes.*.name,nodes.*.indices.mappings.total_estimated_overhead*,nodes.*.jvm.mem.heap_max*'
)
puts response
const response = await client.nodes.stats({
  human: "true",
  filter_path:
    "nodes.*.name,nodes.*.indices.mappings.total_estimated_overhead*,nodes.*.jvm.mem.heap_max*",
});
console.log(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

resp = client.indices.put_settings(
    index="my-index-000001",
    settings={
        "index": {
            "routing.allocation.total_shards_per_node": 5
        }
    },
)
print(resp)
response = client.indices.put_settings(
  index: 'my-index-000001',
  body: {
    index: {
      'routing.allocation.total_shards_per_node' => 5
    }
  }
)
puts response
const response = await client.indices.putSettings({
  index: "my-index-000001",
  settings: {
    index: {
      "routing.allocation.total_shards_per_node": 5,
    },
  },
});
console.log(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查找这些空索引。

resp = client.cat.count(
    index="my-index-000001",
    v=True,
)
print(resp)
response = client.cat.count(
  index: 'my-index-000001',
  v: true
)
puts response
const response = await client.cat.count({
  index: "my-index-000001",
  v: "true",
});
console.log(response);
GET _cat/count/my-index-000001?v=true

一旦您有了空索引列表,您可以使用删除索引 API删除它们。您还可以删除任何其他不需要的索引。

resp = client.indices.delete(
    index="my-index-000001",
)
print(resp)
response = client.indices.delete(
  index: 'my-index-000001'
)
puts response
const response = await client.indices.delete({
  index: "my-index-000001",
});
console.log(response);
DELETE my-index-000001

在非高峰时段强制合并

编辑

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

resp = client.indices.forcemerge(
    index="my-index-000001",
)
print(resp)
response = client.indices.forcemerge(
  index: 'my-index-000001'
)
puts response
const response = await client.indices.forcemerge({
  index: "my-index-000001",
});
console.log(response);
POST my-index-000001/_forcemerge

将现有索引收缩为较少的分片

编辑

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

ILM 还具有用于热阶段索引的收缩操作

合并较小的索引

编辑

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

resp = client.reindex(
    source={
        "index": "my-index-2099.10.*"
    },
    dest={
        "index": "my-index-2099.10"
    },
)
print(resp)
response = client.reindex(
  body: {
    source: {
      index: 'my-index-2099.10.*'
    },
    dest: {
      index: 'my-index-2099.10'
    }
  }
)
puts response
const response = await client.reindex({
  source: {
    index: "my-index-2099.10.*",
  },
  dest: {
    index: "my-index-2099.10",
  },
});
console.log(response);
POST _reindex
{
  "source": {
    "index": "my-index-2099.10.*"
  },
  "dest": {
    "index": "my-index-2099.10"
  }
}

排查与分片相关的错误

编辑

以下是如何解决常见的分片相关错误。

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

编辑

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

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

resp = client.cluster.put_settings(
    persistent={
        "cluster.max_shards_per_node": 1200
    },
)
print(resp)
response = client.cluster.put_settings(
  body: {
    persistent: {
      'cluster.max_shards_per_node' => 1200
    }
  }
)
puts response
const response = await client.cluster.putSettings({
  persistent: {
    "cluster.max_shards_per_node": 1200,
  },
});
console.log(response);
PUT _cluster/settings
{
  "persistent" : {
    "cluster.max_shards_per_node": 1200
  }
}

此增加应仅是暂时的。作为长期解决方案,我们建议您向过度分片的数据层添加节点或减少集群的分片数量。要在进行更改后获取集群的当前分片数量,请使用cluster stats API

resp = client.cluster.stats(
    filter_path="indices.shards.total",
)
print(resp)
response = client.cluster.stats(
  filter_path: 'indices.shards.total'
)
puts response
const response = await client.cluster.stats({
  filter_path: "indices.shards.total",
});
console.log(response);
GET _cluster/stats?filter_path=indices.shards.total

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

resp = client.cluster.put_settings(
    persistent={
        "cluster.max_shards_per_node": None
    },
)
print(resp)
response = client.cluster.put_settings(
  body: {
    persistent: {
      'cluster.max_shards_per_node' => nil
    }
  }
)
puts response
const response = await client.cluster.putSettings({
  persistent: {
    "cluster.max_shards_per_node": null,
  },
});
console.log(response);
PUT _cluster/settings
{
  "persistent" : {
    "cluster.max_shards_per_node": null
  }
}

分片中的文档数量不能超过 [2147483519]

编辑

每个 Elasticsearch 分片都是一个单独的 Lucene 索引,因此它共享 Lucene 的MAX_DOC限制,最多可以有 2,147,483,519 个((2^31)-129)个文档。此每个分片的限制适用于Index stats API报告的docs.countdocs.deleted的总和。超出此限制将导致类似以下的错误

Elasticsearch exception [type=illegal_argument_exception, reason=Number of documents in the shard cannot exceed [2147483519]]

此计算可能与Count API的计算不同,因为 Count API 不包括嵌套文档,也不计算已删除的文档。

此限制远高于每个分片大约 2 亿个文档的建议的最大文档计数

如果您遇到此问题,请尝试使用强制合并 API来合并掉一些已删除的文档,以缓解此问题。例如

resp = client.indices.forcemerge(
    index="my-index-000001",
    only_expunge_deletes=True,
)
print(resp)
const response = await client.indices.forcemerge({
  index: "my-index-000001",
  only_expunge_deletes: "true",
});
console.log(response);
POST my-index-000001/_forcemerge?only_expunge_deletes=true

这将启动一个异步任务,可以通过任务管理 API对其进行监控。

删除不需要的文档,或者将索引拆分重新索引为一个具有更多分片的索引也可能有所帮助。