Loading

Elasticsearch底层原理分析

1. ES基本概念及原理

1.1 索引结构

ES是面向文档的,所有文本数据都是以文档的形式存储在ES中。数据以JSON作为存储格式,由_index、_type、_id标识唯一的文档数据。文档中可以有许多字段,每个字段都是固定的数据类型来描述各种数据信息。

_index:指向一个或多个物理分片的逻辑命名空间(为同一类型文档的集合索引)

_type:ES7版本中默认为_doc。

_id:文档标记符由系统自动生成且唯一。

1.2 分片

在分布式系统中,单机无法存储规模巨大的数据。因此,需要将数据分成若干小块分配到各个机器上。然后通过某种路由策略找到某个数据块所在的位置除了将数据分片以提高水平扩展能力,分布式存储中还会把数据复制成多个副本,放置到不同的机器中,同时数据副本还可以使读操作并发执行。
为了应对并发更新问题,ES将数据副本分为主从两部分,即主分片(primary shard)和副分片(replica shard)主数据作为权威数据,写过程中先写主分片,成功后再写副分片,恢复阶段以主分片为准

数据分片和数据副本的关系如下图所示:

image-20201015122820126

分片(shard)是底层的基本读写单元,分片的目的是分割巨大索引,让读写可以并行操作,由多台机器共同完成。读写请求最终落到某个分片上,分片可以独立执行读写工作。ES利用分片将数据分发到集群内各处。分片是数据的容器,文档保存在分片内,不会跨分片存储分片又被分配到集群内的各个节点里。当集群规模扩大或缩小时,ES会自动在各节点中迁移分片,使数据仍然均匀分布在集群里。

索引与分片的关系图如下:

image-20201015131748320

一个ES索引包含很多分片,一个分片是一个 Lucene的索引,它本身就是一个完整的搜索引擎,可以独立执行建立索引和搜索任务。Lucene索引又由很多分段组成,每个分段都是一个倒排索引。ES每次“refresh”都会生成一个新的分段,其中包含若干文档的数据。

在每个分段内部,文档的不同字段被单独建立索引。每个字段的值由若干词(Term)组成,Term是原文内容经过分词器处理和语言处理后的最终结果(例如,去除标点符号和转换为词根)。

分片与数据

在创建索引时,需要确定好分片数。现在(5x~6x版本之后),ES在一定条件限制下,对某个索引的主分片进行拆分(Splt)或缩小( Shrink)。但是,我们仍然需要在一开始就尽量规划好主分片数量:先依据硬件情况定好单个分片容量,然后依据业务场景预估数据量和增长量,再除以单个分片容量。

分片数不够时,可以考虑新建索引,搜索1个有着50个分片的索引与搜索50个每个都有1个分片的索引完全等价,或者使用 split AP来拆分索引(6版本开始支持)

在实际应用中,我们不应该向单个索引持续写数据,直到它的分片巨大无比。巨大的索引会在数据老化后难以删除,以id为单位删除文档不会立刻释放空间,删除的doc只在 Lucene分段合并时才会真正从磁盘中删除。即使手工触发分段合并,仍然会引起较高的IO压力,并且可能因为分段巨大导致在合并过程中磁盘空间不足(分段大小大于磁盘可用空间的一半)。因此,我们建议周期性地创建新索引。例如,每天创建一个。假如有一个索引 website,可以将它命名为 website20180319。然后创建一个名为 website的索引别名来关联这些索引。这样,对于业务方来说,读取时使用的名称不变,当需要删除数据的时候,可以直接删除整个索引。

索引别名就像一个快捷方式或软链接,不同的是它可以指向一个或多个索引。可以用于实现索引分组,或者索引间的无缝切换

现在我们已经确定好了主分片数量,并且保证单个索引的数据量不会太大,周期性创建新索引带来的一个新问题是集群整体分片数量较多,集群管理的总分片数越多压力就越大。在每天生成一个新索引的场景中,可能某天产生的数据量很小,实际上不需要这么多分片,甚至个就够。这时,可以使用 shrink apl来缩减主分片数量,降低集群负载

