ElasticSearch详解

目录

一、Elasticsearch是什么?

二、为什么要使用ElasticSearch

2.1 关系型数据库有什么问题?

2.2 ElasticSearch有什么优势?

2.3 ES使用场景

三、ElasticSearch概念、原理与实现

3.1 搜索引擎原理

3.2 Lucene 倒排索引核心原理

倒排索引

四、Elasticsearch 整体架构

4.1 集群节点角色

4.2 数据副本

4.3 水平扩容

4.4 故障转移

4.5 路由机制

4.6 新建、更新、删除文档

4.6.1 写数据底层原理

4.6.2 索引的不变性

4.6.3 动态索引创建过程

4.6.4 分步查看数据持久化过程

4.6.5 更新/删除数据底层原理

4.7 查询文档

4.8 更新文档

4.9 分布式检索

五、ElasticSearch实际使用过程中会有什么问题

5.1 分片的设定

5.2 ES数据近实时问题

5.3 深分页问题

六、ES性能优化

七、ElasticSearch运维


一、Elasticsearch是什么?

Elasticsearch(简称ES)是一个分布式、可扩展、实时的搜索与数据分析引擎。ES不仅仅只是全文搜索,还支持结构化搜索、数据分析、复杂的语言处理、地理位置和对象间关联关系等。

ES的底层依赖Lucene,Lucene可以说是当下最先进、高性能、全功能的搜索引擎库。但是Lucene仅仅只是一个库。为了充分发挥其功能,你需要使用Java并将Lucene直接集成到应用程序中。更糟糕的是,您可能需要获得信息检索学位才能了解其工作原理,因为Lucene非常复杂。

鉴于Lucene如此强大却难以上手的特点,诞生了ES。ES也是使用Java编写的,它的内部使用Lucene做索引与搜索,它的目的是隐藏Lucene的复杂性,取而代之的提供一套简单一致的RESTful API。

ES具有如下特点:

  • 一个分布式的实时文档存储引擎,每个字段都可以被索引与搜索

  • 一个分布式实时分析搜索引擎,支持各种查询和聚合操作

  • 能胜任上百个服务节点的扩展,并可以支持PB级别的结构化或者非结构化数据

二、为什么要使用ElasticSearch

2.1 关系型数据库有什么问题?

传统的关系数据库提供事务保证,具有不错的性能,高可靠性,久经历史考验,而且使用简单,功能强大,同时也积累了大量的成功案例。

后来,随着访问量的上升,几乎大部分使用 MySQL 架构的网站在数据库上都开始出现了性能问题,web 程序不再仅仅专注在功能上,同时也在追求性能。

读写分离
由于数据库的写入压力增加,读写集中在一个数据库上让数据库不堪重负,大部分网站开始使用主从复制技术来达到读写分离,以提高读写性能和读库的可扩展性。Mysql 的 master-slave 模式成为这个时候的网站标配了。

分表分库
开始流行使用分表分库来缓解写压力和数据增长的扩展问题。这个时候,分表分库成了一个热门技术,也是业界讨论的热门技术问题。

MySQL 的扩展性瓶颈
大数据量高并发环境下的 MySQL 应用开发越来越复杂,也越来越具有技术挑战性。分表分库的规则把握都是需要经验的。虽然有像淘宝这样技术实力强大的公司开发了透明的中间件层来屏蔽开发者的复杂性,但是避免不了整个架构的复杂性。分库分表的子库到一定阶段又面临扩展问题。还有就是需求的变更,可能又需要一种新的分库方式。

关系数据库很强大,但是它并不能很好的应付所有的应用场景。MySQL 的扩展性差(需要复杂的技术来实现),大数据下 IO 压力大,表结构更改困难,正是当前使用 MySQL 的开发人员面临的问题。

2.2 ElasticSearch有什么优势?

非关系型、搜索引擎、近实时搜索与分析、高可用、天然分布式、横向可扩展

2.3 ES使用场景

  1. 搜索引擎
    电商网站的商品搜索、站内搜索、模糊查询、全文检索服务

  2. 非关系型数据库
    业务宽表(数据库字段太多,查询太慢,索引没有办法再做优化)
    数据库做统计查询

  3. 大数据近实时分析引擎

  4. 日志分析

三、ElasticSearch概念、原理与实现

3.1 搜索引擎原理

一次完整的搜索从用户输入要查询的关键词开始,比如想查找 Lucene 的相关学习资料,我们都会在 Google 或百度等搜索引擎中输入关键词,比如输入“Lucene ,全文检索框架”,之后系统根据用户输入的关键词返回相关信息。一次检索大致可分为四步:

第一步:查询分析
正常情况下用户输入正确的查询,比如搜索“里约奥运会”这个关键词,用户输入正确完成一次搜索,但是搜索通常都是全开放的,任何的用户输入都是有可能的,很大一部分还是非常口语化和个性化的,有时候还会存在拼写错误,用户不小心把“淘宝”打成“涛宝”,这时候需要用自然语言处理技术来做拼写纠错等处理,以正确理解用户需求。

第二步:分词技术
这一步利用自然语言处理技术将用户输入的查询语句进行分词,如标准分词会把“lucene全文检索框架”分成 lucene | 全 | 文|检|索|框|架|, IK分词会分成: lucene|全文|检索|框架|,还有简单分词等多种分词方法。

第三步:关键词检索
提交关键词后在倒排索引库中进行匹配,倒排索引就是关键词和文档之间的对应关系,就像给文档贴上标签。比如在文档集中含有 "lucene" 关键词的有文档1 、文档 6、文档9,含有 "全文检索" 关键词的有文档1 、文档6 那么做与运算,同时含有 "lucene" 和 "全文检索" 的文档就是文档1和文档6,在实际的搜索中会有更复杂的文档匹配模型。

第四步:搜索排序
对多个相关文档进行相关度计算、排序,返回给用户检索结果。

3.2 Lucene 倒排索引核心原理

Lucene 是一个成熟的权威检索库,具有高性能、可伸缩的特点,并且开源、免费。在其基础上开发的分布式搜索引擎便是 Elasticsearch。

Elasticsearch 的搜索原理简单过程是,索引系统通过扫描文章中的每一个词,对其创建索引,指明在文章中出现的次数和位置,当用户查询时,索引系统就会根据事先的索引进行查找,并将查找的结果反馈给用户的检索方式。

倒排索引

倒排索引是整个 ES 的核心,正常的搜索以一本书为例,应该是由 “目录 -> 章节 -> 页码 -> 内容” 这样的查找顺序,这样是正排索引的思想。

但是设想一下,我在一本书中快速查找 “elasticsearch” 这个关键字所在的页面该怎么办?

倒排索引的思路是通过单词到文档ID的关系对应。

倒排索引包含两个部分:

  • 单词词典(Term Dictionary):记录所有文档的单词,记录单词到倒排列表的关联关系(单词词典一般比较大,通过 B+ 树或哈希拉链法实现,以满足高性能的插入与查询)

  • 倒排表(Posting List):记录了单词对应的文档结合,由倒排索引组成。

    • 文档ID

    • 词频 TF - 该单词在文档中分词的位置。用于语句搜索

    • 位置(Position)- 单词在文档中分词的位置,用于语句搜索

    • 偏移(Offset)- 记录单词的开始结束位置,实现高亮显示。

如何理解倒排索引呢?假如现有三份数据文档,文档的内容如下分别是:

  • Java is the best programming language.

  • PHP is the best programming language.

  • Javascript is the best programming language.

为了创建倒排索引,我们通过分词器将每个文档的内容域拆分成单独的词(我们称它为词条或 Term),创建一个包含所有不重复词条的排序列表,然后列出每个词条出现在哪个文档。

结果如下所示:

Term          Doc_1    Doc_2   Doc_3
-------------------------------------
Java        |   X   |        |
is          |   X   |   X    |   X
the         |   X   |   X    |   X
best        |   X   |   X    |   X
programming |   x   |   X    |   X
language    |   X   |   X    |   X
PHP         |       |   X    |
Javascript  |       |        |   X
-------------------------------------

这种结构由文档中所有不重复词的列表构成,对于其中每个词都有一个文档列表与之关联。

这种由属性值来确定记录的位置的结构就是倒排索引。带有倒排索引的文件我们称为倒排文件。

我们将上面的内容转换为图的形式来说明倒排索引的结构信息,如下图所示:

其中主要有如下几个核心术语需要理解:

  • 词条(Term):索引里面最小的存储和查询单元,对于英文来说是一个单词,对于中文来说一般指分词后的一个词。

  • 词典(Term Dictionary):或字典,是词条 Term 的集合。搜索引擎的通常索引单位是单词,单词词典是由文档集合中出现过的所有单词构成的字符串集合,单词词典内每条索引项记载单词本身的一些信息以及指向“倒排列表”的指针。

  • 倒排表(Post list):一个文档通常由多个词组成,倒排表记录的是某个词在哪些文档里出现过以及出现的位置。

每条记录称为一个倒排项(Posting)。倒排表记录的不单是文档编号,还存储了词频等信息。

  • 倒排文件(Inverted File):所有单词的倒排列表往往顺序地存储在磁盘的某个文件里,这个文件被称之为倒排文件,倒排文件是存储倒排索引的物理文件。

像 B+ 树一样,可以在页里实现二分查找。

