Elasticsearch

基本概念

ES 是一个分布式数据库,它和关系型数据库存在明显差异,其基本概念如下:

关系型数据库 (MySQL) Elasticsearch 描述
Table (表) Index (索引) 具有相似特征的文档集合。比如 users 索引。
Row (行) Document (文档) 可被索引的基础信息单元,以 JSON 格式表示。
Column (列) Field (字段) JSON 中的键值对。
Schema (模式) Mapping (映射) 定义字段的类型(如 keyword, text, integer)和索引方式。
SQL DSL (Domain Specific Language) ES 特有的基于 JSON 的查询语言(Rest API)。

节点

每一个 ES 节点就是一个 Java 进程,其中每个节点启动后默认是 Master eligible节点(候选主节点),只有 Master eligible节点能够参与选主流程、成为 master 节点。
每个节点都会保存该集群的状态数据,如分片信息、索引信息、不同的节点信息,这样请求不管打到哪个节点上都能进行转发。但是只有master 节点具有修改集群状态数据的权限。
每个节点按照类型可以分为:

  • 数据节点(Data Node):保存分片数据,可以水平扩展,并执行数据的 CRUD 操作、搜索、聚合。由于需要处理海量数据与计算,对 IO、CPU、内存极为敏感。
  • 协调节点(Coordinating Node):不保存数据,负责接收客户端的请求,并将请求进行分发聚合,最终汇集结果返回给客户端,每个节点默认就是协调节点
  • 候选主节点(Master eligible):不保存数据,它只负责集群层面的轻量级决策。比如:创建或删除索引、决定把哪个分片分配给哪个节点、维护集群状态(Cluster State)。
  • 主节点(Master Node):一个集群在同一时间只能有一个 master 节点,只有当前 master 节点挂掉才会选举出新的 master 节点。

分片

作为一个分布式系统,ES 可以通过分片以及副本来提升扩展性与可用性。索引在创建时需要指定其分片数,并且后续除了 reindex 外无法修改,es 计算文档所在位置公式:shard = hash(_id) % number_of_primary_shards,分片数如果发生修改,id 的映射关系就会被打乱。

Q:ES 如何保证 id 的唯一性?
可以从写入和查询两部分来看这个问题

  • 写入时:
    • 如果用户指定了 id,就直接用提供的 id 进行路由计算
    • 如果没有指定 id,es 接收请求的协调节点就自己生成一个 uuid,为了保证 uuid 的全局唯一性,es 使用了 Time-Based UUID 算法。具体来说就是把 id 拆分为空间坐标、时间坐标、序列号三个维度。通过这种设计要出现重复,必须是“同一台机器”在“同一纳秒”内生成了“同一个序列号”,这个概率在数学上几乎为零。
  • 查询时:
    • 如果用户指定了 id,就直接用提供的 id 进行路由计算
    • 如果没有指定 id,es 的协调节点会进行广播查询,并发的对该索引的所有节点进行倒排索引查询。

主从机制

如果存放某个分片的节点突然宕机了,那部分数据岂不是丢了? 为了解决这个问题,ES 引入了副本概念:

  • **主分片 (Primary Shard)**:负责处理写入请求的“原件”。
  • **副本分片 (Replica Shard)**:主分片的完全拷贝(复印件)。它平时可以分担查询压力(读请求),关键时刻如果主分片挂了,它会立刻升级为主分片,保证服务不断。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    graph TD
    subgraph Cluster ["Elasticsearch 集群 (整个物流园区)"]
    style Cluster fill:#f9f9f9,stroke:#333,stroke-width:2px

    subgraph NodeA ["节点 A (服务器 1)"]
    style NodeA fill:#e1f5fe,stroke:#0288d1
    P0[主分片 P0]:::primary
    R1[副本分片 R1]:::replica
    end

    subgraph NodeB ["节点 B (服务器 2)"]
    style NodeB fill:#e1f5fe,stroke:#0288d1
    P1[主分片 P1]:::primary
    R2[副本分片 R2]:::replica
    end

    subgraph NodeC ["节点 C (服务器 3)"]
    style NodeC fill:#e1f5fe,stroke:#0288d1
    P2[主分片 P2]:::primary
    R0[副本分片 R0]:::replica
    end
    end

    classDef primary fill:#ffecb3,stroke:#ff6f00,stroke-width:2px,color:#000,font-weight:bold;
    classDef replica fill:#c8e6c9,stroke:#388e3c,stroke-width:1px,color:#000;

    %% Add invisible links to layout the graph better
    NodeA ~~~ NodeB ~~~ NodeC

