Es架构初探

2024/06/22 ES 转载 大数据 共 6603 字,约 19 分钟

https://www.bilibili.com/video/BV1yb421J7oX

inverted index 倒排索引

分词,词映射到文档 ID

但是词项太多怎么办?O(n)遍历词项太慢了

可以按照字典序对 term 进行排序,二分查找快速获取到文档 ID。

排序号的 term 就叫作 Term Dictionary,对应的文档 ID 就是 Posting List。

然后就构成了倒排索引。但是 term dict 数据量很大,全部放到内存并不现实,必须放在磁盘中,但是查询磁盘是一个很慢的过程。有办法吗?有!

term index

词项和词项之间,有些前缀是一致的。

目录树结构体积小,适合放在内存中。目录树的叶子节点记录 实际的 term 在磁盘中的偏移量。

中文和英文一样都是转成编码,这里用英语字母只是为了更形象表达

听起来跟 mysql 索引有点像?!

Posting List 里面存放的是文档 ID,我们还需要通过这个 ID 找到文档本身。

stored fields

因此还需要有个地方能够完整地存放文档内容。

但是如果想对检索到的文档按照某个字段进行排序怎么办?

Doc Values

https://mp.weixin.qq.com/s/oRKTQUFAcM9liykkpNeRgA 【拾遗】Elasticsearch 原值访问实现之 DocValue 篇

https://mp.weixin.qq.com/s/VL-v_d3yh7de6k_wAd-XEQ 浅谈 Lucene 中的 DocValues

Elasticsearch 使用 Doc Values 来支持排序和聚合。Doc Values 是列式存储格式,它将每个字段的值按列存储在磁盘上,这样可以非常高效地进行排序和聚合操作。相比传统的行式存储,列式存储在处理排序和聚合时更为高效,因为它可以快速访问特定字段的数据。

在 Elasticsearch,Doc Values 是一个存储字段值的列式数据结构,它允许对字段进行高效的检索和聚合操作。Doc Values 主要用于以下几个方面:

  1. 排序:当需要对搜索结果进行排序时,Doc Values 提供了一种高效的机制来访问和排序字段值。
  2. 聚合:聚合操作,如计数、求和、平均值等,可以通过 Doc Values 快速执行。
  3. 脚本:在脚本中访问字段值时,Doc Values 提供了一种快速访问的方式。
  4. 存储:Doc Values 可以减少磁盘空间的使用,因为它存储的是列式数据,而不是每个文档的重复数据。
  5. 性能:使用 Doc Values 可以提高查询性能,因为它减少了 I/O 操作和内存使用。

默认情况下,Elasticsearch 为所有字段生成 Doc Values,但是可以通过映射设置来禁用特定字段的 Doc Values。需要注意的是,一旦字段的 Doc Values 被禁用,就无法再使用它来进行排序、聚合或在脚本中访问。

简而言之,Doc Values 是 Elasticsearch 中一种优化检索性能的数据结构,它使得对字段的高效操作成为可能。

当我们想对某个字段排序的时候,就只需要将这些集中存放的字段,一次性读取出来,完成针对性排序。

在 Elasticsearch (ES) 中,可以使用排序功能来按照指定字段对检索到的文档进行排序。这可以通过在查询中添加 sort 参数来实现。以下是一些具体的示例和步骤,展示如何按照某个字段进行排序。

假设我们有一个索引 products,每个文档有以下字段:namepricerating。我们想要按 price 字段对检索结果进行排序。

1. 按单个字段排序

GET /products/_search
{
  "query": {
    "match_all": {}
  },
  "sort": [
    {
      "price": {
        "order": "asc"
      }
    }
  ]
}

上面的查询会返回所有 products 索引中的文档,并按照 price 字段升序 (asc) 排序。要按照降序 (desc) 排序,只需将 order 的值改为 desc

GET /products/_search
{
  "query": {
    "match_all": {}
  },
  "sort": [
    {
      "price": {
        "order": "desc"
      }
    }
  ]
}

2. 按多个字段排序

可以同时按多个字段进行排序。例如,先按 price 排序,如果价格相同,再按 rating 排序:

GET /products/_search
{
  "query": {
    "match_all": {}
  },
  "sort": [
    {
      "price": {
        "order": "asc"
      }
    },
    {
      "rating": {
        "order": "desc"
      }
    }
  ]
}

3. 按嵌套字段排序

如果需要对嵌套对象的字段进行排序,需要使用嵌套排序。例如,假设文档有一个嵌套字段 reviews,每个 review 有一个 score 字段:

GET /products/_search
{
  "query": {
    "match_all": {}
  },
  "sort": [
    {
      "reviews.score": {
        "order": "desc",
        "nested": {
          "path": "reviews"
        }
      }
    }
  ]
}

完整示例

假设我们想要按 price 字段升序排序,并且在价格相同的情况下按 rating 字段降序排序:

GET /products/_search
{
  "query": {
    "match": {
      "category": "electronics"
    }
  },
  "sort": [
    {
      "price": {
        "order": "asc"
      }
    },
    {
      "rating": {
        "order": "desc"
      }
    }
  ]
}

