☰
ElasticSearch 面试 4 连炮,你顶得住么?
2026/10/10 2:51:31 网站建设 项目流程

在互联网后端、大数据和搜索推荐相关岗位的面试中,ElasticSearch(以下简称 ES)几乎是一个绕不开的话题。它不像 MySQL 那样人尽皆知,也不像 Redis 那样可以用几个经典问题快速过场,它的难点在于:既要懂底层原理,又要懂工程实践,还要能在架构层面讲清楚利弊取舍。

本文把面试中最容易被打穿的四个方向梳理成"4 连炮":

  • 第一炮:搜索性能—— 为什么 ES 搜索这么快?倒排索引到底怎么工作的?

  • 第二炮:写入与实时性—— 一条文档从写入到能被搜到,中间发生了什么?为什么 ES 不是"实时"的?

  • 第三炮:集群与一致性—— 分片、副本、路由、选主,这些概念背后的分布式问题怎么答?

  • 第四炮:调优与踩坑—— 深分页为什么慢?脑裂怎么回事?生产环境怎么规划集群?


第一炮:搜索性能——倒排索引与搜索原理

面试官的第一个问题通常非常开放:"ES 为什么搜得这么快?"很多候选人张口就是"因为用了倒排索引",然后就没有然后了。要接住这一炮,你需要把"快"这件事拆成三个层次:数据结构层面、查询执行层面和分布式层面。

1.1 从正排索引到倒排索引

正排索引就是"文档 ID 到内容"的映射:给你一个文档编号,你能立刻找到它的标题、正文、标签等字段。MySQL 的主键索引本质上就是一种正排索引。正排索引擅长回答"ID 为 1 的文档内容是什么",却不擅长回答"哪些文档包含了'ElasticSearch'这个词"。

倒排索引的本质是"词项到文档"的映射。它把自然语言中的内容拆分成一个个词项(Term),然后记录下每个词项出现在哪些文档里。

以两篇文档为例:

  • 文档 1:ElasticSearch 是一个搜索引擎

  • 文档 2:搜索引擎需要构建倒排索引

经过分词后,可以得到一个简化的倒排表:

词项出现的文档 ID
ElasticSearchdoc1
搜索doc1, doc2
引擎doc1, doc2
需要doc2
构建doc2
倒排doc2
索引doc2

当用户搜索"搜索引擎"时,分词得到"搜索"和"引擎"两个词项,分别去倒排表中查找,发现它们都命中 doc1 和 doc2,于是这两篇文档成为候选。整个过程是"词项到文档列表"的直接寻址,类似于哈希表的 O(1) 查找,而不是像传统数据库那样全表扫描。这就是"快"的第一个来源。

1.2 倒排索引的数据结构与内存形态

Lucene 是 ES 底层的全文检索引擎,ES 的每个 Shard 本质上就是一个 Lucene 索引。一个 Lucene 索引由多个 Segment 组成,每个 Segment 是独立的、不可变的倒排索引文件。一个 Segment 内部的倒排结构主要包含:

  • Term Dictionary(词项字典):把所有的词项按字典序排序后存储,并在它上面建立索引,通常用有限状态转换器(FST)或前缀树来实现快速定位。FST 的特点是共享前缀和后缀,占用的内存非常小。

  • Term Index(词项索引):为了加速对 Term Dictionary 的定位,Lucene 会在磁盘上再建一层稀疏索引,加载到内存后可以先用二分或前缀匹配快速缩小范围。

  • Posting List(倒排列表):每个词项对应一个文档 ID 列表,还可能携带词频、位置信息等负载。

面试话术:"倒排索引不是一张简单的哈希表。词项按字典序排成 Term Dictionary,上面用 FST 建立索引以支持前缀查找和模糊匹配;每个词项再关联一个 Posting List 记录命中的文档 ID。查询时先从 FST 快速定位词项,再取它的文档列表,整个过程避免了全表扫描。"

1.3 倒排列表的压缩与跳表

Posting List 里的文档 ID 通常是递增存放的,Lucene 不会原样存储每个 ID,而是存差值,再对差值做压缩。最经典的编码是Frame Of Reference(FOR):把文档 ID 列表切分成固定大小的块(例如每 128 个一组),每组先记录一个基准值,再记录每个差值需要多少个 bit,最后把所有差值按统一的 bit 宽度紧凑排列。

