ElasticSearch核心原理

本章主题:
1、es分片存储问题及分片机制
2、es集群架构节点负载均衡问题
3、存储原理 4、集群leader选举、节点类型
5、横向扩容、数据恢复、集群故障探查问题
6、如何避免脑裂问题?
7、路由原理?
8、到底需要多大集群规模的机器?索引设置多少个分片?应该设置多少个副本?

1. 索引分片

ES集群中有多个节点(node),其中有一个为主节点,这个主节点是可以通过选举产生的,主从节点是对于集群内部来说的。

ES的一个概念就是去中心化,字面上理解就是无中心节点,这是对于集群外部来说的,因为从外部来看ES集群,在逻辑上是个整体,你与任何一个节点的通信和与整个ES集群通信是等价的。

ES集群是一个或多个节点的集合,它们共同存储了整个数据集,并提供了联合索引以及可跨所有节点的搜索能力。多节点组成的集群拥有冗余能力,它可以在一个或几个节点出现故障时保证服务的整体可用性。

集群靠其独有的名称进行标识,默认名称为“elasticsearch”。节点靠其集群名称来决定加入哪个ES集群,一个节点只能属一个集群。

es实战与原理解析 es内部原理_数据


一个索引可以存储超出单个结点硬件限制的大量数据。

比如,一个具有10亿文档的索引占据1TB的磁盘空间,而任一节点可能没有这样大的磁盘空间来存储或者单个节点处理搜索请求,响应会太慢。为了解决这个问题,Elasticsearch提供了将索引划分成多片的能力,这些片叫做分片。

ES的“分片(shard)”机制可将一个索引内部的数据分布地存储于多个节点,它通过将一个索引切分为多个底层物理的Lucene索引完成索引数据的分割存储功能,这每一个物理的Lucene索引称为一个分片(shard)。分片分布到不同的节点上。构成分布式搜索。

每个分片其内部都是一个全功能且独立的索引,因此可由集群中的任何主机存储。

创建索引时,用户可指定其分片的数量,默认数量为5个。分片的数量只能在索引创建前指定,并且索引创建后不能更改。

Shard有两种类型:primary和replica,即主shard及副本shard。

1)primary shard
用于文档存储,每个新的索引会自动创建5个Primary shard,当然此数量可在索引创建之前通过配置自行定义,不过,一旦创建完成,其Primary shard的数量将不可更改。

2)Replica shard
是Primary Shard的副本,用于冗余数据及提高搜索性能。 每个Primary shard默认配置了一个Replica shard,但也可以配置多个,且其数量可动态更改。ES会根据需要自动增加或减少这些Replica shard的数量。副本的作用一是提高系统的容错性,当个某个节点某个分片损坏或丢失时可以从副本中恢复。二是提高es的查询效率、吞吐率,es会自动对搜索请求进行负载均衡,转发到压力不高到副本分片上。

2. 负载均衡(故障转移)

es实战与原理解析 es内部原理_数据_02

从图可知:
1)每个索引被分成了5个分片;
2)每个分片有一个副本;
3)5个分片基本均匀分布在3个dataNode上;

注意分片的边框(border)有粗有细,具体区别是:
粗边框代表:primary shard
细边框代表:replica shard

演示负载均衡效果: 关闭集群中其中一个节点,剩余分片将会重新进行均衡分配。

es实战与原理解析 es内部原理_数据_03


重新分配必须满足:自己的主分片 和 副本分配不能在同一个节点


故障转移 ,节点挂掉后,节点上的分片转移时,分片的数据优先从其他节点相同的主分片或副本分片上获取,有可能副本分片直接变成主分片同时会再复制一个副本分片,如果该分片所在的所有节点都挂掉了,则通过IO将挂掉的ES所在的机器上的数据进行拷贝(ES进程挂了,服务器没挂),最后更新分片地址的指向

3. 索引存储原理

3.1. 分片存储原理


3.1.1. 不可变性

