Elasticsearch作为分布式开源的搜索引擎,广泛应用于搜索和实时分析场景。本文简要介绍ES的一些特性、索引执行的原理以及集群架构,以加深理解。
Elasticsearch是基于Apache Lucene的开源、分布式、可扩展、实时的数据搜索和分析引擎。ES不仅仅支持全文搜索,还是一个分布式文档数据库,每个字段都是被索引的数据并且可被搜索。总体来说有如下特性:
现实中的数据分为结构化数据和非结构化数据,结构化数据主要通过关系型数据库进行存储和管理;非结构化数据又称为全文数据,包括各类文本、文档或者图片等。非结构化数据的搜索有两种方式:
因此,全文搜索可以对每个词建立一个索引,指明该词在文本中出现的次数和位置。当用户查询时,根据事先建立的索引进行查找,并返回查找到的结果。
Elasticsearch是以Lucene为底层基础建立的开源全文搜索引擎,Lucene是现在最好的开源全文检索引擎工具,但是Lucene只是一个工具包,并不是一个完整的全文搜索引擎。Lucene的目的是为软件开发人员提供一个简单易用的工具包,以方便的在目标系统中实现全文检索的功能,或者是以此为基础建立起完整的全文检索引擎。
Lucene能够实现全文搜索主要是因为实现了倒排索引的查询功能。倒排索引和正向索引不同,它不是由记录来确定属性的值而是由属性值来确定记录的位置。倒排索引包含两个部分:
假如有以下两段文字,通过分词器将文档的内容拆分成单独的词,再创建倒排索引。
Java is the best programming language.
Python is the best programming language.
以上内容可以转换为以下的倒排索引信息
关键词 | 文章编号 | 出现频率 | 出现位置 |
---|---|---|---|
Java | 1 | 1 | 0 |
Python | 2 | 1 | 0 |
is | 1/2 | 1/1 | 5/7 |
the | 1/2 | 1/1 | 8/10 |
best | 1/2 | 1/1 | 12/14 |
programming | 1/2 | 1/1 | 17/19 |
language | 1/2 | 1/1 | 29/31 |
上表转换为倒排索引的图形结构信息
倒排索引有个很重要的特性是被写入磁盘后是不可改变的:它永远不会修改。不变性有重要的价值:
不变索引的缺点就是不可变的它是不可变的,你不能修改它。如果需要让一个新的文档可被搜索,你需要重建整个索引。
ES将数据存储于一个或多个索引中,索引是具有类似特性的文档的集合,相当于关系型数据库中的一个Database。索引由其名称(必须为全小写字符)进行标识,并通过引用此名称完成文档的创建、搜索、更新及删除操作。一个ES集群中可以按需创建任意数目的索引。
类型是索引内部的逻辑分区,在一个索引内部可定义一个或多个类型(type),类似于传统数据库中的表。一般来说,类型就是为那些拥有相同的域的文档做的预定义。例如,在索引中,可以定义一个用于存储用户数据的类型,一个存储日志数据的类型,以及一个存储评论数据的类型。
文档是索引和搜索的原子单位,它是存储在ES中的一个JSON格式的字符串,其中包含了一个或多个域(Field)的容器。在ES中每个文档都有一个类型和ID,每个文档由一个或多个域组成,每个域拥有一个名字及一个或多个值,有多个值的域通常称为“多值域”。每个文档可以存储不同的域集,但同一类型下的文档至应该有某种程度上的相似之处。
集群由一个或多个节点组成,对外提供索引和搜索功能。在一个集群中所有的节点都有一个唯一的名称默认为“Elasticsearch”,当某个节点被设置为相同的集群名称时,会自动加入到集群。如果有多个集群,需要设置不同的名称,否则节点可能会加入到错误的集群。需要注意的是一个节点只能加入一个集群。
一个运行中的Elasticsearch实例称为一个节点,它是一个逻辑上独立的服务,可以存储数据,是ES集群的一部分。ES集群由一个或者多个拥有相同cluster.name配置的节点组成,它们共同承担数据和负载的压力。ES集群中的节点有三种不同的类型:
在…/config/elasticsearch.yml配置文件中可以设置不同的节点类型
node.master: true //是否候选主节点
node.data: true //是否数据节点
在实际过程中,如果某个节点既是数据节的又是主节点,可能对主节点的性能产生影响。因此为了集群的健康性,需要对Elasticsearch集群中的角色进行划分和隔离,可以使用配置较低的机器作为主节点。
当存储一个文档的时候,它会存储在唯一的主分片中,具体哪个分片是通过散列值进行选择。默认情况下这个值由文档的ID生成,如果文档中指定了一个父文档,则从父文档ID中生成。注:Routing值和路由计算具体到哪个分片有关。
ES中的索引数据量太大的时候,可以通过水平拆分的方式将一个索引上的数据拆分出来分配到不同的数据块上,这个数据块称为分片,相当于水平分表。一个分片便是一个Lucene的实例,ES中的index就是指向主分片和副本分片的逻辑空间。实际的文档数据被存储和索引到分片内,但是应用程序是直接与索引而不是与分片进行交互。
ES实际上就是利用分片来实现分布式,通过将索引分解成多个分片,分布在不同的节点上实现横向扩展。分片是数据的容器,文档保存在分片内,分片又被分配到集群内的各个节点里。当集群规模扩大或者缩小时,ES会自动的在各节点中迁移分片,使得数据仍然均匀分布在集群里。下图是一个3主分片,1副本分片的ES集群。
1)主分片
每个文档都存储在一个分片中,当存储一个文档的时候,ES首先将数据存储在主分片中,然后复制到不同的副本中。ES默认为一个索引创建 5 个主分片, 并分别为每个分片创建一个副本,通过参数可以指定。在索引建立的时候就已经确定了主分片数,但是副本分片数可以随时修改。
"number_of_shards" : 5,
"number_of_replicas" : 1
2)副本分片
副本分片是主分片的复制,每个主分片有0个或者多个副本。当主分片异常的时候,可以从副本分片中选择一个作为主分片,提高可用性。同时,查询可以在副本分片进行,减轻主分片的压力,提高性能。另外副本分片必须和主分片部署在不同的节点上,如果集群中只有一个节点,则副本分片将不会被分配,此时集群健康状态为yellow,存在丢失数据的风险。
与关系型数据库对比,在ElasticSearch中的索引=数据库,类型=表,文档=行数据,如下表所示。
Elasticsearch | 关系型数据库 |
---|---|
Index | Database |
Type | Table |
Document | Row |
Field | Column |
Mapping | Schema |
Everything is indexed | Index |
Query DSL | SQL |
GET http://… | SELECT |
PUT http://… | UPDATE |
分布式搜索的执行过程分为两个阶段:查询和取回。在搜索的API返回结果前,需要将多个分片的结果进行汇总放到一个有序列表中进行返回。
在初始化查询阶段,向索引中的每个分片副本进行广播,每个分片在本地执行搜索并且建立匹配document的优先队列。整个查询阶段包括以下步骤:
在整个查询过程中,协调节点会将所有分片的结果汇总,并进行全局排序,得到最终的查询排序结果。
查询阶段得到哪些满足搜索请求的document,但是需要将这些document返回给客户端。取回阶段过程如下:
为提高搜索效率,协调节点为每个持有相关document的分片建立并发的多点get请求然后发送请求到处理查询阶段的分片副本。
Elasticsearch更新document流程如图所示,主要分为几个阶段:
由上面的流程可以看出,在两次fsync操作之间,存储在内存和文件系统缓存中的文档是不安全的,一旦出现断电这些文档就会丢失。所以ES引入了translog来记录两次fsync之间所有的操作,这样机器从故障中恢复或者重新启动,ES便可以根据translog进行还原。
Elasticsearch中的Translog和MySQL中的binlog类似,用来记录数据的变化用于故障恢复。
Flush操作会执行以下步骤:
注意到translog是每5s刷新一次磁盘,所以故障重启时可能会丢失5s的数据。
由于每一秒就会生成一个新的segment,很快将会有大量的segment,segment数目太多会消耗文件句柄、内存和cpu运行周期。对于一个分片进行查询请求,将会轮流查询分片中的所有segment,这将降低搜索的效率。因此ES会自动启动合并segment的工作,将一部分相似大小的segment合并成一个新的大segment。合并的过程实际上是创建了一个新的segment,当新segment被写入磁盘,所有被合并的旧segment被清除。
在ES后台会有一个线程进行segment合并:
注:在segment merge这块,那些被逻辑删除的document才会被真正的物理删除。
一个Elasticsearch集群由一个或多个拥有相同cluster.name配置的节点组成,这些节点共同承担数据和负载的压力。如下图所示为三个节点的ES集群:
节点1选为主节点时,负责管理集群范围内的所有变更操作,其它数据节点负责对文档的变更和搜索等操作。对客户端来说,请求发送到集群中的任何节点,每个节点都知道文档所处的位置,并将请求转发到对应的分片,并最终将数据返回给客户端。
当客户端请求某个文档时,Elasticsearch通过路由计算文档具体落在哪个分片上。路由到分片位置的算法由下面的公式计算得到:
shard = hash(routing) % number_of_primary_shards
routing值默认是document的ID值,也可以自行指定。先对routing信息求hash值,然后将hash结果对primary_shard的数量求模,比如说primary_shard是5,那么结果肯定落在[0,4]区间内,这个结果值就是该document的分片位置,如示意图所示:
这个求模公式间接的解释了为什么了索引创建时指定了主分片的值,后续就不让改了。因为主分片值作为模数修改了,之前路由的document再执行该公式时,值就可能跟改之前得到的值不一致,这样document就找不到了。
在同一个网络环境下,当启动一个Elasticsearch实例并且cluster.name配置和ES集群一致,该实例就会自动加入到集群中。这依赖于Elasticsearch的自动发现机制Zen Discovery,Zen Discovery是Elasticsearch内置的发现模块,提供单播和基于文件的发现。Elasticsearch中默认使用单播模式,依赖Transport模块实现,节点使用Ping方式查找其它节点。
如果同一台机器上运行不同的节点,这些节点会自动加入到集群。如果集群的节点运行在不同的机器上,使用单播模式可以为Elasticsearch节点配置一个尝试连接的列表,用discovery.zen.ping.unicast.hosts指定。
discovery.zen.ping.unicast.hosts: ["host1", "host2","host3:port"]
当节点启动后,如果设置了discovery.zen.ping.unicast.hosts,会Ping其中的host,否则的话会ping本地的几个端口。当新的节点联系单播列表中的成员时,会得到整个集群所有节点的状态,然后联系Master节点,加入到集群中。
当主节点发生问题的时候,现有的节点会通过Ping的方式重新选举一个新的主节点。
需要注意的是Elasticsearch中支持任意数目的集群,没有限定节点数必须是奇数,而是通过一定的规则来约定。但是在分布式系统中,很容易出现脑裂,解决方案是设置一个Quorum值,并且要求可用节点数超过Quorum才能对外提供服务。
Elasticsearch节点的故障检测有两种方式:第一种是Master节点到所有其它节点,证明它们还活着;另一种是每个节点Ping主节点进行验证,当主节点故障时会启动主节点重新选举过程。故障检测的频率由以下参数控制:
Elasticsearch集群的扩容分为垂直扩容和水平扩容,垂直扩容是增加单台服务器的CPU、内存和磁盘等资源;水平扩容即增加服务器数量,组成计算能力强大的分布式集群。水平扩容是最常用的方法,支撑PB级别的数据规模,当执行扩容操作后,新增加的节点会触发索引分片的重新分配。
每个索引的primary shard数量在索引创建的时候已经确定,如果想调整主分片需要重建索引,但是replica shard是可以动态调整的。如下图所示Elasticsearch集群有两个节点,primary shard设置为3,replica shard设置为1,这样1个索引就有3个primary shard,3个replica shard,P表示primary shard,R表示replica shard。
当加入新的节点Node 3时,触发分片的重新分配,P2和R2迁移到Node 3。如下图所示:
集群扩容时需要注意的是:
Elasticsearch作为分布式架构下的搜索引擎已广泛应用到大数据分析、日志搜索等领域。本文中先介绍了ES中的基本概念,包括索引、文档、集群、路由和分片等;再从索引执行的流程角度,分析Elasticsearch读取和更新索引的流程,以及其中的Translog机制;最后解析ES的集群架构、集群的自动发现机制和水平扩容。
参考资料: