Skip to content

Elasticsearch 索引创建与搜索原理

一、索引与分片结构

  • 一个 Elasticsearch 索引由一个或多个主分片(Primary Shards)组成。
  • 每个分片会进一步由多个segment(段)构成,每个 segment 是一个独立的倒排索引
  • segment 一旦写入后即不可变,只能通过段合并(segment merge)来减少段的数量并提高查询效率。

二、写入流程与原理

  1. 写入请求路由

    • 客户端向任意节点发送写入请求,该节点作为协调节点(Coordinating Node)
    • 协调节点通过 _id路由算法 shard = hash(routing) % number_of_primary_shards 确定文档应该存储到的主分片。

      注意:分片的数量在索引创建时确定,后期只能通过 reindex 重建索引。

    • 协调节点将文档发送至对应主分片所在的节点。主分片节点将文档写入本地并同步到相应的副本分片
    • 所有分片写入成功后,向协调节点返回成功响应,最终返回给客户端。
  2. 写入缓冲与可见性

    • 文档写入时会先记录到 translog(事务日志,存储于磁盘)中,并暂存于 in-memory buffer(内存缓冲区)。
    • 在完成写入后,刚写入的文档仍不可检索,只有执行了 refresh 操作后,文档才会对搜索可见。
  3. Refresh 操作

    • refresh_interval(默认 1s)会由 Elasticsearch 定期自动触发或可手动调用 _refresh 接口,创建一个新的搜索器视图。
    • 执行过程
      1. 将 in-memory buffer 中的文档写入新的 segment(存储于磁盘)。
      2. 清空 in-memory buffer(但不会清空 translog)。
      3. 创建新的搜索器,使新写入的文档可被查询到。
    • 由于 segment 不可变频繁的 refresh 会导致小 segment 过多,需要后台的 segment merge 来优化。
  4. Flush 操作

    • Flush 主要用于生成安全的提交点(commit),确保数据持久化并清空 translog。
    • 执行过程
      1. 将还未刷新到 segment 的文档写入新的 segment 并执行 Lucene commit,确保数据安全落盘。
      2. 清空 translog(因为有最新的 commit 点,重启时可从 segment 重建索引状态)。
    • 注意:Flush 并不是段合并操作,段合并由 Elasticsearch 的后台进程定期执行,用于减少 segment 数量并提高检索性能。

三、查询与检索过程(Query Then Fetch)

Elasticsearch 默认的查询模式是 QUERY_THEN_FETCH,可以分为两个阶段:

  1. 查询阶段(Query)

    • 客户端发送查询请求至协调节点
    • 协调节点将查询分发至集群内每个需要查询的分片(主分片或副本分片)。
    • 每个分片在本地执行查询,对文档打分并根据 from+size 只返回最高分文档 ID 及相关排序信息给协调节点。
    • 协调节点对来自各分片的结果做合并与全局排序,确定最终要返回的文档集合。
  2. 取回阶段(Fetch)

    • 协调节点根据全局排序结果,向对应的分片节点发送 fetch 请求,获取实际文档内容(如 _source)。
    • 分片节点将文档内容返回给协调节点;协调节点汇总后,把最终结果返回给客户端。

集群规划及节点角色规划最佳实践

在设计和规划 Elasticsearch 集群时,需要确定节点角色与数量,保证稳定、高效。

一、节点角色

  • 候选主节点(Master-eligible Node)

    • 负责创建/删除索引、管理分片分配。
    • 重要性:一个稳定的主节点对保持集群健康至关重要。
    • 通过投票选举产生实际主节点。
  • 协调节点(Coordinating Node)

    • 类似“智能负载均衡器”,负责查询的分发(scatter)与结果汇总(gather)。
    • 可以减轻数据节点在查询时的合并压力。
  • 数据节点(Data Node)

    • 执行大部分 CRUD、搜索和聚合操作。
    • I/O、内存、CPU 等资源使用都相对密集。
  • Ingest 节点

    • 负责写入前的数据预处理(如管道处理、数据清洗)。

