跳到主要内容

性能优化

Elasticsearch 的性能直接影响搜索体验和系统稳定性。本章将从写入、查询、索引、JVM 和系统层面介绍 Elasticsearch 的性能优化策略,帮助你构建高性能的搜索系统。

写入优化

批量写入

批量写入是提高索引性能最有效的方法。使用 Bulk API 可以在单次请求中处理多个文档:

POST /_bulk
{"index": {"_index": "articles", "_id": "1"}}
{"title": "文章1", "content": "内容1"}
{"index": {"_index": "articles", "_id": "2"}}
{"title": "文章2", "content": "内容2"}

最佳实践:

  • 批量大小控制在 5-15 MB
  • 文档数量控制在 1000-5000 个
  • 使用多线程并发提交,但注意不要超过集群承载能力

批量大小选择原则:

批量太小,网络开销占比高;批量太大,可能导致内存压力和请求超时。建议从较小的批量开始测试,逐步增加直到性能不再提升。

调整刷新间隔

Elasticsearch 默认每秒刷新一次索引,使新文档可见。批量导入时可以临时调整:

# 导入前:禁用刷新和副本
PUT /articles/_settings
{
"index": {
"refresh_interval": "-1",
"number_of_replicas": 0
}
}

# 执行批量导入...

# 导入后:恢复设置
PUT /articles/_settings
{
"index": {
"refresh_interval": "1s",
"number_of_replicas": 1
}
}

# 手动刷新
POST /articles/_refresh

# 强制合并段
POST /articles/_forcemerge?max_num_segments=1

为什么要这样做?

  • 禁用刷新:减少段数量,降低合并开销
  • 副本数为 0:避免副本同步的写入开销
  • 强制合并段:减少段数量,优化查询性能

调整事务日志

事务日志(Translog)保证数据的持久性。对于大批量写入场景,可以调整事务日志策略:

PUT /articles/_settings
{
"index": {
"translog": {
"durability": "async", # 异步刷新
"sync_interval": "5s", # 同步间隔
"flush_threshold_size": "512mb" # 刷新阈值
}
}
}

durability 选项:

说明性能数据安全
request每次请求后同步(默认)较低最高
async异步定时同步较高可能丢失 5s 内数据

建议: 生产环境保持 request,批量导入时可以临时使用 async

使用文档 ID 或自定义路由

Elasticsearch 默认使用文档 ID 的哈希值进行分片路由。如果业务有自然的 ID(如用户 ID、商品 ID),直接使用这些 ID 可以避免额外的 ID 生成开销。

对于有强关联的数据,使用自定义路由可以将相关数据放在同一分片:

# 写入时指定路由
PUT /articles/_doc/1?routing=user_123
{
"title": "用户文章",
"user_id": "user_123"
}

# 查询时指定路由
GET /articles/_search?routing=user_123

查询优化

使用 Filter 代替 Query

Filter 不计算评分,且结果会被缓存,适合精确过滤场景:

# 不推荐:使用 must(会计算分数)
GET /articles/_search
{
"query": {
"bool": {
"must": [
{ "term": { "status": "published" } }
]
}
}
}

# 推荐:使用 filter(会缓存结果)
GET /articles/_search
{
"query": {
"bool": {
"filter": [
{ "term": { "status": "published" } }
]
}
}
}

Filter 缓存机制:

Elasticsearch 使用过滤器缓存存储过滤结果。当相同的过滤器再次使用时,直接从缓存读取,避免重新计算。缓存大小默认为堆内存的 10%。

使用路由减少搜索范围

对于有明确数据分区的场景,使用路由可以减少搜索的分片数量:

# 查询时指定路由
GET /articles/_search?routing=user_123
{
"query": {
"match_all": {}
}
}

这个查询只会在包含 user_123 数据的分片上执行,大大减少搜索范围。

只返回需要的字段

避免返回不必要的字段,减少网络传输和序列化开销:

# 不推荐:返回所有字段
GET /articles/_search
{
"query": { "match_all": {} }
}

# 推荐:只返回需要的字段
GET /articles/_search
{
"query": { "match_all": {} },
"_source": ["title", "author", "views"]
}

# 或使用 filter_path 过滤响应
GET /articles/_search?filter_path=hits.hits._id,hits.hits._source.title
{
"query": { "match_all": {} }
}