Lucene 的倒排索引,增加了最左边的一层「字典树」term index,它不存储所有的单词,只存储单词前缀,通过字典树找到单词所在的块,也就是单词的大概位置,再在块里二分查找,找到对应的单词,再找到单词对应的文档列表。

Lucene 的实现会要更加复杂,针对不同的数据结构采用不同的字典索引,使用了FST模型、BKDTree等结构。

真实的倒排记录也并非一个链表,而是采用了SkipList、BitSet等结构

四、Elasticsearch 整体架构

Document:文档,指一行数据;
Index:索引,是多个document的集合(和sql数据库的表对应);
Shard:分片,当有大量的文档时,由于内存的限制、磁盘处理能力不足、无法足够快的响应客户端的请求等,一个节点可能不够。这种情况下,数据可以分为较小的分片。每个分片放到不同的服务器上。
当你查询的索引分布在多个分片上时,ES会把查询发送给每个相关的分片,并将结果组合在一起,而应用程序并不知道分片的存在。即:这个过程对用户来说是透明的
Replia:副本,为提高查询吞吐量或实现高可用性,可以使用分片副本。
副本是一个分片的精确复制,每个分片可以有零个或多个副本。ES中可以有许多相同的分片,其中之一被选择更改索引操作,这种特殊的分片称为主分片。
当主分片丢失时,如:该分片所在的数据不可用时,集群将副本提升为新的主分片。
Node:节点,形成集群的每个服务器称为节点,一个节点可以包含多个shard
Cluster:集群,ES可以作为一个独立的单个搜索服务器。不过,为了处理大型数据集,实现容错和高可用性,ES可以运行在许多互相合作的服务器上。这些服务器的集合称为集群。

4.1 集群节点角色

ES集群的服务器主要分为以下三种角色:

1)master节点:负责保存和更新集群的一些元数据信息,之后同步到所有节点,所以每个节点都需要保存全量的元数据信息:

  • 集群的配置信息

  • 集群的节点信息

  • 模板template设置

  • 索引以及对应的设置、mapping、分词器和别名

  • 索引关联到的分片以及分配到的节点

主节点负责创建索引、删除索引、跟踪哪些节点是群集的一部分,并决定哪些分片分配给相关的节点、追踪集群中节点的状态等,稳定的主节点对集群的健康是非常重要的。

2)data节点:负责数据存储和查询

数据节点负责数据的存储和相关的操作,例如对数据进行增、删、改、查和聚合等操作,所以数据节点(Data 节点)对机器配置要求比较高,对 CPU、内存和 I/O 的消耗很大。

3)coordinator节点(协调节点):
路由索引请求
聚合搜索结果集
分发批量索引请求

4)候选主节点:

可以被选举为主节点(Master 节点),集群中只有候选主节点才有选举权和被选举权,其他节点不参与选举的工作。

master选举选举策略
1. 如果集群中存在master,认可该master,加入集群

2. 如果集群中不存在master,从具有master资格的节点中选id最小的节点作为master选举时机

如果你使用 Master 候选节点作为单播列表,你只要列出三个就可以了。这个配置在 elasticsearch.yml 文件中:

discovery.zen.ping.unicast.hosts: ["host1", "host2:port"]

每个节点既可以是候选主节点也可以是数据节点,通过在配置文件 ../config/elasticsearch.yml 中设置即可,默认都为 true。

node.master: true  //是否候选主节点
node.data: true    //是否数据节点

3. 集群启动:后台启动线程去ping集群中的节点,按照上述策略从具有master资格的节点中选举出master
4. 现有的master离开集群:后台一直有一个线程定时ping master节点,超过一定次数没有ping成功之后,重新进行master的选举避免脑裂
脑裂问题是采用master-slave模式的分布式集群普遍需要关注的问题,脑裂一旦出现,会导致集群的状态出现不一致,导致数据错误甚至丢失。
5. ES避免脑裂的策略:过半原则,可以在ES的集群配置中添加一下配置,避免脑裂的发生

4.2 数据副本

ES通过副本分片的方式,保证集群数据的高可用,同时增加集群并发处理查询请求的能力,相应的,在数据写入阶段会增大集群的写入压力。

数据写入的过程中,首先被路由到主分片,写入成功之后,将数据发送到副本分片,为了保证数据不丢失,最好保证至少一个副本分片写入成功以后才返回客户端成功。

4.3 水平扩容

Node 1 和 Node 2 上各有一个分片被迁移到了新的 Node 3 节点,现在每个节点上都拥有2个分片,而不是之前的3个。 这表示每个节点的硬件资源(CPU, RAM, I/O)将被更少的分片所共享,每个分片的性能将会得到提升。

