3.4 Elasticsearch的架构设计

Elasticsearch的架构设计图如图3-11所示。

图3-11

如图3-11所示,我们将Elasticsearch的架构自底向上分为五层,分别是核心层、数据处理层、发现与脚本层、协议层和应用层。

其中,核心层是指Lucene框架——Elasticsearch是基于Lucene框架实现的。

数据处理层主要是指在Elasticsearch中对数据的加工处理方式,常见的主要有Index (索引)模块、Search(搜索)模块和Mapping(映射)模块。

发现与脚本层主要是Discovery(节点发现)模块、Script(脚本)模块和第三方插件模块。Discovery模块是Elasticsearch自动发现节点的机制。Script模块支持脚本的执行,脚本的应用使得我们能很方便的对查询出来的数据进行加工处理,目前Elasticsearch支持JavaScript、Python等多种语言。第三方插件模块表示Elasticsearch支持安装很多第三方的插件,如elasticsearch-ik分词插件、elasticsearch-sql插件等。

协议层是Elasticsearch中的数据交互协议。目前Elasticsearch支持Thrift、Memcached和HTTP三种协议,默认的是HTTP。

应用层指的是Elasticsearch的API支持模式。Elasticsearch的特色之一就是RESTFul风格的API,这种API接口风格也是当前十分流行的风格之一。

另外,图3-11中的JMX指的是在Elasticsearch中对Java的管理框架,用来管理Elasticsearch应用。

3.4.1 Elasticsearch的节点自动发现机制

在Elasticsearch内部,通过在集群中配置一个相同的集群名称(即cluster.name),就能将不同的节点连接到同一个集群。这是怎么实现的呢?本节就来揭晓节点自动发现机制。

Elasticsearch内嵌自动发现功能,主要提供了4种可供选择的发现机制。其中一种是默认实现,其他都是通过插件实现的,具体如下所示。

(1)Azure discovery插件方式:多播模式。

(2)EC2 discovery插件方式:多播模式。

(3)Google Compute Engine(GCE)discovery插件方式:多播模式。

(4)Zen Discovery,默认实现方式,支持多播模式和单播模式。

Zen Discovery是Elasticsearch内置的默认发现模块。发现模块用于发现集群中的节点及选举主节点(又称master节点)。Zen Discovery提供单播模式和基于文件的发现,并且可以扩展为通过插件支持其他形式的发现机制。

在配置前,我们需要了解多播模式和单播模式的配置参数。主要配置参数如下所示:

● discovery.zen.ping.multicast.enabled表示关闭多播模式的自动发现机制,主要是为了防止其他机器上的节点自动连入。

● discovery.zen.fd.ping_timeout和discovery.zen.ping.timeout表示设置了节点与节点之间连接ping命令执行的超时时长。

● discovery.zen.minimum_master_nodes表示集群中选举主节点时至少需要有多少个节点参与。

● discovery.zen.ping.unicast.hosts表示在单播模式下,节点应该自动发现哪些节点列表。action.auto_create_index:false表示关闭自动创建索引。

1.单播模式

Elasticsearch支持多播模式和单播模式自动两种节点发现机制,不过多播模式已经不被大多数操作系统所支持,加之其安全性不高,所以一般我们会主动关闭多播模式。关闭多播模式的配置如下所示:

在Elasticsearch中,发现机制默认被配置为使用单播模式,以防止节点无意中加入集群。Elasticsearch支持同一个主机启动多个节点,因此只有在同一台机器上运行的节点才会自动组成集群。当集群的节点运行在不同的机器上时,在单播模式下,我们需要为Elasticsearch配置一些它应该去尝试连接的节点列表,配置方式如下所示:

因此,单播模式下的配置信息汇总如下:

配置后,集群构建及主节点选举过程如下:

节点启动后先执行ping命令(这里提及的ping命令不是Linux环境用的ping命令,而是Elasticsearch的一个RPC命令),如果discovery.zen.ping.unicast.hosts有设置,则ping设置中的host;否则尝试ping localhost的几个端口。

