本文是 ES 存储引擎系列的延伸篇,承接段、提交点与 Translog 和近实时搜索与实时 CRUD。理解了 Segment 的不可变性后,就能真正理解 PIT 是如何”冻结视图”实现一致性深度分页的。
一、什么是 PIT
PIT(Point-In-Time)是 Elasticsearch 在 7.10 版本引入的关键特性。它允许你在一个特定的、一致的时间点对索引进行查询,即使在查询过程中数据正在被实时更新、删除或修改。
可以把它想象成给整个索引拍一张**”快照”——但它不复制数据,只记录一个”视图”(即 Lucene Segment 的引用列表)。在这个快照的”时间窗口”内,你执行的所有分页查询看到的都是完全相同的数据状态**。
1 | 时间轴:[创建PIT] ---- [数据变更/段合并] ---- [PIT查询] ---- [PIT删除/过期] |
二、为什么需要 PIT
2.1 传统分页的核心痛点
在频繁更新的索引上使用 from/size 或 search_after 分页,会遇到数据漂移(Data Drift)问题:
- 你执行第一次查询(
from=0, size=10),获取了 10 条结果。 - 这时有一条新数据写入,或原有的一条被删除。
- 你执行第二次查询(
from=10, size=10)获取下一页。 - 由于索引状态已改变,你可能会看到:
- 重复数据:上页的最后一条又被排到了这页的第一条。
- 丢失数据:某条本该在这页的数据,因排序变化被挤到了下一页或消失。
这就像一个按字母排序的名单,翻到第 2 页时正好有人插入到第 1、2 页之间,所有人位置后移一位——第 1 页最后一个人又出现在第 2 页开头。
2.2 各分页方案对比
| 分页方案 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
from/size | 浅分页(前几页) | 简单直观,支持随机跳页 | 深分页性能差;数据不一致 |
scroll | 大批量导出(一次性) | 数据一致,适合全量导出 | 有状态,资源消耗大;不适合实时分页 |
search_after | 深度分页(无跳页需求) | 性能好,无深度限制 | 不支持随机跳页;无 PIT 时数据不一致 |
PIT + search_after | 深度分页(推荐) | 数据一致 + 性能优秀 | 需管理 PIT 生命周期 |
官方推荐:PIT + search_after 是替代 scroll API 的现代方案,比 scroll 更高效、更灵活。
三、PIT 的底层原理
3.1 依赖 Lucene Segment 的不可变性
理解 PIT 的关键在于 ES 底层的 Segment 机制(详见段、提交点与 Translog):
- ES 的数据存储在不可变的 Segment 文件中。
- 新数据写入产生新 Segment;删除/更新只标记旧数据,不立即删除文件。
- Lucene 定期执行 Segment 合并(Merge),清理已删除数据。
3.2 PIT 如何冻结视图
创建 PIT 时,ES 会:
- 记录当前所有 Segment 的引用列表。
- 阻止这些 Segment 被合并/删除,即使后续发生大量写入和合并。
- 后续在该 PIT 下的所有查询,只会看到这些被”冻结”的 Segment。
这就是 PIT 不复制数据却能保证一致性的原因:它只是持有了旧 Segment 的引用,阻止了这些文件被回收。
3.3 PIT 存储的内容
| 内容 | 说明 |
|---|---|
| Segment 列表和位置 | 记录创建时每个分片上活跃的 Segment 文件列表(最重要) |
| 时间戳和存活时间 | 记录创建时间和 keep_alive,用于过期清理 |
| 索引映射和状态 | 记录各索引的 mapping 快照,确保字段解析一致 |
| 唯一的 PIT ID | Base64 编码字符串,包含分片路由信息 |
四、PIT 的使用
4.1 创建 PIT
1 | POST /my-index/_pit?keep_alive=1m |
keep_alive:告诉 ES”请将这个时间点视图至少保持 1 分钟“。- 响应返回一个
id,作为后续查询的标识。支持多索引和通配符(/logs-*/_pit)。
4.2 使用 PIT + search_after 深度分页
第一页查询:
1 | POST /_search |
关键注意事项:
- 使用 PIT 的查询不需要在 URL 中指定索引名(索引信息已编码在 PIT id 里)。
sort必须是唯一的字段组合,推荐_shard_doc(ES 内置唯一排序字段)或时间字段 + _id。keep_alive续期:每次查询带上 PIT id 会重新延长生命周期,相当于”续费”。
取第一页最后一条的 sort 值,作为下一页的 search_after:
1 | POST /_search |
⚠️ 每次响应都会返回一个新的 pit_id,下一次查询要用最新返回的 pit_id,而非最初创建的 id。
4.3 删除 PIT
1 | DELETE /_pit |
PIT 会占用 ES 资源(主要是文件句柄,用于保留旧 Segment 不被删除)。完成分页后必须主动删除,释放资源。
五、最佳实践
5.1 keep_alive 设置原则
| 场景 | 建议 keep_alive |
|---|---|
| 实时用户翻页(前端) | 1m ~ 3m |
| 后台批量数据导出 | 5m ~ 15m |
| 大规模离线数据同步 | 30m(谨慎使用) |
- 不宜过长:太长会导致大量旧 Segment 无法合并释放。
- 必须续期:分页可能超过初始
keep_alive时,务必每次查询续期,否则 PIT 过期会触发search_phase_execution_exception。
5.2 排序唯一性是基石
1 | "sort": [ |
_shard_doc是 ES 7.12+ 提供的内置唯一字段,性能最优。- 也可用
_id,但字符串排序性能略逊。
5.3 资源管理
主动清理:用完即删,不要依赖过期自动清理。业务代码应在
finally块中删除 PIT。1
2
3
4
5
6String pitId = createPit();
try {
doPageSearch(pitId);
} finally {
deletePit(pitId);
}监控 PIT 数量:通过
GET /_nodes/stats/indices/search关注open_contexts字段,持续增大说明有 PIT 泄漏。
5.4 PIT 的局限性
| 限制 | 说明 |
|---|---|
| 不支持随机跳页 | 只能顺序翻页,不能直接跳到第 N 页 |
| 不保留实时数据 | 只是”视图快照”,不适合查询最新数据 |
| 集群重启失效 | 节点宕机或集群重启后自动失效 |
| 资源占用 | 大量并发 PIT 会阻止 Segment 合并,增大磁盘/内存压力 |
不支持 from | 必须配合 search_after,不能使用 from 偏移 |
六、PIT vs Scroll API
| 对比维度 | Scroll API | PIT + search_after |
|---|---|---|
| 引入版本 | 早期版本 | 7.10+ |
| 数据一致性 | ✅ 一致 | ✅ 一致 |
| 性能 | 一般(需维护游标上下文) | 更好(无状态游标,只需 sort 值) |
| 并发查询 | ❌ 不支持 | ✅ 支持(多消费者共享同一 PIT) |
| 适用场景 | 大批量一次性导出(旧方案) | 用户分页、深度翻页(推荐) |
| 官方态度 | 逐渐废弃 | ✅ 推荐 |
ES 官方在 8.x 文档中明确建议:新项目应使用 PIT + search_after 替代 scroll。
七、总结
| 要点 | 说明 |
|---|---|
| 核心价值 | 在频繁变更的索引上实现一致性分页,解决数据漂移问题 |
| 底层机制 | 持有 Lucene Segment 引用,阻止段合并/回收 |
| 推荐搭配 | PIT + search_after,排序字段必须唯一 |
| 生命周期 | 创建 → 续期(每次查询带 keep_alive)→ 主动删除 |
| 适用版本 | ES 7.10+,推荐 7.12+(_shard_doc 支持) |



