文章目录
- 一、同步和异步、阻塞和非阻塞
- 1.1 四种组合
- 二、客户端
- 2.1 高级客户端文档解析
- 2.2 文档索引
- 2.3 构建JSON文档
- 2.4 文档处理过程解析
- 2.5 数据入盘流程
- 2.6 与MongoDB比较
- 三、 文档分片存储
- 3.1 操作文档时分片的工作流程
- 3.2 es的数据分区
- 四、搜索过程解析
- 4.1 对已知文档的搜索
- 4.2 对未知文档的搜索
一、同步和异步、阻塞和非阻塞
常识:
- 对同一个线程而言,会有阻塞和非阻塞之分;对不同线程而言,会有同步和异步之分。
- 阻塞状态一般是当前线程同步等待其他线程的处理结果。非阻塞状态一般是当前线程异步于需配合的其他线程的结果处理过程。
同步和异步:
- 同步状态:指当前线程发起请求调用后,在被调用的线程处理消息过程中,当前线程必须等被调用的线程处理完才能返回结果;如果被调用的线程还未处理完,则当前线程是不能返回结果的,当前线程必须主动等待所需的结果。
- 异步状态:指当前线程发起请求调用后,被调用的线程直接返回,但是并没有返回给当前线程对应的结果,而是等被调用的线程处理完消息后,通过状态、通知或者回调函数来通知调用线程,当前线程处于被动接收结果的状态。
- 总结:同步和异步最主要的区别在于任务或接口调用完成时消息通知的方式。
阻塞和非阻塞:
- 阻塞状态:被调用的线程在返回处理结果前,当前线程会被挂起,不释放CPU 执行权,当前线程也不能做其他事情,只能等待,直到被调用的线程返回处理结果后,才能接着向下执行。
- 非阻塞状态:当前线程在没有获取被调用的线程的处理结果前,不是一直等待,而是继续向下执行。如果此时是同步状态,则当前线程可以通过轮询的方式检查被调用线程的处理结果是否返回:如果此时是异步状态,则只有在被调用的线程处理后才会通知当前线程回调。
- 总结:阻塞和非阻塞最主要的区别在于当前线程发起任务或接口调用后是否能继续执行。
1.1 四种组合
常识:
- 同步和异步、阻塞和非阻塞还可以两两组合,从而产生 4 种组合,即同步阻塞、同步非阳塞、异步阻塞和异步非阻塞。
- 前端人员熟悉的 Nodejs、运维人员和前后端人员都熟悉的 Nginx 就是异步非阻塞的典型代表。
名称 | 释义 |
---|---|
同步阻塞 | 当前线程在得不到调用结果前不返回,当前线程进入阻塞态等待。 |
同步非阻塞 | 当前线程在得不到调用结果前不返回,但当前线程不阻塞,一直在 CPU 中运行。 |
异步阻塞 | 当前线程调用其他线程,当前线程自己并不阻塞,但其他线程会阻塞来等待结果。 |
异步非阻塞 | 当前线程调用其他线程,其他线程一直在运行,直到得出结果。 |
二、客户端
客户端知识的学习偏向于开发,所以我这里只列举出来了解以下即可。
- 客户端分两种,初级客户端和高级客户端。
- 初级客户端:
- 定义:是es为用户提供的官方版初级客户端。
- 作用:初级客户端可以通过 HTTP与es集群进行通信,将请求封装发给es集群,再将es集群的响应封装返回给用户。初级客户端与所有es版本都兼容。
- 功能:
- 跨所有可用节点的负载平衡。
- 在节点故障和特定响应代码时的故障转移。
- 失败连接的惩罚机制。判断一个失败节点是否重试,取决于客户端连接时连续失败的次数:失败的尝试次数越多,客户端再次尝试同一节点之前等待的时间越长。
- 持连接。
- 请求和响应的跟踪日志记录。
- 自动发现群集节点,该功能可选。
- 高级客户端:
- 定义:是用于弹性搜索的高级客户端,是基于初级客户端而来的。
- 作用:高级客户端公开了 API特定的方法,并负责处理未编组的请求和响应。
2.1 高级客户端文档解析
常识:
- 在es中,文档序列化格式为JSON格式。JSON 对象由键值对组成,键 (key) 是字段或属性名称,值 (value)可以是多种类型。比如字符串、数字、布尔值、一个对象、值数组、日期的字符串、地理位置对象等。
- 在es中,存储并索引的JSON 数据被称为文档,文档以唯一 ID 标识存储在es中。文档不仅有自身的键值对数据信息,还包含元数据,也就是关于文档的信息。文档一般包含三个必需的元数据信息:
- index:文档存储的数据结构,即索引,表示的是es存储和索引关联数据的数据结构。
- type:文档代表的对象类型。
- ID:文档的唯一标识。
- 文档数据最终是被存储和索引在分片中的,索引多个分片存储的逻辑空间。
- 在面向对象编程时,每个对象都是一个类的实例,这个类定义了对象的多个属性或与对象关联的数据和方法。在es中,每个对象类型都有自己的映射结构。所有类型下的文档都被存储在同一个索引下。
- type 的命名有一定规范,名字中的字母可以是大写,也可以是小写,但不能包含下画线或逗号。
- id 表示文档的唯一标识,如ID,文档标识会以字符串形式出现。创建文档时,可以自定义id,也可以让 es自动生成。
- id、index 和 type 三个元素组合使用时,可以在es中唯一标识一个文档。
2.2 文档索引
- es对文档的操作不仅限于存储,es还会索引每个文档的内容使之可以被搜索。
- 在es中,用户可以对文档数据进行索引、搜索、排序和过滤等操作,而这也是es能够执行复杂的全文搜索的原因之一。
2.3 构建JSON文档
es中构建一个JSON文档方式:
- 手动使用本地 byte [ ] ,或使用 String 来构建JSON文档。
- 使用 Map,es会自动把它转换成与其等价的JSON文档。
- 使用如 Jackson 这样的第三方类库来序列化开发人员构建的 Java Bean,以便构建JSON 文档。
- 使用内置的帮助类 XContentFactory.isonBuilder 来构建JSON文档。
2.4 文档处理过程解析
为什么es写入磁盘的倒排索引是不可变的?
- 读写操作轻量级,不需要锁。若es从来不需更新一个索引,则就不必担心多个程序同时尝试修改索引的情况。
- 一旦索引被读入文件系统的内存,它就会一直在那儿,因为不会改变。当文件系统内存有足够大的空间时,大部分的索引读写操作是可以直接访问内存,而不是磁盘就能实现的,显然这有助于提升es性能。
- 当写入单个大的倒排索引时,es可以压缩数据,以减少磁盘 I/O 和需要存储索引的内存大小。
使用多个索引的原因?
- 不可变的索引也有缺点, 因为它不可变,所以用户不能改变它。若想要搜索一个新文档,则必须重建整个索引,不仅会严重限制一个索引所能装下的数据,还限制一个索引可以被更新的频率。
- 所以,es使用多个索引,保持倒排索引不可变好处的同时又能更新倒排索引。
多个索引:
- es不是重写整个倒排索引,而是增加额外的索引反映最近的变化。每个倒排索引都可以按顺序查询,从最“老旧”的索引开始查询,最后把结果聚合起来。
- es的底层依赖于 Lucene,Lucene 中的索引其实是es中的分片,es中的索引是分片的集合。当es搜索索引时,它发送查询请求给该索引下的所有分片,然后过滤这些结果,最后聚合成全局的结果。
per-segment search 的引用原因:
- 为了避免混淆,es引入了 per-segment search 的概念。
- 一个段(segment)就一个是有完整功能的倒排索引。Lucene 中的索引指的是段的集合,再加上提交点 (commit point,包括所有段的文件)。新的文档在被写入磁盘的段之前,需要先写入内存区的索引。
per-segment search工作流程:
- 新的文档首先被写入内存区的索引。
- 内存中的索引不断被提交,新段不断产生。当新的提交点产生时就将这些新段的数据写入磁盘,包括新段的名称。写入磁盘是文件同步写入的,换句话说,所有的写操作都需要等待文件系统内存的数据同步到磁盘,确保它们可以被物理写入。
- 新段被打开,于是它包含的文档就可以被检索到。
- 内存被清除,等待接收新的文档。
- 当一个请求被接收,所有段依次被查询时。所有段上的 termm 统计信息会被聚合,确保每个 term 和文档的相关性被正确计算。通过这种方式,新的文档就能够以较小的代价加入索引。
段是不可变的,那么es是如何删除和更新文档数据的呢?
- 段的不可变特性,意味着文档既不能从旧的段中移除,旧的段中的文档也不能被更新。于是es在每一个提交点都引入一个.del 文件,包含了段上已经被删除的文档。
- 当一个文档被删除时,它实际上只是在.del 文件中被标记为删除。在进行文档查询时,被删除的文档依然可以被匹配查询,但是在最终返回之前会从结果中删除。
- 当一个文档被更新时,旧版本的文档会被标记为删除,新版本的文档在新的段中被索引当对文档进行查询时,该文档的不同版本都会匹配一个查询请求,但是较旧的版本会从结果中被删除。
es引入段合并段的原因:
- 被删除的文件越积累越多,每个段消耗的如文件句柄、内存、CPU 等资源越来越大。如果每次搜索请求都需要依次检查每个段,则段越多,查询就越慢。这些势必会影响es的性能,那么es是如何处理的呢?es引入了段合并段。在段合并时,我们会展示被删除的文件是如何从文件系统中清除的。
- es通过后台合并段的方式解决了上述问题,在段合并过程中,小段被合并成大段,大段再合并成更大的段。在合并段时,被删除的文档不会被合并到大段中。
- 在索引过程中,refresh 会创建新的段,并打开它。合并过程是在后台选择一些小的段,把它们合并成大的段。在这个过程中不会中断索引和搜索。当新段合并后,即可打开供搜索:而旧段会被删除。
- 需要指出的是,合并大的段会消耗很多 I/O和CPU。为了不影响 Elasticsearch 的搜索性能在默认情况下,Elasticsearch 会限制合并过程,这样搜索就可以有足够的资源进行了
- 除自动完成段合并外,Elasticsearch 还提供了 optimize API,以便根据需要强制合并段。optimize API 强制分片合并段以达到指定 ax num segments 参数,这会减少段的数量 (通常为1),达到提高搜索性能的目的。
- 需要指出的是,不要在动态的索引上使用 optimize API。optimize API 的典型场景是记录日志。日志是按照每天、周、月存入索引的。旧的索引一般是只可读,不可修改的。在这种场景下,用户主动把每个索引的段降至 1 是有效的,因为搜索过程会用到更少的资源,性能更好。
2.5 数据入盘流程
es数据目录位置:
- es配置文件中有一个配置属性path.data,该属性包含了es存储的数据目录。
data目录的文件怎么来的?
- 在es中,是使用Lucene来处理分片级别的索引和查询的,所以data目录下的文件由es和 Lucene 写入产生的。
Lucene和es作用分工:
- Lucene 负责写和维护 Lucene 索引文件。
- es在 Lucene之上写与功能相关的元数据,如字段映射、索引设置和其他集群元数据等。
各文件作用:
- nodes 文件夹用于存储本机中的节点信息。
- indices 文件夹中的内容为索引信息,每个索引有一个随机字符串的名称,显然该文件夹下的索引数量与本机中的索引数量相同。随机打开一个索引为 3MJyBU9yQ-m0xyjNINQDCQ的文件夹,如图5-8 所示。
- 如图所示,有两类子文件夹:_state 类文件夹和分片类文件夹。其中,0为分片文件夹。
- _state 文件夹包含了indices /{ index-name }/state /state- {version}.st 路径中的索引状态文件。
- 0分片文件夹包含与索引的第一个分片相关的数据文件,即分片 0里的数据。
文件一:版本控制信息、有关分片是主分片还是副本的信息。
文件二:还有本分片下的索引信息。
文件三:translog 日志信息。translog 日志信息是wa的事务日志,在每个分片 translog 目录中的前缀 translog 中存在。
浅谈事物日志:
- 在es中,事务日志用于确保安全地将数据索引到es,而无须为每个文档执行低级 Lucene 提交。当提交 Lucene 索引时,会在 Lucene 级别创建一个新的 segment,即执行 fsync(),会产生大量磁盘I/0,从而影响性能。
- 为了能存储索引文档并使其可搜索,而不需要完整地 Lucene 提交,es将其添加到 Lucene IndexWriter,并将其附加到事务日志中。这样,在每个refresh interval之后,它将在 Lucene 索引上调用 reopen0),这将使数据可以在不需要提交的情况下就能进行搜索。这是 Lucene 近实时搜索 API的一部分。当IndexWriter最终自动刷新事务日志或由于显式刷新操作而提交时,先前的事务日志将被丢弃,新的事务日志将取代它。
- 如果需要恢复,则首先恢复在 Lucene 中写入磁盘的 segments,然后重放事务日志,以防止丢失尚未完全提交到磁盘的操作。
2.6 与MongoDB比较
与es相似,且同为文档型存储的中间件是 MongoDB。
MongoDB是干什么?
- MongoDB是一个基于分布式文件存储的数据库,是基于 C++ 编写的,旨在为 Web 应用提供可扩展的高性能数据存储解决方案。
特点:
- MongoDB与关系数据库很像,也是非关系数据库当中功能最丰富的。
es和 MongoDB 异同之处:
相同之处:
- 相同之处在于都是以 JSON 格式进行数据存储的,都支持对文档数据的增删改查,即 CRUD 操作
- 二者都使用了分片和复制技术,都支持处理超大规模数据。
不同之处:
- 开发语言不同。es基于 Java 编写,而 MongoDB 是基于 C++编写。
- 分片方式不同。es基于 Hash 模式进行分片,而 MongoDB 的分片模式除了Hash模式,还有Range模式。
- 集群的配置方式不同。es天然是分布式的,主副分片自动分配和复制,而MongoDB需要手工配置。
- 全文检索的便捷程度不同。es全文检索功能强大,字段自动索引,而MongoDB 仅支持有限的字段检索,且需人工索引。
使用场景:
- es适用于全文检索场景,而 MongoDB 适用于数据大批量存储的场景。
三、 文档分片存储
数据路由概念:
- 一个索引一般由多个分片构成,当用户执行添加、删除、修改文档操作时,es需要决定把这个文档存储在哪个分片上,这个过程就称为数据路由。把文档路由到分片上是通过路由算法实现的。
文档路由到分片的过程:
- 假设某个索引由 3个主分片组成,用户每次对文档进行增删改查时,都有一个 routing值,默认是该文档ID 的值。随后对这个 routing 值使用 Hash 函数进行计算,计算出的值再和主分片个数取余数,余数的取值范围永远是(0~number of primary shards-1)之间,文档知道应该存储在哪个对应的分片上。
- routing 值也可以手动指定一个值。手动指定对于负载均衡及提升批量读取的性能有一定的帮助。
为什么索引建立后不能修改主分片个数?
- 正是es的这种路由机制,主分片的个数在索引建立之后不能修改。因为修改索引主分片数目会直接导致路由规则出现严重问题,部分数据将无法被检索。
3.1 操作文档时分片的工作流程
常识了解:
- 项目中,出于数据安全和容灾等因素考虑,相同的分片不会放在同一个节点上。
- 一般来说,用户能够发送请求给集群中的任意一个节点。每个节点都有能力处理用户提交的任意请求,每个节点都知道任意文档所在的节点,所以也可以将请求转发到需要的节点。
文档进行增删改查时,主分片和副本分片是如何工作的呢?
- 比如我这里es集群有三个节点,R0主分片和R0副本分片、P0主分片和P0副本分片、R1主分片和R1的副分片,node1为主节点,该节点也作为请求节点。集群中有一个索引叫qingjun,并拥有两个主分片,每个主分片有两个副本分片。
- 当用户执行新建索引、更新和删除请求等写操作时,文档必须在主分片上成功完成请求才能复制到相关的副本分片上。
分片数据同步过程:
- 第一步:客户端发送一个索引或者删除的请求给主节点 node1。
- 第二步:node1 通过请求中的文档的ID 值来判断该文档应该被存储在哪个分片上,比如这里已经判断出需要分片编号为0的分片在节点3 中,则node1会把这个请求转发到node3上。
- 第三步:node3 在分片编号为0的分片上执行请求。若请求执行成功,则node3 将并行地将该请求发给分片编号为 0的所有副本上,也就是上图中node1和node2上的R0。
- 第四步:当所有的副本分片都成功地执行了请求,那么将向node3 回复一个请求执行成功的确认消息。node 3 收到所有副本节点的确认信息后,会向客户端返回一个成功的响应消息。
- 第五步:当客户端收到成功的响应信息时,文档的操作就已经被应用于主分片和所有的副本分片上,此时操作就会生效。
3.2 es的数据分区
搜索引擎有两种数据分区方式:
- 基于文档的分区方式:指每个文档只存一个分区,每个分区持有整个文档集的一个子集。这里说的分区是指一个功能完整的倒排索引,es就采用的此种方式。
- 基于词条的分区方式:指每个分区拥有一部分词条,词条里面包含了与该词条相关的整个 index 的文档数据。常见的搜索引擎系统有Riak Search、Lucandra和 Solandra。
优点 | 缺点 | |
---|---|---|
基于文档 | 1.每个分区都可以独立地处理查询。 2.可以非常方便地添加以文档为单位的索引信息。 3.在搜索过程中网络开销很小,每个节点可以分别独立地执行搜索,执行完之后只需返回文档的ID 和评分信息即可。而呈现给用户的结果集是在执行分布式搜索的节点上执行合并操作实现的。 | 1.消耗磁盘资源。如果查询需要在所有的分区上执行,则它将执行O(KxN) 次磁盘操作(K 是词条 term 的数量,N是分区的数量)。 |
基于词条 | 1.部分分区执行查询就可以查出结果。比如用户有 3 个 term 词条的查询,则 es在搜索时将至多命中 3 个分区。 2.时间复杂度低。当对应 K 个 term 词条的查询时,用户只需执行 O(K) 次磁盘查找即可。 | 1.复杂的查询会大大提高网络开销,并且可能使得系统可用性大大降低,特别是注入前置搜索或模糊搜索的场景。 2.获取每个文档的信息将会变得非常困难。因为这种分区方式使得文档的数据被分散到了不同的地方,所以实现注入评分、自定义评分等都将变得难以实现。 |
为什么es会采用基于文档分区方式?
- 从实用性角度来看,基于文档的分区方式已经被证明是一个构建大型的分布式信息检索系统的行之有效的方法,因此es使用的是基于文档的分区方式。
四、搜索过程解析
4.1 对已知文档的搜索
基本了解:
- 如果被搜索的文档能够从主分片或任意一个副本分片中被检索到,不论是单个文档,还是批量文档,则与索引文档过程相同。
- 对已知文档的搜索也会用到路由算法为“Shard = hash(routing) % number_of_primary_shards”。
主分片上搜索一个文档的必要步骤:
- 第一步:客户端给node1发送文档的 Get 请求,此时node1就成为协同节点。node1使用路由算法算出文档所在的主分片,随后node1节点将请求转发给主分片所在的node2,当然,也可以基于轮询算法转发给副本分片。
- 第二步:node1 根据文档的 ID 确定文档属于分片 R0。分片 R0对应的副本分片在三个节点上都有。此时,node1转发请求到node2。
- 第三步:node2在本地分片进行搜索,并将目标文档信息作为结果返给node1。
- 注意事项:对于读请求,为了在各节点间负载均衡,请求节点一般会为每个请求选择不同的分片,一般采用轮询算法循环在所有副本分片中进行请求。
4.2 对未知文档的搜索
基本了解:
- 除对已知文档的搜索外,大部分请求实际上是不知道查询条件会命中哪些文档的。
- 这些被查询条件命中的文档可能位于es集群中的任意位置上。因此,搜索请求的执行不得不去询问每个索引中的每一个分片。
两个阶段:
在es中,搜索过程分为查询阶段(Query Phase) 和获取阶段 (Fetch Phase)。
在查询阶段,查询请求会广播到索引中的每一个主分片和备份中,每一个分片都会在本地执行检索,并在本地各建立一个优先级队列 (Priority Queue)。该优先级队列是一份根据文档相关度指标进行排序的列表,列表的长度由 from 和 size 两个分页参数决定。
- 第一个小阶段:客户端发送一个检索请求给某节点 A,此时节点 A 会创建一个空的优先级队列,并
配置好分页参数from与size。- 第二个小阶段:节点 A 将搜索请求发送给该索引中的每一个分片,每个分片在本地执行检索,并将结果添加到本地优先级队列中。
- 第三个小阶段:每个分片返回本地优先级序列中所记录的 ID 与 sort 值,并发送给节点 A。节点A将这些值合并到自己的本地的优先级队列中,并做出全局的排序。
在获取阶段,主要是基于上一阶段找到所要搜索文档数据的具体位置,将文档数据内容取回并返回给客户端。