ping命令的返回结果会包含该节点的基本信息及该节点认为的主节点。

在选举开始时,主节点先从各节点认为的master中选。选举规则比较简单,即按照ID的字典序排序,取第一个。

如果各节点都没有认为的master,则从所有节点中选择,规则同上。

需要注意的是,这里有个集群中节点梳理最小值限制条件,即discovery.zen.minimum_master_nodes。如果节点数达不到最小值的限制,则循环上述过程,直到节点数超过最小限制值,才可以开始选举。

最后选举出一个主节点,如果只有一个本地节点,则主节点就是它自己。

如果当前节点是主节点,则开始等待节点数达到minimum_master_nodes,再提供服务。如果当前节点不是主节点,则尝试加入主节点所在集群。

2.多播模式

在多播模式下,我们仅需在每个节点配置好集群名称和节点名称即可。互相通信的节点会根据Elasticsearch自定义的服务发现协议,按照多播的方式寻找网络上配置在同样集群内的节点。

3.4.2 节点类型

在Elasticsearch中,每个节点可以有多个角色,节点既可以是候选主节点,也可以是数据节点。

节点的角色配置在配置文件/config/elasticsearch.yml中设置即可,配置参数如下所示。在Elasticsearch中,默认都为true。

其中,数据节点负责数据的存储相关的操作,如对数据进行增、删、改、查和聚合等。正因为如此,数据节点往往对服务器的配置要求比较高,特别是对CPU、内存和I/O的需求很大。此外,数据节点梳理通常随着集群的扩大而弹性增加,以便保持Elasticsearch服务的高性能和高可用。

候选主节点是被选举为主节点的节点,在集群中,只有候选主节点才有选举权和被选举权,其他节点不参与选举工作。

一旦候选主节点被选举为主节点,则主节点就要负责创建索引、删除索引、追踪集群中节点的状态,以及跟踪哪些节点是群集的一部分,并决定将哪些分片分配给相关的节点等。

3.4.3 分片和路由

在Elasticsearch中,若要进行分片和副本配置,则需要尽早配置。因为当在一个多分片的索引中写入数据时,需要通过路由来确定具体写入哪一个分片中,所以在创建索引时需要指定分片的数量,并且分片的数量一旦确定就不能修改。

分片的数量和副本数量都可以通过创建索引时的Settings来配置,Elasticsearch默认为一个索引创建5个主分片,并分别为每个分片创建一个副本。配置的参数如下所示:

对文档的新建、索引和删除请求等写操作,必须在主分片上面完成之后才能被复制到相关的副本分片。Elasticsearch为了加快写入的速度,写入过程往往是并发实施的。为了解决在并发写的过程中出现的数据冲突的问题,Elasticsearch通过乐观锁进行控制,每个文档都有一个version (版本号),当文档被修改时版本号递增。

那分片如何使用呢?

当我们向Elasticsearch写入数据时,Elasticsearch根据文档标识符ID将文档分配到多个分片上。当查询数据时,Elasticsearch会查询所有的分片并汇总结果。对用户而言,这个过程是透明的,用户并不知道数据到底存在哪个分片上。

为了避免在查询时部分分片查询失败影响结果的准确性,Elasticsearch引入了路由功能,即数据在写入时,通过路由将数据写入指定分片;在查询时,可以通过相同的路由指明在哪个分片将数据查出来。在默认情况下,索引数据的分片算法如下所示:

其中,routing字段的取值默认是id字段或者是parent字段。routing字段在Hash分片之后再与有分片的数量取模,最终得到这条数据应该被分配在哪一个分片上。

这样做的目的是通过Hash分片来保证在每个分片上数据量的均匀分布,避免各个分片的存储负载不均衡。在做数据检索时,Elasticsearch默认会搜索所有分片上的数据,最后在主节点上汇总各个分片数据并进行排序处理后,返回最终的结果数据。