分片是一个功能完整的搜索引擎,它拥有使用一个节点上的所有资源的能力。 我们这个拥有6个分片(3个主分片和3个副本分片)的索引可以最大扩容到6个节点,每个节点上存在一个分片,并且每个分片拥有所在节点的全部资源。

但是如果我们想要扩容超过6个节点怎么办呢?

主分片的数目在索引创建时就已经确定了下来。实际上,这个数目定义了这个索引能够 存储 的最大数据量。(实际大小取决于你的数据、硬件和使用场景。) 但是,读操作——搜索和返回数据——可以同时被主分片 或 副本分片所处理,所以当你拥有越多的副本分片时,也将拥有越高的吞吐量。

在运行中的集群上是可以动态调整副本分片数目的,我们可以按需伸缩集群。让我们把副本数从默认的 1 增加到 2 :

4.4 故障转移

如果我们关闭第一个节点,这时集群的状态为:

我们关闭的节点是一个主节点。而集群必须拥有一个主节点来保证正常工作,所以发生的第一件事情就是选举一个新的主节点: Node 2 。

在我们关闭 Node 1 的同时也失去了主分片 1 和 2 ,并且在缺失主分片的时候索引也不能正常工作。 如果此时来检查集群的状况,我们看到的状态将会为 red :不是所有主分片都在正常工作。

幸运的是,在其它节点上存在着这两个主分片的完整副本, 所以新的主节点立即将这些分片在 Node 2 和 Node 3 上对应的副本分片提升为主分片。

4.5 路由机制

当索引一个文档的时候,文档会被存储到一个主分片中。 Elasticsearch 如何知道一个文档应该存放到哪个分片中呢?当我们创建文档时,它如何决定这个文档应当被存储在分片 1 还是分片 2 中呢?

首先这肯定不会是随机的,否则将来要获取文档的时候我们就不知道从何处寻找了。实际上,这个过程是根据下面这个公式决定的:

shard = hash(routing) % number_of_primary_shards

routing 是一个可变值,默认是文档的 _id ,也可以设置成一个自定义的值。 routing 通过 hash 函数生成一个数字,然后这个数字再除以 number_of_primary_shards (主分片的数量)后得到 余数 。这个分布在 0 到 number_of_primary_shards-1 之间的余数,就是我们所寻求的文档所在分片的位置。

这就解释了为什么我们要在创建索引的时候就确定好主分片的数量 并且永远不会改变这个数量:因为如果数量变化了,那么所有之前路由的值都会无效,文档也再也找不到了。

4.6 新建、更新、删除文档

  • 客户端选择一个 node 发送请求过去,这个 node 就是 coordinating node (协调节点)。

  • coordinating node 对 document 进行路由,将请求转发给对应的 node(有 primary shard)。

  • 实际的 node 上的 primary shard 处理请求,然后将数据同步到 replica node 。

  • coordinating node 如果发现 primary node 和所有 replica node 都搞定之后,就返回响应结果给客户端。

4.6.1 写数据底层原理

write -> refresh -> flush -> merge

4.6.2 索引的不变性

由于倒排索引的结构特性,在索引建立完成后对其进行修改将会非常复杂。再加上几层索引嵌套,更让索引的更新变成了几乎不可能的动作。
所以索性设计成不可改变的:倒排索引被写入磁盘后是不可改变的,它永远不会修改。不变性有重要的价值:
1. 不需要锁。如果你从来不更新索引,你就不需要担心多进程同时修改数据的问题。
2. 一旦索引被读入内核的文件系统缓存,便会留在哪里,由于其不变性。只要文件系统缓存中还有足够的空间,那么大部分读请求会直接请求内存,而不会命中磁盘。这提供了很大的性能提升。
3. 其它缓存(像filter缓存),在索引的生命周期内始终有效。它们不需要在每次数据改变时被重建,因为数据不会变化。

4. 写入单个大的倒排索引允许数据压缩,减少磁盘 I/O 和 需要被缓存到内存的索引的使用量。

当然,一个不变的索引也有不好的地方。主要事实是它是不可变的,你不能修改它。如果你需要让一个新的文档 可被搜索,你需要重建整个索引。这要么对一个索引所能包含的数据量造成了很大的限制,要么对索引可被更新的频率造成了很大的限制。

怎样在保留不变性的前提下实现倒排索引的更新?答案是: 用更多的索引。

通过增加新的补充索引来反映新近的修改,而不是直接重写整个倒排索引。每一个倒排索引都会被轮流查询到—从最早的开始—查询完后再对结果进行合并。

4.6.3 动态索引创建过程

  • write+route:协调节点默认使用文档ID进行计算(也支持通过 routing),为路由提供合适的分片。