倒排索引被写入磁盘后是不可改变的,它永远不会修改。
这样做就带来了以下几个好处:

  • 没有必要给逆向索引加锁,因为不允许被更改,只有读操作,所以就不用考虑多线程导致互斥等问题。
  • 索引一旦被加载到了缓存中,大部分访问操作都是对内存的读操作,省去了访问磁盘带来的io开销。
  • 因为逆向索引的不可变性,所有基于该索引而产生的缓存也不需要更改,因为没有数据变更。
  • 使用逆向索引可以压缩数据,减少磁盘io及对内存的消耗。

既然逆向索引是不可更改的,那么如何添加新的数据,删除数据以及更新数据?为了解决这个问题,lucene将一个大的逆向索引拆分成了多个小的段segment。每个segment本质上就是一个逆向索引。在lucene中,同时还会维护一个文件commit point,用来记录当前所有可用的segment,当我们在这个commit point上进行搜索时,就相当于在它下面的segment中进行搜索,每个segment返回自己的搜索结果,然后进行汇总返回给用户。

3.1.2. 存储结构

引入了segment和commit point的概念之后,索引的存储结构如下图,每个段本身就是一个倒排索引,提交点文件中有一个列表存放着所有可用的段,下面是一个带有1个提交点和3个段的Index示意图:

es实战与原理解析 es内部原理_缓存_04

3.2. 文档存储流程

3.3.1. 文档的新增

数据的新增流程如下图:

es实战与原理解析 es内部原理_elasticsearch_05


1.新增的文档首先会被存放在内存的缓存中

2.当文档数足够多或者到达一定时间点时,就会对缓存进行commit

  • a.生成一个新的segment,并写入磁盘
  • b.生成一个新的commit point,记录当前所有可用的segment
  • c.等待所有数据都已写入磁盘

3.开启新增的segment,这样我们就可以对新增的文档进行搜索了

4.清空缓存,准备接收新的文档

下面展示了这个过程完成后的段和提交点的状态:

es实战与原理解析 es内部原理_搜索_06

3.3.2. 文档的更新与删除

segment是不能更改的,那么如何删除或者更新文档?

每个commit point都会维护一个.del文件,文件内记录了在某个segment内某个文档已经被删除。在segment中,被删除的文档依旧是能够被搜索到的,不过在返回搜索结果前,会根据.del把那些已经删除的文档从搜索结果中过滤掉。

对于文档的更新,采用和删除文档类似的实现方式。当一个文档发生更新时,首先会在.del中声明这个文档已经被删除,同时新的文档会被存放到一个新的segment中。这样在搜索时,虽然新的文档和老的文档都会被匹配到,但是.del会把老的文档过滤掉,返回的结果中只包含更新后的文档。

3.3.3. refresh

ES的一个特性就是提供实时搜索,新增加的文档可以在很短的时间内就被搜索到。在创建一个commit point时,为了确保所有的数据都已经成功写入磁盘,避免因为断电等原因导致缓存中的数据丢失,在创建segment时需要一个fsync的操作来确保磁盘写入成功。但是如果每次新增一个文档都要执行一次fsync就会产生很大的性能影响。在文档被写入segment之后,segment首先被写入了文件系统的缓存中,这个过程仅使用很少的资源。之后segment会从文件系统的缓存中逐渐flush到磁盘,这个过程时间消耗较大。但是实际上存放在文件缓存中的文件同样可以被打开读取。ES利用这个特性,在segment被commit到磁盘之前,就打开对应的segment,这样存放在这个segment中的文档就可以立即被搜索到了。

es实战与原理解析 es内部原理_缓存_07


上图中灰色部分即存放在缓存中,还没有被commit到磁盘的segment。此时这个segment已经可以进行搜索。

在ES中,将缓存中的文档写入segment,并打开segment使之可以被搜索的过程叫做refresh。默认情况下,分片的refresh频率是每秒1次。这就解释了为什么es声称提供实时搜索功能,新增加的文档会在1s内就可以进行搜索了。

Refresh的频率通过index.refresh_interval:1s参数控制,一条新写入es的文档,在进行refresh之前,是不能立即搜索到的。

