前言

最近面试过程中遇到问Elasticsearch的问题不少,这次总结一下,然后顺便也了解一下Elasticsearch内部是一个什么样的结构,毕竟总不能就只了解个倒排索引吧。本文标题就是我遇到过的两个问题,所以此次基本上只是围绕着这两个问题来总结。

ES写入数据

在介绍写入数据的过程时,先明确一下ES中的一些关键性的概念:

Clouster:集群,由一到N个Elasticsearch服务节点组成。

Node:节点,组成Elasticsearch集群的基本单元,单个集群内节点名称唯一。通常一个节点中分配一到多个分片。

Shards:分片,当ES的索引数据过大时,会进行水平拆分,拆分出来的每一个单元都称为分片。在进行写入数据的时候,会通过路由来确定具体写到哪个分片上,所以在创建索引的时候就要确定好分片数量,并且一旦确定不可更改。索引数据在经过分片后,在数据管理和性能上都有很大提升,并且每一个分片都是一个Lucende的索引,每个分片都必须有一个主分片和零到多个副分片

Replicas:副本或备份,副本是指对主分片的备份分片,无论是主分片还是副本分片都可以对外提供查询服务。但是写入操作时是先写入主分片,然后再分发到副本上。

当主分片不可用时会在副本分片上选举一个作为主分片,因此副本不仅可以保证系统的高可用性,还可以提升搜索时的并发性能(主副分片都可以提供查询)。但并不是副本越多越好,副本数量过多会导致数据同步的负担过大。

分片数 (副本数+1)= 所需的最大节点数

举例:你计划5个分片和1个副本,那么所需要的最大的节点数为:5(1+1)=10个节点。

Index:索引,由一个和多个分片组成,单个集群内索引名字是唯一的。

Type:类型,指索引内部的逻辑分区,一般是通过Type的名字来进行分区,若是查询条件中没有该值,则说明在整个索引中执行查询。

Document:文档,ES索引中的每一条数据都称为一个Document,基本上和关系型数据库中的一个记录意思相同,通过_id,在Type内进行唯一标识。

Settings:对集群中索引的设定,例如默认的分片数量,副本数等信息。

Mapping:这里的Mapping类似于,关系型数据库的表结构信息,这里面包含了索引中字段的存储类型,分词方式,是否分词等信息。

Elasticsearch中的Mapping是可以动态识别的,Elasticsearch字段的数据格式识别它的类型,但是若是需要对Filed字段进行特殊设置时,就需要手动创建Mapping了。注意:一个Mapping一旦创建成功后,若是已经存储了数据了,就不可以修改了。

Analyzer:字段的分词方式的定义,一个Analyzer,通常由一个Tokenizer,零到多个Filter组成。例如默认的标准Analyzer包含一个标准的Analyzer和三个Filter(Standard Token Filter、Lower Case Token Filter、Stop Token Filter)。

Elasticsearch的节点分类

Master Node(主节点):主节点主要负责创建索引,删除索引,分配分片,追踪集群中的节点状态等工作。在配置文件中设置node.master=true 来将该节点设置成候选主节点,在集群中只有候选主节点才有选举权和被选举权。

Data Node(数据节点):数据节点负责数据的存储和相关具体操作,例如索引数据的创建,更新,搜索,聚合等操作。因此,数据节点对机器的要求比较高无论是在磁盘空间还是CPU、内存、I/O性能等。

集群在扩大后,需要增加更多的数据节点来提高可用性,在配置文件中通过node.data=true 来设置当前节点为数据节点。

Client Node(客户端节点):客户端节点是既不做候选主节点也不做数据节点的节点,只负责请求的分发、汇总等。若是单独增加这种节点主要是更多地为了提高并发性。

Tribe Node(部落节点):部落节点可以跨越多个集群,它可以接收每个集群的状态,然后合并成一个全局集群状态。

Coordinating Node(协调节点):协调节点只是一个角色,并不是指具体的某个集群节点,也没法通过配置来指定某个节点为协调节点,这也就说明集群中任何一个节点都可以是协调节点。

