分库分表理论千万别盲目应用到Elasticsearch……

铭毅天下 2023-08-02 09:57:58

 

一、问题描述

 

 

近期在优化索引时,我遇到了一些挑战。我们的环境是7节点16*32G的机器,我在尝试内存优化。当前的文档总量为5亿,然而mapping设计和shard设计都出现了问题。每个节点上有480个shard,这是一个相当离谱的数量。

 

当我试图分析内存消耗的时候,遇到了更大的问题。尽管 fielddata、completion、segments、query_cache和translog占用的内存量可以计算出来,但是Heap的内存占用量达到了15G,让我困惑的是,剩下的内存究竟消耗在哪里呢?

 

sharding设计应该是参考了mysql的分表的思路,给一个 index 拆成了 300 个 index,比如 index_1,index_2...index_300。

 

我觉得这两个问题可能会导致内存的问题。

 

——来自:死磕Elasticsearch 知识星球 

 

https://t.zsxq.com/10fKvwz0k

 

二、问题拆解分析

 

同学给出了完整的 stats 分析结果数据。我们拆开一段一段解读看看以便复盘发现问题所在。如下内容来自于如下 stats 命令的返回结果。 

 

GET _cluster/stats

 

 
发现问题1:存在大量的删除或更新操作

 

  •  
  •  
  •  
  •  
  "docs": {      "count": 331681467,      "deleted": 73434046    },

 

删除文档的数量(deleted:73434046)相当高。这可能是因为当前业务场景在频繁地更新或删除文档,这样在 Elasticsearch 中会产生很多被标记为已删除的文档。

 

在 Elasticsearch 中,更新一个文档实际上是删除旧文档然后索引新文档。被删除的文档在一段时间内仍会占据空间,直到进行下一次 segment merge 时才会被真正删除。

 

如果应用有大量的删除或更新操作,可能会导致性能问题,因为 segment merge 是一个相对昂贵的操作。另外,过多的被删除的文档也会占用更多的存储空间。这种情况下,可以考虑调整数据模型或者索引策略。比如,避免过多的更新操作,或者使用 time-based indices(基于时间的索引)。在使用基于时间的索引时,可以定期(如每天)创建新的索引,删除老的索引,这样可以避免大量的删除操作。

 

 
发现问题2:存有大量已删除但未被清理的文档

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
"segments": {     "count": 3705,     "memory_in_bytes": 43210351,     "terms_memory_in_bytes": 34680393,     "stored_fields_memory_in_bytes": 1870504,     "term_vectors_memory_in_bytes": 0,     "norms_memory_in_bytes": 1604160,     "points_memory_in_bytes": 0,     "doc_values_memory_in_bytes": 5055294,     "index_writer_memory_in_bytes": 54801608,     "version_map_memory_in_bytes": 211869,     "fixed_bit_set_memory_in_bytes": 50741120,     "max_unsafe_auto_id_timestamp": 1687046400930,     "file_sizes": {}   }

 

核心结果及释义如下:

 

参数 解释
count 3705 索引的段数,这个值看起来正常,因为 Elasticsearch 会自动进行段合并操作。
memory_in_bytes 43210351 所有段使用的内存总量,包括terms、stored_fields、norms和doc_values等。这个值依赖于索引数据和查询负载,只要不超出你的节点总内存,就没有问题。
index_writer_memory_in_bytes 54801608 当前被索引写入器(Index Writer)使用的内存总量。这个值看起来正常,因为索引写入器需要一定的内存来处理正在进行的索引操作。
version_map_memory_in_bytes 211869 用于保存文档版本信息的内存使用量。看起来也在正常范围内。
fixed_bit_set_memory_in_bytes 50741120 存储已删除文档的信息的内存使用量,这个值相对较高,可能表示索引中存在大量已删除但未被清理的文档。

 