shard = hash(document_id) % (num_of_primary_shards)
  • refresh:当分片所在的节点接收到来自协调节点(Coordinating Node)的请求后,将请求写入到 Memory Buffer,然后定时(默认是每隔1s)写入到 Filesystem Cache,这个从 Memery Buffer 到 Filesystem Cache 的过程叫做 refresh。

  • flush:假设当 Memory Buffer 和 Filesystem Cache 的数据丢失时,ES 通过 translog 的机制来保证数据的不丢失。其实现机制是接收到请求后,同时也会写入到 translog 中,当 Filesystem Cache 中的数据写入到磁盘中时,才会清除掉,这个过程叫做 flush。在 flush 过程中,内存中的缓冲将被清除,内容被写入一个新的 Segment,Segment 的 fsync 将创建一个新的提交点( Commit Point),并将内容刷新到磁盘,旧的translog将被删除并开始一个新的translog,flush 触发的时机是定时触发(默认30分钟)或者 translog 变得太大(默认512M)时。

  • merge:由于自动刷新流程每秒会创建一个新的 Segment ,这样会导致短时间内的 Segment 数量暴增。每一个 Segment 都会消耗文件句柄、内存和cpu运行周期。更重要的是,每个搜索请求都必须轮流检查每个Segment ;所以 Segment 越多,搜索也就越慢。小的 Segment 被合并到 大的 Segment,然后这些大的Segment 再合并到更大的 Segment。

# 一个 Segment 的内部
$ tree xgChCUdvTZ2G0dyJjGcp6A
xgChCUdvTZ2G0dyJjGcp6A
├── 0
│   ├── _state
│   │   ├── retention-leases-42.st
│   │   └── state-3.st
│   ├── index
│   │   ├── _1e.fdm
│   │   ├── _1e.fdt
│   │   ├── _1e.fdx
│   │   ├── _1e.fnm
│   │   ├── _1e.kdd
│   │   ├── _1e.kdi
│   │   ├── _1e.kdm
│   │   ├── _1e.si
│   │   ├── _1e_1.fnm
│   │   ├── _1e_1_Lucene90_0.dvd
│   │   ├── _1e_1_Lucene90_0.dvm
│   │   ├── _1e_Lucene90_0.doc
│   │   ├── _1e_Lucene90_0.dvd
│   │   ├── _1e_Lucene90_0.dvm
│   │   ├── _1e_Lucene90_0.tim
│   │   ├── _1e_Lucene90_0.tip
│   │   ├── _1e_Lucene90_0.tmd
│   │   ├── _1f.cfe
│   │   ├── _1f.cfs
│   │   ├── _1f.si
│   │   ├── segments_e
│   │   └── write.lock
│   └── translog
│       ├── translog-16.tlog
│       └── translog.ckp
└── _state
    └── state-8.st

4.6.4 分步查看数据持久化过程

write -> refresh -> flush -> merge

  • write 过程

一个新文档过来,会存储在 in-memory buffer 内存缓冲区中,顺便会记录 Translog(Elasticsearch 通过事务日志,在每一次对 Elasticsearch 进行操作时记录操作)。

这个时候数据还没有到 segment,搜不到新文档。数据只有被 refresh 后,才可以被搜索到。

  • refresh 过程

refresh 默认1s,可通过 index.refresh_interval 设置。refresh 流程大致如下:

  1. in-memory buffer 中的文档写入到新的 segment 中,但 segment 是存储在文件系统的缓存中。此时文档已经可以被搜索到。

  2. 最后清空 in-memory buffer。注意:Translog 没有被清空,为了将 segment 写到磁盘中。

  3. 文档经过 refresh 后,segment 暂时写到文件系统缓存。这样避免了性能 IO 操作,又可以使文档搜索到, refresh 每秒执行一次,性能损耗较大,一般建议稍微延长这个 refresh 时间间隔,比如 5s。这样做到准实时就可以了。

  • flush 过程

每隔一段时间,如果 translog 变得越来越大,索引会被刷新(flush);一个新的 translog 被创建,并且一个全量提交被执行

上个过程 segment 在文件系统缓存中,会有意外故障文档丢失,那么,为了保证文档不会丢失,需要将文档从文件缓存写入磁盘的过程就是 flush,写入磁盘后,清空 translog,具体过程如下:

  1. 所有的内存缓冲区的文档被写入一个新的 Segment。

  2. 缓冲区被清空。

  3. 一个 Commit Point 被写入硬盘。

  4. 文件系统缓存通过 fsync 被刷新(flush)。

  5. 老的 translog 被删除。

  • merge 过程