1.3 什么是倒排序

原数据如下:

ID Name Age Sex
1 Kate 24 Female
2 John 24 Male
3 Bill 29 Male

ID是Elasticsearch自建的文档id,那么Elasticsearch建立的索引如下:

Name:

Term Posting List
Kate 1
John 2
Bill 3

Age:

Term Posting List
24 [1,2]
29 3

Sex:

Term Posting List
Female 1
Male [2,3]

Elasticsearch分别为每个Field都建立了一个倒排索引,Kate, John, 24, Female这些叫term,而[1,2]就是Posting List。Posting list就是一个int的数组,存储了所有符合某个term的文档id。

倒排序就是:可以根据文档的信息,反向检索出所有符合条件的文档集合。比如:查找Male的信息,就会检索出2,3 两个人的信息。

1.4 动态更新索引

为文档建立索引,使其每个字段都可以被搜索,通过关键词检索文档内容,会使用倒排素引的数据结构。

倒排索引一旦被写入文件后就具有不变性,不变性具有许多好处:对文件的访问不需要加锁,读取索引时可以被文件系统缓存等。

那么索引如何更新,让新添加的文档可以被搜索到?

新增的内容首先会内存中缓存,当达到一定条件后,会触发一次refresh操作,刷新操作将缓存数据写入到文件系统并生成一个新的倒排序索引(新的Lucene段),此时处于文件系统缓存中的数据可以被检索。查询时,每个倒排索引都被轮流查询,查询完再对结果进行合并——也就是所有条件的倒排序索引都会被检索出来,然后对检索结果进行合并处理。

  • 在缓存中的数据并不是以段的形式存储,因此不能被检索。只要生成新段,即使还没有写入磁盘,还是可以被被检索出。

  • 由于段的不变性,更新、删除等操作实际上是将数据标记为删除,记录到单独的位置,这种方式称为标记删除。因此删除部分数据不会释放磁盘空间。

1.5 近实时搜索

在写操作中,一般会先在内存中缓冲一段数据,再将这些数据写入硬盘,每次写入硬盘的这批数据称为一个分段。一般情况下,操作系统write接口写到磁盘的数据先到达系统缓存(内存),write函数返回成功时,数据未必被刷到磁盘。通过手工调用fush,或者操作系统通过一定策略将系统缓存刷到磁盘。从wite函数返回成功开始,无论数据有没有被刷到磁盘,该数据已经对读取可见。
ES每秒会对缓存中的数据进行一次刷新,正是利用这种特性实现了近实时搜索。ES将内存缓冲数据,先写入文件系统缓存,并产生一个新的分段,一旦写操作完成,基于段的倒排序索引就具备了可检索性。稍后再执行fush刷盘操作写入磁盘。

由于系统先缓冲一段数据才写,且新段不会立即刷入磁盘,这两个过程中如果出现某些意外情况(如主机断电),则会存在丢失数据的风险。通用的做法是记录事务日志,每次对ES进行操作时均记录事务日志,当ES启动的时候,重放 translog中所有在最后一次提交后发生的变更操作。比如 HBase等都有自己的事务日志。

1.6 段合并

在ES中,每秒清空一次写缓冲,将这些数据写入文件,这个过程称为 refresh,每次 refresh会创建一个新的 Lucene段。但是分段数量太多会带来较大的麻烦,每个段都会消耗文件句柄、内存。

每个搜索请求都需要轮流检查每个段,查询完再对结果进行合并;所以段越多,搜索也就越慢。因此需要通过一定的策略将这些较小的段合并为大的段,常用的方案是选择大小相似的分段进行合并。在合并过程中,标记为删除的数据不会写入新分段,当合并过程结束,旧的分段数据被删除,标记删除的数据才从磁盘删除。
HBase、Cassandra等系统都有类似的分段机制,写过程中先在内存缓冲一批数据,不时地将这些数据写入文件作为一个分段,分段具有不变性,再通过一些策略合并分段。分段合并过程中,新段的产生需要一定的磁盘空间,我们要保证系统有足够的剩余可用空间。

