Elasticsearch核心技术(四):分布式存储架构与索引原理分析

本文探讨Elasticsearch的数据请求、路由和写入过程的原理,主要涉及ES的分布式存储架构、节点和副本的写入过程、近实时搜索的原因、持久化机制等。

4.1 ES存储架构

我们经常说,看一件事情千万不要直接陷入细节里,应该先鸟瞰全貌,这样才有助于从高维度理解问题。分析ES的索引原理和写入过程也是一样,首先需要了解ES的存储架构。

4.1.1 集群、节点、分片

ES天生就是分布式架构的。ES的底层是Lucene,而Lucene只是一个搜索引擎库,没有并发设计 ,没有分布式相关的设计,因此要想使用Lucene来处理海量数据,并利用分布式的能力,就需要在其之上进行分布式的相关设计。ES就是这样一款建立在Lucene基础之上,赋予其分布式能力的存储引擎,说成天生就是分布式架构的一点也不过分。

集群是有多个节点组成的,在上图中可以看到集群中有多个不同种类型的节点。

节点是一个Elasticsearch的实例,本质上是一个Java进程。每个节点上面都保存着集群的状态信息,包括所有的节点信息、所有的索引和相关的Mapping于Setting信息和分片的路由信息等。节点按照角色可以划分为主节点、数据节点、协调节点和预处理节点等。

Master节点负责管理集群状态信息,包括处理创建、删除索引等请求,决定分片被分配到哪个节点,维护和更新集群状态。值得注意的是,只有Master节点才能修改集群的状态信息,并负责同步给其他节点。可见,Master节点非常重要,在部署上需要考虑单点风险。

协调节点负责接收客户端的请求,将请求路由到到合适的节点,并将结果汇集到一起。

数据节点是保存数据的节点,增加数据节点可以解决水平扩展和解决数据单点的问题。

预处理节点是数据前置处理转换的节点,支持 pipeline管道设置,可以对数据进行过滤、转换等操作。

更多关于节点内容参考:https://www.elastic.co/guide/en/elasticsearch/reference/7.10/modules-node.html

分片是ES分布式存储的基石,是底层的基本读写单元。分片的目的是分割巨大的索引,将数据分散到集群内各处。分片分为主分片和副本分片,一般情况,一个主分片有多个副本分片。主分片负责处理写入请求和存储数据,副本分片只负责存储数据,是主分片的拷贝,文档会存储在具体的某个主分片和副本分片上。

数据分片技术是指分布式存储系统按照一定的规则将数据存储到对应的存储节点中,或者到对应的存储节点中获取想要的数据。
数据复制技术是指将数据进行备份,使得多个节点都存储该数据,提高系统可用性和可靠性。

4.2 写入过程分析

4.2.1 路由和读写过程

ES集群中的协调节点负责接收来自客户端的读写请求,当协调节点收到请求时,ES通过文档到分片的映射算法找到对应的主分片,将请求转发给主分片处理,主分片完成写入之后,将写入同时发送给副本分片,副本分片执行写入操作后返回主分片,主分片再将结果返回给协调节点,协调节点将结果返回给客户端,完成一次完整的写入过程。

4.2.2 文档到分片的路由算法

一个优秀的映射算法,需要将文档均匀分布在所有分片上面,并且充分利用硬件资源。

根据历史经验判断,潜在的映射算法大概有这几种:

  1. 随机算法、轮询算法

如果ES参考Nginx,采用这两种算法,写入的时候比较简单,也可以使得数据均匀分布。但是查询某个特定数据的时候,无法知道数据保存在哪个分片上面,需要遍历所有分片查询,效率会很低,当分片数多的时候,对性能的影响效果会愈发明显。所以ES没有采用随机算法和轮询算法。

  1. 关键字哈希算法,空间换时间