避免深度分页

from + size 默认最大值是 10000。对于深度分页,使用 search_after

# 不推荐:from + size 超过 10000
GET /articles/_search
{
"from": 10000,
"size": 10
}

# 推荐:使用 search_after
GET /articles/_search
{
"size": 10,
"sort": [
{ "created_at": "desc" },
{ "_id": "desc" }
],
"search_after": ["2024-01-15", "abc123"]
}

深度分页的问题:

假设 from=10000, size=10,每个分片需要返回 10010 个文档到协调节点排序。深度越深,内存消耗越大。

使用 Pre-filter 优化聚合

聚合前先过滤数据,减少聚合的数据量:

GET /articles/_search
{
"size": 0,
"query": {
"range": {
"created_at": {
"gte": "now-7d"
}
}
},
"aggs": {
"by_category": {
"terms": { "field": "category.keyword" }
}
}
}

合理使用 Keyword 类型

对于需要排序、聚合的字段,使用 keyword 类型而不是 text

PUT /articles
{
"mappings": {
"properties": {
"title": { "type": "text" },
"status": { "type": "keyword" },
"category": {
"type": "text",
"fields": {
"keyword": { "type": "keyword" }
}
}
}
}
}

索引优化

合理设置分片数量

分片数量是索引最重要的配置之一:

分片数量建议:

数据量建议分片数单分片大小
< 1 GB1< 1 GB
1-10 GB1-21-5 GB
10-100 GB3-510-30 GB
100 GB-1 TB10-2010-50 GB
> 1 TB按需增加10-50 GB

分片数量原则:

  • 每个分片大小控制在 10-50 GB
  • 分片数不要超过节点数的 3-5 倍(避免过多分片竞争资源)
  • 考虑未来的数据增长
  • 单个节点分片数建议不超过 20 个(每个 GB 堆内存)

分片过多的危害:

  • 每个分片都有内存开销
  • 分片越多,合并操作越频繁
  • 搜索时需要合并更多分片的结果
  • 集群状态管理更复杂

使用索引别名

索引别名提供了灵活性,支持零停机切换:

# 创建别名
POST /_aliases
{
"actions": [
{ "add": { "index": "articles_v2", "alias": "articles" } }
]
}

# 零停机切换索引
POST /_aliases
{
"actions": [
{ "remove": { "index": "articles_v1", "alias": "articles" } },
{ "add": { "index": "articles_v2", "alias": "articles" } }
]
}

使用索引生命周期管理

索引生命周期管理(ILM)自动管理索引从创建到删除的全生命周期:

# 创建生命周期策略
PUT /_ilm/policy/logs_policy
{
"policy": {
"phases": {
"hot": {
"min_age": "0ms",
"actions": {
"rollover": {
"max_size": "50gb",
"max_age": "30d"
},
"set_priority": { "priority": 100 }
}
},
"warm": {
"min_age": "30d",
"actions": {
"shrink": { "number_of_shards": 1 },
"forcemerge": { "max_num_segments": 1 },
"set_priority": { "priority": 50 }
}
},
"delete": {
"min_age": "90d",
"actions": {
"delete": {}
}
}
}
}
}

关闭不需要索引的特性

对于某些场景,可以关闭不需要的特性以提升性能:

PUT /articles
{
"mappings": {
"properties": {
"content": {
"type": "text",
"norms": false, # 禁用评分标准化
"index_options": "freqs" # 减少索引信息
},
"views": {
"type": "integer",
"index": false, # 不索引,只存储
"doc_values": true # 用于排序和聚合
}
}
}
}

常用优化选项:

选项说明适用场景
norms: false禁用评分标准化不需要评分的字段
index_options: "freqs"只存储词频不需要位置信息的全文搜索
index: false不索引字段只用于存储、显示,不搜索
doc_values: false禁用 Doc Values不用于排序、聚合
enabled: false完全禁用索引只存储原始 JSON

JVM 优化

堆内存设置

堆内存是 JVM 最重要的配置:

# jvm.options
-Xms4g
-Xmx4g

设置原则:

  • Xms 和 Xmx 设置相同,避免堆大小调整
  • 堆内存不超过物理内存的 50%
  • 堆内存不超过 32 GB(超过会禁用压缩指针)
  • 剩余内存留给文件系统缓存