潜在风险问题——fixed_bit_set_memory_in_bytes的值相对较高(50741120字节,约48.4MB)。这部分内存主要用于存储已删除文档的信息。在Elasticsearch中,当一个文档被删除或更新时,它的旧版本不会立即被物理删除,而是被标记为已删除,直到下一次段合并时才会被清除。这意味着索引中可能有大量已删除但未被清理的文档。

 

这种情况可能会降低查询性能并占用额外的存储空间。可以通过force merge操作来清理这些已删除的文档,但请注意,force merge是一个I/O密集型操作,可能会在执行期间影响集群性能。通常,force merge操作应该在业务低峰期进行。另外,如果频繁地更新或删除文档,可能需要调整索引策略或者数据模型以减少这种操作。

 

 
发现问题3:有大量的操作尚未被提交到 Lucene 索引

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
"translog": {      "operations": 4171567,      "size_in_bytes": 2854130582,      "uncommitted_operations": 4171567,      "uncommitted_size_in_bytes": 2854130582,      "earliest_last_modified_age": 0    },

 

核心结果及释义如下:

 

参数 解释
operations 4171567 translog中的操作数,这个值相当大,表示有大量的操作尚未被提交到Lucene索引。
size_in_bytes 2854130582 translog的大小,这个值也相当大,可能会在系统崩溃时导致数据恢复时间变长。
uncommitted_operations 4171567 尚未提交的操作数,与"operations"相同,表示这些操作尚未被提交。
uncommitted_size_in_bytes 2854130582 尚未提交的操作的大小,与"size_in_bytes"相同。
earliest_last_modified_age 0 最早的未提交操作的时间,这个值为0表示所有操作都是最新的。

 

潜在风险问题——这可能会在系统崩溃时导致数据恢复时间变长,因为需要重新执行这些操作以恢复到最新的状态。

 

虽然这可能不会对正在运行的集群造成太大的影响,但是在某些情况下,例如节点宕机或集群恢复,可能会影响Elasticsearch的性能和数据恢复速度。因此,通常建议定期将Translog的数据提交到Lucene索引以保持其大小在合理范围内。

 

 
发现问题4:集群所在的操作系统的内存使用率非常高

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
"os": {    "timestamp": 1687165428228,    "cpu": {      "percent": 13,      "load_average": {        "1m": 2.11,        "5m": 1.68,        "15m": 1.75      }    },    "mem": {      "total_in_bytes": 32822083584,      "free_in_bytes": 260890624,      "used_in_bytes": 32561192960,      "free_percent": 1,      "used_percent": 99    },    "swap": {      "total_in_bytes": 0,      "free_in_bytes": 0,      "used_in_bytes": 0    },    "cgroup": {      "cpuacct": {        "control_group": "/user.slice",        "usage_nanos": 15187558135108329      },      "cpu": {        "control_group": "/user.slice",        "cfs_period_micros": 100000,        "cfs_quota_micros": -1,        "stat": {          "number_of_elapsed_periods": 0,          "number_of_times_throttled": 0,          "time_throttled_nanos": 0        }      },      "memory": {        "control_group": "/",        "limit_in_bytes": "9223372036854771712",        "usage_in_bytes": "31734857728"      }    }  }, 

 

核心结果及释义如下:

 

参数 解释
cpu.percent 13 CPU 使用率,该值在正常范围内。
mem.total_in_bytes 32822083584 总内存量。
mem.free_in_bytes 260890624 空闲内存量,非常低,这可能导致性能问题。
mem.used_in_bytes 32561192960 已使用的内存量。
mem.free_percent 1 空闲内存百分比,非常低,这可能导致性能问题。
mem.used_percent 99 已使用内存百分比,非常高,可能需要进行调整以提高性能。
swap.total_in_bytes, swap.free_in_bytes, swap.used_in_bytes 0 交换空间的总量、空闲量和使用量,均为 0,表明没有使用交换空间。

 

潜在风险问题——Elasticsearch 集群所在的操作系统(OS)的内存使用率非常高("used_percent": 99),可用内存非常低("free_percent": 1)。这可能会导致性能问题,因为系统可能不得不频繁地使用磁盘进行交换操作,这会大大降低性能。

 