Cassandra系统在段合并过程中的一个问题就是,当持续地向一个表中写入数据,如果段文件大小没有上限,当巨大的段达到磁盘空间的一半时,剩余空间不足以进行新的段合并过程。如果段文件设置定上限不再合并,则对表中部分数据无法实现真正的物理删除。ES存在同样的问题。

2. 集群内部原理

2.1 集群中的节点

1. 主节点(Master node)

主节点负责创建索引、删除索引、追踪集群中节点的状态。
通过配置 node.master:true(默认)使节点具有被选举为Master的资格,主节点是全局唯一的,将从有资格成为 Master的节点中进行选举。
主节点也可以作为数据节点,但尽可能做少量的工作,因此生产环境应尽量分离主节点和数据节点,创建独立主节点的配置:node.master:true

​ node.data:false

为了防止数据丢失,每个主节点应该知道有资格成为主节点的数量,默认为1,为避免网络分区时出现多主的情况,配置是discovery.zen.minimum_master_nodes原则上最小值应该 (master_eligible_nodes/2)+1

2. 数据节点

负责保存数据、执行数据相关操作:CRUD、搜索、聚合等。数据节点对CPU、内存、1O要求较高。

一般情况下,数据读写流程只和数据节点交互,不会和主节点打交道(异常情况除外)。
通过配置node.data:true(默认)来使一个节点成为数据节点。

3.预处理节点

​ 预处理操作允许在索引文档之前,即写入数据之前,通过事先定义好的一系列的 processors(处理器)和 pipeline(管道),对数据进行某种转换、富化。
processors和 pipeline拦截buk和 index请求,在应用相关操作后将文档传回给 index或 bulk Api。

​ 配默认情况下,在所有的节点上启用 ingest,如果想在某个节点上禁用 Ingest,则可以添加配置node.ingest:false

也可以通过下面的配置创建一个仅用于预处理的节点:

node.master:false 
node data:false 
node.ingest:true

4. 协调节点

​ 客户端请求可以发送到集群的任何节点,每个节点都知道任意文档所处的位置,然后转发这些请求,收集数据并返回给客户端,处理客户端请求的节点称为协调节点。
协调节点将请求转发给保存数据的数据节点。每个数据节点在本地执行请求,并将结果返回协调节点。协调节点收集完数据后,将每个数据节点的结果合并为单个全局结果。对结果收集和排序的过程可能需要很多CPU和内存资源。
通过下面的配置创建一个仅用于协调的节点

node.master:false 
node data:false 
node.ingest:false

5. 部落节点

tribes(部落)功能允许部落节点在多个集群之间充当联合客户端。
在ES5.0之前还有一个客户端节点( Node Client)的角色,客户端节点有以下属性

node.master:false 
node data:false 

它不做主节点,也不做数据节点,仅用于路由请求,本质上是一个智能负载均衡器(从负载均衡器的定义来说,智能和非智能的区别在于是否知道访问的内容存在于哪个节点),从5.0版本开始,这个角色被协调节点取代。

2.2 集群健康状态

从数据完整性的角度划分,集群健康状态分为三种

  • Green,所有的主分片和副分片都正常运行。

  • Yellow,所有的主分片都正常运行,但不是所有的副分片都正常运行。这意味着存在单点故障风险。

  • Red,有主分片没能正常运行。

每个索引也有上述三种状态,假设丢失了一个副分片,该分片所属的索引和整个集群变为Yeow状态,其他索引仍为 Green

posted @ 2020-10-17 15:47  codeduck  阅读(1635)  评论(0编辑  收藏  举报