在求多个词项 Posting List 的交集时,Lucene 会使用跳表(Skip List)来快速跳过不可能命中的区间:每个词项的列表每隔一段距离就记录一个跳跃点,携带该位置的文档 ID 和偏移量。两个列表求交集时,如果一方当前位置远小于另一方,就可以直接用跳表跳到更靠后的位置,复杂度从 O(m+n) 优化到接近亚线性。

总结:"倒排列表做了两件事:一是 FOR 编码压缩存储,二是加跳表加速多词项交集。压缩决定空间成本,跳表决定 Bool 查询组合多个条件时的性能。"

1.4 分词器:搜索快慢的第一道关口

ES 的分词由 Analyzer 负责,一个 Analyzer 通常由三部分组成:

  • Character Filter(字符过滤器):在分词前对原始文本做预处理,比如去掉 HTML 标签、把全角字符转成半角。

  • Tokenizer(分词器):把文本按照一定规则切成一个个词,是分词的核心。

  • Token Filter(词项过滤器):对切出来的词做进一步加工,比如转小写、去掉停用词、同义词扩展。

中文分词的常见方案:

  • ES 内置的 standard / cjk 分词器:cjk 会把连续汉字按二元切分,会产生大量碎词和噪声,不适合生产环境中文场景。

  • IK 分词器:国内最流行的中文分词插件,支持ik_max_word(细粒度,利于召回)和ik_smart(粗粒度,更接近自然语义)两种模式,还支持自定义词典、停用词和热词更新。

  • 拼音分词器(pinyin):常和 IK 组合使用,用于支持"es"搜"ElasticSearch"、拼音搜中文等电商场景。

关键面试点:索引时和查询时最好使用相同的分词器。如果索引时用ik_max_word,查询时却用ik_smart,可能导致词项对不上,影响召回。

1.5 相关性评分:搜得快还不够,还要排得对

ES 5.0 之后默认的相关性算法是BM25。它的核心思想来自 TF-IDF,但做了重要改进:

  • TF(词频):一个词在文档中出现的次数越多,通常相关性越高。但传统 TF 会线性增长,BM25 引入了饱和曲线,词频超过一定限度后,权重的增长趋缓,避免"刷词"被过度放大。

  • IDF(逆文档频率):一个词出现在越多的文档里,说明它越稀松平常,区分度越低,权重应该越小。

  • 文档长度归一化:同样的词频,在短文里比在长文里更能代表这篇文档的主题。BM25 会综合考虑字段长度做归一化。

面试话术:"BM25 用饱和的词频、逆文档频率和字段长度归一化,综合衡量词项与文档的相关性,解决了 TF-IDF 在词频无限放大和长文档偏差上的问题。"

干预排序的方式:自定义打分脚本(Script Score)、Function Score 按业务字段加权、Field Value Factor 乘上销量或热度、Boost 提升字段权重。电商场景常把销量、好评率等因素引入分数,做"相关性 + 业务权重"的组合排序。


第二炮:写入与实时性——一条文档的生命周期

结论:ES 是近实时(Near Real-Time,NRT)的,不是实时。一条文档从写入到能被检索到,延迟默认在一秒左右。

2.1 一条文档写入 ES 的完整流程

假设一个三节点的集群,索引配置为 3 个主分片、每个主分片 1 个副本。客户端写入一条文档时,大致经历以下步骤:

  1. 路由选择:客户端把请求发给任意协调节点(Coordinating Node)。协调节点根据文档 ID 的哈希值对主分片数量取模,算出这条文档应该落在哪个主分片。路由公式为:shard = hash(routing) % number_of_primary_shards,其中 routing 默认是文档 ID。这也是为什么主分片数量在索引创建后不能随便修改——一旦主分片数变了,取模结果全变,历史数据会找不到。

  2. 写入主分片:协调节点把写请求转发到对应主分片所在的节点。主分片先对文档做字段类型校验、解析和标准化,然后写入 Lucene 索引。

  3. 同步副本:主分片写入成功后,把操作同步给所有副本分片,等待副本确认。

  4. 返回确认:副本写入成功,协调节点收到所有要求的确认后,向客户端返回成功。

