3.3 Elasticsearch 的核心概念

想要学好、用好 Elasticsearch,首先要了解其核心概念、名词和属性。

Elasticsearch 的核心概念有 Node、Cluster、Shards、Replicas、Index、Type、Document、Settings、Mapping 和 Analyzer,其含义分别如下所示。

(1)Node:即节点。节点是组成 Elasticsearch 集群的基本服务单元,集群中的每个运行中的 Elasticsearch 服务器都可称之为节点。

(2)Cluster:即集群。Elasticsearch 的集群是由具有相同 cluster.name (默认值为 elasticsearch)的一个或多个 Elasticsearch 节点组成的,各个节点协同工作,共享数据。同一个集群内节点的名字不能重复,但集群名称一定要相同。

在实际使用 Elasticsearch 集群时,一般需要给集群起一个合适的名字来替代 cluster.name 的默认值。自定义集群名称的好处是,可以防止一个新启动的节点加入相同网络中的另一个同名的集群中。

在 Elasticsearch 集群中,节点的状态有 Green、Yellow 和 Red 三种,分别如下所述。

① Green:绿色,表示节点运行状态为健康状态。所有的主分片和副本分片都可以正常工作,集群 100% 健康。

② Yellow:黄色,表示节点的运行状态为预警状态。所有的主分片都可以正常工作,但至少有一个副本分片是不能正常工作的。此时集群依然可以正常工作,但集群的高可用性在某种程度上被弱化。

③ Red:红色,表示集群无法正常使用。此时,集群中至少有一个分片的主分片及它的全部副本分片都不可正常工作。虽然集群的查询操作还可以进行,但是也只能返回部分数据(其他正常分片的数据可以返回),而分配到这个有问题分片上的写入请求将会报错,最终导致数据丢失。

(3)Shards:即分片。当索引的数据量太大时,受限于单个节点的内存、磁盘处理能力等,节点无法足够快地响应客户端的请求,此时需要将一个索引上的数据进行水平拆分。拆分出来的每个数据部分称之为一个分片。一般来说,每个分片都会放到不同的服务器上。

进行分片操作之后,索引在规模上进行扩大,性能上也随之水涨船高的有了提升。

Elasticsearch 依赖 Lucene,Elasticsearch 中的每个分片其实都是 Lucene 中的一个索引文件,因此每个分片必须有一个主分片和零到多个副本分片。

当软件开发人员在一个设置有多分片的索引中写入数据时,是通过路由来确定具体写入哪个分片中的,因此在创建索引时需要指定分片的数量,并且分片的数量一旦确定就不能更改。

当软件开发人员在查询索引时,需要在索引对应的多个分片上进行查询。Elasticsearch 会把查询发送给每个相关的分片,并汇总各个分片的查询结果。对上层的应用程序而言,分片是透明的,即应用程序并不知道分片的存在。

在 Elasticsearch 中,默认为一个索引创建 5 个主分片,并分别为每个主分片创建一个副本。

(4)Replicas:即备份,也可称之为副本。副本指的是对主分片的备份,这种备份是精确复制模式。每个主分片可以有零个或多个副本,主分片和备份分片都可以对外提供数据查询服务。当构建索引进行写入操作时,首先在主分片上完成数据的索引,然后数据会从主分片分发到备份分片上进行索引。

当主分片不可用时,Elasticsearch 会在备份分片中选举出一个分片作为主分片,从而避免数据丢失。

一方面,备份分片既可以提升 Elasticsearch 系统的高可用性能,又可以提升搜索时的并发性能;另一方面,备份分片也是一把双刃剑,即如果备份分片数量设置得太多,则在写操作时会增加数据同步的负担。

(5)Index:即索引。在 Elasticsearch 中,索引由一个和多个分片组成。在使用索引时,需要通过索引名称在集群内进行唯一标识。