通过执行curl -X POST127.0.0.1:9200/_refresh,可以手动触发refresh行为。

3.3.4. flush与translog

前面讲到,refresh行为会立即把缓存中的文档写入segment中,但是此时新创建的segment是写在文件系统的缓存中的。如果出现断电等异常,那么这部分数据就丢失了。所以es会定期执行flush操作,将缓存中的segment全部写入磁盘并确保写入成功,同时创建一个commit point,整个过程就是一个完整的commit过程。

但是如果断电的时候,缓存中的segment还没有来得及被commit到磁盘,那么数据依旧会产生丢失。为了防止这个问题,es中又引入了translog文件。

1.每当es接收一个文档时,在把文档放在buffer的同时,都会把文档记录在translog中。

es实战与原理解析 es内部原理_数据_08


2.执行refresh操作时,会将缓存中的文档写入segment中,但是此时segment是放在缓存中的,并没有落入磁盘,此时新创建的segment是可以进行搜索的。

es实战与原理解析 es内部原理_搜索_09


3.按照如上的流程,新的segment继续被创建,同时这期间新增的文档会一直被写到translog中。

es实战与原理解析 es内部原理_数据_10

4.当达到一定的时间间隔,或者translog足够大时,就会执行commit行为,将所有缓存中的segment写入磁盘。确保写入成功后,translog就会被清空。

es实战与原理解析 es内部原理_elasticsearch_11

执行commit并清空translog的行为,在es中可以通过_flush api进行手动触发。

如:curl -X POST127.0.0.1:9200/tcpflow-2015.06.17/_flush?v

通常这个flush行为不需要人工干预,交给es自动执行就好了。同时,在重启es或者关闭索引之间,建议先执行flush行为,确保所有数据都被写入磁盘,避免造成数据丢失。通过调用sh service.sh start/restart,会自动完成flush操作。

3.3.5 Segment的合并

前面讲到es会定期的将收到的文档写入新的segment中,这样经过一段时间之后,就会出现很多segment。但是每个segment都会占用独立的文件句柄/内存/消耗cpu资源,而且,在查询的时候,需要在每个segment上都执行一次查询,这样是很消耗性能的。

为了解决这个问题,es会自动定期的将多个小segment合并为一个大的segment。前面讲到删除文档的时候,并没有真正从segment中将文档删除,而是维护了一个.del文件,但是当segment合并的过程中,就会自动将.del中的文档丢掉,从而实现真正意义上的删除操作。

当新合并后的segment完全写入磁盘之后,es就会自动删除掉那些零碎的segment,之后的查询都在新合并的segment上执行。Segment的合并会消耗大量的IO和cpu资源,这会影响查询性能。

在es中,可以使用optimize接口,来控制segment的合并。

如:POST/logstash-2014-10/_optimize?max_num_segments=1

这样,es就会将logstash-2014-10中的segment合并为1个。但是对于那些更新比较频繁的索引,不建议使用optimize去执行分片合并,交给后台的es自己处理就好了。

ES通过后台合并段解决这个问题。ES利用段合并的时机来真正从文件系统删除那些version较老或者是被标记为删除的文档。被删除的文档(或者是version较老的)不会再被合并到新的更大的段中。

ES对一个不断有数据写入的索引处理流程如下:

索引过程中,refresh会不断创建新的段,并打开它们。 合并过程会在后台选择一些小的段合并成大的段,这个过程不会中断索引和搜索。合并过程如图:

es实战与原理解析 es内部原理_数据_12

从上图可以看到,段合并之前,旧的被Commit和没Commit的小段皆可被搜索。段合并后的操作:

  • 新的段flush到硬盘
  • 编写一个包含新段的新提交点,并排除旧的较小段。
  • 新的段打开供搜索
  • 旧的段被删除

合并完成后新的段可被搜索,旧的段被删除,如下图所示:

es实战与原理解析 es内部原理_elasticsearch_13

3.3. 存储流程总结