由于自动刷新流程每秒会创建一个新的 Segment ,这样会导致短时间内的 Segment 数量暴增。而 Segment 数目太多会带来较大的麻烦。 每一个 Segment 都会消耗文件句柄、内存和cpu运行周期。更重要的是,每个搜索请求都必须轮流检查每个Segment ;所以 Segment 越多,搜索也就越慢。

Elasticsearch 通过在后台进行 Merge Segment 来解决这个问题,小的 Segment 被合并到 大的 Segment,然后这些大的Segment 再合并到更大的 Segment。

当索引的时候, refresh 操作会创建新的 Segment 并将Segment 打开供搜索使用,合并进程选择一小部分大小相似的段,并且在后台将他们合并到更大的段中。这并不会中断索引和搜索。

合并大的段需要消耗大量的I/O和CPU资源,如果任其发展会影响搜索性能。Elasticsearch在默认情况下会对合并流程进行资源限制,所以搜索仍然 有足够的资源很好地执行。

4.6.5 更新/删除数据底层原理

如果是删除操作,commit 的时候会生成一个 .del 文件,里面将某个 doc 标识为 deleted 状态,那么搜索的时候根据 .del 文件就知道这个 doc 是否被删除了。

如果是更新操作,就是将原来的 doc 标识为 deleted 状态,然后新写入一条数据。

buffer 每 refresh 一次,就会产生一个 segment file ,所以默认情况下是 1 秒钟一个 segment file ,这样下来 segment file 会越来越多,此时会定期执行 merge。每次 merge 的时候,会将多个 segment file 合并成一个,同时这里会将标识为 deleted 的 doc 给物理删除掉,然后将新的 segment file 写入磁盘,这里会写一个 commit point ,标识所有新的 segment file ,然后打开 segment file 供搜索使用,同时删除旧的 segment file 。

4.7 查询文档

可以从主分片或者从其它任意副本分片检索文档

可以通过 doc id 来查询,会根据 doc id 进行 hash,判断出来当时把 doc id 分配到了哪个 shard 上面去,从那个 shard 去查询。

shard = hash(document_id) % (num_of_primary_shards)
  • 客户端发送请求到任意一个 node,成为 coordinate node 。

  • coordinate node 对 doc id 进行哈希路由,将请求转发到对应的 node,此时会使用 round-robin 随机轮询算法,在 primary shard 以及其所有 replica 中随机选择一个,让读请求负载均衡。

  • 接收请求的 node 返回 document 给 coordinate node 。

  • coordinate node 返回 document 给客户端。

4.8 更新文档

以下是部分更新一个文档的步骤:

  1. 客户端向 Node 1 发送更新请求。

  2. 它将请求转发到主分片所在的 Node 3 。

  3. Node 3 从主分片检索文档,修改 _source 字段中的 JSON ,并且尝试重新索引主分片的文档。 如果文档已经被另一个进程修改,它会重试步骤 3 ,超过 retry_on_conflict 次后放弃。

  4. 如果 Node 3 成功地更新文档,它将新版本的文档并行转发到 Node 1 和 Node 2 上的副本分片,重新建立索引。 一旦所有副本分片都返回成功, Node 3 向协调节点也返回成功,协调节点向客户端返回成功。

4.9 分布式检索

所有的搜索系统一般都是两阶查询,第一阶段查询到匹配的 DocID,第二阶段再查询 DocID 对应的完整文档。

  1. 在初始查询阶段时,查询会广播到索引中每一个分片拷贝(主分片或者副本分片),每个分片在本地执行搜索并构建一个匹配文档大小为 from + size 的优先队列。(搜索时会查询 Filesystem Cache ,但有部分数据还在 Memory Buffer,所以搜索是近实时的)。

  2. 每个分片返回各自优先队列中所有文档 ID 和排序值给协调结点。

  3. 协调节点辨别出哪些些文档需要被取回并向相关的分片提交多个 GET 请求,每个分片加载并丰富文档,所有文档取回后,协调结点返回结果给客户端。

查询阶段

在初始查询阶段时,查询会广播到索引中每一个分片拷贝(主分片或者副本分片)。 每个分片在本地执行搜索并构建一个匹配文档的优先队列。

查询阶段包含以下三个步骤:

  1. 客户端发送一个 search 请求到 Node 3 , Node 3 会创建一个大小为 from + size 的空优先队列。

  2. Node 3 将查询请求转发到索引的每个主分片或副本分片中。每个分片在本地执行查询并添加结果到大小为 from + size 的本地有序优先队列中。

  3. 每个分片返回各自优先队列中所有文档的 ID 和排序值给协调节点,也就是 Node 3 ,它合并这些值到自己的优先队列中来产生一个全局排序后的结果列表。

当一个搜索请求被发送到某个节点时,这个节点就变成了协调节点。 这个节点的任务是广播查询请求到所有相关分片并将它们的响应整合成全局排序后的结果集合,这个结果集合会返回给客户端。