下表总结了常见节点角色及所需资源需求(仅作参考):

角色描述存储内存计算网络
数据节点存储和检索数据极高
主节点管理集群状态
Ingest 节点转换/预处理输入数据
机器学习节点内置的机器学习分析极高极高
协调节点请求转发与结果合并

二、实战建议

  1. 不混合部署

    • 一台物理机或虚拟机只部署一个 Elasticsearch 节点,避免端口冲突与资源争用。
    • 不要在同一台机器上混合部署其他应用,如 Redis、Kafka 等。
  2. 大型集群要独立节点角色

    • 不建议将主节点当作协调节点,尽量有独立的协调节点。
    • 不建议将主节点和数据节点混合部署,尽可能保证角色单一。
    • 客户端配置连接的节点应尽量指定为协调节点
  3. 超大规模集群建议

    • 典型规划:3 个主节点 + 3 个协调节点 + 其他数据节点。
    • 主节点硬件配置建议:8C16G 以上。
    • 当写入量极大时,数据节点建议配置:32C64GB(可支撑约 5 万/s 写入)。

集群硬件与规模规划

在正式部署前,需要对集群所处环境做硬件与资源层面的规划。

一、硬件层面

  1. CPU

    • 直接决定计算能力;影响索引、查询、聚合的并发度。
  2. 内存

    • 堆内存:一般建议占系统内存的 50%,且不超过 32GB;过大可能导致 G1、CMS 等 GC 阶段效率下降。
    • 堆外内存(操作系统缓存):用于缓存 Lucene 段文件,加快全文检索、聚合与排序。
  3. 磁盘

    • SSD:适用于高并发、低延迟要求的“热”数据存储。
    • 机械硬盘:可用于存放“暖”、“冷”数据。结合生命周期管理,减少硬件成本。
  4. 网络

    • 在大规模集群中,ingest、搜索和副本复制都会产生大量数据传输,可能导致带宽瓶颈。
    • 要确保节点之间有足够的网络带宽与低延迟。

二、节点规划

  • 参考前文角色规划,根据业务场景与预算确定节点数量与类型。
  • 小规模集群可混合角色,但仍需注意 Master 节点的稳定性。

三、分片和副本规划

1. 用途

  • 分片(Shards)

    • 提升系统水平扩展能力,使大型索引能分割到多台机器并行处理读写。
    • 分割超大索引,便于并行读写。
  • 副本分片(Replicas)

    • 增强高可用:若主分片故障,可用副本快速切换。
    • 提升读取吞吐:查询可以在副本分片上并行执行。
    • 副本分片数可动态修改,但主分片数不可

2. 主分片数量设置不当

  • 分片设置过少

    • 一旦出现故障,可能导致数据恢复难度增大。
    • 无法充分利用多节点资源,影响写入/查询并行度和效率。
  • 分片设置过多

    • 会产生大量的 segment,文件句柄数升高,可能导致集群崩溃。
    • 主节点需要管理大量元数据,可能超载甚至导致集群无响应。
    • 写入/查询请求被拆分得过碎,跨分片的搜索会耗尽搜索线程池。
    • 分片过多会增加内存和 CPU 的开销,导致查询和写入性能下降。

3. 分片设置参考

  • 单个 Lucene segment 最大支持:约 2^32 ≈ 20 亿文档。
  • 单个分片大小官方建议 30GB-50GB 左右。
  • 1GB 堆内存大约支持 20-30 个分片。
  • 单节点支持分片数不建议超过 1000(cluster.max_shards_per_node 默认为 1000)。