当一个写请求发送到 es 后,es 将数据写入 memory buffer 中,并添加事务日志( translog )。如果每次一条数据写入内存后立即写到硬盘文件上,由于写入的数据肯定是离散的,因此写入硬盘的操作也就是随机写入了。硬盘随机写入的效率相当低,会严重降低es的性能。

因此 es 在设计时在 memory buffer 和硬盘间加入了 Linux 的页面高速缓存( File system cache )来提高 es 的写效率。

当写请求发送到 es 后,es 将数据暂时写入 memory buffer 中,此时写入的数据还不能被查询到。默认设置下,es 每1秒钟将 memory buffer 中的数据 refresh每次refresh会生成一个segment段) 到 Linux 的 File system cache ,并清空 memory buffer ,此时写入的数据就可以被查询到了。

es实战与原理解析 es内部原理_缓存_14

但 File system cache 依然是内存数据,一旦断电,则 File system cache 中的数据全部丢失。默认设置下,es 每30分钟调用 fsync 将 File system cache 中的数据 flush 到硬盘。因此需要通过translog 来保证即使因为断电 File system cache 数据丢失,es 重启后也能通过日志回放找回丢失的数据。

translog 默认设置下,每一个 index 、 delete 、 update 或 bulk 请求都会直接 fsync 写入硬盘。为了保证 translog 不丢失数据,在每一次请求之后执行 fsync 确实会带来一些性能问题。对于一些允许丢失几秒钟数据的场景下,可以通过设置 index.translog.durability 和 index.translog.sync_interval 参数让 translog 每隔一段时间才调用 fsync 将事务日志数据写入硬盘。

与mysql很相似,redo日志。

4. 集群选举

4.1. 涉及配置参数

1、如果同时启动,按照nodeid进行排序,取出最小的做为master节点
2、如果不是同时启动,则先启动的候选master节点,会竞选为master节点
3、可以通过配置文件cluster.initial_master_nodes参数指定可以成为master的节点列表

# 如果`node.master`设置为了false,则该节点没资格参与`master`选举。
node.master = true
# 默认3秒,最好增加这个参数值,避免网络慢或者拥塞,确保集群启动稳定性
discovery.zen.ping_timeout: 3s
# 用于控制选举行为发生的集群最小master节点数量,防止脑裂现象
discovery.zen.minimum_master_nodes : 2
# 新节点加入集群的等待时间
discovery.zen.join_timeout : 10s

4.2. 新节点加入

节点完成选举后,新节点加入,会发送 join request 到 master 节点。默认会重试20次。

4.3. 宕机再次选举

如果宕机,集群node会再次进行 ping 过程,并选出一个新的 master 。

一旦一个节点被明确设为一个客户端节点( node.client设为true ),则不能再成为主节点( node.master会自动设为false )。

5. 节点类型

在elasticsearch.yml中配置节点类型:

#配置文件中给出了三种配置高性能集群拓扑结构的模式,如下:
#1. 如果你想让节点从不选举为主节点,只用来存储数据,可作为负载器
node.master: false
node.data: true
#2. 如果想让节点成为主节点,且不存储任何数据,并保有空闲资源,可作为协调器
node.master: true
node.data: false
#3. 如果想让节点既不称为主节点,又不成为数据节点,那么可将他作为搜索器,从节点中获取数据,生成搜索结果等
node.master: false
node.data: false

1)master主节点
Master : 主节点
node.master : true (才可以参与主节点竞选,作为主节点候选节点)

2)数据节点
Datanode:数据节点
node.data : true 默认就是true,默认就是数据节点

3)协调节点
Coordingnate node : 协调节点
如果节点仅仅只作为协调节点,必须将上面2个配置全部设置为false.

注意:
一个节点可以充当一个或多个角色。默认 3个角色都有。
协调节点:负责接受请求,转发请求(把请求路由到各个分片节点)

6. 数据恢复

横向扩容:

es实战与原理解析 es内部原理_es实战与原理解析_15