建议尽快采取措施释放内存或增加更多的内存,以提高 Elasticsearch 的性能。

 

 
发现问题5:堆内存使用率相当高

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
"jvm": {    "timestamp": 1687165428234,    "uptime_in_millis": 5988052030,    "mem": {      "heap_used_in_bytes": 16480235136,      "heap_used_percent": 76,      "heap_committed_in_bytes": 21474836480,      "heap_max_in_bytes": 21474836480,      "non_heap_used_in_bytes": 309785016,      "non_heap_committed_in_bytes": 354152448,      "pools": {        "young": {          "used_in_bytes": 8967421952,          "max_in_bytes": 0,          "peak_used_in_bytes": 12968787968,          "peak_max_in_bytes": 0        },        "old": {          "used_in_bytes": 7479258752,          "max_in_bytes": 21474836480,          "peak_used_in_bytes": 11748245496,          "peak_max_in_bytes": 21474836480        },        "survivor": {          "used_in_bytes": 33554432,          "max_in_bytes": 0,          "peak_used_in_bytes": 1610612736,          "peak_max_in_bytes": 0        }      }    },    "threads": {      "count": 268,      "peak_count": 314    },    "gc": {      "collectors": {        "young": {          "collection_count": 434416,          "collection_time_in_millis": 18999559        },        "old": {          "collection_count": 0,          "collection_time_in_millis": 0        }      }    },

 

核心结果及释义如下:

 

参数 解释
heap_used_in_bytes 16480235136 堆内存使用量,这个值相当大。
heap_used_percent 76 堆内存使用百分比,这个值也相当大,可能会影响性能。
heap_committed_in_bytes 21474836480 提交用于 JVM 的堆内存量。
heap_max_in_bytes 21474836480 最大堆内存量,应该持续关注并确保heap_used_percent的值不要靠近这个值太近以避免内存压力。
non_heap_used_in_bytes 309785016 非堆内存使用量。
non_heap_committed_in_bytes 354152448 提交用于 JVM 的非堆内存量。
young.used_in_bytes 8967421952 新生代使用的内存量。
old.used_in_bytes 7479258752 老年代使用的内存量。
gc.collectors.young.collection_count 434416 新生代垃圾收集的次数。
gc.collectors.old.collection_count 0 老年代垃圾收集的次数,这个值为0,表示老年代垃圾收集器尚未运行,这是正常的,除非在内存压力很大的情况下。

 

潜在风险问题——堆内存使用率相当高("heap_used_percent": 76)。虽然这个值可能不会立即导致问题,但如果索引负载增加,或者有更多的查询,可能会使内存压力增加,导致更频繁的垃圾收集,从而影响性能。

 

建议监控这些值的变化,并在需要时调整 JVM 的内存设置,以保持 Elasticsearch 的性能。

 

 
发现问题6:读操作比写操作多很多

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
"io_stats": {      "devices": [        {          "device_name": "dm-0",          "operations": 5250539512,          "read_operations": 4478787246,          "write_operations": 771752266,          "read_kilobytes": 129711481927,          "write_kilobytes": 23684659984        }      ],      "total": {        "operations": 5250539512,        "read_operations": 4478787246,        "write_operations": 771752266,        "read_kilobytes": 129711481927,        "write_kilobytes": 23684659984      }    }  }

 

核心结果及释义如下:

 

参数 解释
operations 5250539512 I/O 操作的总数。
read_operations 4478787246 读操作的总数,这个数值比写操作多。
write_operations 771752266 写操作的总数。
read_kilobytes 129711481927 读操作的总量,单位为 KB。
write_kilobytes 23684659984 写操作的总量,单位为 KB。

 

潜在风险问题——上述内容显示了 Elasticsearch 集群的 I/O 操作统计信息。看起来读操作比写操作多很多,但这并不一定是问题,这完全取决于应用程序使用 Elasticsearch 的方式。如果当前业务场景主要是查询数据,那么这个读取操作的数量就可以解释了。

 