协调节点将这些分片级的结果合并到自己的有序优先队列里,它代表了全局排序结果集合。至此查询过程结束。

取回阶段

查询阶段标识哪些文档满足搜索请求,但是我们仍然需要取回这些文档,这是取回阶段的任务。

分布式阶段由以下步骤构成:

  1. 协调节点辨别出哪些文档需要被取回并向相关的分片提交多个 GET 请求。

  2. 每个分片加载并 丰富 文档,如果有需要的话,接着返回文档给协调节点。

  3. 一旦所有的文档都被取回了,协调节点返回结果给客户端。

协调节点首先决定哪些文档确实需要被取回。例如,如果我们的查询指定了 { "from": 90, "size": 10 } ,最初的90个结果会被丢弃,只有从第91个开始的10个结果需要被取回。这些文档可能来自和最初搜索请求有关的一个、多个甚至全部分片。

深分页
每个分片必须先创建一个 from + size 长度的队列,协调节点需要根据 number_of_shards * (from + size) 排序文档,来找到被包含在 size 里的文档。
取决于你的文档的大小,分片的数量和你使用的硬件,给 10,000 到 50,000 的结果文档深分页( 1,000 到 5,000 页)是完全可行的。但是使用足够大的 from 值,排序过程可能会变得非常沉重,使用大量的CPU、内存和带宽。

五、ElasticSearch实际使用过程中会有什么问题

5.1 分片的设定

分片数过小,数据写入形成瓶颈,无法水平拓展

分片数过多,每个分片都是一个lucene的索引,分片过多将会占用过多资源

如何计算分片数

需要注意分片数量最好设置为节点数的整数倍,保证每一个主机的负载是差不多一样的,特别的,如果是一个主机部署多个实例的情况,更要注意这一点,否则可能遇到其他主机负载正常,就某个主机负载特别高的情况。

一般我们根据每天的数据量来计算分片,保持每个分片的大小在 50G 以下比较合理。如果还不能满足要求,那么可能需要在索引层面通过拆分更多的索引或者通过别名 + 按小时 创建索引的方式来实现了。

5.2 ES数据近实时问题

ES数据写入之后,要经过一个refresh操作之后,才能够创建索引,进行查询。但是get查询很特殊,数据实时可查。

ES5.0之前translog可以提供实时的CRUD,get查询会首先检查translog中有没有最新的修改,然后再尝试去segment中对id进行查找。5.0之后,为了减少translog设计的负责性以便于再其他更重要的方面对translog进行优化,所以取消了translog的实时查询功能。

get查询的实时性,通过每次get查询的时候,如果发现该id还在内存中没有创建索引,那么首先会触发refresh操作,来让id可查。

5.3 深分页问题

解决方案1:服务端缓存 Scan and scroll API

为了返回某一页记录,其实我们抛弃了其他的大部分已经排好序的结果。那么简单点就是把这个结果缓存起来,下次就可以用上了。根据这个思路,ES提供了Scroll API。它概念上有点像传统数据库的游标(cursor)。

scroll调用本质上是实时创建了一个快照(snapshot),然后保持这个快照一个指定的时间,这样,下次请求的时候就不需要重新排序了。从这个方面上来说,scroll就是一个服务端的缓存。既然是缓存,就会有下面两个问题:

  1. 一致性问题。ES的快照就是产生时刻的样子了,在过期之前的所有修改它都视而不见。

  2. 服务端开销。ES这里会为每一个scroll操作保留一个查询上下文(Search context)。ES默认会合并多个小的索引段(segment)成大的索引段来提供索引速度,在这个时候小的索引段就会被删除。但是在scroll的时候,如果ES发现有索引段正处于使用中,那么就不会对它们进行合并。这意味着需要更多的文件描述符以及比较慢的索引速度。

其实这里还有第三个问题,但是它不是缓存的问题,而是因为ES采用的游标机制导致的。就是你只能顺序的扫描,不能随意的跳页。而且还要求客户每次请求都要带上”游标”。

解决方案2:Search After

Scroll API相对于from+size方式当然是性能好很多,但是也有如下问题:

  1. Search context开销不小。

  2. 是一个临时快照,并不是实时的分页结果。

针对这些问题,ES 5.0 开始推出了Search After机制可以提供了更实时的游标(live cursor)。它的思想是利用上一页的分页结果来提高下一页的分页请求。

六、ES性能优化

存储设备

磁盘在现代服务器上通常都是瓶颈。Elasticsearch 重度使用磁盘,你的磁盘能处理的吞吐量越大,你的节点就越稳定。