数据恢复的基本概念:

  • 代表数据恢复或叫数据重新分布,es在有节点加入或退出时会根据机器的负载对索引分片进行重新分配,挂掉的节点重新启动时也会进行数据恢复。
  • GET /_cat/health?v   #可以看到集群状态

分片之所以重要,主要有两方面的原因:

  • 允许你水平分割/扩展你的内容容量
  • 允许你在分片(位于多个节点上)之上进行分布式的、并行的操作,进而提高性能/吞吐量 至于一个分片怎样分布,它的文档怎样聚合回搜索请求,是完全由Elasticsearch管理的,对于作为用户的你来说,这些都是透明的。

在一个网络/云的环境里,失败随时都可能发生。在某个分片/节点因为某些原因处于离线状态或者消失的情况下,故障转移机制是非常有用且强烈推荐的。为此, Elasticsearch允许你创建分片的一份或多份拷贝,这些拷贝叫做复制分片,或者直接叫复制。

复制之所以重要,有两个主要原因:

  • 在分片/节点失败的情况下,复制提供了高可用性。复制分片不与原/主要分片置于同一节点上是非常重要的。因为搜索可以在所有的复制上并行运行,复制可以扩展你的搜索量/吞吐量
  • 总之,每个索引可以被分成多个分片。一个索引也可以被复制0次(即没有复制) 或多次。一旦复制了,每个索引就有了主分片(作为复制源的分片)和复制分片(主分片的拷贝)。
  • 分片和复制的数量可以在索引创建的时候指定。在索引创建之后,你可以在任何时候动态地改变复制的数量,但是你不能再改变分片的数量。
  • 默认5:1 5个主分片,1个复制分片

默认情况下,Elasticsearch中的每个索引分配5个主分片和1个复制。这意味着,如果你的集群中至少有两个节点,你的索引将会有5个主分片和另外5个复制分片(1个完全拷贝),这样每个索引总共就有10个分片。

总结:

  • ES集群可由多个节点组成,各Shard分布式地存储于这些节点上。
  • ES可自动在节点间按需要移动shard,例如增加节点或节点故障时。简而言之,分片实现了集群的分布式存储,而副本实现了其分布式处理及冗余功能。

7. 故障探查

ES有两种集群故障探查机制:

  1. 通过master进行的,master会ping集群中所有的其他node,确保它们是否是存活着的。
  2. 每个node都会去ping master来确保master是存活的,否则会发起一个选举过程。

有下面三个参数用来配置集群故障的探查过程:

ping_interval : 每隔多长时间会ping一次node,默认是1s
ping_timeout : 每次ping的timeout等待时长是多长时间,默认是30s
ping_retries : 如果一个node被ping多少次都失败了,就会认为node故障,默认是3次

8. 脑裂问题


8.1. 什么是脑裂现象

由于部分节点网络断开,集群分成两部分,且这两部分都有master选举权。就成形成一个与原集群一样名字的集群,这种情况称为集群脑裂(split-brain)现象。这个问题非常危险,因为两个新形成的集群会同时索引和修改集群的数据。

8.2. 解决方案

# 决定选举一个master最少需要多少master候选节点。默认是1。
# 这个参数必须大于等于为集群中master候选节点的quorum数量,也就是大多数。
# quorum算法:master候选节点数量 / 2 + 1
# 例如一个有3个节点的集群,minimum_master_nodes 应该被设置成 3/2 + 1 = 2(向下取整)
discovery.zen.minimum_master_nodes:2
# 等待ping响应的超时时间,默认值是3秒。如果网络缓慢或拥塞,会造成集群重新选举,建议略微调大这个值。
# 这个参数不仅仅适应更高的网络延迟,也适用于在一个由于超负荷而响应缓慢的节点的情况。
discovery.zen.ping.timeout:10s
# 当集群中没有活动的Master节点后,该设置指定了哪些操作(read、write)需要被拒绝(即阻塞执行)。有两个设置值:all和write,默认为wirte。
discovery.zen.no_master_block : write

8.3. 场景分析