在这个示例中,查询会匹配 categoryelectronics 的文档,然后首先按 price 字段升序排序,如果价格相同,再按 rating 字段降序排序。

总结

使用 Elasticsearch 的 sort 参数,可以非常灵活地按照一个或多个字段对检索到的文档进行排序。你可以根据需要调整 orderascdesc,并且支持对嵌套字段进行排序。以上示例展示了各种排序方法的具体用法。

segment 是什么?

倒排索引用于搜索,

term index 用于加速搜索

stored fields 用于存放文档的原始信息

doc values 用于排序和搜索

上面这四个结构共同组成了一个文件,也就是 segment。他是一个具备完整搜索功能的最小单元。

Lucene 是什么

我们可以用多份文档生成一个 segment,如果新增文档的时候还是写入到这份 segment,那就得同时更新原来 segment 的多个数据结构,这样并发读写的时候,性能肯定会收到影响,那怎么办呢?

我们定个规矩,segment 一旦生成,就只能读不能写。如果还有新的文档要写入,则生成新的 segment。老的只负责读,写则生成新的 segment,同时保证了写和读的性能。

但是 segment 变多了,我们怎么知道要搜索的东西在哪个 segment 里面呢?没关系,并发同时读取多个 segment 就好了。

但是 segment 越写越多,文件句柄被耗尽了,但是我们可以定期合并小的 segment,也就是段合并,segment merging。

这样上面提到的多个 segment 就构成了一个单机文本检索库,它其实就是非常有名的开源基础搜索库,Lucene,不少知名搜索引擎都是基于它构建的。但是它没有高性能、高可用、高可扩展性都没有。

我们来看一下怎么优化他!

高性能

有啥解决办法吗?

对写入 Lucene 的数据进行分类,不同类型的数据读写不同的 indexname,有点像 MySQL 里面的分库分表,是吧。这个其实也是一个统一的设计思想。

单个 index name 里面的数据也有可能过多,可以讲单个 index name 里面的同类数据拆分成好多份,每一份是一个 shard 分片,每个 shard 分片本质上就是一个独立的 lucene 库。

这样就可以将多个读写操作分摊到不同的分片中去。

对写入 Lucene 的数据进行分类,不同类型的数据读写不同的 lucene,有点像 MySQL 里面的分表,是吧。这个其实也是一个统一的设计思想。

确实,将数据根据不同类型分类并写入不同的 Lucene 索引,这种方法类似于 MySQL 中的分表策略,是一种常见的设计思想。该设计思想的核心在于将数据分散到不同的存储单元中,从而提高读写性能、可扩展性和数据管理的灵活性。以下是这一设计思想的详细分析:

设计思想

  1. 分离关注点
    • 不同类型的数据:将不同类型的数据分开存储,每种数据类型可以有独立的索引配置和优化策略。例如,日志数据、用户数据、商品数据等分别存储在不同的索引中。
    • 读写优化:根据数据类型的访问模式优化索引。高写入频率的数据可以使用一种配置,而高查询频率的数据可以使用另一种配置。
  2. 提高性能
    • 并行读写:通过分离数据,多个索引可以同时进行读写操作,减少单一索引的压力,提高整体系统的并行处理能力。
    • 优化索引结构:根据数据特点调整索引结构和配置。例如,对日志数据可以使用分片较多的索引配置,而对用户数据可以使用较少分片但更多副本的配置。
  3. 增强可扩展性
    • 分片管理:通过分片机制,可以轻松扩展存储容量和计算能力。当数据量增加时,可以添加新的索引或分片。
    • 负载均衡:将数据分布到不同的索引和节点上,均衡负载,防止某些节点成为瓶颈。

实现方式

1. 在 Elasticsearch 中分索引存储

Elasticsearch 提供了对索引的灵活管理,可以轻松创建和管理多个索引。以下是一些具体的实现方法:

  • 按类型创建索引: ```bash PUT /logs_index PUT /users_index PUT /products_index
> - **数据写入不同索引**:
	- 日志数据写入 `logs_index`
	- 用户数据写入 `users_index`
	- 商品数据写入 `products_index`
> - **查询时指定索引**:
	- 查询日志数据:
		```json
GET /logs_index/_search
{
  "query": {
    "match_all": {}
  }
}
  • 查询用户数据: ```json GET /users_index/_search { “query”: { “match_all”: {} } }
> #### 2. 动态索引策略
> 根据数据的时间、地理位置等属性动态创建索引。例如,按月创建日志索引:
> ```bash
PUT /logs_2024_06
PUT /logs_2024_07

在写入数据时,根据日期动态选择索引:

POST /logs_2024_06/_doc { “timestamp”: “2024-06-21T12:34:56”, “message”: “log message” }

> 优点
> 1. **性能提升**
	- 通过将数据分散到多个索引上,减少单个索引的负载,提高写入和查询性能。
> 2. **灵活性**
	- 各索引可以根据数据类型独立配置和优化,满足不同的性能和功能需求。
> 3. **扩展性**
	- 通过动态创建和管理索引,可以轻松扩展存储和计算资源,适应数据量的增长。