可以根据这些信息来调整 Elasticsearch 的 I/O 配置,比如,如果读操作非常多,可能需要在硬件或配置上进行优化以提高读取速度。

 

 
发现问题7:缓存命中率低

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
"query_cache": {    "memory_size_in_bytes": 422629063,    "total_count": 18178614894,    "hit_count": 4107645935,    "miss_count": 14070968959,    "cache_size": 405975,    "cache_count": 16870486,    "evictions": 16464511  }

 

核心结果及释义如下:

 

参数 释义
memory_size_in_bytes 422629063 查询缓存内存大小(字节)
total_count 18178614894 查询缓存请求总数
hit_count 4107645935 查询缓存命中次数
miss_count 14070968959 查询缓存未命中次数
cache_size 405975 当前查询缓存个数
cache_count 16870486 查询缓存创建的总个数
evictions 16464511 查询缓存逐出的总次数

 

潜在风险问题——查询缓存命中率似乎有些低,这可能意味着当前业务查询有很大的多样性,或者缓存设置不够理想。

 

建议:如果想提高查询缓存的效率,可能需要调整查询缓存的大小,或者看看是否有一些查询可以做些修改以适应缓存。此外,一些不需要缓存的查询,可以明确地在查询中设置 ——"cache": false 来避免对缓存造成不必要的压力。

 

三、问题总结

 

我们从响应中得到了一些显著的内存相关统计信息:操作系统级别的内存使用非常高,只剩下1%的总内存空闲。如果内存使用继续上升,可能会导致性能问题或崩溃。

 

  • JVM内存使用

 

首先,JVM的堆内存使用了76%,接近80%的警戒线。如果内存使用超过80%,将会触发更频繁的垃圾收集,可能会对性能产生影响。同时,“young”内存池的使用超过了其“max”值,这也可能是一个需要进一步调查的问题。

 

  • 操作系统内存使用

 

操作系统的内存使用很高,仅剩1%的空闲内存,这可能会导致系统性能降低,甚至导致进程被操作系统杀死以释放内存。

 

  • 索引压力

 

"Indexing_pressure.memory.total.combined_coordinating_and_primary_in_bytes"远远大于"indexing_pressure.memory.limit_in_bytes",这表示索引操作产生的内存压力超过了预设的限制。这可能导致新的写入操作被拒绝,以防止内存耗尽。

 

  • 索引失败

 

“indexing.index_failed”值为10253,这表示有一些索引操作失败。可能需要查看Elasticsearch的日志来确定失败的原因。

 

  • 缓冲区使用

 

“buffer_pools.mapped.used_in_bytes”值很高,表示映射的文件缓冲区使用了很大的内存。这通常是由于大量的文件被打开并映射到内存中,可能是由于大量的读取操作或大量的小文件。

 

  • 可能存在大量删除或更新操作

 

因为在Elasticsearch中,删除的文档不会立即被清除,而是在下次合并段时才被清除,这可能会占用额外的空间。

 

 
1、可能的原因

 

上述问题可能由以下几个原因引起:

 

1)大量的数据操作

 

频繁的索引、更新和删除操作可能会使Elasticsearch需要更多的内存来处理这些操作。

 

2)大量的并发查询

 

高并发查询会使Elasticsearch需要在短时间内处理大量请求,也可能导致内存使用上升。

 

3)大量的数据段合并

 

数据段合并需要消耗大量的计算和内存资源。

 

4)数据库分库分表理论直接迁移到 Elasticsearch

 

分片设置不合理,sharding(分片)设计应该是参考了 mysql 的分表的思路,给一个 index 拆成了300个 index。

 

解决这些问题通常需要结合监控数据和日志来确定具体原因,然后根据具体情况进行优化或扩容。

 

 
2、根因:MySQL 的分库分表理论不直接适用于 Elasticsearch

 