3.4.4 数据写入过程

数据写入操作是在Elasticsearch的内存中执行的,数据会被分配到特定的分片和副本上,但最终数据是需要存储到磁盘上持久化的。

在Elasticsearch中,数据的存储路径在配置文件../config/elasticsearch.yml中进行设置,具体设置如下:

注:建议不要使用默认值,主要是考虑到当Elasticsearch升级时数据的安全性问题,防止因升级Elasticsearch而导致数据部分甚至全部丢失。

1.分段存储

索引数据在磁盘上的是以分段形式存储的。

“段”是Elasticsearch从Lucene中继承的概念。在索引中,索引文件被拆分为多个子文件,其中每个子文件就叫作段,每个段都是一个倒排索引的小单元。

段具有不变性,一旦索引的数据被写入硬盘,就不能再修改。

为什么要引入分段呢?

可以试想一下,如果我们全部的文档集合仅构建在一个很大的倒排索引文件中,且数据量还在不断增加,当进行修改时,我们需要全量更新当前的倒排索引文件。这会使得数据更新时效性很差、且耗费大量资源,显然这不是我们希望看到的。

其实在Lucene中,分段的存储模式可以避免在读写操作时使用锁,从而大大提升Elasticsearch的读写性能。这有点类似于CurrentHashMap中“分段锁”的概念,二者有异曲同工之妙,都是为了减少锁的使用,提高并发。

当分段被写入磁盘后会生成一个提交点,提交点意味着一个用来记录所有段信息的文件已经生成。因此,一个段一旦拥有了提交点,就表示从此该段仅有读的权限,永远失去了写的权限。

当段在内存中时,此时分段拥有只写的权限,数据还会不断写入,而不具备读数据的权限,意味着这部分数据不能被Elasticsearch用户检索到。

那么,既然索引文件分段存储并且不可修改,那么新增、更新和删除如何处理呢?

其实新增是比较容易处理的。既然数据是新的,那么只需在当前文档新增一个段即可。

删除数据时,由于分段不可修改的特性,Elasticsearch不会把文档从旧的段中移除,因而是新增一个.del文件,.del文件中会记录这些被删除文档的段信息。被标记删除的文档仍然可以被查询匹配到,但它会在最终结果被返回前通过.del文件将其从结果集中移除。

当更新数据时,由于分段不可修改的特性,Elasticsearch无法通过修改旧的段来反映文档的更新,于是,更新操作变成了两个操作的结合,即先删除、后新增。Elasticsearch会将旧的文档从.del文件中标记删除,然后将文档的新版本索引到一个新的段中。在查询数据时,两个版本的文档都会被一个查询匹配到,但被删除的旧版本文档在结果集返回前就会被移除。

综上所述,段作为不可修改是具有一定优势的,段的优势主要表现在:不需要锁,从而提升Elasticsearch的读写性能。

分段不变性的主要缺点是存储空间占用量大——当删除旧数据时,旧数据不会被马上删除,而是在.del文件中被标记为删除。而旧数据只能等到段更新时才能被移除,这样就会导致存储空间的浪费。倘若频繁更新数据,则每次更新都是新增新的数据到新分段,并标记旧的分段中的数据,存储空间的浪费会更多。

在删除和更新数据时,存储空间会浪费;在检索数据时,依然有局限——在查询得到的结果集中会包含所有的结果集,因此主节点需要排除被标记删除的旧数据,随之带来的是查询的负担。

2.延迟写策略

在Elasticsearch中,索引写入磁盘的过程是异步的。

因此,为了提升写的性能,Elasticsearch并没有每新增一条数据就增加一个段到磁盘上,而是采用延迟写策略。延迟写策略的执行过程如下。

