Elasticsearch的优化
Elasticsearch高效使用与优化指南 【摘要】本文从硬件配置、分片策略、写入优化、内存管理、分页查询、容灾方案六个维度系统总结Elasticsearch核心优化方法。关键点包括:1)使用SSD硬盘并配置多路径存储;2)分片大小控制在10-50GB,遵循"节点数≤主分片数*(副本数+1)"原则;3)写入优化采用批量提交、减少刷新频率、调整translog策略;4)JVM堆
1、硬件选择
ES 的基础是 Lucene,所有的索引和文档数据是存储在本地的磁盘中,路径在 ES 的配置文件 ../config/elasticsearch.yml 中配置data 与 logs。
磁盘在现代服务器上通常都是瓶颈。ES 重度使用磁盘,磁盘能处理的吞吐量越大,节点就越稳定。这里有一些优化磁盘 I/O 的技巧:
- 使用 SSD(固态硬盘)。
- 使用 RAID0 。条带化 RAID 会提高 磁盘 I/O,代价显然就是当一块硬盘故障时整个就故障了。不要使用 镜像 或者 奇偶校验 RAID,因为副本已经提供了这个功能。
- 使用多块硬盘,并允许 ES 通过多个path.data 目录配置把数据条带化分配到它们上面。
- 不要使用远程挂载的存储,比如 NFS 或者 SMB/CIFS。这个引入的延迟对性能来说完全是背道而驰的。
2、分片策略
如何合理设置ES的分片数?根据什么标准设置ES分片数?
合理设置Elasticsearch分片数需综合数据规模、硬件资源及业务需求,以下是具体标准和评估方法:
一、分片数设置的核心标准
1. 数据总量
- 单分片容量:推荐 10–50GB(时序数据建议20–40GB),避免超过50GB导致恢复缓慢或查询性能下降。
- 计算方式:分片数 = 总数据量 / 单分片理想容量
例:500GB数据 → 10个分片(按50GB/分片)。
2. 硬件资源限制
- 分片/节点内存比:每1GB堆内存对应不超过20个分片(含副本)。
例:节点堆内存30GB → 最多600个分片,建议控制在300以内。
- 节点数与分片分布:确保分片均匀分布,避免单节点负载过高。
公式:节点数 ≥ 总分片数 / 单节点承载分片数。
3. 业务场景需求
- 写入密集型:适当增加分片提升并行写入能力。
- 查询密集型:
- 简单查询:较少分片降低协调开销。
- 复杂聚合:增加分片利用并行计算(如15-20个分片)。
- 高可用性:至少1个副本分片(副本数=1)防止数据丢失。
二、分片配置合理性的评估指标
监控关键性能参数
|
监控项 |
合理范围 |
异常表现 |
关联问题 |
|
CPU 利用率 |
≤70% |
持续 >80% |
分片过多、查询过重、线程阻塞 |
|
磁盘 IO 延迟 |
≤50ms |
持续 >100ms 或周期性飙升 |
分片过大(>50GB)、段合并频繁 |
|
查询延迟 |
95%请求 ≤100ms |
波动 >30% 或 P99 >500ms |
分片分布不均、节点热点 |
|
Segment 数量 |
单分片 ≤1,000 |
>1,500(小文件过多) |
合并压力大,需 force_merge |
|
未分配分片数 |
0 |
>0 且持续增长 |
磁盘不足、节点故障、配置错误 |
|
分片恢复速度 |
≥50MB/s |
<10MB/s |
网络带宽或磁盘瓶颈 |
针对性测试方法
1. 扩容压力测试:模拟数据增长%,观察分片重平衡是否触发节点过载。
2. 故障恢复测试:主动关闭一个节点,监控分片恢复时间和集群稳定性。
3. 查询负载测试:高并发查询下,检查是否有节点出现线程池拒绝(rejected)。
三、优化分片配置的实操建议
1. 动态调整策略
- 增加副本:直接修改number_of_replicas应对查询压力:
PUT /index/_settings
{ "number_of_replicas": 2 } // 假设原来副本分片数是1,从1调整为2
- 缩减分片:对历史索引使用_shrink API合并分片(需只读状态)。
- 时序数据:按周期(日/月)建新索引,动态调整分片数。
2. 避免的陷阱
- 主分片数不可修改:创建索引时需谨慎设定 number_of_shards。
- 分片过小:若分片大小<1GB,则导致段过多,开销增大(建议分片大小>5GB)。
- 跨节点分布不均:检查_cat/shards?v确保分片均衡。
四、总结:分片配置决策流程
预估数据量 → 按10–50GB/分片计算基数。
匹配硬件 → 按堆内存(每GB内存的分片数不能超过20个)和节点数调整。
业务校准 → 写入密集型增加分片,查询密集型优化副本。
持续监控 → 关注CPU/IO/查询延迟,定期执行负载测试。
示例验证:假设2节点各30GB堆内存,设置了5主分片+1副本的配置(总分片10个)
- 数据量:若总数据250GB → 单分片50GB(合理)。
- 硬件:2节点各30GB堆内存 → 总分片上限1200(当前10个安全)。
- 风险点:节点故障时,10个分片需在2节点间迁移,可能短暂影响性能 → 建议扩容至4节点。
ES分片数设置后能否修改?
- 主分片数(number_of_shards):创建索引后不可修改(因数据路由依赖哈希计算)。
- 副本分片数(number_of_replicas):支持动态调整(立即生效):
PUT /your_index/_settings
{ "number_of_replicas": 2 } // 从1调整为2
如何判断分片是否设置得过大?
|
症状 |
临界值 |
优化方案 |
|
查询延迟 >1s |
分片 >50GB |
拆分索引(_split API) |
|
恢复时间 >30分钟 |
分片 >100GB |
重建索引并增加主分片数 |
|
Segment文件>10,000 |
分片 >30GB |
执行 force_merge |
|
节点频繁 Full GC |
单节点分片 >1,000 |
减少分片或扩容节点 |
如何动态调整Elasticsearch的分片数?
|
方法 |
适用场景 |
操作示例 |
限制 |
|
增加副本分片 |
提升查询性能/高可用 |
PUT /index/_settings { "number_of_replicas": 2 } |
无 |
|
分片拆分(Split) |
主分片过少且索引只读 |
POST /old_index/_split/new_index { "settings": { "index.number_of_shards": 8 } } |
目标分片数需为原数倍数 |
|
重建索引(Reindex) |
需彻底重分布分片 |
POST _reindex { "source": { "index": "old" }, "dest": { "index": "new" } } |
停机时间长,资源消耗大 |
|
集群水平扩容 |
总分片数不足导致写入瓶颈 |
增加数据节点 → 分片自动重平衡 |
需预留存储空间 |
合理设置分片数和推迟分片分配。
分片策略之一:合理设置分片数
- 分片和副本的设计为 ES 提供了支持分布式和故障转移的特性,但并不意味着分片和副本是可以无限分配的。
- 而且索引的分片完成分配后由于索引的路由机制,我们是不能重新修改分片数的。
可能有人会说,我不知道这个索引将来会变得多大,并且过后我也不能更改索引的大小,所以为了保险起见,还是给它设为 1000 个分片吧。但是需要知道的是,一个分片并不是没有代价的。需要了解:
- 一个分片的底层即为一个 Lucene 索引,会消耗一定文件句柄、内存、以及 CPU 运转。
- 每一个搜索请求都需要命中索引中的每一个分片,如果每一个分片都处于不同的节点还好, 但如果多个分片都需要在同一个节点上竞争使用相同的资源就有些糟糕了。
- 用于计算相关度的词项统计信息是基于分片的。如果有许多分片,每一个都只有很少的数据会导致很低的相关度。
一个业务索引具体需要分配多少分片可能需要架构师和技术人员对业务的增长有个预先的判断,横向扩展应当分阶段进行,为下一阶段准备好足够的资源。只有当你进入到下一个阶段,你才有时间思考需要作出哪些改变来达到这个阶段。
一般来说,我们遵循一些原则:
- 原则1:控制每个分片占用的硬盘容量不超过 ES 的最大 JVM 的堆空间设置(一般设置不超过 32G,参考下文的 JVM 设置原则),因此,如果索引的总容量在 500G 左右,那分片大小在 16 个左右即可。当然,最好同时考虑原则 2。
- 原则2:考虑一下 node 数量,一般一个节点有时候就是一台物理机,如果分片数过多,大大超过了节点数,很可能会导致一个节点上存在多个分片,一旦该节点故障,即使保持了 1 个以上的副本,同样有可能会导致数据丢失,集群无法恢复。所以, 一般都设置分片数不超过节点数的 3 倍。
- 原则3:主分片,副本和节点最大数之间数量,我们分配的时候可以参考以下关系:节点数 <= 主分片数 *( 副本数 + 1 )。
分片策略之二:推迟分片分配
对于节点瞬时中断的问题,默认情况,集群会等待一分钟来查看节点是否会重新加入,如果这个节点在此期间重新加入,重新加入的节点会保持其现有的分片数据,不会触发新的分片分配。这样就可以减少 ES 在自动再平衡可用分片时所带来的极大开销。通过修改参数 delayed_timeout,可以延长再均衡的时间,可以全局设置也可以在索引级别进行修改:

3、ES 写入速度优化策略
ES 的默认配置,是综合了数据可靠性、写入速度、搜索实时性等因素。实际使用时,需要根据公司要求,进行偏向性的优化。
针对于搜索性能要求不高,但是对写入要求较高的场景,需要尽可能的选择恰当写优化策略。
综合来说,可以考虑以下几个方面来提升写索引的性能:
- 加大 Translog Flush ,目的是降低 Iops、Writeblock。
- 增加 Index Refresh 间隔,目的是减少 Segment Merge 的次数。
- 调整 Bulk 线程池和队列。
- 优化节点间的任务分布。
- 优化 Lucene 层的索引建立,目的是降低 CPU 及 IO。
1、批量数据提交
ES 提供了 Bulk API 支持批量操作,当我们有大量的写任务时,可以使用 Bulk 来进行批量写入。
通用的策略如下:Bulk 默认设置批量提交的数据量不能超过 100M。数据条数一般是根据文档的大小和服务器性能而定的,但是单次批处理的数据大小应从 5MB~15MB 逐渐增加,当性能没有提升时,把这个数据量作为最大值。
2、优化存储设备
ES 是一种密集使用磁盘的应用,在段合并的时候会频繁操作磁盘,所以对磁盘要求较高,当磁盘速度提升之后,集群的整体性能会大幅度提高。
3、合理使用合并
Lucene 以段的形式存储数据,当有新的数据写入索引时,Lucene 就会自动创建一个新的段。随着数据量的变化,段的数量会越来越多,消耗的多文件句柄数及 CPU 就越多,查询效率就会下降。
由于 Lucene 段合并的计算量庞大,会消耗大量的 I/O,所以 ES 默认采用较保守的策略,让后台定期进行 段合并。
4、减少 Refresh 的次数
Lucene 在新增数据时,采用了延迟写入的策略,默认情况下索引的 refresh_interval 为1 秒。Lucene 将待写入的数据先写到内存中,超过 1 秒(默认)时就会触发一次 Refresh,然后 Refresh 会把内存中的的数据刷新到操作系统的文件缓存系统中。
如果对搜索的实效性要求不高,可以将 Refresh 周期延长,例如 30 秒。这样还可以有效地减少段刷新次数,但这同时意味着需要消耗更多的 Heap 内存。
5、加大 Flush 设置
Flush 目的是把文件缓存系统中的段持久化到硬盘,当 Translog 的数据量达到 512MB 或者 30 分钟时,会触发一次 Flush。
index.translog.flush_threshold_size 参数的默认值是 512MB,可以进行修改。增加参数值意味着文件缓存系统中可能需要存储更多的数据,所以需要为操作系统的文件缓存系统留下足够的空间。
6、减少副本的数量
ES 为了保证集群的可用性,提供了 Replicas(副本)支持,然而每个副本也会执行分析、索引及可能的合并过程,所以 Replicas 的数量会严重影响写索引的效率。
当写索引时,需要把写入的数据都同步到副本节点,副本节点越多,写索引的效率就越慢。如果需要大批量写入操作,可以先禁止 Replica 复制 , index.number_of_replicas: 0 关闭副本。在写入完成后,Replica 修改回正常的状态。
4、内存设置
ES 默认安装后设置的内存是 1GB,对于任何一个现实业务来说,这个设置都太小了。如果是通过解压安装的 ES,则在 ES 安装文件中包含一个 jvm.option 文件,添加如下命令来设置 ES 的堆大小。
Xms 表示堆的初始大小,Xmx 表示可分配的最大内存,都是 1GB。
确保 Xmx 和 Xms 的大小是相同的,其目的是为了能够在 Java 垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小而浪费资源,可以减轻伸缩堆大小带来的压力。
假设有一个 64G 内存的机器,按照正常思维思考,可能会认为把 64G 内存都给 ES 比较好,但现实是这样吗, 越大越好?虽然内存对 ES 来说是非常重要的,但是答案是否定的!
ES 堆内存的分配需要满足以下两个原则:
原则1:不要超过物理内存的 50%:Lucene 的设计目的是把底层 OS 里的数据缓存到内存中。
Lucene 的段是分别存储到单个文件中的,这些文件都是不会变化的,所以很利于缓存,同时操作系统也会把这些段文件缓存起来,以便更快的访问。如果设置的堆内存过大,Lucene 可用的内存将会减少,就会严重影响降低 Lucene 的全文本查询性能。
原则2:堆内存的大小最好不要超过 32GB。
在 Java 中,所有对象都分配在堆上,然后有一个 Klass Pointer 指针指向它的类元数据。这个指针在 64 位的操作系统上为 64 位,64 位的操作系统可以使用更多的内存 (2 ^ 64)。在 32 位的系统上为 32 位,32 位的操作系统的最大寻址空间为 4GB (2^32)。但是 64 位的指针意味着更大的浪费,因为指针本身大了,浪费内存不算,更糟糕的是,更大的指针在主内存和缓存器(例如 LLC、L1 等)之间移动数据的时候,会占用更多的带宽。
最终都会采用31G设置:-Xms 31g -Xmx 31g
假设有个机器有 128 GB 的内存,可以创建两个节点,每个节点内存分配不超过 32 GB。 也就是说不超过 64 GB 内存给 ES 的堆内存,剩下的超过 64 GB 的内存给 Lucene。
5、深度分页
在使用Elasticsearch(ES)进行查询时,特别是在处理大量数据和需要实现深度分页的场景下,直接使用from和size参数进行分页可能会导致性能问题。这是因为Elasticsearch默认的分页机制是基于深度分页的,即每次查询都会从索引的开始位置扫描到指定的from位置,然后再返回结果。
默认情况下,Elasticsearch集群中每个分片的搜索结果数量限制为10000,这是为了避免潜在的性能问题。
在使用from和size参数进行分页查询时,如果from+size > 10000,则请求会直接报错,如下图所示。

解决深度分页查询问题的方案有:使用游标 scroll API 和 使用search_after。
另外,根据上图的错误提示,可以修改index. max_result_window的值(默认是10000)来解决上述问题。
PUT wkl_test/_settings
{
"index":{
"max_result_window":100000
}
}
不建议使用这个方案,因为当数据量很大的时候,from + size 超过10000后性能急剧下降,另外过大的数据量可能导致es的内存溢出,可能引发协调节点 OOM。
1. 使用游标 scroll API
scroll深度分页查询的原理:
- 快照机制:首次查询时创建数据快照,后续滚动过程中数据状态固定(不反映实时变更)。
- 游标管理:通过 scroll_id 标记当前遍历位置,每次请求基于该游标获取下一批数据。
- 分片协同:协调节点管理各分片的滚动状态,按批次(size)逐步拉取数据。
如何定位下一页?
第一次查询时会得到一个scroll_id,每次查询需携带前一次返回的 scroll_id,ES 根据该值定位下一页的游标位置。
scroll API的使用场景是:离线大数据导出和批量处理。
官方不推荐使用scroll API的原因:
1. 资源长期占用问题
Scroll 会在 ES 集群中保留搜索上下文(保持快照状态),直至 scroll 参数设定的超时时间结束(如 ?scroll=1m)。大量并发请求可能导致内存堆积,引发集群稳定性风险。
2. 数据非实时性
快照创建后的数据变更(增删改)在 Scroll 遍历过程中不可见,导出结果可能与实际数据不一致。
3. 替代方案更优
ES 7.x+ 推出 Point-in-Time (PIT) 替代 Scroll,在保证数据一致性的同时降低资源消耗。
官方建议:实时交互场景用 Search After,离线导出用 PIT。
使用scroll API的java代码样例:
import org.elasticsearch.action.search.*;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.builder.SearchSourceBuilder;
public class ScrollApiDemo {
public static void scrollQuery(RestHighLevelClient client, String index) throws Exception {
// 1. 初始化Scroll请求(保留快照5分钟)
SearchRequest searchRequest = new SearchRequest(index);
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.query(QueryBuilders.matchAllQuery());
sourceBuilder.size(100); // 每批拉取100条
searchRequest.source(sourceBuilder);
searchRequest.scroll(TimeValue.timeValueMinutes(5L)); // 保留快照5分钟
// 2. 执行首次查询,获取scroll_id
SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT);
String scrollId = response.getScrollId();
SearchHit[] hits = response.getHits().getHits();
// 3. 循环遍历所有数据
while (hits != null && hits.length > 0) {
for (SearchHit hit : hits) {
System.out.println("文档数据: " + hit.getSourceAsString());
}
// 4. 使用scroll_id获取下一批数据
SearchScrollRequest scrollRequest = new SearchScrollRequest(scrollId);
scrollRequest.scroll(TimeValue.timeValueMinutes(5L));
response = client.scroll(scrollRequest, RequestOptions.DEFAULT);
// 定位游标,将当前查询的scrollId作为下一次查询的起始位置
scrollId = response.getScrollId();
hits = response.getHits().getHits();
}
// 5. 显式释放scroll资源
ClearScrollRequest clearRequest = new ClearScrollRequest();
clearRequest.addScrollId(scrollId);
client.clearScroll(clearRequest, RequestOptions.DEFAULT);
}
}
代码说明:
- 通过 scroll_id 迭代获取数据,适合离线导出。
- 必须显式释放 scroll_id 避免内存泄漏。
2.使用search_after
search_after深度分页查询的原理:
- 动态游标:基于上一页最后一条数据的排序值(如 timestamp + _id)作为下一页查询的起始锚点。
- 实时性:每次查询实时扫描最新索引数据,支持数据变更可见。
- 唯一排序:必须指定全局唯一的排序字段组合(如 _id),否则分页可能错乱。
如何定位下一页?
首次查询需指定唯一排序字段(如 "sort": [{"timestamp": "desc"}, {"_id": "asc"}]),后续请求将上一页最后一条数据的 sort 值作为 search_after 参数传入。
「优点:」
- 无状态查询,可以防止在查询过程中,数据的变更无法及时反映到查询中。
- 不需要维护scroll_id,不需要维护快照,因此可以避免消耗大量的资源。
「缺点:」
- 由于无状态查询,因此在查询期间的变更可能会导致跨页面的不一致。
- 排序顺序可能会在执行期间发生变化,具体取决于索引的更新和删除。
- 至少需要指定一个唯一的不重复字段来排序。
- 它不适用于大幅度跳页查询,或者全量导出,对第N页的跳转查询相当于对es不断重复的执行N次search after,而全量导出则是在短时间内执行大量的重复查询。
search_after的使用场景是:实时分页查询最新数据。
使用search_after的java代码样例:
import org.elasticsearch.action.search.*;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.sort.*;
public class SearchAfterDemo {
public static void searchAfterQuery(RestHighLevelClient client, String index) throws Exception {
// 1. 首次查询:指定唯一排序字段(时间戳降序,_id升序)
SearchRequest searchRequest = new SearchRequest(index);
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.query(QueryBuilders.matchAllQuery());
sourceBuilder.size(10); // 每页10条
sourceBuilder.sort("timestamp", SortOrder.DESC);
sourceBuilder.sort("_id", SortOrder.ASC); // 确保唯一性
searchRequest.source(sourceBuilder);
// 首次查询,得到查询结果SearchHit[] hits
SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT);
SearchHit[] hits = response.getHits().getHits();
// 2. 循环遍历所有数据
while (hits != null && hits.length > 0) {
for (SearchHit hit : hits) {
System.out.println("文档数据: " + hit.getSourceAsString());
}
// 3. 获取最后一条数据的sort值作为下一页锚点
Object[] lastSortValues = hits[hits.length - 1].getSortValues();
// 4. 查询下一页,往后的每次请求都携带上一次的sort_id进行访问
sourceBuilder.searchAfter(lastSortValues);
response = client.search(searchRequest, RequestOptions.DEFAULT);
hits = response.getHits().getHits();
}
}
}
代码说明:
- 通过 searchAfter 参数实现实时分页。
- 排序字段必须唯一(如 _id),否则分页可能重复。
Search After如何确保排序字段的唯一性?
Search After通过强制要求排序字段组合具备全局唯一性来确保分页准确性,避免因排序值重复导致的数据丢失或重复。具体实现机制如下:
一、唯一性问题的根源
当排序字段存在重复值时(如仅用非唯一时间戳排序),可能出现以下问题:
1. 数据丢失:
若第一页最后一条数据的排序值为 A,而下一页存在相同排序值 A 的其他文档,ES 会跳过这些文档,导致分页遗漏。
2. 数据重复:
若排序值相同的文档跨越两页,可能被重复返回。
示例:对字段值 [1, 2, 3, 3, 4, 5] 分页(每页3条)
第一页:[1, 2, 3](最后一条排序值=3)
第二页:search_after=3 → 仅返回 [4, 5],遗漏第二个3。
二、确保唯一性的三大方案
✅ 方案1:组合唯一字段(推荐)
在排序条件中添加业务主键或 ES 内置唯一标识,形成复合排序:
"sort": [
{"timestamp": "desc"}, // 主要排序字段
{"_id": "asc"} // 唯一性保障字段
]
原理:当 timestamp 相同时,通过 _id 的唯一性精准定位文档位置。
优势:兼容所有数据类型,无需改造索引结构。
✅ 方案2:创建专用唯一字段
若业务主键(如 product_id)已存在,可直接将其加入排序:
"sort": [
{"create_time": "desc"},
{"product_id": "asc"} // 业务唯一字段
]
注意事项:
- 需确保该字段在索引中启用 doc_values,即doc_values:true,否则无法排序。
- 禁止直接用 _id 排序:因其默认关闭 doc_values,强行启用会引发性能问题。
✅ 方案3:数值化时间戳(针对日期字段)
若使用日期字段排序,将其转为毫秒时间戳避免格式歧义:
"sort": [
{
"created_at": { // 日期字段
"order": "desc",
"format": "epoch_millis" // 转换为毫秒数值
}
},
{"_id": "asc"}
]
适用场景:解决不同日期格式(如 yyyy-MM-dd HH:mm:ss 与毫秒戳)排序错乱问题。
三、核心使用规范
1. 禁止修改查询条件:分页过程中若改变 query 或 sort 字段逻辑,会导致锚点失效。
2. 严格保持排序顺序:所有分页请求的排序字段数量、顺序、方向必须完全一致。
3. 锚点值直接复用:直接使用上一页最后一条文档的 sort 值数组,无需人工处理。
// Java代码示例:获取锚点值
SearchHit lastHit = hits[hits.length - 1];
Object[] lastSortValues = lastHit.getSortValues();
sourceBuilder.searchAfter(lastSortValues); // 将上一次查询的sort值直接用作search_after参数
四、错误配置示例分析
|
错误案例 |
后果 |
修正方案 |
|
单字段排序:{"price": "desc"} |
价格相同时分页错乱 |
增加 _id 或业务主键 |
|
修改排序方向:第二页改为 "price": "asc" |
数据顺序断裂 |
全程固定排序方向 |
|
使用未启用 doc_values 的字段排序 |
ES 拒绝查询 |
检查字段映射,确保 doc_values: true |
五、最佳实践总结
1. 强制组合排序:主字段 + 唯一字段(如 timestamp + _id);
2. 数值化时间类型:避免日期格式解析差异;
3. 监控排序字段:确保其具备唯一性和稳定性;
4. 禁止中途修改:分页链中保持 query 和 sort 绝对一致。
通过以上策略,Search After 可精准定位分页边界,实现安全高效的深度分页
Scroll API和Search After关键区别对比:
|
对比维度 |
Search After |
Scroll API |
|
实时性 |
实时查询最新数据 |
基于快照(数据创建后不更新) |
|
内存消耗 |
无长期资源占用(客户端维护游标) |
需在服务端保留搜索上下文(可能导致 OOM) |
|
适用场景 |
用户实时分页(如列表页连续浏览) |
离线大数据导出/批量处理 |
|
排序要求 |
必须指定全局唯一排序字段组合(如 时间戳+_id) |
无强制要求 |
|
跳页能力 |
仅支持顺序翻页(下一页),不支持随机跳页 |
支持顺序翻页,但无法跳页 |
|
最大返回量 |
无硬性限制 |
受限于 scroll 存活时间 |
在7.*版本中,ES官方不再推荐使用Scroll方法进行深度分页,而是推荐使用带Point-in-Time (PIT)的search_after进行查询。使用SEARCH_AFTER参数通过上一页中的一组排序值检索下一页命中,使用SEARCH_AFTER需要多个具有相同查询和排序值的搜索请求。如果这些请求之间发生刷新,则结果的顺序可能会更改,从而导致页面之间的结果不一致。为防止出现这种情况,可以创建一个时间点(PIT)在搜索过程中保留当前索引状态。
通过ES语句实现search_after+PIT查询:
1.生成pit
#keep_alive必须要加上,它表示这个pit能存在多久,这里设置的是1分钟
POST test_index/_pit?keep_alive=1m

2. 在搜索请求中指定pit
在每个搜索请求中添加 keep_alive 参数来延长 PIT 的保留期,相当于是重置了一下时间。
GET _search
{
"query": {
"match_all": {}
},
"pit":{
"id":"t_yxAwEId2tsX3Rlc3QWU0hzbEJkYWNTVEd0ZGRoN0xsQVVNdwAWUGQtaXJpT0xTa2VUN0RGLXZfTlBvZwAAAAAACHG1fxY1UWNKX1RHOFMybXBaV20zbWx3enp3ARZTSHNsQmRhY1NUR3RkZGg3TGxBVU13AAA=",
"keep_alive":"5m"
},
"sort": [
{
"seq": {
"order": "asc"
}
}
],
"size": 200
}

3.删除pit
DELETE _pit
{
"id":"t_yxAwEId2tsX3Rlc3QWU0hzbEJkYWNTVEd0ZGRoN0xsQVVNdwAWUGQtaXJpT0xTa2VUN0RGLXZfTlBvZwAAAAAACHG1fxY1UWNKX1RHOFMybXBaV20zbWx3enp3ARZTSHNsQmRhY1NUR3RkZGg3TGxBVU13AAA="
}
总结:
- 禁用深度分页的 from + size,优先使用 Search After;
- Scroll API 已过时,仅在 ES 6.x 以下版本必要时使用,推荐使用带PIT的search_after;
- 深度页码控制应通过业务逻辑实现,而非盲目调整 max_result_window。
3.search_after支持的排序字段
Search After支持哪些数据类型排序?
Search After 支持多种数据类型排序,其核心要求是字段必须启用 doc_values(默认开启)且具备可排序性,具体支持类型如下:
一、支持的核心数据类型
1. 数值类型
- 整型:integer、long、short、byte
- 浮点型:float、double、half_float、scaled_float
应用场景:价格排序、年龄排序等。
示例:"sort": [{"price": "desc"}]。
2. 日期类型
标准日期:date(支持多种格式,如 yyyy-MM-dd、epoch_millis)
注意事项:需统一格式(建议转为毫秒时间戳避免歧义)
示例:
"sort": [
{
"created_at": {
"order": "desc",
"format": "epoch_millis" // 转为毫秒数值
}
}
]
3. 字符串类型
Keyword 字段:keyword(精确值排序)
限制:text 类型默认不可排序(需用 keyword 子字段)
示例:"sort": [{"name.keyword": "asc"}]
4. 布尔类型
boolean:按 true/false 排序(true > false)
5. 地理坐标
geo_point:支持距离排序(如 _geo_distance)
二、特殊字段的使用限制
❌ 禁止单独使用的字段
|
字段类型 |
问题 |
修正方案 |
|
_id |
默认关闭 doc_values |
需启用 doc_values(不推荐) |
|
text |
分词后无法直接排序 |
改用 keyword 子字段 |
✅ 唯一性保障方案
若主排序字段可能重复(如时间戳精度不足),需添加辅助字段保证全局唯一性:
"sort": [
{"timestamp": "desc"},
{"_id": "asc"} // 辅助唯一字段:ml-citation{ref="2,5" data="citationList"}
]
原理:当主字段相同时,通过辅助字段(如 _id 或业务主键)精准定位文档。
三、数据类型支持总结
|
数据类型 |
是否支持 |
唯一性保障 |
使用场景 |
|
数值类型 |
✅ |
需组合唯一字段 |
价格/销量排序 |
|
日期类型 |
✅ |
转毫秒时间戳 |
按创建时间排序 |
|
Keyword 字符串 |
✅ |
自身可唯一(如ID) |
名称/编码排序 |
|
布尔类型 |
✅ |
需组合唯一字段 |
状态过滤排序 |
|
地理坐标 |
✅ |
需组合唯一字段 |
距离排序 |
|
text 类型 |
❌ |
不可用 |
需改用 keyword 子字段 |
|
_id 字段 |
⚠️(受限) |
自身唯一但需配置 |
不推荐直接排序 |
四、search_after支持多字段复合排序,这是其实现精准分页的核心能力
GET /orders/_search
{
"size": 10,
"sort": [
{ "order_date": "desc" }, // 主排序字段
{ "product_id": "asc" } // 辅助唯一字段
],
"search_after": [
"2025-07-01T12:00:00.000Z", // 上一页最后一条的 order_date
"prod-10086" // 上一页最后一条的 product_id
]
}
复合排序原则:
1. 字段顺序固定:所有请求中字段顺序必须一致(如先日期后ID);
2. 方向一致性:排序方向(asc/desc)不可中途变更;
3. 唯一性保障:末尾字段必须全局唯一(如业务主键或 _id)。
多数据类型复合排序示例:
1. 数值 + 唯一标识组合
"sort": [
{ "rating": "desc" }, // 数值评分
{ "user_id": "asc" } // 唯一用户ID
]
2. 日期转毫秒时间戳,避免日期格式歧义:
"sort": [
{
"created_at": {
"order": "desc",
"format": "epoch_millis" // 转为数值
}
},
{ "_id": "asc" }
]
3. 布尔值 + 地理坐标
"sort": [
{ "is_premium": "desc" }, // 布尔值(true > false)
{
"_geo_distance": {
"location": [116.4, 39.9], // 坐标点
"order": "asc",
"unit": "km"
}
}
]
4. Keyword 多字段排序
"sort": [
{ "category.keyword": "asc" }, // 分类名称
{ "brand.keyword": "desc" } // 品牌名称
]
避坑指南:
- 禁止单独排序 text 类型(需用 .keyword 子字段);
- _id 字段需手动启用 doc_values(不推荐)。
如何通过Elasticsearch命令和java代码启用doc_values?
1. Elasticsearch 映射配置
doc_values 默认对非文本字段(数值/日期/keyword)开启,如需显式控制:
PUT /your_index
{
"mappings": {
"properties": {
"price": {
"type": "integer",
"doc_values": true // 显式启用(默认true,可省略)
},
"description": {
"type": "text", // text类型默认关闭doc_values
"fields": {
"keyword": {
"type": "keyword",
"doc_values": true // 为子字段启用
}
}
}
}
}
}
注意:text类型需通过keyword子字段启用doc_values。
2. Java 代码实现
import org.elasticsearch.client.indices.CreateIndexRequest;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentFactory;
// 创建索引时启用 doc_values
CreateIndexRequest request = new CreateIndexRequest("products");
XContentBuilder mapping = XContentFactory.jsonBuilder()
.startObject()
.startObject("properties")
.startObject("price")
.field("type", "double")
.field("doc_values", true) // 显式启用
.endObject()
.startObject("product_name")
.field("type", "text")
.startObject("fields")
.startObject("keyword")
.field("type", "keyword") // 启用keyword子字段
.field("doc_values", true)
.endObject()
.endObject()
.endObject()
.endObject()
.endObject();
request.mapping(mapping);
client.indices().create(request, RequestOptions.DEFAULT);
6、ES容灾高可用方案
以下是针对Elasticsearch节点故障与集群崩溃场景的高可用保障方案,结合原理与操作步骤详细说明。
一、单节点崩溃保障措施
核心原理:副本分片自动接管
当某数据节点宕机时,其持有的主分片若存在副本,集群会触发以下流程(30秒内完成):
1. 检测离线:Master节点通过心跳检测(默认10秒)发现节点失联。
2. 副本提升:该节点的主分片对应的健康副本分片立即升级为新主分片(如原P0在node1,其副本R0在node2自动升级)。
3. 重建副本:在新节点上重建缺失的副本分片(集群状态Yellow→Green)。
实现步骤(需提前配置):
1. 配置副本数≥1(新建索引时设置):
PUT /your_index
{
"settings": {
"number_of_replicas": 1 // 关键参数!至少1个副本
}
}
2. 验证分片分布:
GET _cat/shards?v # 检查每个分片是否有副本且分布在不同节点
3. 故障模拟验证:
- 手动停止一个节点:docker pause es-node01
- 监控恢复状态:GET _cluster/health?pretty(观察unassigned_shards归零时间)
效果:读写服务不中断,查询可能短暂降级(副本提升期间)。
二、全集群崩溃保障措施
核心原理:跨集群容灾(CCR/多可用区)
|
方案 |
原理 |
适用场景 |
|
多可用区部署 |
数据节点跨3个物理隔离区域,某区故障时负载均衡自动剔除故障节点 |
云服务环境(如AWS/Aliyun) |
|
跨集群复制(CCR) |
主集群(Leader)数据实时同步到备用集群(Follower),故障时秒级切换 |
混合云/自建集群 |
1、多可用区部署步骤(以腾讯云为例):
1. 购买集群时选择多可用区:
- 数据节点数需为可用区倍数(如3可用区选6节点)
- 强制启用3个专用Master节点(跨区部署防脑裂)
2. 配置分片分布规则:
PUT _cluster/settings
{
"persistent": {
"cluster.routing.allocation.awareness.attributes": "zone" // 按可用区均衡分布
}
}
2、CCR容灾同步步骤:
1. 配置集群间联通(主集群→备集群):
PUT _cluster/settings
{
"persistent": {
"cluster.remote.backup_cluster.seeds": "192.168.1.10:9300"
}
}
2. 创建Follower索引(在备集群操作):
POST /source_index/_ccr/follow?wait_for_active_shards=1
{
"remote_cluster": "backup_cluster",
"leader_index": "source_index"
}
3. 故障切换流程(主集群崩溃时):
# 1. 暂停CCR同步
POST /follower_index/_ccr/pause_follow
# 2. 关闭Follower只读状态
POST /follower_index/_close
PUT /follower_index/_settings
{ "index.blocks.write": false }
# 3. 开放读写
POST /follower_index/_open # 备集群正式接管读写
三、增强防御的关键措施
1、防脑裂配置(自建集群必做):
# elasticsearch.yml
discovery.zen.minimum_master_nodes: 2 # 公式=(master节点数/2)+1
cluster.no_master_block: write # 无主节点时拒绝写入防数据冲突:ml-citation{ref="5,6" data="citationList"}
2、快照冷备份(最后防线):
- 创建仓库:PUT _snapshot/my_backup { "type": "fs", "settings": { "location": "/mnt/backups" } }
- 定时任务:PUT _slapshot/my_backup/snapshot_20230702?wait_for_completion=true
3、客户端重试机制:
// Java示例:配置失败重试
RestClientBuilder.build()
.setRetryHandler(new RetryHandler() {
public boolean retryRequest(...) {
return statusCode == 503; // 遇到集群不可用状态码时重试
}
});
火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。
更多推荐
所有评论(0)