前言

Github:https://github.com/HealerJean

博客:http://blog.healerjean.com

一、基础介绍

1、设计目标:面向搜索和实时数据分析,需快速适应多变的查询需求,允许数据结构随业务迭代动态调整。

2、典型场景:日志分析、电商搜索、内容推荐等。

3、需求特点

  • 数据字段可能随业务迭代频繁新增(如用户行为日志新增埋点字段)。
  • 查询条件灵活多变(如临时增加筛选维度),需支持动态字段索引。
  • 在搜索引擎和日志分析中表现出色,它具有快速、可扩展、实时搜索等特点,适用于实时搜索、日志分析、数据挖掘等场景

3、存储逻辑

  • 数据以 JSON 文档形式存储,每个文档有独立的字段集合,字段可动态增删,允许同一索引下的不同文档具有不同的字段,甚至字段的数据类型也可动态调整。
  • 倒排索引(Inverted Index)直接关联字段值与文档 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 而非偏移量分页,通过记录上一页最后一条文档的排序值,作为下一页查询的起点,通过 “接力” 方式获取下一页数据,每次查询都基于最新索引状态,避免全量扫描。

关键逻辑

  1. 首次查询按排序字段返回前 size 条数据,并记录最后一条的排序值(sort_values)。
  2. 后续请求通过 search_after 参数传入 sort_values,获取下一页数据。

优势:时间复杂度降为O(size),支持无限滚动,且不受 from + size限制。

关键优化点

  • 排序字段选择
    • 主排序字段建议为时间戳、数值型字段(如 ID),确保有序性;
    • 必须包含 _id 作为辅助排序,避免相同主排序值导致结果重复。
  • 分页大小控制
    • 每页数据量建议 size= 100 -1000,过大可能导致内存溢出,过小会增加分页次数。

实现步骤

  1. 首次查询:指定排序字段(需包含唯一字段,如时间戳 +_id),获取第一页数据:
GET /index/_search
{
  "size": 100,
  "sort": [
    {"timestamp": "asc"},  // 主排序字段(如时间戳)
    {"_id": "asc"}         // 辅助排序字段(保证唯一性)
  ],
  "query": { ... }
}
  1. 提取游标:从返回结果中获取最后一条文档的排序值(如sort: [1685432100, "doc100"])。

  2. 后续查询:通过 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 游标会保持索引分片锁定,不适合实时数据场景,仅用于离线批量处理。

  • 关键逻辑
    1. 第一次查询返回前 size 条数据,并生成 scroll_id
    2. 后续请求通过 scroll_id 持续获取数据,直到所有结果被遍历。

实现步骤

  1. 创建游标

    GET /index/_search?scroll=1m  // 游标存活时间1分钟
    {
      "query": { ... },
      "size": 1000
    }
    
  2. 获取数据:通过返回的_scroll_id持续拉取数据:

    POST /_search/scroll
    {
      "scroll": "1m",
      "scroll_id": "DXF1ZXJ5QW5kRmV0Y2g..."
    }
    
  3. 处理数据:循环获取直至无结果返回,适合导出到文件或数据湖。

示例代码(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、HBaseES 的核心差异

数据模型与查询逻辑的本质区别

维度 HBase(分布式列式数据库) Elasticsearch(分布式搜索引擎)
数据模型 列式存储,数据按列族组织,弱灵活(列族修改成本高) 文档型存储(JSON 格式),Schema 动态映射(字段可随时添加)
查询核心 基于RowKey 的点查 / 范围查询,依赖 RowKey 设计优化 基于倒排索引的全文检索 / 复杂条件查询,支持分词、聚合、排序
存储与计算架构 底层依赖 HDFS,计算与存储分离(RegionServer 处理查询) 存储与计算一体化(节点同时负责索引和查询)
事务支持 仅支持单行事务,不支持跨行 / 跨表事务 无事务支持,仅保证最终一致性

2、为什么 HBase 的 “查询效率” 不能直接与 ES 对比?

1、HBase 的 “慢”:特定场景下的局限性

  • 复杂查询慢:无原生二级索引,多条件查询需全表扫描或依赖 Phoenix(性能仍弱于 ES)。
  • 索引维护成本:若强行用 HBase 实现 ES 的查询能力(如为每个查询条件建索引),会导致写入性能暴跌(索引更新开销)。

2. ES 的 “快”:特定场景下的优势

  • 全文检索快:倒排索引天然适合关键词搜索、模糊匹配(如 “查询包含‘云计算’的文档”)。
  • 聚合分析快:内置分词、统计函数(如 avggroup by),适合分析型查询(如 “按地区统计订单量”)。

3. HBase 的 “快”:ES 无法替代的场景

  • 海量数据点查快:单 RowKey 查询延迟 10ms 内,支持百万级 QPSES 单点查延迟 50ms+QPS 约十万级)。
  • 高吞吐写入快:分布式架构下写入吞吐量可达 GB/sES 写入受索引更新影响,吞吐量约百 MB/s)。
  • 时序数据存储高效:列式存储对稀疏数据(如监控指标)压缩率高(比 ES 节省 50% 以上存储)。