一个生产环境的es集群,至少要有3个节点,同时将discovery.zen.minimum_master_nodes设置为2,那么这个是参数是如何避免脑裂问题的产生的呢?

比如我们有3个节点,quorum是2。现在网络故障,1个节点在一个网络区域,另外2个节点在另外一个网络区域,不同的网络区域内无法通信。这个时候有两种情况情况:

  • (1) 如果master是单独的那个节点,另外2个节点是master候选节点,那么此时那个单独的master节点因为没有指定数量的候选master node在自己当前所在的集群内,因此就会取消当前master的角色,尝试重新选举,但是无法选举成功。然后另外一个网络区域内的node因为无法连接到master,就会发起重新选举,因为有两个master候选节点,满足了quorum,因此可以成功选举出一个master。此时集群中就会还是只有一个master。
  • (2) 如果master和另外一个node在一个网络区域内,然后一个node单独在一个网络区域内。那么此时那个单独的node因为连接不上master,会尝试发起选举,但是因为master候选节点数量不到quorum,因此无法选举出master。而另外一个网络区域内,原先的那个master还会继续工作。这也可以保证集群内只有一个master节点。

综上所述,通过在 elasticsearch.yml 中配置 discovery.zen.minimum_master_nodes: 2 ,就可以避免脑裂问题的产生。

但是因为ES集群是可以动态增加和下线节点的,所以可能随时会改变 quorum 。所以这个参数也是可以通过api随时修改的,特别是在节点上线和下线的时候,都需要作出对应的修改。而且一旦修改过后,这个配置就会持久化保存下来。

PUT /_cluster/settings { “persistent” : {“discovery.zen.minimum_master_nodes” : 2 } }

新版本ES 不需要维护这个配置了

9. 集群路由


9.1. 文档路由

1)document路由到shard分片上,就叫做文档路由 ,如何路由?

2)路由算法
算法公式:shard = hash(routing)%number_of_primary_shards

例子:
一个索引index ,有3个primary shard : p0,p1,p2
增删改查 一个document文档时候,都会传递一个参数 routing number, 默认就是document文档 _id (也可以手动指定)
Routing = _id,假设: _id = 1

算法:
Hash(1) = 21 % 3 = 0 表示 请求被 路由到 p0分片上面。

3)自定义路由

请求:
PUT /index/item/id?routing = _id (默认)
PUT /index/item/id?routing = user_id(自定义路由)---- 指定把某些值固定路由到某个分片上面。

4)primary shard不可变原因

即使加服务器也不能改变主分片的数量

9.2. 增删改原理

  1. 客户端选择一个node发送请求过去,这个node就是coordinating node(协调节点)
  2. coordinating node,对document进行路由,将请求转发给对应的node(有primary shard)
  3. 实际的node上的primary shard处理请求,然后将数据同步到replica node
  4. coordinating node,如果发现primary node和所有replica node都搞定之后,就返回响应结果给客户端

10. 实战的几个问题


1.我们需要多大规模的集群网络?
2.索引应该设置多少个分片?
3.分片应该设置多少个副本?

首先ES性能因素:

  • 每个分片最大存储30G - 50G,再多会影响性能
  • 一个ES节点的堆内存推荐为宿主机内存的一半和31GB,两个值中,取最小值
  • 每个节点的分片数量保持在低于每1GB堆内存对应集群的分片在20-25之间

假设处理10TB数据规模:

  • 所以我们至少要10TB / 50GB = 10240G / 50G = 205 个主分片
  • 一般情况下,推荐 1 - 2 个副本分片,假设设置2个副本分片,即总共有615 个分片
  • 615个分片则至少需要615 / 20 = 31 g内存

即只考虑硬盘和内存的情况下,至少需要30TB以上的的硬盘、31g以上的内存。

考虑到CPU性能,尽量让服务器节点更多,避免多个分片竞争一个服务器资源(CPU,IO,内存)

如果我们按照 32g内存,2T的服务器,则至少需要16台服务器(内存完全足够)。