性能优化
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 GB | 1 | < 1 GB |
| 1-10 GB | 1-2 | 1-5 GB |
| 10-100 GB | 3-5 | 10-30 GB |
| 100 GB-1 TB | 10-20 | 10-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 调优参数:
| 参数 | 说明 | 默认值 |
|---|---|---|
G1HeapRegionSize | Region 大小 | 自动计算 |
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 性能优化的各个方面:
- 写入优化:批量写入、调整刷新间隔、事务日志配置
- 查询优化:使用 Filter、路由优化、避免深度分页
- 索引优化:分片数量、别名使用、生命周期管理
- JVM 优化:堆内存设置、垃圾收集器配置
- 系统优化:文件描述符、虚拟内存、禁用 Swap
- 监控指标:关键指标、慢查询日志、Profile API
练习
- 使用 Bulk API 批量导入数据,测试不同批量大小对性能的影响
- 对比 filter 和 query 的查询性能差异
- 配置慢查询日志,分析并优化慢查询
- 使用 Profile API 分析一个复杂查询的性能瓶颈