3、适用场景对比:选 HBase 还是 ES

1、优先选 HBase 的场景

  • 实时高频点查:如用户画像查询(根据用户 ID 查详情)、订单状态查询(根据订单号查状态)。
  • 海量时序数据:如物联网设备日志(按时间戳 + 设备 ID 查询)、金融交易流水(按交易 ID 检索)。
  • 高吞吐写入 + 低频查询:如日志采集系统(先存后查,写入量远大于查询量)。

2、优先选 ES 的场景

  • 全文搜索与模糊查询:如电商商品搜索(关键词匹配标题、描述)、日志分析(按日志内容检索)。
  • 复杂分析与聚合:如用户行为分析(按标签、地域分组统计)、报表生成(多维度聚合计算)。
  • 动态 Schema 需求:数据字段频繁变化(如用户自定义属性),ES 可自动映射新字段。

4、生产实践:HBaseES 如何互补?

1、双写架构:HBase 存全量数据,ES 做查询加速

  • 场景:用户画像系统,既需要按用户 ID 快速查询详情(HBase 点查),又需要按标签筛选用户(ES 聚合)。
  • 实现:数据写入时同时写入 HBaseESHBase 作为主存储,ES 作为查询索引。
  • 优势HBase 保证数据可靠性,ES 提供灵活查询,两者性能互补。

2、分场景使用:HBase 处理实时交易,ES 处理历史分析

  • 场景:金融交易系统,实时交易数据(当日)用 HBase 存储(低延迟点查),历史数据(按月)同步到 ES(分析统计)。
  • 优势:避免 ES 存储海量数据导致索引膨胀,同时利用 HBase 的高吞吐处理实时写入。

5、总结:技术选型的核心逻辑**

  • 不要用 ES 替代 HBase:若业务以 “高频点查 + 海量存储” 为主(如 Kafka 消息队列下游存储),ES 的写入性能和存储成本会成为瓶颈。
  • 也不要用 HBase 硬扛复杂查询:若业务需要 “全文搜索 + 多维度分析”(如日志检索平台),强行用 HBase 开发会导致系统低效。
  • 最佳实践:根据业务场景拆分数据链路 ——HBase 负责 “存储与实时点查”,ES 负责 “查询与分析”,通过数据同步工具(如 CanalLogstash)保持两者数据一致性。

6)如何同时写入 eshbase

方案 场景 推荐方案 理由
同步双写模式 开发成本敏感 客户端同步双写 实现简单,快速迭代
异步双写模式(通过消息队列) 写入性能敏感 消息队列异步双写 解耦业务逻辑,提升吞吐量
基于 HBase 触发 ES 同步 数据一致性要求极高 HBase 协处理器 + 异步写入 ES 确保 HBase 写入成功后 ES 必定更新

a、同步双写模式

  • 原理:应用程序同时向 ES 和 HBase 发送写入请求,等待两者都返回成功后,再确认操作完成。
  • 优点:实现简单,数据一致性高。
  • 缺点:写入性能受限于最慢的系统,可能影响业务响应速度。

b、异步双写模式(通过消息队列)

  • 原理:应用程序先将数据写入消息队列(如 Kafka),再由消费者分别写入 ESHBase
  • 优点:解耦业务系统与存储系统,提升写入性能,支持流量削峰。
  • 缺点:存在短暂的数据不一致风险(需通过重试机制保证最终一致性)。

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 灵活查询)

ContactAuthor