ES是面向文档的。各种文本内容以文档的形式存储到ES中,一般使用JSON作为文档的序列化格式。
在存储结构上,由_index、_type和_id唯一标识一个文档。
_index指向一个或多个物理分片的逻辑命令空间。
_type类型用于区分同一个集合中的不同细分,在不同的细分中,数据的整体模式是相同或相似的,不适合完全不同的类型数据。
_id文档标记符由系统自动生成或使用者提供。
不应该将_index理解成RDBMS中的数据库,_type理解成表。在ES 6.x版本中,一个索引已经只允许存在一个_type,在未来的版本还会移除_type概念。
在分布式系统中,单机无法存储规模巨大的数据,都需要依靠集群处理和存储这些数据,一般通过加机器的方式来提高系统的水平拓展能力。这就需要将数据切分成N个小块,然后把这些小块均匀分布到集群中的所有机器,然后通过某种路由策略找到某个数据块所在的位置。这就是“分片”。
将数据分片能提高水平拓展能力,但除了拓展能力,还要考虑系统的可用性,即集群中的某个节点挂了,不会影响整个集群的运行。所以在分布式存储中,会把数据复制成多个副本,放置到不同的机器上。不过引入了副本的概念,也会带来一致性的问题:部分副本写成功,部分写失败。
ES将分片分为主分片和副分片。写过程先写主分片,成功后再写副分片,恢复阶段以主分片为准。
一个ES的分片就是一个luncene的索引,它本身就是一个完整的搜索引擎,可以独立执行建立索引和搜索任务。每个luncene索引够若干个分段组成,每个分段就是一个倒排索引,ES每次“refresh”都会产生一个新的分段。在每个分段内部,文档的不同字段(Field)被单独建立索引。每个字段的值由若干个词(Term)组成。
ES index->ES shard(luncene index)->luncene segment->luncene field->lucene term。
搜索1个有着50个分片的索引和搜索50个只有单分片的索引,效果相同。
段在生成之后就是不可变的,这样设计有许多好处:对文件的访问不需要加锁,读取索引时可以被文件系统缓存等。
段不可变的话,更新和删除是怎么做的?
更新和删除等操作是将数据标记为删除,记录到单独的位置,这种方式称为标记删除。因此删除部分数据不会释放空间。
ES执行写操作,会将数据在内存中缓存,到达一定的时间间隔—默认1秒或者一定数据量,才会把这些数据写入磁盘,每次写入硬盘的这批数据就是一个分段(Segment)。
一般情况下,通过操作系统write接口写到磁盘的数据先到达系统缓存,write函数返回成功时,数据未必刷到磁盘。不过数据进入系统缓存的时候,文件已经能像其他文件一样被打开和读取。
ES利用了这种特性实现了近实时搜索。每秒产生一个新分段,将新分段写入文件系统缓存。
不过这种方式,存在丢失数据的风险。所以ES引入了translo机制,每次对ES进行操作时都会记录事务日志,当ES启动的时候,重放translog中所有在最后一次提交后发生的变更操作。
ES每秒都会产生一个段,但是分段数量太多会带来性能问题,每个段都会消耗文件句柄、内存;每个搜索请求都需要轮流检查每个段,然后把结果合并;段越多,搜索越慢。所以会通过一定的策略,将小段合并为大段。合并的过程中,标记为删除的数据不会被写入新的分段中,即标记删除的数据,只有到了段合并的时候,才会释放磁盘空间。
主节点
主节点负责集群层面的相关操作,管理集群变更。
通过配置node.master:true,默认为true使节点具有被选举为主节点的资格。主节点是唯一的,从所有具有被选举资格的节点中选举出来。
为避免网络分区时出现多主的情况,配置discovery.zen.minimum_master_nodes原则上最小值应该为(master_eligible_nodes/2)+1
数据节点
负责保存数据、执行数据相关操作:CRUD、搜索、聚合等。通过配置node.data:true,默认为true。
预处理节点
预处理操作允许在索引文档前,即写入文档之前,通过事先定义好的一系列processors和pipeline,对数据进行转换、富化。通过配置node.ingest:true,默认为true。
协调节点
客户端请求可以发送到集群的任意节点,每个节点都知道任意文档所在的位置,然后转发这些请求,收集数据并返回给客户端,处理客户端请求的节点称为协调节点。
Green,所有的主分片和副分片都正常运行。
Yellow,所有的主分片都正常运行,但不是所有的副分片都正常运行。
Red,有主分片没能正常运行。
集群状态元数据是全局信息,包括内容路由信息、配置信息等,其中最重要的是内容路由信息,描述了“哪个分片位于哪个节点”。
集群状态由主节点负责维护,如果主节点从数据节点接收更新,则将这些更新广播到集群的其他节点,让每个节点上的集群状态保持最新。
当扩容集群、添加节点时,分片会均衡地分配到集群的各个节点,从而对索引和搜索过程进行负载均衡,这些都是系统自动完成。
分片分配过程中出了让节点间均匀存储,还要保证不把主副分片分配到一个节点上,避免单个节点故障引起数据丢失。
选举主节点 -> 选举集群元信息 -> allocation过程 -> index recovery
ES的选主算法是基于Bully算法的改进,Bully算法的主要思路是对节点ID排序,取ID值最大的节点最为Master,每个节点都运行这个流程。这种做法选举出来的节点不一定持有最新的元数据信息,所以在选举出Master之后,还需要从其他机器上把最新的元数据信息同步过来。
基于节点ID排序的简单选举算法有三个附加条件:
ES集群并不知道自己一共有多少个节点,quorum(过半数节点)值从配置读取的discovery.zen.minimum_master_nodes——最小主节点数,这是防止脑裂、防止数据丢失的极其重要的参数。
该配置除了用于“多数”,还用于多处重要的判断,至少包含以下时机:
先选举临时Master,如果本节点当选,则等待确立Master,如果其他节点当选,则尝试加入集群,然后启动节点失效探测器。
选举临时Master
投票与得票
发送投票就是向目标节点发送加入集群的请求。得票就是申请加入该节点的请求数量。
确立Master
加入集群
节点失效检测
选举成功后,节点需要开启失效检测器:
两种探测器都是通过定期(默认1秒)发送ping请求探测节点是否正常的,当失败一定次数(默认为3次),或者收到来自底层连接模块的节点离线通知时,处理节点离开事件。
从上一小节,我们能知道,选举出来的主节点元数据信息不一定是最新的,所以当Master被选举出来后,第一件事情就是让所有节点把各自存储的元信息发给它,进行元信息选举,选举的过程中,不接受新节点的加入请求。
主节点根据版本号确定最新的元信息,然后再把这个信息广播出去,让所有节点的元信息都变成最新的。
为了集群的一致性,参与选举的元信息数量需要过半,主节点发布集群状态成功的规则也是等待发布成功的节点数过半。
元数据信息只包含两个级别:集群级和索引级。不包含哪个分片存在于哪个节点这种信息。
集群级和索引级的元数据选举完成后,就开始选举分片级元信息,构建内容路由表,这是在allocation模块完成的。
在初始阶段,所有的分片都处于未分配状态。ES通过分配过程决定哪个分片位于哪个节点上,构建路由表信息。
假设现在要选分片A的主分片,主节点会询问集群中的所有节点,让大家点把A分片的元信息发给它,主节点收到所有请求的返回后,根据一定策略,从中选择一个作为主分片。
这种方式效率比较低,询问量=分片数*节点数,所以分片的数量不适合太多。
如何选择合适的分片作为主分片呢?
ES会给每个分片都设置一个UUID,然后在集群的元信息中记录哪些分片是最新的。选主分片的时候,就选择汇报中存在于“最新分片列表”的分片。
在选主分片的时候,已经收集了分片的所有副本信息。如果汇总信息中不存在,则分配一个全新副本——例如副本数目前是3,但是汇总中只拿到2个。
创建全新副本的操作不是马上执行的,而是根据延迟配置项:index.unassigned.node_left.delayed_timeout。
分片分配成功后进入recovery流程。主分片的recovery不会等待其副本分片分配成功才开始。它们是独立的流程,只是副分片的recovery需要等它的主分片恢复完毕。
在节点意外挂掉的时候,可能有一些数据没来得及刷盘,主分片的recovery,就是为了恢复这部分未刷盘的数据。
ES的写操作都会记录事务日志(translog),事务日志记录了相关的数据变更。因此将最后一次提交(Lucene的一次提交就是一次fsync刷盘过程)之后的事务日志进行重放,建立Luncene索引,这样就完成了主分片的recovery。
在节点意外挂掉的时候,可能主分片已经写完数据,但是副分片没来得及同步,主副分片的数据不一致。
副分片需要恢复成与主分片一致,同时,恢复期间允许新的索引操作。恢复的过程分为两阶段:
阶段1完成,副分片就开始接受新请求,但是阶段2的时候,还需要重放操作,这两者不会有冲突么?
不会,ES中的数据是有版本号的概念,只要根据版本号进行过滤,只有最新一次操作生效。
第一阶段需要完整传输整个分片的数据,数据量大,恢复会变得很漫长,能避免这种全量同步么?
能,ES每个写入成功的操作,都会分配一个序号——SequenceNumber,通过比较主副分配的差异范围,如果差异范围目前还在事务日志中保留着,则可以通过主分片的事务日志增量恢复。或者主副分片的syncid和文档数都相同,可以直接跳过阶段1。
ES的数据副本模型基于主从模式,在实现上参考了PacificA算法,该算法有几个特点:
PacificA算法涉及的几个术语如下:
设计前提与假设:
整个系统框架主要由两部分组成:存储管理和配置管理
多个副本中存在一个主副本和多个从副本。所有写操作都进入主副本,当主副本出现故障,系统会从其他从副本选择合适的副本作为新的主副本。
数据的写入流程:
本质上就是一个两阶段提交,committed_R<=committed_P<=prepared_R。
全局的配置管理器负责管理所有副本组的配置。节点可以向管理器提出添加/移除副本的请求,每次请求都会附带当前的配置版本号,只有这个版本号和管理器记录的版本号一致,请求才会被执行。如果请求成功,则版本号会被更新。
PacificA算法使用租约(lease)机制来解决网络分区的问题:
只要不发生时钟漂移,确保grace period>=lease period,则租约机制能保证主副本节点比其他从副本节点先感知到租约的失效。同时任何一个从副本只有在它租约失效时,才会去争取当主副本,因此保证了新主副本产生前,旧的主副本已经降级,不会产生两个主副本。
ES中的每个索引都会拆分多个分片,并且每个分片都有多个副本。这些副本称为replication group(副本组,与PacificA的副本组概念一致)。保持副本之间的同步,以及从中读取的过程称为数据副本模型。
写入流程:
每个分片副本都会被分配一个ID,集群元数据中会维护一个最新的分片副本的ID,成为in-sync allocation IDS。只有ID在该集合里的副本分片才可能被选择为主分片。
主分片出问题怎么办?
主分片所在节点会通知Master主节点,Master主节点会把一个副分片提升为主分片。
主分片所在节点挂了怎么办?
Master主节点会监控集群节点的健康状态,做故障转移。
哪些副本分片能被提升为主分片?
主节点回维护一个包含最新数据的副本子集(in-sync副本集合),存储在集群状态中,只有在该子集中的副本分片才会被提升为主分片(可人工干预)。
主分片转发操作到副分片的时候,转发失败或者没收到回复,怎么办?
主分片会通知Master主节点,将它认为有问题的副分片从in-sync副本集合中移除,主节点移除后,主分片才会这次操作成功,主节点也会指导另一个节点重新建立副分片。
脏读
主分片是先写本地,再同步到副分片,副分片写成功,才回复客户端成功。但是主分片写入后,从主分片就已经能读取到刚写入的数据。
某个分片慢,可能降低索引速度
有一个分片写入特别慢,写入操作都需要等这个分片,就会导致整个写入操作慢。
读取流程:
当选择的活跃副本不能响应,怎么办?
协调节点会从副本组中选择另一个副本,将请求转发新的副本。
节点关闭对写入过程的影响
在写数据的时候,会对Engine加写锁。IndicesService的doStop方法最终会调用Engine的flushAndClose方法,该方法也会对Engine加写锁。由于写入操作已经获取了Engine的写锁,此时尝试获取写锁会等待,直到写操作完成。
但是由于网络模块被关闭,客户端的连接会被断开,客户端作为失败处理,而ES服务端的写流程还是在继续,直到完成。
节点关闭对读过程的影响
读数据的时候,会对Engine加读锁。同样道理,执行Engine的flushAndClose方法会一直等待,直到读操作完成。但是客户端因为连接断开,判定为读失败。
1、Cluster
2、allocation
3、Discovery
4、gateway
5、Indices
6、HTTP
7、Transport
8、Engine