例如:节点A接收到用户的查询请求,会把查询语句分发到其他节点,然后合并各个节点返回查询结果,最后将完成的聚合结果返回给用户。这个请求中节点A的扮演的就是协调节点的角色。

还有就是集群节点有三个颜色状态:绿色、黄色、红***>。

绿色:代表健康状态,所有的主副分片都可正常工作,集群100%健康。

黄色:预警,所有的主分片都可以正常工作,但是至少有一个副分片是不能正常工作的。虽然集群能正常工作,但是高可用性已经有所降低。

红色:异常,集群不可正常使用。集群中至少有一个分片的主分片和全部副分片不可用。此时虽然查询操作可以返回数据,但也只是返回可用分片的那部分数据,并非全部的正确数据。若此时写请求打到异常分片会造成数据丢失。

写入过程

Elasticsearch写入数据到索引的过程大致是这样的:

首先客户端会根据配置的连接节点,通过轮询的方式选择一个coordinate节点。

coordinate节点通过路由函数(shard = hash(routing)%number_of_primary_shards),计算出数据应该落到那个shard中,根据coordinate节点上维护的shard信息,将请求发送到Node1上。

Node1先校验索引数据,然后在主分片上执行请求,执行成功后,将请求并行转发到副本集存在Node2、Node3。

Node2、Node3写入成功数据成功后,发送ack信息给主分片所在的Node1节点。

Node1节点再将ack信息发送给coordinate节点。

coordinate 节点 发送ack节点给客户端。

在主分片上执行写入请求的过程如下:

当有数据写入时,为了提升写速度,也是先将数据写入到内存(Memory Buffer)中的;

因为先写入了内存所以为了保证内存中的数据不丢失,也会同时写入Translog到内存中。

每隔一定时间(可配置)将数据从Memory Buffer中refresh到FileSystemCache中,生成segment文件,一旦生成segment文件,就能通过索引查询到了。

refresh完,memory buffer就清空了。Translog 也是从buffer flush到磁盘中。

定期/定量从FileSystemCache中,结合Translog内容flush index到磁盘中。做增量flush的。

因为Elasticsearch的这个刷盘机制,也说明并非是一个实时的搜索引擎。

更新数据

在早期的全文检索中为整个文档建立了很大的倒排索引,并将其写入到磁盘。如果索引有更新,就需要重新全量创建一个索引来替换原来的索引。

这种方式在数据量很大时效率很低,并且由于创建一次索引的成本很高,所以对数据的更新不能过于频繁,也就不能保证实效性。

分段存储

在搜索中引入了段(segment)的概念(将一个索引文件拆分为多个子文件,则每个子文件叫做段),每个段都是一个独立的可被搜索的数据集,并且段具有不变性,一旦索引的数据被写入硬盘,就不可修改

由于Elasticsearch中的每个分片包含多个segment(段),每一个segment都是一个倒排索引;因此在在查询的时,会把所有的segment查询结果汇总归并为最终的分片查询结果返回。

那么在这种分段存储的模式下Elasticsearch是如何进行数据操作的呢?

新增: 当有新的数据需要插入索引时,由于段的不可变性,会新建一个段来存储新增数据。

删除: 也是由于段的不可变,所以删除的时候会新增一个.del文件,专门用来存储被删除的数据id。这样虽然查询的时候还是能查到,但是在进程查询结果汇总的时候会将已删除的数据id过滤掉。

更新: 更新操作其实就是删除和新增的组合操作,先在.del文件中积累旧数据,然后在新段中添加一条更新后的数据。

分段存储的优点和缺点

segment优点

读写都不用加锁,不直接更新数据,所以不用考虑多线程下读写不一致的问题。

长驻内存,由于segment的不可变性,这样只要空间足够大,数据都是直接存储在内存中的。因此查询数据时直接访问内存,不用频繁操作磁盘。

增量创建,分段可以做到增量创建索引,即轻量级的对索引进行改变,不用操作整个索引文件,这样在频繁更新数据时,使系统接近实时更新。

segment缺点