高频面试细节:

  • 写一致性(Consistency Level):旧版本有 one / all / quorum 等参数,quorum 是默认值,即要求大多数分片副本可用才允许写入,计算方式为(primary + number_of_replicas) / 2 + 1。现在更常见的是通过wait_for_active_shards指定写入前需要多少活跃分片,以及timeout控制等待时间。

  • 协调节点的作用:它不直接存储数据,只负责路由、分发、聚合结果和返回,是整个分布式写入和查询的"交通枢纽"。

2.2 内存缓冲、refresh 与近实时的真相

很多人以为"写入 Lucene 索引"就等于"立刻可搜索",其实中间隔着一层内存缓冲和一次 refresh 操作。写入的微观过程:

  1. 文档到达主分片后,先被写入内存缓冲区(In-memory Buffer),同时追加到Translog(事务日志)中。Translog 是写操作的第一道持久化保障,文档写进 Translog 后才能保证断电不丢。

  2. 默认每1 秒,ES 会执行一次refresh:把内存缓冲区里的文档生成一个新的 Segment,这个 Segment 被打开并加入可搜索的集合,同时清空缓冲区。此时新文档才能被检索到。

  3. refresh 只是让文档变得"可搜索",并不做 fsync 落盘。Lucene 的写入依赖操作系统的页缓存,真实持久化由后面的 flush 和文件系统缓存刷盘来完成。

所以"写入后立刻查不到"的最常见原因,就是还没到下一次 refresh 周期。如果要强制立即可见,可以在写入后调用refresh=wait_for或显式调用 Refresh API,但生产环境不建议对每一条写入都这么做,会带来大量小 Segment,拖慢查询和合并。

一句话总结:"ES 的'写入成功'只代表文档进了内存缓冲和 Translog,'能搜到'则要等到下一次 refresh 生成新 Segment。中间的默认延迟约 1 秒,这就是近实时的本质。"

2.3 Translog、flush 与数据持久化

需要把 refresh、fsync、flush 三个概念彻底分清:

概念作用默认频率是否落盘
refresh内存缓冲 → 新 Segment,让数据可搜索1 秒否
fsync / translog 落盘把 Translog 同步刷到磁盘后才返回成功每个写请求(durability=request)是
flush执行 Lucene 的 commit,把内存中的 Segment 经 fsync 真正持久化到磁盘,同时提交新的 commit point,最后清空旧 Translog30 分钟或 Translog 达到一定大小是
  • refresh:内存缓冲 → 新 Segment,让数据可搜索。默认 1 秒一次,不保证落盘。

  • fsync / translog 落盘:写入请求时,数据会先写 Translog。默认情况下,每个写请求都会触发 Translog 的index.translog.durability=request,把 Translog 同步刷到磁盘后才返回成功,这是"写入成功不丢数据"的底气。也可以配置为 async(异步刷盘),提高吞吐但可能丢极小窗口的数据。

  • flush:执行 Lucene 的 commit,把内存中的 Segment 经 fsync 真正持久化到磁盘,同时提交一个新的 commit point,最后清空相应的旧 Translog。默认每 30 分钟执行一次,或在 Translog 达到一定大小时触发。


第三炮:集群与一致性——分片、副本、路由与选主

3.1 分片与副本

分片(Shard)是 ES 实现水平扩展的基本单位。一个索引可以被切分成多个主分片,每个主分片是一个独立的 Lucene 索引。主分片数量在索引创建时确定,创建后不能修改。

副本(Replica)是主分片的拷贝,作用有两个:

  • 高可用:主分片所在节点故障时,副本可以提升为新的主分片。

  • 提升读吞吐:读请求可以同时打到主分片和副本分片。

3.2 路由机制

ES 通过shard = hash(routing) % number_of_primary_shards计算文档应该落在哪个主分片,routing 默认是文档 ID。这也解释了为什么主分片数量在索引创建后不能随便修改。

3.3 选主与集群状态

ES 集群中的节点通过 Zen Discovery(7.x 之前)或新的集群协调层(7.x 之后)进行选主。主节点(Master Node)负责集群层面的管理,如创建/删除索引、跟踪节点状态、分配分片等,不负责文档级别的读写。

集群状态(Cluster State)包括:

  • 节点列表

  • 索引列表及其映射和设置

  • 分片分配情况

  • 其他元数据

主节点维护集群状态,并将其同步给其他节点。

