调整分片大小

编辑

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 分片成本高得多。但是,非常大的分片也可能导致搜索速度变慢,并且在故障后恢复的时间更长。

分片的物理大小没有硬性限制,理论上每个分片最多可以包含超过二十亿个文档。但是,经验表明,对于许多用例来说,大小在 10GB 到 50GB 之间的分片通常效果很好,只要每个分片的文档数量保持在 2 亿以下。

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

如果您使用 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 大小而导致性能下降,您可以考虑修复索引的分片大小。分片是不可变的,因此它们的大小是固定的,因此必须使用正确的设置复制索引。这需要首先确保有足够的磁盘空间来复制数据。之后,您可以通过以下选项之一使用正确的设置复制索引的数据

  • 运行拆分索引以增加主分片的数量
  • 创建一个具有已更正设置的目标索引,然后运行重新索引

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

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

请观看此修复分片大小视频,了解故障排除演练示例。

具备主节点资格的节点应至少拥有每 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 个冻结的分片。确保您的集群中有足够数量的每种类型的节点来处理您需要的分片数量。

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

编辑

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

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

集群中的每个节点都有一份 集群状态 的副本。集群状态包含有关每个索引的字段映射的信息。此信息有堆空间开销。您可以使用 集群统计信息 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
    }
  }
}
检索堆大小和字段映射器开销
编辑

您可以使用 节点统计信息 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 将具有类似映射的索引合并到单个大型索引中。对于时间序列数据,您可以将短期时间段的索引重新索引到涵盖较长时间段的新索引中。例如,您可以将 10 月份的每日索引(具有共享索引模式,例如 my-index-2099.10.11)重新索引到每月的 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
  }
}

此增加应该只是临时的。作为长期解决方案,我们建议您向过度分片的数据层添加节点或 减少集群的分片数量。要获取在进行更改后集群的当前分片数量,请使用 集群统计信息 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) 个文档。此分片限制适用于 索引统计 API 报告的 docs.count 加上 docs.deleted 的总和。超过此限制将导致以下错误

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

此计算结果可能与 计数 API 的计算结果不同,因为计数 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 进行监控。

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