这里有一些优化磁盘 I/O 的技巧:

  • 使用 SSD。就像其他地方提过的, 他们比机械磁盘优秀多了。

  • 使用 RAID 0。条带化 RAID 会提高磁盘 I/O,代价显然就是当一块硬盘故障时整个就故障了。不要使用镜像或者奇偶校验 RAID 因为副本已经提供了这个功能。

  • 另外,使用多块硬盘,并允许 Elasticsearch 通过多个 path.data 目录配置把数据条带化分配到它们上面。

  • 不要使用远程挂载的存储,比如 NFS 或者 SMB/CIFS。这个引入的延迟对性能来说完全是背道而驰的。

  • 如果你用的是 EC2,当心 EBS。即便是基于 SSD 的 EBS,通常也比本地实例的存储要慢。

内部索引优化

Elasticsearch 为了能快速找到某个 Term,先将所有的 Term 排个序,然后根据二分法查找 Term,时间复杂度为 logN,就像通过字典查找一样,这就是 Term Dictionary。

现在再看起来,似乎和传统数据库通过 B-Tree 的方式类似。但是如果 Term 太多,Term Dictionary 也会很大,放内存不现实,于是有了 Term Index。

就像字典里的索引页一样,A 开头的有哪些 Term,分别在哪页,可以理解 Term Index是一棵树。

这棵树不会包含所有的 Term,它包含的是 Term 的一些前缀。通过 Term Index 可以快速地定位到 Term Dictionary 的某个 Offset,然后从这个位置再往后顺序查找。

在内存中用 FST 方式压缩 Term Index,FST 以字节的方式存储所有的 Term,这种压缩方式可以有效的缩减存储空间,使得 Term Index 足以放进内存,但这种方式也会导致查找时需要更多的 CPU 资源。

对于存储在磁盘上的倒排表同样也采用了压缩技术减少存储所占用的空间。

调整配置参数

调整配置参数建议如下:

  • 给每个文档指定有序的具有压缩良好的序列模式 ID,避免随机的 UUID-4 这样的 ID,这样的 ID 压缩比很低,会明显拖慢 Lucene。

  • 对于那些不需要聚合和排序的索引字段禁用 Doc values。Doc Values 是有序的基于 document=>field value 的映射列表。

  • 不需要做模糊检索的字段使用 Keyword 类型代替 Text 类型,这样可以避免在建立索引前对这些文本进行分词。

  • 如果你的搜索结果不需要近实时的准确度,考虑把每个索引的 index.refresh_interval 改到 30s 。

如果你是在做大批量导入,导入期间你可以通过设置这个值为 -1 关掉刷新,还可以通过设置 index.number_of_replicas: 0 关闭副本。别忘记在完工的时候重新开启它。

  • 避免深度分页查询建议使用 Scroll 进行分页查询。普通分页查询时,会创建一个 from+size 的空优先队列,每个分片会返回 from+size 条数据,默认只包含文档 ID 和得分 Score 给协调节点。

如果有 N 个分片,则协调节点再对(from+size)×n 条数据进行二次排序,然后选择需要被取回的文档。当 from 很大时,排序过程会变得很沉重,占用 CPU 资源严重。

  • 减少映射字段,只提供需要检索,聚合或排序的字段。其他字段可存在其他存储设备上,例如 Hbase,在 ES 中得到结果后再去 Hbase 查询这些字段。

  • 创建索引和查询时指定路由 Routing 值,这样可以精确到具体的分片查询,提升查询效率。路由的选择需要注意数据的分布均衡。

JVM 调优

JVM 调优建议如下:

  • 确保堆内存最小值( Xms )与最大值( Xmx )的大小是相同的,防止程序在运行时改变堆内存大小。

Elasticsearch 默认安装后设置的堆内存是 1GB。可通过 ../config/jvm.option 文件进行配置,但是最好不要超过物理内存的50%和超过 32GB。

  • GC 默认采用 CMS 的方式,并发但是有 STW 的问题,可以考虑使用 G1 收集器。

  • ES 非常依赖文件系统缓存(Filesystem Cache),快速搜索。一般来说,应该至少确保物理上有一半的可用内存分配到文件系统缓存。

七、ElasticSearch运维

  1. 磁盘打满:

删除不用的索引

获取磁盘空间情况:
GET /_cat/allocation?v
查看索引占用情况:
GET _cat/indices?v
查看索引占用情况(按索引排序):
GET _cat/indices?v&s=index:asc
删除索引:
DELETE onebyone-bizmember-k8s-logs-2023.05.30-171317
  1. 节点挂掉

参考:

理解ElasticSearch工作原理

看完这篇还不会 Elasticsearch,我跪搓衣板!

elasticsearch 原理及入门

  • 7
    点赞
  • 42
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值