目录
1.基本概念和原理使用者
es是实时的分布式搜索分析引擎,内部使用Lucene做索引与搜索。
何谓实时? 新增到ES中的数据在1秒后几句能够被检索到,这种哦过年新增数据对搜索的可见性称为“准实时搜索”。分布式意味着可以动态调整集群规模,弹性扩容,而这一切操作起来都非常简便,用户甚至不需要了解集群的原理就可以实现。按官方描述,集群规模支持“上百”个节点,相比HDFs等上千台的集群这个规模是“小了点”,因此我们认为ES适合中等数据量的业务,不适合存储海量数据。
Lucene是Java语言编写的全文搜索框架,用于处理纯文本数据,但是它只是一个库,提供建立索引,执行搜索等接口,但不包含分布式服务,这些正式ES做的,什么是全文,对全部的文本内容进行分析,建立索引,使之可以被搜索,因此称为全文。
基于ES,你可以很容易搭建自己的搜索引擎,用于分析日志,或者配合开源爬虫建立某个垂直领域的搜索引擎。ES易用的产品设计使得它很容易上手。除了搜索,ES还提供了大量的聚合功能,所以它不单是一个搜索引擎,还可以进行数据分析,统计,生成指标数据。而这些功能都快速迭代,目前没两周就会发布新版本。
1.1 索引结构
ES是面向文档的,各种文本结构是以文档的形式存储到ES中,文档可以是一封邮件,一条日志,或者是一个网页的内容。一般使用JSON作为文档的序列化格式,文档可以有很多字段,在创建索引的时候我们需要描述文档中每个字段的数据类型,并且可能需要指定不同的分析器,就像关系型数据库中的CREATE TABLE一样。
在存储结构上,由_index、_type和_id唯一标识一个文档。
_index指向一个或多个物理分片的逻辑命名空间,_type类型用于区分同一个集合中的不同细分,在不同的细分中,数据的整体模式是相同或相似的,不适合完全不同类型的数据。多个_type可以在相同的索引中存在,只要他们的字段不冲突即可(对于整个索引,映射在本质上被扁平化为一个单一的,全局的模式)。_id文档标记由系统自动生成或者使用者提供。
很多初学者喜欢套用DBMS的概念将index_理解成数据库,将_type理解为表,这是一个很牵强的理解,实际上是完全不同的概念,没有什么相似性,不同_type下的字段不能冲突,删除整个_type也不会释放空间,在实际运用中,数据模型不同,有不同_type需求的时候,我们应该建立单独的索引,而不是在一个索引下建立不同的_type。删除过期老化的数据时,最好以索引为单位,而不是_type和_id,正由于_type在实际应用中容易引起概念混淆,以及允许索引存在多_type并且没有什么实际意义,在ES 6.x 版本中,一个索引只语序存在一个_type,未来的7.x版本将完全删除_type的概念。
1.2 分片
在分布式系统中单机无法存储规模巨大的数据,要依靠大规模集群处理和存储这些数,一般通过增加机器数量来提高系统水平扩展能力。因此,需要将数据分成若干大小块分配到各个机器上。然后通过某种路由策略找到某个数块所在的位置。
除了将数据分片以提高水平扩展能力,分布式存储中还会把数据复制成多个副本,放置到不同的机器上,这样一来,可以增加系统可用性,同时数据副本还可以使读操作并发执行,分担集群压力,但是多数副本也带来了一致性的问题,部门副本写成功,部分副本写实效,我们随后讨论。
为了应对并发更新的问题,ES将数副本分为主从两部分,即主分片(primary shard) 和副本分片(replica shard)。主数作为权威数,写过程先写朱分片,成功后再写副分片,恢复阶段以朱分片为主。数据分片和数据副本的关系如下图所示:
分片(shard)是底层的基本读写单元,分片的目的是为了分割巨大索引,让读写可并行操作,由多台机器共同完成,读写请求最终落到某个分片上,分片可以独立执行读写工作。ES利用分片将数据分发到集群内各处。分片是数的容器,文档保存在分片内,不会跨分片存储。分片又被分配到集群内的各个结点中。当集群规模扩大或缩小时,ES会自动再各个结点中迁移分片,使数据任然均匀的分布在集群中。
索引与分片的关系如下图所示:
一个ES索引包含多个分片,一个分片是一个Lucene的索引,它本身就是一个完整的搜索引擎,可以独立执行建立索引和搜索任务。Lucene索引又由很多分段组成,每个分段是一个倒排索引。ES每次refresh都会创建一个新的分段。其中包含若干文档的数据。在每个分段的内部,文档的不同字段被单独创建索引。每个分段的值由若干词组成(Term),Term是原文本内容进过分词器处理和语言处理后的最终结果(例如,去除标点符号和转换为词根)。
索引建立的时候就需要确定好主分片数,在较老的版本中(5.x之前)主分片数量不可以修改,副本分片数量可以随时去修改。5.x-6.x之后,已经支持一定条件的限制下,对某个索引的主分片进行拆分活缩小。但是我们任需要在一开始就尽量规划好主分片的数量;先依据硬件情况定好单个分片的容量,然后依据业务长宁预估数据量和增长量,再除以单个分片的容量。
分片数不够的时候,可以考虑新建索引,搜索一个有着50个分片的索引与搜索50个每个都有一根分片的索引完全等价,或者使用_split API来拆分索引。
在实际应用中,我们不应该向单个索引持续写数据,知道他们的分片巨大无比。巨大的索引会在数据老化后难以删除,以_id为单位的删除文档不会立即释放空间。删除的doc中只在Lucene分段合并时才会真正的从磁盘中删除。即手工触发分段合并,然然会引起较高的I/O压力,并且可能因为分段巨大导致在合并过程中磁盘不足(分段大小大于磁盘可用空间的一半)。因此我们建议周期性的创建新索引,例如每天创建一个,假如有一个索引website,可以将它命名为website_20180319。然后创建一个名为website的索引来关联这些索引。这样对于业务方来说读取时使用的名称不变,当需要删除数据的时候可以直接删除整个索引。
索引的别名就像一个快捷方式或软链接,不同的是它可以指向一个或者多个索引。可以用于实现索引分组,或者索引间无缝切换。
现在我们已经确定好主分片的数量,并且保证单个索引的数据量不会太大,周期性的穿件新索引带来的一个新问题是集群整体数量较多,集群管理的总分片数越多压力就愈大,在每天生成一个索引的场景中,可能某天产生的数据量很小,实际上不需要这么多分片,甚至一个就够,可以使用_shrink API来缩减主分片的数量,降低集群负载。
1.3 动态更新索引
为文档建立索引,使每个字段都可以被搜索,通过关键词检索文档内容,会使用倒排索引的数据结构,倒排索引一旦被写入文件后,就具有不变性,不变性具有很多好处:对文件的访问不需要加锁,读取索引时可以被文件系统缓存等。
那么索引如何更新,让新添加的文档可以被搜索到? 答案是使用更多的索引,新增内容并写到一个新的倒排索引中,查询时,每个倒排索引都可以被轮流查询,查询完再对结果进行合并。
每次内存缓冲的数据被写入文件时,会产生新的Lucene段,每个段都是一个倒排索引。在一个记录元信息的文件中描述当前Lucene索引都包含哪些分段。
由于分段的不变性,更新,删除等操作实际上是将数标记为删除记录到单独的位置,这种方式称为标记删除。因此删除数据不会释放磁盘空间
1.4 近实时搜索
在写操作中一般会先在内存中缓冲一段数据,再将这些数据写入硬盘,每次写入硬盘的这批数据称为一个分段,如同任何写操作一样,一般情况下(direct方式除外),通过操作系统write接口写到磁盘的数据先到达系统缓存(内存),write函数返回成功时,数据未必被刷到磁盘,通过手工调用flush,或者操作系统通过一定策略将系统缓存刷到磁盘。这种策略大幅提升了写入肖略。从write函数返回成功开始,无论数据有没有被刷新到磁盘,该数据已经对读取可见。
ES正是利用这种特性实现了近实时搜索,每秒产生一个新的分段,新段写入文件系统缓存,但稍后再执行flush刷盘操作,写操作很快会执行完,一旦写成功,就可以像其它文件一样被打开和读取了。
由于系统先缓冲一段数据才写,且新段不会立即被刷入磁盘,这两个过程中如果出现某些意外情况(如主机断电),则会存在丢失数据的风险。通用的做法是记录事务日志,每次对ES进行操作时均记录日志文件,当ES启动的时候,重放translog中所有最后一次提交后发生的变更操作,比如HBASE等都有自己的事务日志。
1.5 段合并
在ES中,每秒清空一次写缓冲,将这些数据写入文件,这个过程称为refresh,每次refresh会创建一个新的Lucene段。但是分段的数量太多会带来较大的麻烦,每个段都会消耗文件句柄、内存。每个搜索请求都需要轮流检查每个段,查询完再对结果进行合并:所以段越多,搜索也就越慢。因此需要通过一定的策略将这些较小的段合并为较大的段,常用的方案是选择大小相似的分段进行合并。在合并的过程中,标记为删除的数据不会写入新分段,当合并过程结束,旧的分段数据被删除,标记删除的数据才从磁盘删除。
Hbase,Cassandra等系统都有类似的分段机制,写过程先在内存缓冲一批数据,不时的将这些数据写入文件作为一个分段,分段具有不变性,再通过一些策略合并分段。分段合并过程中,新段产生需要一定的磁盘空间,我们要保证系统有足够的剩余可用空间。Cassandra系统在段合并的过程中的一个问题就是,当持续的向一个表中写入数据,如果段大小没有上线,当巨大的段达到磁盘空间的一半时,剩余空间不足以进行新段的合并过程,如果段文件设置一定的上限不再合并,则对表中部分数据无法真正的物理删除。ES存在同样的问题。
2.集群内部原理
分布式系统的集群方式大致可以分为主从(Master-Slave)模式和无主模式。ES,HDFS,HBASE使用主从模式,Cassandra使用无主模式,主从模式可以简化系统设计,Master作为权威节点,部分操作仅由Master执行,并负责维护集群元信息。缺点是Master节点存在单点故障,需要解决灾备问题,并且集群规模受限于Master节点的管理能力。
因此从集群节点角色的角度花粉,至少存在主节点和数据节点,另外还有协调节点、预处理节点和部落节点,下面分别介绍各种类型节点的职能。
2.1 集群节点角色
1.主节点
主节点负责集群层面的相关操作,管理集群的变更。
通过配置 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、内存、I/O要求较高,一般情况下(除一些例外)数据读写流程只和数据节点交互,不会和主节点打交道(异常情况除外)。
可以通过配置node.data:true(默认) 来使一个结点成为数据节点,也可以通过如下配置创建衣蛾数据节点:
node.master: false
node.data: true
node.ingest: false
3. 预处理结点 (Ingest node)
这是从5.0版本开始引入的概念,预处理操作允许在索引文档之前,即写入数据之前,通过事先定义好的一系列processor(处理器)和pipeline(管道),对数据进行某种转换,富华。processors和pipeline拦截bulk和index请求,在应用相关操作后将文档传回给index或bulk API。
默认情况下,在所有的结点上启用ingest,如果想在某个节点上禁用ingest,则可以添加配置node.ingest:false ,也可以通过如下配置创建一个仅用于预处理的结点:
node.master: false
node.data: false
node.ingest:true
4.协调节点(Coordinating node)
客户端请求可以发送到集群的任何节点,每个结点都知道任意文档所处的位置,然后转发这些请求,收集数据并返回给客户端,处理客户端请求的结点称为协调节点。
协调节点将请求转发给保存数据的数据节点。每个数据节点在本地执行请求,并将结果返回协调节点。协调节点手机完数据后,将每个数据节点的结果合并为单个全局结果。对结果手机和排序的过程可能需要很多的CPU和内存资源。(类似于vmselect)
通过下面的配置创建衣蛾仅用于协调的节点:
node.master: false
node.data: false
node.ingest: false
5. 部落节点(Tribe node)
tribes(部落) 功能允许部落节点再多个集群之间充当联合客户端。
在ES 5.0之前还有一个客户端节点(Node Client)的角色,客户端节点有以下属性:
node.master:fasle
node.data: false
它不做主节点,也不做数据节点,仅用于路由请求,本质上是一个智能的负载均衡器(从负载均衡器的定义来说,智能和非智能的区别在于是否知道访问的内容属于哪个节点)从5.0版本开始这个角色被协调节点(Coordinating only node )取代
3.集群启动流程
3.1选举主节点
假设有若干节点正在启动,集群启动的第一件事是从已知的活跃机器中选择一个作为主节点,选主之后的流程由主节点触发。
ES的选主算法是基于Bully算法的改进,主要思路是对节点ID排序,取ID值最大的结点作为Master,每个结点都运行这个流程。选主的目的是确定唯一的主节点,初学者可能以为选举出的主节点持有最新的元数据信息,实际上这个问题是在实现上被分解为两步:先确定唯一的、大家公认的主节点,再想办法把最新的机器元数据复制到选举出的主节点上。
基于节点ID排序的简单选举算法有三个附加约定条件:
(1)参选人数得过半,达到quorum(多数)后就选出了临时的主。为什么是临时的? 每个结点运行排序取最大值的算法,结果不一定相同。例如集群有五台机器,节点ID分别是1、2、3、4、5.当产生网络分区或者节点启动速度差异较大时,节点1看到的节点列表是1、2、3、4 选出4,节点2看到的节点列表是2、3、4、5 选出5 结果就不一致了,由此产下面两条限制。
(2) 得票数得过半。某节点被选择为主节点,必须判断加入它的节点数过半,才确认Master身份,解决第一个问题。
(3) 当探测到节点离开事件时,必须判断当前节点数是否过半,如果达不到quorum,则放弃Master身份,重新假如集群。如果不这么做,设想以下情况:假设5台机器组成的集群产生网络分区,2台一组,3台一组,产生分区前Master位于2台中的一个,此时3台一组的节点会重新并成功选取出Master,产生双主,俗称脑裂。
集群并不知道自己共有多少个节点quorum值从配置中读取,我们需要设置配置项:
discovery.zen.minimum_master_nodes
3.2 选举集群元信息
被选出的Master和集群元信息的新旧程度没有关系。因此它的第一个任务是选举元信息,让各节点把各自存储的元信息发过来,根据版本号确定最新的元信息,然后把这个信息广播下去,这样集群的所有结点都有了最新的元信息。
集群元信息的选举包括两个级别:集群级和索引级。不包含哪个shard属于哪个节点这种信息。这种信息以磁盘存储为准,需要上报。为什么呢,因为读写流程是不经过Master的,Master不知道各shard副本直接的数据差异。HSFS也有类似的机制,block信息依赖于DataNode的上报。
为了集群的一致性,参与选举的元信息数量需要过半,Master发布集群状态成功的规则也是等待发布成功的结点数过半。
在选举过程中,不接受新节点的加入请求。
集群元信息选举完毕后,Master发布首次集群状态,然后开始选举shard级元信息。
3.3 allocation过程
选举shard级元信息,构建内容路由表,是在allocation模块完成的,在初始阶段,所有的shard都处于UNASSIGNED(未分配)状态。ES通过分配过程决定哪个分片位于哪个节点,重构内容路由表。此时,首先要做的事就是分配主分片。
1. 选主分片
现在看某个主分片[website][0]是怎么分配的。所有的分配工作都是Master来做的,此时Master并不知道主分片在哪,它向集群中的所有节点询问:大家把[website][0]分片的元信息发给我,然后Master等待所有的请求返回,正常情况下它就有了这个shard的信息,然后根据某种策略选一个愤怒片作为主分片,是不是效率有点低?这种询问量=shard数x节点数。所以说我们最好控制shard的总规模别太大。
现在有了shard[website][0]的分片的多份信息,具体数量却绝育副本设置了多少。现在考虑把哪个分片作为主分片。在ES5.X版本一下,通过对比shard级元信息的版本号来决定。在多副本的情况下,考虑到如果只有一个shard信息汇报过来,则它一定被选择为主分片,但也许数据不是最新的。版本号比它大的那个shard所在的节点还没启动。在解决这个问题的时候ES5.X开始实施了一种新的策略:给每个shard设置一个UUID,然后在集群级的元信息中记录哪个shard是最新的,因为ES是先写主分片,再由主分片节点转发去写副分片,所以主分片所在的节点肯定是最新的,如果它转发失败了,则要求Master删除那个节点。所以从ES5.X开始,主分片选举的过程是通过集群级元信息中记录的“最新主分片的列表”来确定主分片的:汇报信息中存在,并且这个列表中也存在。
如果集群设置了:
"cluster.routing.allocation.enable":"none"
禁止分配分片,集群仍会强制分配主分片,因此在设置了上述选项的情况下,集群重启后状态为YELLOW,而非RED.
2.选副分片
主分片选举完成后,从上一个过程汇总汇总的shard信息中选择衣蛾副本作为副分片。如果汇总信息中不存在,则分配一个全新副本的操作依赖于延迟配置项:
index.unassigned.node_left.delayed_timeout
我们的线上环境中最大的集群有100+节点,掉节点的情况并不罕见,很多时候不能第一时间处理,这个延迟我么一般设置以天为单位。
最后,allocation过程中允许新启动的节点加入集群。
3.4 index recovery
分片分配成功后进入recovery流程。主分片的recovery不会等待其副分片成功才开始recovery,她们是独立的流程,只是副分片的recoeery需要主分片恢复完毕才开始。
为什么需要recovery? 对于主分片来说,可能有一些数据还没来得及刷盘;对于副分片来说一是没刷盘,而是主分片写完了,副分片还没来得及写,主副分片数据不一致。
1. 主分片recovery
由于每次写都会记录事务日志(translog),事务日志中记录了哪些操作,以及相关的数据。因此将最后一次提交(Lucene的一次提交就是一次fsync刷盘的过程)之后的translog中进行重放,建立Lucene索引,如此完成主分片的recovery
2.副分片recovery
副分片的恢复是比较复杂的,在ES的版本迭代中,副分片恢复策略有不少调整。
副分片需要恢复成与主分片一致,同时,恢复期间允许新的索引操作。在目前的6.0版本中,恢复分成两阶段执行。
- phase1: 在主分片所在的节点,获取translog保留锁,从获取保留锁开始,会保留translog不受其刷盘清空的影响。然后调用Lucene接口把shard做快照,这是已经刷盘中的分片数据,把这些shard数据复制到副本节点。在phased1完毕前,回向副本节点发送告知对方启动engine,在phase2开始之前, 副本分片就可以正常处理写请求了。
- phase2: 对translog做快照,这个快照里包含从phase1开始,到执行translog快照期间的新增索引。将这些translog发送到副分片所在节点进行重放。