删除,对段数据进行删除时,旧数据在.del文件中并不会马上删除。而旧数据只有等到段合并时才会被删除,这样会造成大量的空间浪费。

更新,因为更新操作是有删除和新增组合而成,若是频繁的更新也会造成大量的空间浪费。

新增,由于每次新增数据都是新建一个段,当段的数量过多时,对服务器的资源的消耗会非常大,查询性能也会受到影响。

过滤,查询后的结果再汇总时需要对已删除的数据进行过滤,增加了系统的处理负担。

这里要注意一点,并不是每新增一条记录(Document)就创建一个段(segment)的,而是数据先落到Memory Buffer中后,批量刷到Segment中的。

段合并

由于Memory Buffer中的数据在很短的时间(1s)内不断的进行刷盘,这样就会造成短时间内段的数量暴增,当索引中的段数量太多时不仅会严重消耗服务器资源,还会影响检索性能。

所以必须进行定期段合并操作,小的段被合并到大的段,然后这些大的段再被合并到更大的段。

段合并的主要动作有两个:

对索引中的段进行分组,把大小相近的段分到一组。

将属于同一分组的段合并成一个更大的段。

还有一个逻辑是段合并的时候会将那些旧的已删除文档从文件系统中清除。

段合并的好处和问题

segment合并的好处

减少索引段的数量,提升了内存空间,从而提高了检索速度。

减少索引的容量(文档数)——段合并会移除被标记为已删除的那些文档。提高了全文检索的速度,并移除了旧版本的数据。

segment合并带来的问题

磁盘IO操作的代价;因为段合并操作是非常耗I/O的,因为需要从旧的索引段中读取数据然后合并到新的索引段。

查询性能有一定影响;虽然说索引段合并的操作是异步进行的,但由于合并操作非常耗I/O,若合并时,正好也在进行大量的查询操作,在那些I/O处理速度慢的系统中,系统性能会受到影响。

如何快速更新索引数据?

通过对上面索引的分段存储和索引段合并的介绍,已经可以清楚的知道,在更新索引数据的时候,其实都是在操作索引段,对一段的索引数据进程操作,这样就能实现快速更新索引数据了。

Elasticsearch 的并发处理和数据一致性处理

并发处理(Concurrency)

Elasticsearch在接收到写请求时,是先将数据写入到主分片的,然后再将写请求同步到各个副本分片,但是同步这些副本分片的时间是无序的。

这个时候,Elasticsearch 就会采用乐观并发控制(Optimistic Concurrency Control)来保证新版本的数据不会被旧版本的数据覆盖。

这个乐观并发控制,就类似于Java的CAS机制,就是比较交换的意思。

乐观并发控制(OCC)认为事务间的竞争并不激烈,所以任何事务来了就先执行,等到提交的时候再 检查一下数据有没有变化,若没有变化就直接提交,如果有变化就直接重试再提交。这种适用于写冲突比较少的场景。

一致性(Consistency)

写一致性

Elasticsearch 集群保证写一致性的方式是在写之前检查有多少分片可供写入,若符合条件则执行写操作,否则会进行等待,等待符合条件,默认等待时间1分钟。

满足写入分片的条件有如下三个配置

One,代表只要主分片可用,就执行写操作。

All,代表只有当主分片和所有副本分片可用时,才执行写操作。

Quorum(k-wu-wo/reng,法定人数):这是 Elasticsearch 的默认选项。当有大部分的分片可用时才允许写操作。其中,对“大部分”的计算公式为 int((primary+number_of_replicas)/2)+1。 意思是大于主副分片之和的二分之一才可以执行写操作。

读写一致性

Elasticsearch 集群保证读写一致性的方式是,将副本分片的同步方式设置为replication=Sync(默认值),指的是只有主分片和所有副本分片都写入成功后才返回请求结果。

这种方式,可以保证后面的读请求无论请求到那个分片,返回的数据都是最新的版本。

但是为了提升写的效率也可以通过设置replication=async,这种模式是指,只要写入主分片成功,就返回写请求结果,降低了数据一致性提升了写的效率。