进阶设计

ES相比于MySQL,为什么更快?

ES 之所以能做到全文检索快,是因为它根据不同的使用场景在磁盘和内存中构建了不同的数据结构,核心在于”倒排索引”的设计。
“倒排索引”是相比于”正排索引”来说的,MySQL 中的存储方式就叫做正排索引,具体来说是通过 id 指向记录明细(可以拿到记录全部内容),而倒排索引是关键词指向记录 id(无法获取记录内容,需要再次查询)。
ES 的倒排索引采用了以下三层结构来保证查询的高效:

  • Posting List :倒排表,存储在磁盘上。它保存了包含不同关键词(term)的所有文档 id,数据量巨大。注意,posting list 是在 term 维度的,文档的不同字段信息的 term 是不同的,在 Lucene 的底层实现中,Term 是包含字段信息的
  • Term Dictionary:有序的 term 列表,可以通过二分查找快速定位到对应 term 的 Posting List在磁盘中的具体位置,数据量较大,无法完全加载到内存中
  • Term Index:帮助快速定位Term Dictionary中的位置,通过FST(Finite State Transducer)的格式保存在内存中。它不存完整的单词,只存单词的前缀结构。
    MySQL的存储结构相比于 ES 只实现到了第二层,即 term dictionary,只是 MySQL 使用 B+树来组织数据,而 ES 的整体结构如下图所示

当有一次 term 查询时,ES 首先通过保存在内存中的 term index 定位到指定 term 在磁盘上的位置,然后从磁盘上加载 term dictionary 找到对应的 posting list 来获取文档 id,最后根据文档 id 获取文档的全部数据。

ES 的存储压缩

https://www.cnblogs.com/tech-lee/p/15225276.html

ES 是如何实现排序聚合的?

明白了倒排索引机制后,它就像一个 kv 存储,数据结构非常适合点查而不擅于排序,为了实现排序的能力,ES 使用了另外的一套数据结构,也就是列式存储,在数据写入时 ES 除了构建倒排索引还会构建列式存储。
继续拿 MySQL 举例,关系型数据库使用的叫行式存储,它们的对比如下:

1
2
3
4
5
6
7
// 行式存储
Row 1: [ID:1, Name:iPhone, Price:6000, Category:Phone]
Row 2: [ID:2, Name:Pixel, Price:5000, Category:Phone]

// 列式存储
Price Column: [6000, 5000, ...] <-- 紧凑排列在磁盘上
Category Column: [Phone1, Phone2, ...]

ES 为什么是准实时性的存储搜索引擎?

MySQL 只要成功提交事务,当前保存的数据就立马可以对外检索。ES 却并非这样,它并没有事务的概念,ES 数据写入经历了以下历程:

1. 写入与双写 (Write & Double Write)

当请求到达 Data Node 时,为了保证速度和可靠性,ES 做了两件事:

  • Memory Buffer:数据先写入内存缓冲区。此时数据不可被搜索
  • **Translog (Write Ahead Log)**:同时追加写入磁盘上的事务日志。这是为了防止断电数据丢失(和 MySQL 的 Redo Log 类似)。

如果在写入 TransLog 时失败,ES 会直接抛出异常让客户端重试,它并不会去清洗 Memory Buffer 内的数据。

