大数据_ES
前言
Github:https://github.com/HealerJean
一、基础介绍
1、设计目标:面向搜索和实时数据分析,需快速适应多变的查询需求,允许数据结构随业务迭代动态调整。
2、典型场景:日志分析、电商搜索、内容推荐等。
3、需求特点
- 数据字段可能随业务迭代频繁新增(如用户行为日志新增埋点字段)。
- 查询条件灵活多变(如临时增加筛选维度),需支持动态字段索引。
- 在搜索引擎和日志分析中表现出色,它具有快速、可扩展、实时搜索等特点,适用于实时搜索、日志分析、数据挖掘等场景
3、存储逻辑
- 数据以
JSON文档形式存储,每个文档有独立的字段集合,字段可动态增删,允许同一索引下的不同文档具有不同的字段,甚至字段的数据类型也可动态调整。 - 倒排索引(
InvertedIndex)直接关联字段值与文档ID,支持快速全文检索和动态字段查询。
4、灵活性体现
- 字段动态添加:当写入新文档时,若包含索引中不存在的字段,
ES会自动识别并添加该字段(可通过dynamic参数控制)。 - 类型动态识别:字段类型(如字符串、数值、日期)可根据首次写入的数据自动推断,无需预先定义。
- 嵌套结构支持:
JSON天然支持嵌套对象和数组,ES 可直接索引嵌套字段(如address.city),无需额外建模。
5、灵活性优势
- 字段的增删不影响其他文档的存储结构,只需更新索引元数据。
- 不同文档的字段差异被索引自动处理,查询时可通过
_source字段灵活选择返回内容。
6、动态修改结构
- 新增字段:直接写入新字段,自动更新索引,无性能影响。
- 修改字段类型:可通过
update mapping动态修改(需重建索引)。 - 删除字段:从索引中移除字段元数据,文档物理数据保留(可通过
GC清理)。 - 修改列族结构:无列族概念,无需处理。
二、亿级数据分页
1、分页挑战
1)深度分页性能问题
查询逻辑:传统 from + size 分页(如 from=10000, size=10)在亿级数据下,ES 需:每个分片查询from + size条数据(如 10010 条);汇总所有分片结果并排序;截取 from 到 from+size的数据。
问题出现:时间复杂度为 O(from + size) ,当 from 超过 1 万时,查询耗时呈指数级增长,甚至触发 ES 默认限制(from + size ≤ 10000)。
2)高效分页方案:Search_After(推荐)
核心原理:基于游标(Cursor) 而非偏移量分页,通过记录上一页最后一条文档的排序值,作为下一页查询的起点,通过 “接力” 方式获取下一页数据,每次查询都基于最新索引状态,避免全量扫描。
关键逻辑
- 首次查询按排序字段返回前
size条数据,并记录最后一条的排序值(sort_values)。 - 后续请求通过
search_after参数传入sort_values,获取下一页数据。
优势:时间复杂度降为O(size),支持无限滚动,且不受 from + size限制。
关键优化点
- 排序字段选择
- 主排序字段建议为时间戳、数值型字段(如
ID),确保有序性; - 必须包含
_id作为辅助排序,避免相同主排序值导致结果重复。
- 主排序字段建议为时间戳、数值型字段(如
- 分页大小控制
- 每页数据量建议
size= 100 -1000,过大可能导致内存溢出,过小会增加分页次数。
- 每页数据量建议
实现步骤
- 首次查询:指定排序字段(需包含唯一字段,如时间戳 +
_id),获取第一页数据:
GET /index/_search
{
"size": 100,
"sort": [
{"timestamp": "asc"}, // 主排序字段(如时间戳)
{"_id": "asc"} // 辅助排序字段(保证唯一性)
],
"query": { ... }
}
-
提取游标:从返回结果中获取最后一条文档的排序值(如
sort: [1685432100, "doc100"])。 -
后续查询:通过
search_after参数传入游标,继续获取下一页:
GET /index/_search
{
"size": 100,
"sort": [{"timestamp": "asc"}, {"_id": "asc"}],
"search_after": [1685432100, "doc100"],
"query": { ... }
}
示例代码(Java):
// 首次查询
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder()
.query(QueryBuilders.matchAllQuery())
.size(10)
.sort(new FieldSortBuilder("timestamp").order(SortOrder.DESC))
.sort(new FieldSortBuilder("_id").order(SortOrder.DESC));
SearchRequest request = new SearchRequest(INDEX_NAME).source(sourceBuilder);
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 后续查询(假设已有上一页的sortValues)
Object[] lastSortValues = ...; // 上一页最后一条的排序值
sourceBuilder.searchAfter(lastSortValues);
response = client.search(request, RequestOptions.DEFAULT);
3)大数据导出方案:Scroll API(适合离线场景)
核心原理
- 一次性查询所有符合条件的文档并生成游标,通过游标分批获取数据,适合全量导出(如亿级数据导出到
CSV)。 - 首次查询时生成一个 “快照”,将结果数据缓存在内存中,后续通过滚动参数逐步获取数据,不维护查询状态。
-
注意:
Scroll游标会保持索引分片锁定,不适合实时数据场景,仅用于离线批量处理。 - 关键逻辑
- 第一次查询返回前
size条数据,并生成scroll_id。 - 后续请求通过
scroll_id持续获取数据,直到所有结果被遍历。
- 第一次查询返回前
实现步骤
-
创建游标
GET /index/_search?scroll=1m // 游标存活时间1分钟 { "query": { ... }, "size": 1000 } -
获取数据:通过返回的
_scroll_id持续拉取数据:POST /_search/scroll { "scroll": "1m", "scroll_id": "DXF1ZXJ5QW5kRmV0Y2g..." } -
处理数据:循环获取直至无结果返回,适合导出到文件或数据湖。
示例代码(Java):
// 初始化Scroll查询
SearchRequest request = new SearchRequest(INDEX_NAME);
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder()
.query(QueryBuilders.matchAllQuery())
.size(1000)
.scroll(new TimeValue(60, TimeUnit.SECONDS)); // 设置滚动超时
request.source(sourceBuilder);
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
String scrollId = response.getScrollId();
// 滚动获取后续数据
while (true) {
Scroll scroll = new Scroll(new TimeValue(60, TimeUnit.SECONDS));
ScrollRequest scrollRequest = new ScrollRequest(scrollId);
scrollRequest.scroll(scroll);
response = client.scroll(scrollRequest, RequestOptions.DEFAULT);
SearchHit[] hits = response.getHits().getHits();
if (hits.length == 0) break;
// 处理数据...
}
2、性能对比
| 维度 | Scroll |
Search_After |
|---|---|---|
| 数据实时性 | 基于快照,不反映后续更新 | 每次查询基于最新索引状态 |
| 交互响应 | 批量处理,适合离线场景 | 单次查询轻量,适合高频实时请求 |
| 内存占用 | 缓存全量结果,可能占用大量内存 | 仅处理当前页数据,内存占用低 |
| 长时间查询 | 适合批量导出全量数据(如数据备份) | 适合实时分页(如前端列表翻页) |
| 性能影响 | 长时间运行可能导致集群负载升高 | 单次查询轻量,适合高频分页 |
| 动态条件支持 | 快照生成后无法修改查询条件 | 每次查询可重新设置筛选、排序条件 |
| 排序稳定性 | 结果顺序固定(基于快照) | 若排序字段值重复,可能出现数据重复 |
| 跳页支持 | 需按顺序滚动,无法直接跳页 | 理论上可通过计算排序值跳页,但页数过大时性能差 |
1)本质差异点
Scroll是 “离线式” 批量处理,适合一次性获取全量数据(如导出),但不适合实时交互。Search_After是 “在线式” 实时分页,适合前端用户翻页,但不适合跨页跳转(如直接跳转到第 1000 页)。
2)实际场景选择建议
| 方案 | 适合场景 | 性能 | 复杂度 | 一致性 |
|---|---|---|---|---|
Search_After |
实时交互分页(如前端列表) | 秒级响应 | 中等 | 近实时(可能漏新数据) |
Scroll API |
离线全量导出(如数据备份) | 分钟级(亿级) | 低 | 快照级一致性 |
FROM + SIZE |
浅分页(页数 < 100) | 毫秒级 | 低 | 实时 |
a、选择 Scroll 的场景:
-
离线数据导出:如将
Elasticsearch数据全量迁移到数据库,不要求实时性。 - 历史数据归档:处理数周 / 数月前的历史日志,不关注数据更新。
- 批量索引重建:重新索引大量文档时,通过
Scroll分批处理。
b、选择 Search_After 的场景:
-
前端列表分页:如电商商品、新闻资讯的分页展示,要求实时反映数据变更。
-
实时日志查询:监控系统的日志分页,需展示最新产生的日志。
c、跳页需求的解决方案:
- 若需要支持任意跳页(如直接访问第
N页),且数据量较小(如10万级以内),可使用传统from + size(但from过大时性能极差)。 - 若数据量极大,建议结合业务逻辑限制跳页范围(如只允许跳转到前
100页),或通过Search_After预计算目标页排序值(但页数过大时仍不推荐)。
三、Hbase + Elasticsearch
1、HBase 与 ES 的核心差异
数据模型与查询逻辑的本质区别
| 维度 | HBase(分布式列式数据库) |
Elasticsearch(分布式搜索引擎) |
|---|---|---|
| 数据模型 | 列式存储,数据按列族组织,弱灵活(列族修改成本高) | 文档型存储(JSON 格式),Schema 动态映射(字段可随时添加) |
| 查询核心 | 基于RowKey 的点查 / 范围查询,依赖 RowKey 设计优化 |
基于倒排索引的全文检索 / 复杂条件查询,支持分词、聚合、排序 |
| 存储与计算架构 | 底层依赖 HDFS,计算与存储分离(RegionServer 处理查询) |
存储与计算一体化(节点同时负责索引和查询) |
| 事务支持 | 仅支持单行事务,不支持跨行 / 跨表事务 | 无事务支持,仅保证最终一致性 |
2、为什么 HBase 的 “查询效率” 不能直接与 ES 对比?
1、HBase 的 “慢”:特定场景下的局限性
- 复杂查询慢:无原生二级索引,多条件查询需全表扫描或依赖
Phoenix(性能仍弱于ES)。 - 索引维护成本:若强行用
HBase实现ES的查询能力(如为每个查询条件建索引),会导致写入性能暴跌(索引更新开销)。
2. ES 的 “快”:特定场景下的优势
- 全文检索快:倒排索引天然适合关键词搜索、模糊匹配(如 “查询包含‘云计算’的文档”)。
- 聚合分析快:内置分词、统计函数(如
avg、groupby),适合分析型查询(如 “按地区统计订单量”)。
3. HBase 的 “快”:ES 无法替代的场景
- 海量数据点查快:单
RowKey查询延迟10ms内,支持百万级QPS(ES单点查延迟50ms+,QPS约十万级)。 - 高吞吐写入快:分布式架构下写入吞吐量可达
GB/s(ES写入受索引更新影响,吞吐量约百MB/s)。 - 时序数据存储高效:列式存储对稀疏数据(如监控指标)压缩率高(比
ES节省50%以上存储)。
3、适用场景对比:选 HBase 还是 ES
1、优先选 HBase 的场景
- 实时高频点查:如用户画像查询(根据用户
ID查详情)、订单状态查询(根据订单号查状态)。 - 海量时序数据:如物联网设备日志(按时间戳 + 设备
ID查询)、金融交易流水(按交易ID检索)。 - 高吞吐写入 + 低频查询:如日志采集系统(先存后查,写入量远大于查询量)。
2、优先选 ES 的场景
- 全文搜索与模糊查询:如电商商品搜索(关键词匹配标题、描述)、日志分析(按日志内容检索)。
- 复杂分析与聚合:如用户行为分析(按标签、地域分组统计)、报表生成(多维度聚合计算)。
- 动态
Schema需求:数据字段频繁变化(如用户自定义属性),ES 可自动映射新字段。
4、生产实践:HBase 与 ES 如何互补?
1、双写架构:HBase 存全量数据,ES 做查询加速
- 场景:用户画像系统,既需要按用户
ID快速查询详情(HBase点查),又需要按标签筛选用户(ES聚合)。 - 实现:数据写入时同时写入
HBase和ES,HBase作为主存储,ES作为查询索引。 - 优势:
HBase保证数据可靠性,ES提供灵活查询,两者性能互补。
2、分场景使用:HBase 处理实时交易,ES 处理历史分析
- 场景:金融交易系统,实时交易数据(当日)用
HBase存储(低延迟点查),历史数据(按月)同步到ES(分析统计)。 - 优势:避免
ES存储海量数据导致索引膨胀,同时利用HBase的高吞吐处理实时写入。
5、总结:技术选型的核心逻辑**
- 不要用
ES替代HBase:若业务以 “高频点查 + 海量存储” 为主(如Kafka消息队列下游存储),ES的写入性能和存储成本会成为瓶颈。 - 也不要用
HBase硬扛复杂查询:若业务需要 “全文搜索 + 多维度分析”(如日志检索平台),强行用HBase开发会导致系统低效。 - 最佳实践:根据业务场景拆分数据链路 ——
HBase负责 “存储与实时点查”,ES负责 “查询与分析”,通过数据同步工具(如Canal、Logstash)保持两者数据一致性。
6)如何同时写入 es 和 hbase
| 方案 | 场景 | 推荐方案 | 理由 |
|---|---|---|---|
| 同步双写模式 | 开发成本敏感 | 客户端同步双写 | 实现简单,快速迭代 |
| 异步双写模式(通过消息队列) | 写入性能敏感 | 消息队列异步双写 | 解耦业务逻辑,提升吞吐量 |
基于 HBase 触发 ES 同步 |
数据一致性要求极高 | HBase 协处理器 + 异步写入 ES |
确保 HBase 写入成功后 ES 必定更新 |
a、同步双写模式
- 原理:应用程序同时向 ES 和 HBase 发送写入请求,等待两者都返回成功后,再确认操作完成。
- 优点:实现简单,数据一致性高。
- 缺点:写入性能受限于最慢的系统,可能影响业务响应速度。
b、异步双写模式(通过消息队列)
- 原理:应用程序先将数据写入消息队列(如
Kafka),再由消费者分别写入ES和HBase。 - 优点:解耦业务系统与存储系统,提升写入性能,支持流量削峰。
- 缺点:存在短暂的数据不一致风险(需通过重试机制保证最终一致性)。
c、基于 HBase 触发 ES 同步(适用于 HBase 为主存储的场景)
- 原理:通过
HBase的协处理器(Coprocessor)或触发器,在数据写入HBase后自动同步到ES。 - 优点:数据一致性由存储层保证,减少应用层逻辑。
- 缺点:开发复杂度较高,需深入理解
HBase底层机制。
7)查询性能对比
| 查询场景 | HBase(优化后) | MySQL(InnoDB) | Elasticsearch |
|---|---|---|---|
单 RowKey 查询 |
10ms 内(百万级 QPS) | 5~10ms(万级 QPS) | 50~100ms(十万级 QPS) |
| 范围查询(10 万条) | 100ms~1s(分布式并行) | 50ms~500ms(索引扫描) | 50~200ms(倒排索引) |
| 复杂多条件查询 | 性能较差(需二级索引) | 优秀(原生 SQL 支持) | 优秀(DSL 灵活查询) |