每当有新的数据写入时,就将其先写入JVM的内存中。在内存和磁盘之间是文件系统缓存,文件缓存空间使用的是操作系统的空间。当达到默认的时间或者内存的数据达到一定量时,会触发一次刷新(Refresh)操作。刷新操作将内存中的数据生成到一个新的分段上并缓存到文件缓存系统,稍后再被刷新到磁盘中并生成提交点。

需要指出的是,由于新的数据会继续写入内存,而内存中的数据并不是以段的形式存储的,因此不能提供检索功能。只有当数据经由内存刷新到文件缓存系统,并生成新的段后,新的段才能供搜索使用,而不需要等到被刷新到磁盘才可以搜索。

在Elasticsearch中,写入和打开一个新段的过程叫作刷新。在默认情况下,每个分片会每秒自动刷新一次。这就是Elasticsearch能做到近实时搜索的原因,因为文档的变化并不是立即对搜索可见的,但会在一秒之内变为可见。

当然,除自动刷新外,软件开发人员也可以手动触发刷新。

我们还可以在创建索引时,在Settings中通过配置refresh_interval的值,来调整索引的刷新频率。在设置值时需要注意后面带上时间单位,否则默认是毫秒。当refresh_interval=-1时,表示关闭索引的自动刷新。

虽然延迟写策略可以减少数据往磁盘上写的次数,提升Elasticsearch的整体写入能力,但文件缓存系统的引入同时也带来了数据丢失的风险,如机房断电等。

为此,Elasticsearch引入事务日志(Translog)机制。事务日志用于记录所有还没有持久化到磁盘的数据。

于是,在添加了事务日志机制后,数据写入索引的流程如下所示。

(1)新文档被索引之后,先被写入内存中。为了防止数据丢失,Elasticsearch会追加一份数据到事务日志中。

(2)新的文档持续在被写入内存时,同时也会记录到事务日志中。当然,此时的新数据还不能被检索和查询。

(3)当达到默认的刷新时间或内存中的数据达到一定量后,Elasticsearch会触发一次刷新,将内存中的数据以一个新段形式刷新到文件缓存系统中并清空内存。这时新段虽未被提交到磁盘,但已经可以对外提供文档的检索功能且不被修改。

(4)随着新文档索引不断被写入,当日志数据大小超过某个值(如512MB),或者超过一定时间(如30 min)时,Elasticsearch会触发一次Flush。

此时,内存中的数据被写入一个新段,同时被写入文件缓存系统,文件缓存系统中的数据通过Fsync刷新到磁盘中,生成提交点。而日志文件被删除,创建一个空的新日志。

3.段合并

在Elasticsearch自动刷新流程中,每秒都会创建一个新的段。这自然会导致短时间内段的数量猛增,而当段数量太多时会带来较大的资源消耗,如对文件句柄、内存和CPU的消耗。而在内容搜索阶段,由于搜索请求要检查到每个段,然后合并查询结果,因此段越多,搜索速度越慢。

为此,Elasticsearch引入段合并机制。段合并机制在后台定期进行,从而小的段被合并到大的段,然后这些大的段再被合并到更大的段。

在段合并过程中,Elasticsearch会将那些旧的已删除文档从文件系统中清除。被删除的文档不会被拷贝到新的大段中,当然,在合并的过程中不会中断索引和搜索。

段合并是自动进行索引和搜索的,在合并进程中,会选择一小部分大小相似的段,在后台将它们合并到更大的段中,这些段既可以是未提交的,也可以是已提交的。

在合并结束后,老的段会被删除,新的段被Flush到磁盘,同时写入一个包含新段且排除旧的和较小的段的新提交点。打开新的段之后,可以用来搜索。

由于段合并的计算量较大,对磁盘I/O的消耗也较大,因此段合并会影响正常的数据写入速率,因此Elasticsearch不会放任自流,让段合并影响搜索性能。Elasticsearch在默认情况下会对合并流程进行资源限制,这就是搜索服务仍然有足够的资源仍然可以执行的原因。