2. 刷新 (Refresh) —— “近实时”的原因

这是 ES 与数据库最大的区别。ES 默认每隔 1 秒执行一次 refresh 操作:

  • 动作:将 Memory Buffer 中的数据生成一个新的 **Segment (段)**,此时buffer 内那些没有TransLog支撑的数据会被丢弃。
  • 位置:这个 Segment (只增不改,删除也是递增打标记)被写入 OS Filesystem Cache (操作系统的文件缓存),而不是直接刷入磁盘物理文件。
  • 状态只要 Segment 进入了 OS Cache,它就可以被打开并搜索到了!

这就是为什么刚写入的数据,可能需要等 1 秒(默认)才能查到。这也是 ES 被称为 NRT (Near Real-Time) 的根本原因。

3. 冲刷 (Flush) —— 数据落盘

  • 随着时间推移,Translog 越来越大。ES 会执行 flush
  • 强制执行一次 Refresh。
  • 调用操作系统的 fsync,将 OS Cache 里的所有 Segment 真正写入物理磁盘。
  • 清空 Translog。
    refresh 保证了数据可以被检索到,秒级频率。
    flush 真正落盘,分钟级频率。

4. 段合并 (Merge)

  • 由于每秒生成一个 Segment,文件数量暴增会导致搜索变慢。后台线程会默默将小 Segment 合并成大 Segment,并物理剔除被标记删除的文档(ES 的删除是逻辑删除,Merge 才是真正的物理删除)。
  • 由于删除操作执行时数据并没有删除,ES 会在专门的.del文件中,把文档 id 标记为删除。后续查询仍然会被倒排索引查询到,只是在返回给客户端前根据.del文件进行了一次过滤。
    段合并阶段是 ES 释放磁盘空间的唯一时机

ES 数据的持久性与一致性如何保障?

这里可以分为两种情况,分别是Translog写入成功与Translog写入失败

1. Translog 写入成功

作为一个分布式的系统,ES并不是数据 Translog 写入成功后,数据就完全保障持久化了。考虑一种情况:

  1. 主分片 Translog 写入成功
  2. 主分片告诉客户端返回成功
  3. 此时主分片挂掉,数据还未来得及同步到从分片

在这种场景下,即使客户端写入没有报错,对 es 集群来说数据也丢失掉了。为了解决这个问题,ES 集群提供一个配置wait_for_active_shards,这个参数来控制主分片何时才向客户端返回成功。

2. Translog写入失败

当日志写入失败时,客户端直接返回错误码,并没有”回滚机制”来撤销Memory Buffer内的数据。这样设计的原因是由于 ES 的架构决定的。

在 MySQL 中为了实现”撤销”能力,引擎层维护了 undo log,每行数据可以存在多个版本,这时如果出现异常数据可以安全回滚。

  1. 而 ES 为了追求极致的写入速度,它支持的数据写入量是 MySQL 的几十上百倍,如果每写入一条数据,ES 要想 MySQL 去维护锁机制、undo log,写入性能会大幅下降。
  2. 于此同时 ES 的底层数据结构 segment 是不可变的,每次修改都是新增。
    ES 的设计优先考虑 CAP 理论中的可用性与分区容错性,牺牲了事务换取极致的性能可扩展性。

没有事务,ES 如何避免写丢失?

ES 中保存的每条数据都有序列号和版本字段,每次发生变更时字段都会递增。
ES 提供了乐观锁机制,可以在发起修改操作时带上字段,ES 就会进行判断并更新

1
2
3
4
5
6
7
8
9
10
11
12
{
"_index": "products",
"_id": "100",
"_version": 5, // 👈 旧版本的锁机制用这个
"_seq_no": 32, // 👈 新版核心:序列号
"_primary_term": 1, // 👈 新版核心:主分片任期
"found": true,
"_source": {
"name": "iPhone",
"price": 5999
}
}