如果使用表记录文档和分片的映射关系,貌似可以达到空间换时间的效果,但是一旦文档和分片的映射关系发生改变(例如增加分片),就要修改映射关系表。如果是单线程操作表,所有操作都要串行执行,如果是多线程操作表,就涉及到加锁开销。另外,由于整个集群的文档数量是无法预估的,数据非常多的情况下, 如果ES直接记录映射关系,整个映射表会非常庞大,这个映射表存储在服务端会占用很大的空间。所以ES也没有采用该算法。

  1. 关键字哈希算法,实时计算

这种算法是根据关键字(如文档id)自动计算出需要去哪个分片上面去写入和查询。该算法相当于消耗了很少的CPU资源,不但让数据分布更加均衡,还可以让省去映射表存储和维护的成本,是个聪明的选择。没错,ES采用的就是这种方式实现从文档到分片的路由的。具体的路由算法如下:shard_num=hash(_routing) % num_primary_shards

  • 默认的_routing值是文档id
  • 可以自行设置routing数值,通过API中的_routing参数指定
  • 创建索引时,主分片数一经设定,无法随意更改的原因;如果修改,将重建索引

注:

  1. ES最新版本7.15将算法进行优化为:

关于更多_routing参数内容可参考:https://www.elastic.co/guide/en/elasticsearch/reference/7.10/mapping-routing-field.html#mapping-routing-field

  1. 许多编程语言内置的简单哈希函数可能并不适合分片,例如,Java的Object.hashCode和Ruby的Object#hash,同一键在不同的进程中可能返回不同的哈希值。许多数据系统使用MurmurHash算法进行哈希计算,例如Redis、Memcached等。

4.2.3 写入数据与持久化过程

当有新数据写入的时候,ES首先写入Index Buffer区域,此时是无法检索的。

默认情况下ES每秒钟进行一次Refresh操作,将Index Buffer中的index刷新到文件系统缓存,在文件系统缓存中,是以Segment进行存储的,而且是可以被搜索到的,这就是ES实现近实时搜索:文档的更改无法立即被搜索到,但是在一定时间会变得可见。

By default, Elasticsearch periodically refreshes indices every second, but only on indices that have received one search request or more in the last 30 seconds. This is why we say that Elasticsearch has near real-time search: document changes are not visible to search immediately, but will become visible within this timeframe.

需要说明的是,Refresh 触发的情况有3种:

  1. 按照时间频率触发,默认情况是每 1 秒触发 1 次 Refresh,可通过index.refresh_interval 设置;
  2. 当Index Buffer 被占满的时候,会触发 Refresh,Index Buffer 的大小默认值是 JVM 所占内存容量的 10%;
  3. 手动调用调用Refresh API。

由于Refresh操作默认间隔为1s,因此会产生大量的小Segment,ES查询时会同时查询所有的Segment,并对结果进行汇总,大量小Segment会使性能变差。因此ES会对小Segment进行段合并(Merge),合并操作会丢弃掉重复的键,并只保留每个键最近的更新。段合并之后搜索请求可以直接访问合并之后的Segment,从而提高搜索性能。

Merge触发的情况有2种:

  1. ES自动启动后台任务进行Merge;
  2. 手动调用_forcemerge API主动触发。

在段合并完成之后,ES会将Segment文件Flush到磁盘中,并创建一个Commit Point文件,用来标识被Flush到磁盘的Segment。Commit Point其实是记录所有的Segment信息,关于移除的Segment的信息会记录在“.del”文件中,查询结果后会从该文件中进行过滤。

Flush操作是将Segment从文件系统缓存写入到磁盘进行持久化,在执行 Flush 的时候会依次执行下面操作:

  1. 清空Index Buffer
  2. 记录 Commit Point
  3. 刷新Segment到磁盘
  4. 删除translog

4.2.4 Translog机制

为了保障数据安全,ES增加了Translog, 在数据写入Index Buffer的同时,写入一份到Translog。默认每个写入请求,Translog会追加写入磁盘的,这样就可以防止服务器宕机后数据丢失。如果对可靠性要求不是很高,也可以设置异步落盘,由配置参数 index.translog.durabilityindex.translog.sync_interval控制。

