ES读写原理
# ES读写原理
# 读流程
以上图三个节点为例,三个节点组成一个es集群,一个索引,3主3从。
- 当查询从客户端发送到节点2时,此客户端发送的请求被节点2接受并处理,此时我们称节点2为此次请求的协调节点,节点2会将查询发送到索引对应的三个分片,三个分片可能是主可能是从,es会根据负载均衡算法来决定查询请求发送到哪个分片。
- 各个分片会将查询结果的docid和排序字段返回给协调节点,注此时并不返回文档的全部数据。
- 协调节点根据各个分片返回的docid和排序字段进行排序,筛选出需要返回给客户端的文档docid 使用docid去对应分片上拉取文档真实数据,返回给客户端
- 过程排序,数据若能完全在内存中,则使用快速排序(logn ~ n), 若内存放不下所有数据,就要借助文件进行排序,使用归并排序算法(nlogn)
# es二阶段查询
query阶段, 2,3,4为query阶段
fetch阶段, 5,6为fetch阶段
es是一个分布式的存储和检索系统,在存储的时候默认是根据每条记录的_id字段做路由分发的,ES会把index中的多个document分配到多个不同的分片上,且尽量保持每个分片上document的数量平衡。es服务端是准确知道每个document分布在哪个分片(shard)上的。一个document相当于关系型数据库中的一行数据,它在ES中是最小的数据单位,因此一个document不会被拆分到多个分片上。
当我们读取数据时候,我们不知道那些document会被匹配到,任何一个分片上都有可能,所以一个查询请求必须查询索引的一个或多个分片才能完整的查询到我们想要的结果。找到所有匹配的结果是查询的第一步,来自多个分片上的数据集在分页返回到客户端之前会被合并到一个排序后的list列表,由于需要经过一步取top N的操作,所以读取需要进过两个阶段才能完成,分别是query和fetch。
当发出一个查询请求的时候,这个查询会被广播到索引里面的每一个分片(主分片或副本分片),每个分片会在本地执行查询请求后会生成一个命中文档的优先级队列。这个队列是一个排序好的top N数据的列表,它的size等于from+size的和,也就是说如果你的from是10,size是10,那么这个队列的size就是20。这也正是ES不支持使用From+Size来进行深度分页的原因,(ES查询from+size>10000则不支持)from+size越大,那么每个分片都要返回大量数据给coordinate node协调节点,会消耗大量的带宽,内存,CPU。因此ES推荐使用滚屏查询来进行深度分页。
读完后每个分片返回数据ID和所有参与排序字段的值例如_score到优先级队列里面,然后再返回给coordinating node(协调节点,这里面是A节点,即请求第一个打到的节点),然后A节点负责将所有分片里面的数据给合并到一个全局的排序的列表。
上面提到一个术语叫coordinating node,这个节点是当search请求随机负载的发送到一个节点上,然后这个节点就会成为一个coordinating node,它的职责是广播search请求到所有相关的分片上,然后合并他们的响应结果到一个全局的排序列表中然后进行第二个fetch阶段,注意这个结果集仅仅包含docId和所有排序字段的值,search请求可以被主分片或者副本分片处理,这也是为什么我们说增加副本的个数就能增加搜索吞吐量的原因,coordinating节点将会通过round-robin的方式自动负载均衡。
取阶段(fetch)
上面我们讲到当每个节点前N条数据的ID汇总到了协调节点A之后,A就会再对这所有数据再进行排序,然后取出所需的那几条的ID,再根据ID发送MultiGet请求到相关分片上得到对应的doucument返回给客户端。
SearchType
在查询时ES有多种搜索的方式,分别是query then fetch、query and fetch、DFS query and fetch、DFS query then fetch,文章上面讲到的是ES默认的搜索方式query then fetch。
query then fetch(默认搜索方式)
搜索时,没有指定搜索方式,就是使用的这种搜索方式。这种搜索方式两个步骤,第一步,先向所有的分片发出请求,各分片只返回排序和排名相关的信息(注意,不包括文档document),然后按照各分片返回的分数进行重新排序和排名,取前size个文档。然后进行第二步,去相关的分片根据ID取document。这种方式返回的document与用户要求的size是相等的。
query and fetch
向索引的所有分片(shard)都发出查询请求,各分片返回的时候把元素文档(document)和计算后的排名信息一起返回。这种搜索方式是最快的。因为相比下面的几种搜索方式,这种查询方法只需要去每个分片查询一次。但是各个分片返回的结果的数量之和可能是用户要求的size的n倍,因为每个节点都是返回的from+size的条数。
DFS query then fetch
这种搜索方式的流程是在上面query then fetch的基础上,在执行query前先进行了初始化散发的工作。官网文档的意思大概是初始化散发其实就是在进行真正的查询之前,先把各个分片的词频率和文档频率收集一下,然后进行词搜索的时候,各分片依据全局的词频率和文档频率进行搜索和排名。这两种方式搜索精度最高但是效率是最慢的。
### 写流程
- 客户端请求发送到一个节点, 该节点被称为协调节点,协调节点解析请求发现为写请求,解析其doc id,计算其路由,将其发送到对应的主分片上 primary shard
- 数据会先被写入内存buff中,然后再写入translog,进行 2.1 2.2 的操作,数据会被写入磁盘上的translog文件。一旦数据被写入磁盘,es 宕机不会丢失数据。
- 3过程可以在2.1操作成功之后进行,也可以在 2.2操作成功之后进行。 如果3过程在2.1之后进行,那么es会存在数据丢失的风险,因为给客户端返回写入成功,但translog并未入磁盘,仍存在os cache中,os cache也在内存中,内存数据断电丢失,如果4过程之后,2.2过程执行之前宕机,数据丢失。如果3过程在2.2之后进行,则es不会出现数据丢失情况。 可以通过es的参数 来进行控制,设置为同步,会在每次写入translog时都写入磁盘,设置为异步,则2.2操作会异步进行,默认5s进行一次写磁盘操作(fsync)
- 数据经过5过程才会被search操作搜索得到
- 当使用文档id进行get查询时,即使数据在内存buff中也可以搜索得到,5过程默认1s操作一次, 将内存buff中的数据写入os cache对应的segment file
- 6过程是将os cache中的segment file 写入磁盘,操作本质上与2.2一致,默认每半小时更新一次
- flush过程同时也会写一个commit到commit point中
所以,ES 使用了一个内存缓冲区 Buffer,先把要写入的数据放进 buffer。内存性能好,但不安全,会丢数据,所以 es 使用了一个日志文件 Translog。就像 MySQL 的 Binlog,记录着每一条操作日志,如果 ES 出现故障,重启之后可以从 Translog 中恢复数据。因为日志文件只是单纯的做内容追加,没有其他逻辑操作,所以写入速度很快。所以数据来到 primary shard 之后,先是进入 buffer,并把操作记录写入 Translog。
ES 默认每隔一秒执行一次 refresh 操作,会创建一个 Segment 文件,将 buffer 中的数据写入这个 segment,并清空 buffer。 ES 有一个后台程序,用于 merge 合并这些 segment 文件,把小 segment 整合到一个大的 segment 中,并修改 commit point 的 segment 记录。merge 过程还会清理被删除的数据。es 接收到删数据请求时,不会真的到 segment 中把数据删了,而是把要删除的数据写到 ‘.del’ 文件中,在读操作时,会根据这个文件进行过滤。merge 合并时才真正删除,合并后的 segment 中就没有已经删除的数据了。 进入 segment 的数据就进入了 Lucene,建立好了倒排索引,可以被搜索到。
ES 虽然把数据写入了 segment 文件,但实际上还没有真正落盘,因为操作系统的文件系统也是有缓存的,这是操作系统层面的性能优化,ES每隔5秒会异步执行一次fsync操作,即把系统缓存的内容真正强制写到硬盘上.
每次写buffer时会同时追加TransLog日志,那随着数据的写入,这个日志文件肯定会越来越大,触发清理TransLog文件的两个条件是文件大小达到阈值、或时间超过30分钟,二者满足任意一个就会执行一次flush操作。 flush操作的执行共有4个步骤:
- 执行 refresh 操作。
- 把这次提交动作之前所有没有落盘的 segment 强制fsync,确保写入物理文件。
- 创建一个提交点,记录这次提交对应的所有 segment,写入 commit point 文件。
- 清空 Translog,因为 Segment 都已经踏实落地了,之前的 Translog 就不需要了。