(6)Type:即类别。类别指的是索引内部的逻辑分区,通过 Type 的名字在索引内进行唯一标识。在查询时如果没有该值,则表示需要在整个索引中查询。

(7)Document:即文档。索引中的每一条数据叫作一个文档,与关系数据库的使用方法类似,一条文档数据通过_id 在 Type 内进行唯一标识。

(8)Settings:Settings 是对集群中索引的定义信息,比如一个索引默认的分片数、副本数等。

(9)Mapping:Mapping 表示中保存了定义索引中字段(Field)的存储类型、分词方式、是否存储等信息,有点类似于关系数据库(如 MySQL)中的表结构信息。

在 Elasticsearch 中,Mapping 是可以动态识别的。如果没有特殊需求,则不需要手动创建 Mapping,因为 Elasticsearch 会根据数据格式自动识别它的类型。当需要对某些字段添加特殊属性时,如定义使用其他分词器、是否分词、是否存储等,就需要手动设置 Mapping 了。一个索引的 Mapping 一旦创建,若已经存储了数据,就不可修改了。

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

3.4 Elasticsearch 的架构设计

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

es 某个节点所有分片都是INITIALIZING es的节点是什么_Elastic

如图 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 提供单播模式和基于文件的发现,并且可以扩展为通过插件支持其他形式的发现机制。

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

es 某个节点所有分片都是INITIALIZING es的节点是什么_搜索_02

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

es 某个节点所有分片都是INITIALIZING es的节点是什么_数据_03

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

es 某个节点所有分片都是INITIALIZING es的节点是什么_数据_04

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

es 某个节点所有分片都是INITIALIZING es的节点是什么_搜索_05

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

节点启动后先执行 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。

es 某个节点所有分片都是INITIALIZING es的节点是什么_数据_06

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

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

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

3.4.3 分片和路由

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

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

es 某个节点所有分片都是INITIALIZING es的节点是什么_搜索_07

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

那分片如何使用呢?

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

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

es 某个节点所有分片都是INITIALIZING es的节点是什么_Elastic_08

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

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

3.4.4 数据写入过程

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

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

es 某个节点所有分片都是INITIALIZING es的节点是什么_数据_09

:建议不要使用默认值,主要是考虑到当 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 在默认情况下会对合并流程进行资源限制,这就是搜索服务仍然有足够的资源仍然可以执行的原因。

3.5 知识点关联

1.乐观锁

Elasticsearch 引入了乐观锁机制来解决并发写过程中数据冲突的问题,其实乐观锁在多个维度均有应用。

在数据库中,我们用乐观锁来控制表结构,减少长事务中数据库加锁的开销,达到数据表「读多写少」场景下的高性能;

在 Java 中,Java 引入了 CAS(Compare And Swap)乐观锁实现机制实现多线程同步的原子指令,如 AtomicInteger。

命名的艺术

本章重点介绍了 Elasticsearch 的核心概念,这些概念的英文命名方法很值得我们学习借鉴,如 Shard 英文原意为碎片,这个词很形象地解释了倒排索引分解的结果,我们通过这个单词就能见名知意。

其实,命名的学问不仅在 Elasticsearch 中用得很巧,在 Java 中也随处可见。如研发人员经常使用的「<>」操作符,英文原意为 Diamond Operator。这个命名很有想象力,「<>」很像一个菱形,而菱形的英文单词是 Diamond,同时 Diamond 还表示钻石。

2.配置文件格式

前面我们介绍了配置文件格式 YML。该文件格式是由 Clark Evans、Ingy döt Net 和 Oren Ben-Kiki 在 2001 年首次发表的。

YAML 是「YAML Ain't a Markup Language」(YAML 不是一种置标语言)的首字母缩写。有意思的是,在开发这种语言时,YAML 的初衷本是「Yet Another Markup Language」(仍是一种置标语言)。后来为了强调 YAML 语言以数据作为中心,而不是以置标语言为重点,因而采用返璞词来重新命名。