3.4 脑裂问题

脑裂(Split Brain)是指集群中出现多个主节点,各自认为自己是唯一的主节点,导致集群状态不一致。

产生原因:网络分区导致部分节点无法与主节点通信,重新选举出新的主节点,而旧主节点仍然认为自己是主节点。

预防措施:

  • 配置discovery.zen.minimum_master_nodes(7.x 之前)为(master_eligible_nodes / 2) + 1,确保只有获得多数票的节点才能成为主节点。

  • 7.x 之后,ES 引入了新的集群协调层,自动处理选主和脑裂问题,不再需要手动配置minimum_master_nodes。


第四炮:调优与踩坑——深分页、脑裂与集群规划

4.1 深分页为什么慢?

ES 的分页使用from + size参数,from表示起始位置,size表示返回的文档数量。深分页指的是from值很大的情况,例如from = 10000, size = 10。

为什么慢:ES 的分页是在每个分片上独立执行的。当from = 10000, size = 10时,每个分片需要返回前 10010 条文档给协调节点,协调节点再合并排序,最后取出第 10000 到 10010 条。这意味着:

  • 每个分片都要扫描并排序大量文档。

  • 协调节点需要接收和处理大量数据。

  • 随着from增大,内存和 CPU 开销急剧增加。

解决方案:

  1. 使用search_after:基于上一页的最后一条文档的排序值继续查询,避免扫描大量文档。

  2. 使用 Scroll API:适用于大量数据的导出场景,但不适合实时分页。

  3. 限制分页深度:通过index.max_result_window限制from + size的最大值,默认是 10000。

  4. 使用track_total_hits:如果不需要精确的总数,可以设置为false或一个较小的值,减少计算开销。

4.2 脑裂问题

已在第三炮中详细说明。核心是确保只有获得多数票的节点才能成为主节点。

4.3 生产环境集群规划

节点角色规划:

  • Master 节点:负责集群管理,建议配置 3 个专用 Master 节点,避免与数据节点混用。

  • Data 节点:负责存储数据和执行查询,根据数据量和查询负载配置。

  • Coordinating 节点:负责路由和聚合,适合查询量大的场景。

  • Ingest 节点:负责数据预处理,适合需要在写入前做转换的场景。

分片规划:

  • 单个分片大小建议在10GB 到 50GB之间。

  • 分片数量不宜过多,避免集群状态过大和查询开销增加。

  • 根据数据量和节点数量合理设置主分片和副本数量。

硬件规划:

  • CPU:查询密集场景需要更多 CPU 核心。

  • 内存:ES 依赖文件系统缓存,建议分配足够的堆内存(不超过物理内存的 50%,且不超过 32GB)。

  • 磁盘:推荐使用 SSD,提高 IO 性能。

  • 网络:节点间通信频繁,建议使用高速网络。

4.4 常见调优参数

索引层面:

  • refresh_interval:默认 1 秒,可以调大以减少 refresh 频率,提升写入性能。

  • number_of_replicas:根据高可用需求设置,写入密集场景可以临时设置为 0,写入完成后再恢复。

  • translog.durability:默认request,可以设置为async提升写入吞吐,但可能丢数据。

查询层面:

  • 使用filter代替query:filter 不计算相关性分数,可以利用缓存。

  • 避免使用wildcard和regexp查询,它们性能较差。

  • 使用keyword字段做精确匹配,使用text字段做全文检索。

集群层面:

  • 合理设置indices.memory.index_buffer_size:控制索引缓冲大小。

  • 监控thread_pool的队列和拒绝情况,及时调整。

  • 定期执行force merge减少 Segment 数量,但不要在高峰期执行。


总结

ES 面试的 4 连炮可以浓缩为:

  1. 搜索性能:倒排索引(Term Dictionary + FST + Posting List)、FOR 压缩、跳表加速、分词器、BM25 相关性评分。

  2. 写入与实时性:路由 → 主分片 → 副本同步 → 返回;内存缓冲 → Translog → refresh(1 秒)→ flush(30 分钟);近实时的本质是 refresh 周期。

  3. 集群与一致性:分片与副本、路由公式、选主与集群状态、脑裂预防。

  4. 调优与踩坑:深分页用 search_after、脑裂预防、节点角色规划、分片规划、硬件规划、常见调优参数。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询