4. 数据量参考

  • 数据量较小(<100GB)的索引

    • 一般可设置 3-5 个主分片(视节点数而定),副本数设置为 1。
  • 数据量较大(>100GB)的索引

    • 单分片控制在 30GB-50GB,让索引压力分摊到多个节点。
    • 可通过 index.routing.allocation.total_shards_per_node 参数进行分片均匀分配。
  • 大规模集群

    • 若 shard 数量(不含副本)超过 50,常会引发查询拒绝率上升。
    • 可考虑将一个 index 拆分为多个 index,或采用 rollover/ILM 等方案。
    • 搭配自定义 routing,降低每个查询需要访问的 shard 数量。

四、容量规划

1. 需要考虑的关键点

  • 每天索引多少原始数据?保留多少天?
  • 副本设置多少?
  • 单个数据节点内存/磁盘配置多少?
  • 业务是否有足够的预留空间以应对突发增长或后期扩容?

2. 基本计算方法

  • 总数据量(GB)
    [ \text{每天原始数据量(GB)} \times \text{保留天数} \times \text{净膨胀系数} \times (\text{副本数}+1) ]

  • 磁盘存储(GB)
    [ \text{总数据量(GB)} \times (1 + 0.15 + 0.05) ]

    这里的 0.15、0.05 分别表示给磁盘保留的 15% “警戒水位”空间和 5%“活动余量”。

  • 数据节点数量
    [ \left\lceil \frac{\text{磁盘存储(GB)}}{\text{每个数据节点的磁盘空间} \times 0.85} \right\rceil ]

    乘以 0.85 是预留一些富余空间。

  • 可结合测试工具 esrally 等,对性能和容量做更准确测算。


集群性能调优及原理

一、性能调优整体思路