配置文件先后经历了 ini 格式、JSON 格式、XML 格式、Properties 格式和 HOCON 格式。其中,HOCON(Human-Optimized Config Object Notation)格式由 Lightbend 公司开发,它被用于 Sponge,以及利用 SpongeAPI 的独立插件以储存重要的数据,HOCON 文件通常以.conf 作为后缀名。

在配置文件的格式变迁中,我们能看到配置的方式都在追求语法简单、能继承、支持注释等特性。

如表 3-1 所示,Elasticsearch 中的索引(Index)如果对标关系数据库中的数据库(DataBase)的话,则表(Table)与类型(Type)对应——一个数据库下面可以有多张表(Table),就像 1 个索引(Index)下面有多种类型(Type)一样。

es 某个节点所有分片都是INITIALIZING es的节点是什么_Elastic_10

行(ROW)与文档(Document)对应——一个数据库表(Table)下的数据由多行(ROW)组成,就像 1 个类型 Type 由多个文档(Document)组成一样。

列(column)与字段(Field)对应——数据库表(Table)中一行数据由多列(column)组成,就像 1 个文档(Document)由多个字段(Field)组成一样。

关系数据库中的 schema 与 Elasticsearch 中的映射(Mapping)对应——在关系数据库中,schema 定义了表、表中字段、表和字段之间的关系,就像在 Elasticsearch 中,Mapping 定义了索引下 Type 的字段处理规则,即索引的建立、索引的类型、是否保存原始索引 JSON 文档、是否压缩原始 JSON 文档、是否需要分词处理、如何进行分词处理等。

关系数据库中的增(Insert)删(Delete)改(Update)查(Select)操作可与 Elasticsearch 中的增(Put/Post)删(Delete)改(Update)查(GET)一一对应。

3.副本

副本技术是分布式系统中常见的一种数据组织形式,在日常工作中,「副本」技术也十分常见。比如各级领导都需要指定和培养「二责」人选,当自己出差或请假时,「二责」可以组织团队中的工作。又如在团队中,团队成员之间的工作往往需要多人间相互备份,防止某位成员有事或离职时,相关工作不能继续展开。

在分布式系统中,副本是如何由来的,为什么这么有必要性呢?

副本(Replica 或称 Copy)一般指在分布式系统中为数据或服务提供的冗余。这种冗余设计是提高分布式系统容错率、提高可用性的常用手段。

在服务副本方面,一般指的是在不同服务器中部署同一份代码。如 Tomcat/Jetty 集群部署服务,集群中任意一台服务器都是集群中其他服务器的备份或称副本。

在数据副本方面,一般指的是在不同的节点上持久化同一份数据。当某节点中存储的数据丢失时,系统就可以从副本中读到数据了。

可以说,数据副本是分布式系统解决数据丢失或异常的唯一手段,因此副本协议也成为贯穿整个分布式系统的理论核心。

副本的数据一致性

分布式系统通过副本控制协议,让用户通过一定的方式即可读取分布式系统内部各个副本的数据,这些数据在一定的约束条件下是相同的,即副本数据一致性(Consistency)。副本数据一致性是针对分布式系统中各个节点而言的,不是针对某节点的某个副本而言的。

在分布式系统中,一致性分为强一致性(Strong Consistency)、弱一致性(Week Consistency),还有介于二者之间的会话一致性(Session Consistency)和最终一致性(Eventual Consistency)。

其中,强一致性最难实现。强一致性要求任何时刻用户都可以读到最近一次成功更新的副本数据。弱一致性与强一致性正好相反,数据更新后,用户无法在一定时间内读到最新的值,因此在实际中使用很少。

会话一致性指的是在一次会话内,用户一旦读到某个数据的某个版本的更新数据,则在这个会话中就不会再读到比当前版本更老旧的数据。最终一致性指的是集群中各个副本的数据最终能达到完全一致的状态。