> 4. **数据管理**
	- 分离不同类型的数据,便于管理和维护。可以对不同的数据类型应用不同的备份、恢复和生命周期管理策略。
> ##### 统一的设计思想
> 这种设计思想不仅在 Elasticsearch 和 MySQL 中广泛应用,还适用于许多分布式系统和数据库架构。其核心思想是通过分离和分散数据,提高系统的性能和可扩展性,同时增强数据管理的灵活性。以下是其他一些应用这种设计思想的场景:
> 1. **分库分表**
	- 在关系型数据库中,按某种规则将数据分散到多个数据库和表中,减少单库单表的压力,提高系统性能。
> 2. **分区表**
	- 在数据仓库和大数据系统中,按时间、地理位置等维度对数据进行分区,优化查询性能和数据管理。
> 3. **分布式文件系统**
	- 将大文件分割成小块,分散存储在不同的节点上,提供高效的存储和读取性能。
> #### 总结
> 通过将数据按类型分离存储到不同的索引中,Elasticsearch 实现了类似于 MySQL 分表的功能。这种设计思想通过分散数据负载,提高了系统的性能、可扩展性和数据管理灵活性。这种方法不仅适用于 Elasticsearch,还广泛应用于其他数据库和分布式系统中,是一种通用的架构优化策略。

### 高扩展性

![](/images/JcRbbDRbyoNPgUxrNvucbfNnn2R.png)

分散shard到不同的机器,形成Node。

### 高可用

![](/images/CNNhbTNVOotjrMxbYP9cWNkYnxc.png)

副本分片既可以提供读操作,又可以在主分片挂的时候升级为新的分片。当然里面的数据同步问题,以及延迟问题,又是一个老生常谈的话题了。

### Node角色分化

搜索架构需要支持的功能很多。

如果每个node都支持这些功能,那当Node有数据压力需要扩容的时候,就会把其他能力也顺带扩容,但是其实 其他能力完全够用,有些浪费。

![](/images/RUoEbGrmqosbjCxTULecbeGAnwc.png)

因此我们可以将这几类功能拆开,赋予node不同的身份。不同的角色负责不同的功能。

规模小的时候,可以一个Node多个角色

![](/images/ETOVbR0oQoeFN1xnFeUcJOxcnEh.png)

规模变大的时候,可以一个Node一个角色。

![](/images/Mo5fbj3e3oTeQ8xZb9lcZ19snMf.png)

### 去中心化

上面提到了主节点,那就意味着还有一个选主的过程。现在每个Node都是独立的,需要有个机制协调node之间的数据。

我们可以想到可以像Kafka那样引入一个中心节点Zookeeper。

![](/images/HqcPbIOgWojmkixhXjFcItfnnEb.png)

但是如果不引入中心节点,还有更轻量的方案吗?有!去中心化!

引入魔改的Raft模块,

![](/images/LlQXbmAN9oSXYlxtFc5cLoUcnDh.png)

### 那么,ES是什么?

从上面的那些演变过来,就变成了现在的ES。

![](/images/T6JWbMpnso2EUzxZFikc4GWJnth.png)

它对外提供HTTP接口,各个语言的客户端都可以介入进来。

现在我们看两个实际的例子,把上面提到的内容串联起来。

### ES的写入流程

当客户端应用发起数据写入请求,请求会先发到协调节点,协调节点根据Hash路由,判断数据应该写入到哪个数据节点里的哪个数据分片(即Which Node,Which Shard),找到主分片并写入,

![](/images/OX5NbYS8Io0hATx2BuccpanwnHc.png)

分片底层是Lucene,将数据固化为倒排索引以及组成segment的多种结构。

![](/images/Xjf3bhnFVofyHXxEZ67cTzsHnBh.png)

主分片写入成功后,会将分片同步给副本分片,副本分片写入成功后,主分片会响应给协调节点一个ack,最后协调节点响应客户端应用写入完成。

![](/images/UV3YbHJovos8u1xSY02cfgDBnGd.png)

### ES的搜索流程

#### 搜索阶段

ES的搜索流程分为两个阶段,分别是查询阶段Query Phase,和获取阶段Fetch Phase。请求会先发到集群中的协调节点,协调节点根据indexname的信息,可以了解到index name被分为了几个分片,以及这些分片shard分散在哪个数据节点上,将请求转发到这些分片上

搜索请求到达分片之后,分片底层的lucene库会并发搜索多个segment。根据倒排索引获取到文档ID,并结合doc values获取到排序信息。

![](/images/LOoObk1xIozrdNxb3mVcUkMNnjh.png)

分片将结果聚合返回给协调节点。

![](/images/J92iby7DZoMPUcx6ckCcHYTJnIg.png)

协调节点对多个分片中拿到的结果进行排序聚合,舍弃大部分不需要的数据。

### 获取阶段

协调节点再次拿着文档ID请求数据节点里面的分片,分片内的lucene库会丛segment的Stored Fields中读取出完成的文档内容,并返回给协调节点,最终将结果返回给客户端

![](/images/R2cYb2TcfoaDUnx8lWGctiZmnZf.png)

文档信息

Search

    Table of Contents