为什么不超过 32 GB?

JVM 使用压缩指针(Compressed OOPs)来减少内存占用。当堆内存超过 32 GB 时,压缩指针失效,内存占用反而增加。实际上,堆内存的最佳上限是接近但不超过 32 GB 的值。

垃圾收集器配置

Elasticsearch 8.x 默认使用 G1GC:

# jvm.options
-XX:+UseG1GC
-XX:G1HeapRegionSize=16m
-XX:InitiatingHeapOccupancyPercent=30

G1GC 调优参数:

参数说明默认值
G1HeapRegionSizeRegion 大小自动计算
InitiatingHeapOccupancyPercent启动并发 GC 的堆占用百分比45
G1ReservePercent保留空间防止晋升失败10

监控 GC

# 查看 GC 统计
GET /_nodes/stats?filter_path=nodes.*.jvm.gc

# 关键指标
# - gc.collection_count:GC 次数
# - gc.collection_time_in_millis:GC 总时间
# - 年轻代 GC 频率高是正常的
# - 老年代 GC 频繁说明内存不足

系统优化

文件描述符

Elasticsearch 需要大量文件描述符:

# /etc/security/limits.conf
elasticsearch soft nofile 65536
elasticsearch hard nofile 65536

# 验证
GET /_nodes/stats?filter_path=nodes.*.process.max_file_descriptors

虚拟内存

Elasticsearch 使用 mmapfs 存储索引:

# /etc/sysctl.conf
vm.max_map_count=262144

# 应用配置
sysctl -p

禁用 Swap

Swap 会导致严重的性能问题:

# 临时禁用
swapoff -a

# 永久禁用:注释 /etc/fstab 中的 swap 行

# 或锁定内存(elasticsearch.yml)
bootstrap.memory_lock: true

验证内存锁定:

GET /_nodes?filter_path=nodes.*.mlockall

磁盘 I/O 优化

使用 SSD 可以显著提升性能。如果使用 HDD,可以考虑以下优化:

# 调整 I/O 调度器
echo noop > /sys/block/sda/queue/scheduler

# 禁用访问时间更新
mount -o remount,noatime /mount/point

监控指标

关键指标

# JVM 堆使用率
GET /_nodes/stats?filter_path=nodes.*.jvm.mem.heap_used_percent

# 索引速度
GET /_nodes/stats?filter_path=nodes.*.indices.indexing.index_total

# 搜索延迟
GET /_nodes/stats?filter_path=nodes.*.indices.search.query_time_in_millis

# 线程池状态
GET /_nodes/stats?filter_path=nodes.*.thread_pool

# GC 统计
GET /_nodes/stats?filter_path=nodes.*.jvm.gc

慢查询日志

配置慢查询日志可以帮助发现性能问题:

# elasticsearch.yml
index.search.slowlog.threshold.query.warn: 10s
index.search.slowlog.threshold.query.info: 5s
index.search.slowlog.threshold.query.debug: 2s

index.indexing.slowlog.threshold.index.warn: 10s
index.indexing.slowlog.threshold.index.info: 5s

Profile API

使用 Profile API 分析查询性能:

GET /articles/_search
{
"profile": true,
"query": {
"match": { "title": "Elasticsearch" }
}
}

响应会包含查询各阶段的耗时明细,帮助定位性能瓶颈。

节点热点线程

GET /_nodes/hot_threads

这个 API 显示各节点 CPU 占用最高的线程,帮助定位性能问题。

小结

本章我们系统学习了 Elasticsearch 性能优化的各个方面:

  1. 写入优化:批量写入、调整刷新间隔、事务日志配置
  2. 查询优化:使用 Filter、路由优化、避免深度分页
  3. 索引优化:分片数量、别名使用、生命周期管理
  4. JVM 优化:堆内存设置、垃圾收集器配置
  5. 系统优化:文件描述符、虚拟内存、禁用 Swap
  6. 监控指标:关键指标、慢查询日志、Profile API

练习

  1. 使用 Bulk API 批量导入数据,测试不同批量大小对性能的影响
  2. 对比 filter 和 query 的查询性能差异
  3. 配置慢查询日志,分析并优化慢查询
  4. 使用 Profile API 分析一个复杂查询的性能瓶颈

参考资料