index.translog.durability:默认是request,每个请求都落盘;设置成async,可异步写入
index.translog.sync_interval:默认5s,不能小于100ms

官方文档地址:https://www.elastic.co/guide/en/elasticsearch/reference/7.10/index-modules-translog.html

Translog落盘有2种情况:

  1. 每个请求同步或者异步落盘
  2. Flush的时候,内存中的Segment和Translog同时落盘

4.2.5 并发控制

针对写入和更新时可能出现的并发问题,ES是通过文档版本号来解决的。当用户对文档进行操作时,并不需要对文档加锁和解锁操作,只需要带着版本号。当版本号冲突的时候,ES会提示冲突并抛出异常,并进行重试,重试次数可以通过参数retry_on_conflict进行设置。

4.3 总结

4.3.1 分布式存储系统数据分区

文档到分片的路由算法,从本质上讲,其实是分布式存储系统数据分区的问题。如果数据量太大,单台机器进行存储和处理就会成为瓶颈,因此需要引入数据分区机制。分区的目的是通过多台机器均匀分布数据和查询负载,避免出现热点。这需要选择合适的数据分区方案,在节点添加或删除时重新动态平衡分区。

数据分区方式主要有两种,一种是顺序分布,另外一种是哈希分布。

顺序分布具体做法是对关键字进行排序,每个分区值负责一段包含最小到最大关键字范围的一段关键字。对关键字排序的优点是可以支持高效的区间查询。

哈希分布具体做法是根据数据的某个关键字计算哈希值,并将哈希值与集群中的服务器建立关系,从而将不同哈希值的数据分布到不同的服务器上。传统哈希算法是将哈希值和服务器个数做除法取模映射。这种方法的优点是计算方式简单;缺点是当服务器数量改变时,数据映射会被完全打乱,数据需要重新分布和迁移,频繁的迁移会大大增加再平衡的成本。还有一点,通过关键字哈希分区,丧失了良好的区间查询特性。

对于分区再平衡数据迁移,解决思路是引入中间层,用中间层来维护哈希值和服务器节点之间的映射关系。有一个简单的解决方案是这样的,首先创建远超实际节点数的分区数,然后为每个节点分配多个分区。这种方案维持分区总数不变,也不改变关键字到分区的映射关系,仅需要调整的是分区和节点的对应关系。

Elasticsearch支持这种动态平衡方法。使用该策略时,分区数量一般在创建之初就确定好了,之后不会改变。

4.3.2 分布式存储系统数据复制

为了保证分布式系统的高可用,数据在系统中一般存储多个副本。当某个副本所在的存储节点发生故障的时候,分布式系统能够自动将服务切换到其他的副本,从而实现自动容错。

当数据写入主副本的时候,由主副本进行写入操作,并复制到其他副本。如果主副本和副本分片都写入成功才返回客户端写入成功,是同步复制技术,属于强一致性。强一致性的好处在于如果主副本出现故障,至少有一个备副本拥有完整数据,系统可以自动进行切换而不必担心数据丢失。但是如果副本写入出现问题将阻塞主副本的写操作,系统可用性变差。

如果主副本写入成功,立刻返回客户端写入成功,采用异步的方式进行数据同步,而不等待副本写入成功,通过定时任务补偿等手段达到最终一致性。最终一致性好处是系统可用性较好,但是一致性较差,如果主副本发生不可恢复故障,可能丢失最后一次更新的数据。

一致性 可用性 应用场景
同步复制技术 适用于对数据一致性有严格要求的场景
异步复制技术 适用于对行囊够要求很高的场景
半同步复制技术 较强 较弱 适用于大多数的分布式场景

Elasticsearch天生是分布式架构的,满足分区容错性,在数据复制写入时,在CP和AP之间做了取舍,选择了CP,做到数据写入不丢失,而丢失了高可用。

posted @ 2021-12-16 08:33  James_Shangguan  阅读(5704)  评论(0编辑  收藏  举报