1. 硬件资源优化

  1. CPU(线程池与队列)

    • 主要关注 Search Thread Poolsearch / suggest / count 等)和写入(bulk)线程池等。
    • 建议的线程池大小可参考:int((# of allocated processors * 3)/2) + 1,也需要结合实际负载进行调优。
  2. 内存

    • JVM 堆:一般设置为物理内存的 50%,上限为 32GB。
    • 堆外内存:留给操作系统做文件系统缓存。
  3. 磁盘

    • 优先使用本地 SSD,远程文件系统(如 NFS)会导致双重开销。
    • 根据冷热数据分层,匹配不同磁盘类型。

2. 参数与集群配置调优

  • 写入独占节点:在大集群中,把大批量写入任务独立到专用节点,减少对检索的干扰。
  • 路由设置:针对同一业务数据采用固定的路由,既能减少查询时的分片访问范围,又能提高写入的局部性。
  • Index Sorting:对满足排序需求的字段提前进行物理排序,可减少查询阶段的数据扫描量。

3. 调优经验

  1. 使用缓存

    • 合理利用操作系统文件系统缓存。
    • 针对不参与打分的过滤场景,可使用 filter 查询,大幅提高缓存命中率。
  2. “不不不不” 策略

    1. 滥用前缀模糊匹配(wildcard 等)。
    2. 执行深度分页(from+size 过大影响性能)。
    3. 返回不必要的字段(限制 _sourcestored_fields)。
    4. 执行复杂度极高的嵌套聚合或关联(joinnested)。
  3. 查询语句优化

    • 合理使用 match_phrasetermrange 等不同查询子句。
    • 合并小查询,减少多次 I/O 开销。
  4. 数据模型优化

    • 避免复杂关联,多表 join 或 nested 结构会造成查询负担。
    • 进行冷热分层,存储与检索重点不同的数据于不同硬件。
    • 重要数据可在系统初始化或更新后进行“预热”,提升缓存命中。

二、检索性能调优

1. 常见检索类型及优化要点

  1. Wildcard 查询

    • 高危操作,若可行应采用 ngram 分词或 match_phrase + slop 替代。
    • 避免通配符 * 出现在开头(*foo)。
    • 正则查询也需谨慎,限制用户随意使用复杂正则。
  2. match_phrase 替代 match

    • match_phrase 精度更高,更少无关文档的干扰。
  3. query_string

    • 若需复杂的布尔逻辑检索,可使用该查询,自带解析器。
  4. 合并检索

    • 一次多条件综合检索通常比多次单一检索效率更高。
  5. range 查询替换

    • 若字段只有几个固定枚举值,可用 term 替换 range
  6. 充分利用缓存

    • 无需打分的查询部分可采用 filter,极大提升缓存命中。
  7. 精简 DSL

    • 仅检索与返回必要字段,减少 _sourcestored_fields 的开销。
  8. 聚合优化

    • 默认聚合结果是非精确的 Top-N,如需全量精确统计,开销更大。
    • 业务可接受近似时,可限制聚合深度或使用分段统计策略。
  9. 深度翻页

    • from-size 大会影响查询性能,上限默认 10000。
    • 可用 scroll 处理离线批量查询;可用 search_after 实现实时深度分页,但也要谨慎。
    • 业务侧应避免一次性全量聚合或全量分页的需求。
  10. 避免返回大数据集

    • 大批量数据处理可用 Scroll API,并控制单批大小。
    • 高亮或大字段会增加 I/O 与缓存负载;仅索引/存储关键字段。
    • 仅统计总数时使用 _count
  11. 冷热数据隔离

    • 近期热数据放在 SSD 等高性能存储节点,历史数据可迁移到低成本节点。
    • 搭配 ILM 或 curator 进行自动 rollover 和别名管理。

三、写入性能调优

  1. 副本分片设置

    • 大批量写入前可将副本数设为 0,待写入结束再恢复以减少同步负载。
  2. ID 生成

    • 优先使用 ES 自动生成的 _id,避免自定义 ID 带来的额外 hash 开销。
  3. 刷新频率

    • 调大 refresh_interval(如 30s)可减少频繁 segment merge 对写入性能的影响。
  4. 索引缓冲区大小

    • 合理增大 index_buffer,减少频繁写入和合并,但要注意 JVM 堆占用。
  5. 保证堆外内存空间

    • 预留足够物理内存给操作系统缓存,可有效提升索引写入与检索效率。
  6. Bulk 批量写入

    • 批量写入性能远高于单条写入,也可减少网络 overhead 与段合并开销。
  7. 多线程并发

    • 在客户端或中间层多线程并行写入,配合 Bulk 降低单节点写入压力。
  8. 线程池与队列大小

    • 调整 write/bulk 线程池大小和队列长度,避免阻塞或拒绝请求。
  9. 合理设置 Mapping

    • 生产环境避免默认 dynamic mapping,对各字段类型(text/keyword/number 等)进行显式定义。
    • 分词器(如 IK)需根据业务场景设置。
  10. 分词器选择

    • 分词越细,索引体量越大,但检索召回率更好;需要在性能和需求间平衡。
  11. 磁盘选择

    • 高写入量场景下,SSD 依旧是“终极手段”之一。
  12. 集群节点角色拆分

    • 大规模集群中,主节点、数据节点、协调节点分开,保证写入和检索的各自效率。
  13. 官方客户端

    • 官方客户端更好地支持连接池、连接管理与版本兼容性。

四、典型实战问题分析

1. 用户画像宽表性能问题

问题场景

  • 某些团队将用户画像存入单一宽表索引,字段上千甚至破千后性能显著下降。

解决思路

  1. 字段裁剪:保留真正业务所需字段,减少无关字段的索引和存储。
  2. Mapping 优化:针对不同字段类型(textkeywordnested 等)做精细设计。
  3. 索引分拆:按业务功能或字段分组拆分为多个索引或使用索引模式(index pattern)。
  4. 冷热分层:将部分历史字段或数据做分层,减少单索引宽度。

2. 高写入量导致查询性能下降

问题场景

  • indexing rate 达到 5 万条/s,查询性能显著下降。

解决思路

  1. 专用写入节点:把写操作和检索操作物理隔离。
  2. Bulk 写入:减少单条写入带来的管理开销。
  3. 刷新频率:调大 refresh_interval
  4. 分片数量:平衡分片数量,不宜过多也不可过少。
  5. 线程池searchwrite 线程池独立配置,防止相互抢占。
  6. 冷热数据:历史数据迁移至冷节点,减轻热节点压力。

3. 一次性召回 4000 篇文档,目标 30ms 内完成

思路

  1. 路由或索引拆分:若可确定召回范围,使用路由减少分片查询量。
  2. 预热与缓存:对热门查询或常用 segment 做预热。
  3. 硬件与角色分离:使用 SSD、大内存,拆分协调节点和数据节点,提高查询并行度。
  4. DSL 精简:只返回必要字段,减少网络开销。
  5. Index Sorting:对常用排序字段预排序。

4. 检索 30 天数据,共 1.9 亿条记录,耗时 50s

场景

  • 三台服务器(8 核 32GB),ES 堆内存仅 6GB,其余资源被其他服务占用;每天一个索引,6 个分片,保留 30 天共 180 个分片。
  • Kibana 默认查询过去 30 天且无过滤条件,耗时 50s。

瓶颈与优化

  1. 查询无过滤:每个索引 20GB,全部分片都要扫描,耗时巨大。
  2. 内存不足:堆外缓存也有限,难以缓冲大规模 segment。
  3. 过多分片:180 个分片会增加并发开销及主节点调度负荷。
  4. 聚合/排序:Kibana 可能带默认聚合,进一步增大开销。

解决思路

  1. 先做业务过滤:限定时间或类型范围,减少扫描量。
  2. 减少分片数量:对旧索引进行合并或减少分片。
  3. 增大内存:适度提升堆内存 (不超过 32GB) 并给操作系统更多缓存空间。
  4. 冷热分层:热数据放 SSD,历史数据放冷节点。
  5. 精简聚合:尽量避免无必要的全量聚合。

数据建模最佳实践

数据建模是确保高效写入和检索的关键,以下总结了常见的设计要点。

一、Mapping 设计

  • Elasticsearch 不支持对已有字段做删除或类型修改,如需变更只能通过 reindex。
  • 避免使用默认 dynamic mapping,尽量在设计阶段对核心字段进行明确定义(text/keyword/number 等)。
  • 对需要全文检索的字段使用 text;只做精确匹配、聚合、排序的字段用 keyword;数值字段选择最合适的类型(long、integer 等)。
  • 可以结合 multi_fields(例如对同一字符串字段既做 IK 分词,又做 keyword)满足多种检索需求。
  • 对一些不需要检索或排序聚合的字段,可通过 index:falseenabled:false 或关闭 doc_values 等方式减少开销。

常用字段类型及建议

  • text:用于分词检索,常搭配 analyzer 进行中文分词。
  • keyword:适用于精确匹配(如状态码、标签等),也支持排序、聚合。
  • numeric:long、integer、double 等,多用于数值查询、聚合。
  • date:日期字段,内部以 long 存储,支持 range 查询及聚合。
  • boolean:只存 true/false。
  • nested / join:一对多或父子关系,更新与查询都更复杂,需谨慎使用。

二、单索引建模

1. Settings

  • index.number_of_shards(主分片数,创建后不可改)
  • index.number_of_replicas(副本数,可动态调整)
  • index.refresh_interval(默认 1s,业务对实时性要求不高可调大)
  • index.max_result_window(默认 10000,深度分页需求大时可调高,但会影响性能)

2. Mapping

  • 强烈建议提前定义 Mapping,严格控制 dynamicstrictfalse
  • 优化字段类型、分词器和索引选项(index_optionsnormsdoc_values 等)。

3. Multi-fields 示例

json
PUT mix_index
{
  "mappings": {
    "properties": {
      "content": {
        "type": "text",
        "analyzer": "ik_max_word", 
        "fields": {
          "standard": {
            "type": "text",
            "analyzer": "standard"
          },
          "keyword": {
            "type": "keyword",
            "ignore_above": 256
          }
        }
      }
    }
  }
}

content 字段既用 ik_max_word 做细粒度中文分词,又可以通过 content.keyword 精确匹配或聚合。

三、多索引建模

  • 建议**使用别名(alias)**访问索引,可在做索引切换或数据迁移时实现“热切换”而不影响上层应用。
  • 可使用索引模板对相同模式的多个索引统一管理(Settings、Mappings、Aliases 等)。
  • 在海量数据(TB 级甚至 PB 级)场景下,可使用 ILM(Index Lifecycle Management)或 rollover 分段管理索引。

1. 索引模板(template)

  • index_patterns:匹配特定前缀或通配的索引名称。
  • order:多个模板生效时的优先级,数字越大优先级越高。
  • 设置默认别名、默认 pipeline、映射等。

2. 别名(alias)

  • 可以为索引创建一个或多个别名,读写都可通过别名进行操作。
  • 可以原子化操作一个别名从旧索引切换到新索引,实现在用户无感知下完成数据更新/迁移。

3. Pipeline

  • Ingest Pipeline 可在文档写入时进行预处理(例如字段转换、日志解析等)。
  • 可通过 index.default_pipeline 指定默认的 pipeline。

4. ILM / Rollover

  • ILM(Index Lifecycle Management):自动管理索引从 hot → warm → cold → delete 各阶段。
  • Rollover:当索引达到一定大小或文档数后,自动滚动创建新索引。
  • 结合别名可将“写”切换到新索引,“读”既可在新旧索引上进行,也能进一步用 curator 或 ILM 做清理。

冷热集群架构

将数据分层为“热数据”(hot)与“冷数据”(cold),并分配到硬件条件不同的节点上,以节约成本并提高查询效率。

实现步骤

  1. 设置节点属性
    elasticsearch.yml 中配置:

    yaml
    node.attr.box_type: hot

    可通过 _cat/nodeattrs 查看节点属性。

  2. 分片分配策略

    http
    PUT logs_01
    {
      "settings": {
        "index.routing.allocation.include.box_type": "hot",
        "number_of_replicas": 0
      }
    }

    表示此索引仅能分配到 box_type=hot 的节点上。

  3. 数据迁移
    当数据老化,需要迁移到冷节点时:

    http
    PUT logs_01/_settings
    {
      "index.routing.allocation.include.box_type": "cold"
    }

    Elasticsearch 会将该索引分片迁移至 box_type=cold 节点。


索引生命周期管理(ILM)实战

  1. 配置 ILM Policy
    • 定义各个阶段(hot、warm、cold、delete)和执行的操作(rollover、shrink、freeze、delete 等)。
  2. 创建模板并绑定该 Policy,指定别名。
  3. 创建初始索引后,后续自动按照 Policy 进行滚动、迁移或删除。

跨集群检索实战

  • Cross Cluster Search:可以在一个集群上查询另一个远程集群的索引。
  • 需要在本地集群做远程集群配置,并通过 remote_cluster_name:index_name 来检索。

分片分配策略

分片分配是主节点的核心职能之一,会在以下情况触发:

  • 集群启动或分片初始恢复时。
  • 新副本分配或重新平衡时。
  • 有节点加入或退出时。

一、集群级分片分配策略

  1. 基于磁盘的分片分配

    • cluster.routing.allocation.disk.watermark.low(默认 85%):超过此阈值会阻止新的副本分配到该节点上。
    • cluster.routing.allocation.disk.watermark.high(默认 90%):超过此阈值会触发分片重新分配。
    • cluster.routing.allocation.disk.watermark.flood_stage(默认 95%):超出后对索引进行只读保护。
  2. 跨机房 / 分区感知

    • 可以设置节点属性 node.attr.rack_id 并通过 cluster.routing.allocation.awareness.attributes 强制分片在不同机架/机房之间进行分配,提升容灾能力。

二、索引级分片分配策略

  • index.routing.allocation.total_shards_per_node:控制单个索引每个节点上能分配的最大分片数,防止分片过度集中。