在进行深入分析之后,我发现主要问题出在mapping和sharding的设计上。

 

  • 一开始,我们的mapping设计比较粗糙,甚至对一些hash也进行了分词。这导致了索引非常大,占用了大量的内存。

     

  • 另外,我们在设计sharding时,参考了MySQL的分表思路,给一个index拆成了300个index,例如index_1, index_2...index_300。

 

这两个问题都可能导致内存问题。

 

  • 一方面,mapping设计使索引很大,占用大量内存。

     

  • 另一方面,一次查询可能会打开300个shard,每个shard都有自己的pool,这可能就是导致“buffer_pools.mapped.used_in_bytes”值较大的原因。比如进行分页查询时,每次打开300个shard或segment,那就意味着一次查询打开了6000个文档。

 

因此,优化的当务之急就是合并索引。当前的单分片应该是不到 2G,小的分片应该是几百兆,分片并不均匀。

 

我计算了一下,这些分片应该可以合并到8个分片(原来数百个)。这种优化应该能够显著减少内存的消耗,进一步提升Elasticsearch的性能。

 

四、小结

 

Elasticsearch的设计理念和关系型数据库(例如MySQL)的设计理念是有明显区别的。在关系型数据库中,分表是常见的处理大量数据的策略,但是在Elasticsearch中,过度分片会导致效率降低和内存占用过高。以下是一些深入的分析和后续开发人员的注意事项:

 

 
1、Mapping 的设计

 

在设计字段类型时,尽量使用更加精确的数据类型,避免不必要的文本字段,特别是对于一些hash值或者ID值,它们无需分词,直接用keyword类型存储即可。

 

如果必须要分词的话,合理选择分词器。例如,对于中文,ik_max_word可能会产生大量的词条,而ik_smart则更为节省资源。

 

对于大文本字段,可以考虑禁用倒排索引,或者只对部分关键内容做索引,避免索引过大。

 

 
2、Sharding 分片的设计

 

Elasticsearch的每个shard都是一个完全的Lucene索引,拥有自己的数据结构和资源开销,所以shard的数量不应该过多。过多的shard会消耗大量的内存和CPU,降低查询性能。

 

单个shard的大小通常建议在20GB-40GB之间。过小的shard会增加开销,过大的shard在做recovery时会消耗更多的时间。

 

尽量避免一个查询涉及到太多的shard,这会增加查询时间和资源消耗。如果可能,尽量在一个index内部进行数据的切分和查询,而不是在多个index之间。

 

考虑使用index alias或者routing功能,减少不必要的shard查询。

 

 
3、后续开发人员的注意事项

 

在构建和优化Elasticsearch数据模型时,我们必须深入理解其内在工作机制,并借鉴已有的最佳实践,而非简单地迁移关系型数据库的理论。

 

持续监控Elasticsearch的核心数据,如shard的数量、大小,以及CPU和内存的使用情况,是预防问题、提前发现和处理隐患的关键。

 

此外,我们需要定期进行性能测试,以了解系统的性能瓶颈和限制,并通过对不同shard数量和大小的性能变化的测试,找出最优的shard设计方案。

 

为了更好地使用和优化Elasticsearch,我们必须不断学习和保持对其新功能和最佳实践的关注。

 

遇到问题时,要充分利用Elasticsearch提供的各种分析工具,如_slow log_和_hot threads_,以准确找出问题的根源。这是我们向更高效、更稳定的Elasticsearch 服务迈进的关键步骤。

 

作者丨铭毅天下
来源丨公众号: 铭毅天下Elasticsearch(ID:elastic999)
dbaplus社群欢迎广大技术人员投稿,投稿邮箱:editor@dbaplus.cn
最新评论
访客 2024年04月08日

如果字段的最大可能长度超过255字节,那么长度值可能…

访客 2024年03月04日

只能说作者太用心了,优秀

访客 2024年02月23日

感谢详解

访客 2024年02月20日

一般干个7-8年(即30岁左右),能做到年入40w-50w;有…

访客 2023年08月20日

230721

活动预告