从副本的角度而言,强一致性是最佳的,但对于分布式系统而言,还要考虑其他方面,如分布式系统的整体性能(即系统的吞吐)、系统的可用性、系统的可拓展性等。这也是系统设计要全盘考虑的原因。

副本数据的分布方式

副本的数据是如何分发到位的呢?这就涉及数据的分布方式。

一般来说,数据的分布方式主要有哈希方式、按数据范围分布、按数据量分布和一致性哈希方式(Consistent Hashing)等。

其中哈希方式最为简单,简单是其最大的优势,但缺点同样明显。一方面,可扩展性不高——一旦存储规模需要扩大,则所有数据都需要重新按哈希值分发;另一方面,哈希方式容易导致存储空间的数据分布不均匀。

按数据范围分布也比较常见,一般来说,是将数据按特征值的范围划分为不同的区间,使得集群中不同的服务器处理不同区间的数据。这种方式可以避免哈希值带来的存储空间数据分布不均匀的情况。

按数据量分布和按数据范围分布核心思路比较接近,一般是将数据看作一个顺序增长的,并将数据集按照某一较为固定的大小划分为若干数据块,把不同的数据块分布到不同的服务器上。

而一致性哈希是在工程实践中使用较为广泛的数据分布方式。一致性哈希的基本思路是使用一个哈希函数计算数据的哈希值,而哈希函数的输出值会作为一个封闭的环,我们会根据哈希值将节点随机分布到这个环上,每个节点负责处理从自己开始顺时针至下一个节点的全部哈希值域上的数据。

有了数据分布的方法,那么数据以何种形态进行分布呢?一般来说有两种,一种以服务器为核心,另一种是以数据为核心。

以机器为核心时,机器之间互为副本,副本机器之间的数据完全相同。以机器为核心的策略适用于上述各种数据分布方式,最主要的优点就是简单,容易落地;而缺点也很明显,一旦数据出问题,在数据恢复时就需要恢复多台服务器中的数据,效率很低;而且增加服务器后,会带来可扩展性低的问题。

以数据为核心时,一般将数据拆分为若干个数据段,以数据段为单位去分发。一般来说,每个数据段的大小尽量相等,而且限制数据量大小的上线。在不同的系统中,数据段有很多不同的称谓,如在 Lucene 和 Elasticsearch 中称之为 segment,在 Kafka 中称之为 chunk 和 partition 等。

以数据为核心并不适合所有数据分布方式,一般会采用哈希方式或一致性哈希方式。

将数据拆分为数据段意味着副本的管理将以数据段为单位进行展开,因此副本与机器不再强相关,每台机器都可以负责一定数据段的副本。这带来的好处是当某台服务器中的数据有问题时,我们可以从集群中的任何其他服务器恢复数据,因此数据的恢复效率很高。

副本分发策略

副本分发策略指的是主节点和副本节点之间副本数据同步的方法。一般来说分为两大类:中心化方式和去中心化方式。

中心化方式的基本上线思路是由一个中心节点协调副本数据的更新、维护副本之间的一致性。数据的更新可以是主节点主动向副本节点推送,也可以是副本节点向主节点推送。中心化方式的优点是设计思路较为简单,而缺点也很明显,数据的同步及系统的可用性都有「单点依赖」的风险,即依赖于中心化节点。一旦中心化节点发生异常,则数据同步和系统的可用性都会受到影响。

在去中心化方式中则没有中心节点,所有的节点都是 P2P 形式,地位对等,节点之间通过平等协商达到一致,因此去中心化节点不会因为某个节点的异常而导致系统的可用性受到影响。但有得必有失,去中心化方式的最大的缺点在于各个节点达成共识的过程较长,需要反复进行消息通信来确认内容,实现较为复杂。去中心化方式在区块链中有广泛应用,其共识达成的算法可以参见《区块链底层设计 Java 实战》一书中的「共识算法」部分。

部分内容来自于学习编程期间收集于网络的免费分享资源和工作后购买的付费内容。