经过前面章节的了解,咱们已经知道 Elasticsearch 是一个实时的分布式搜索分析引擎,它能让你以一个以前从未有过的速度和规模,去探索你的数据。它被用做全文检索、结构化搜索、分析以及这三个功能的组合。 Elasticsearch 能够横向扩展至数百(甚至数千)的服务器节点,同时能够处理PB级数据。html
虽说 Elasticsearch 是分布式的,可是对于咱们开发者来讲并未过多的参与其中,咱们只需启动对应数量的 ES 实例(即节点),并给它们分配相同的 cluster.name
让它们归属于同一个集群,建立索引的时候只需指定索引 分片数 和 副本数 便可,其余的都交给了 ES 内部本身去实现。node
这和数据库的分布式和 同源的 solr 实现分布式都是有区别的,数据库分布式(分库分表)须要咱们指定路由规则和数据同步策略等,solr的分布式也需依赖 zookeeper,可是 Elasticsearch 彻底屏蔽了这些。mysql
因此咱们说,Elasticsearch 天生就是分布式的,而且在设计时屏蔽了分布式的复杂性。Elasticsearch 在分布式方面几乎是透明的。咱们可使用笔记本上的单节点轻松地运行Elasticsearch 的程序,但若是你想要在 100 个节点的集群上运行程序,一切也依然顺畅。web
Elasticsearch 尽量地屏蔽了分布式系统的复杂性。这里列举了一些在后台自动执行的操做:redis
虽然咱们能够不了解 Elasticsearch 分布式内部实现机制也能将Elasticsearch使用的很好,可是了解它们将会从另外一个角度帮助咱们更完整的学习和理解 Elasticsearch 知识。接下里咱们从如下几个部分来详细讲解 Elasticsearch 分布式的内部实现机制。sql
对于咱们以前的分布式经验,咱们知道,提高分布式性能能够经过购买性能更强大( 垂直扩容 ,或 纵向扩容 ) 或者数量更多的服务器( 水平扩容 ,或 横向扩容 )来实现。数据库
虽然Elasticsearch 能够获益于更强大的硬件设备,例如将存储硬盘设为SSD,可是 垂直扩容 因为硬件设备的技术和价格限制,垂直扩容 是有极限的。真正的扩容能力是来自于 水平扩容 --为集群添加更多的节点,而且将负载压力和稳定性分散到这些节点中。json
对于大多数的数据库而言,一般须要对应用程序进行很是大的改动,才能利用上横向扩容的新增资源。 与之相反的是,ElastiSearch天生就是 分布式的 ,它知道如何经过管理多节点来提升扩容性和可用性。 这也意味着你的应用无需关注这个问题。那么它是如何管理的呢?数组
启动一个 ES 实例就是一个节点,节点加入集群是经过配置文件中设置相同的 cluste.name
而实现的。因此集群是由一个或者多个拥有相同 cluster.name
配置的节点组成, 它们共同承担数据和负载的压力。当有节点加入集群中或者从集群中移除节点时,集群将会从新平均分布全部的数据。安全
与其余组件集群(mysql,redis)的 master-slave模式同样,ES集群中也会选举一个节点成为主节点,主节点它的职责是维护全局集群状态,在节点加入或离开集群的时候从新分配分片。具体关于主节点选举的内容能够阅读选举主节点。
全部主要的文档级别API(索引,删除,搜索)都不与主节点通讯,主节点并不须要涉及到文档级别的变动和搜索等操做,因此当集群只拥有一个主节点的状况下,即便流量的增长它也不会成为瓶颈。 任何节点均可以成为主节点。若是集群中就只有一个节点,那么它同时也就是主节点。
因此若是咱们使用 kibana 来做为视图操做工具的话,咱们只需在kibana.yml
的配置文件中,将elasticsearch.url: "http://localhost:9200"
设置为主节点就能够了,经过主节点 ES 会自动关联查询全部节点和分片以及副本的信息。因此 kibana 通常都和主节点在同一台服务器上。
做为用户,咱们能够将请求发送到 集群中的任何节点 ,包括主节点。 每一个节点都知道任意文档所处的位置,而且可以将咱们的请求直接转发到存储咱们所需文档的节点。 不管咱们将请求发送到哪一个节点,它都能负责从各个包含咱们所需文档的节点收集回数据,并将最终结果返回給客户端。 Elasticsearch 对这一切的管理都是透明的。
ES 是如何实现只须要配置相同的cluste.name
就将节点加入同一集群的呢?答案是发现机制(discovery module)。
发现机制 负责发现集群中的节点,以及选择主节点。每次集群状态发生更改时,集群中的其余节点都会知道状态(具体方式取决于使用的是哪种发现机制)。
ES目前主要推荐的自动发现机制,有以下几种:
这里额外介绍下单播,多播,广播的定义和区别,方便咱们更好的理解发现机制。
单播,多播,广播的区别:
单播(unicast):网络节点之间的通讯就好像是人们之间的对话同样。若是一我的对另一我的说话,那么用网络技术的术语来描述就是“单播”,此时信息的接收和传递只在两个节点之间进行。例如,你在收发电子邮件、浏览网页时,必须与邮件服务器、Web服务器创建链接,此时使用的就是单播数据传输方式。
多播(multicast):“多播”也能够称为“组播”,多播”能够理解为一我的向多我的(但不是在场的全部人)说话,这样可以提升通话的效率。由于若是采用单播方式,逐个节点传输,有多少个目标节点,就会有多少次传送过程,这种方式显然效率极低,是不可取的。若是你要通知特定的某些人同一件事情,可是又不想让其余人知道,使用电话一个一个地通知就很是麻烦。多播方式,既能够实现一次传送全部目标节点的数据,也能够达到只对特定对象传送数据的目的。多播在网络技术的应用并非不少,网上视频会议、网上视频点播特别适合采用多播方式。
广播(broadcast):能够理解为一我的经过广播喇叭对在场的全体说话,这样作的好处是通话效率高,信息一会儿就能够传递到全体,广播是不区分目标、所有发送的方式,一次能够传送完数据,可是不区分特定数据接收对象。
上面列举的发现机制中, Zen Discovery 是 ES 默认内建发现机制。它提供单播和多播的发现方式,而且能够扩展为经过插件支持云环境和其余形式的发现。因此咱们接下来重点介绍下 Zen Discovery是如何在Elasticsearch中使用的。
集群是由相同cluster.name
的节点组成的。当你在同一台机器上启动了第二个节点时,只要它和第一个节点有一样的 cluster.name
配置,它就会自动发现集群并加入到其中。可是在不一样机器上启动节点的时候,为了加入到同一集群,你须要配置一个可链接到的单播主机列表。
单播主机列表经过discovery.zen.ping.unicast.hosts
来配置。这个配置在 elasticsearch.yml 文件中:
discovery.zen.ping.unicast.hosts: ["host1", "host2:port"]
复制代码
具体的值是一个主机数组或逗号分隔的字符串。每一个值应采用host:port
或host
的形式(其中port
默认为设置transport.profiles.default.port
,若是未设置则返回transport.tcp.port
)。请注意,必须将IPv6主机置于括号内。此设置的默认值为127.0.0.1,[:: 1]
。
Elasticsearch 官方推荐咱们使用 单播 代替 组播。并且 Elasticsearch 默认被配置为使用 单播 发现,以防止节点无心中加入集群。只有在同一台机器上运行的节点才会自动组成集群。
虽然 组播 仍然做为插件提供, 但它应该永远不被使用在生产环境了,不然你获得的结果就是一个节点意外的加入到了你的生产环境,仅仅是由于他们收到了一个错误的 组播 信号。对于 组播 自己并无错,组播会致使一些愚蠢的问题,而且致使集群变的脆弱(好比,一个网络工程师正在捣鼓网络,而没有告诉你,你会发现全部的节点忽然发现不了对方了)。
使用单播,你能够为 Elasticsearch 提供一些它应该去尝试链接的节点列表。当一个节点联系到单播列表中的成员时,它就会获得整个集群全部节点的状态,而后它会联系 master 节点,并加入集群。
这意味着你的单播列表不须要包含你的集群中的全部节点,它只是须要足够的节点,当一个新节点联系上其中一个而且说上话就能够了。若是你使用 master 候选节点做为单播列表,你只要列出三个就能够了。
关于 Elasticsearch 节点发现的详细信息,请参阅 Zen Discovery。
对于分布式系统的熟悉,咱们应该知道分布式系统设计的目的是为了提升可用性和容错性。在单点系统中的问题在 ES 中一样也会存在。
若是咱们启动了一个单独的节点,里面不包含任何的数据和索引,那咱们的集群就是一个包含空内容节点的集群,简称空集群。
当集群中只有一个节点在运行时,意味着会有一个单点故障问题——没有冗余。单点的最大问题是系统容错性不高,当单节点所在服务器发生故障后,整个 ES 服务就会中止工做。
让咱们在包含一个空节点的集群内建立名为 user 的索引。索引在默认状况下会被分配5个主分片和每一个主分片的1个副本, 可是为了演示目的,咱们将分配3个主分片和一份副本(每一个主分片拥有一个副本分片):
PUT /user
{
"settings" : {
"number_of_shards" : 3,
"number_of_replicas" : 1
}
}
复制代码
咱们的集群如今是下图所示状况,全部3个主分片都被分配在 Node 1 。
此时检查集群的健康情况GET /_cluster/health
,咱们会发现:
{
"cluster_name": "elasticsearch",
"status": "yellow", # 1
"timed_out": false,
"number_of_nodes": 1,
"number_of_data_nodes": 1,
"active_primary_shards": 3,
"active_shards": 3,
"relocating_shards": 0,
"initializing_shards": 0,
"unassigned_shards": 3, # 2
"delayed_unassigned_shards": 0,
"number_of_pending_tasks": 0,
"number_of_in_flight_fetch": 0,
"task_max_waiting_in_queue_millis": 0,
"active_shards_percent_as_number": 50
}
复制代码
#1 集群的状态值是 yellow #2 未分配的副本数是 3
集群的健康情况为 yellow 则表示所有 主 分片都正常运行(集群能够正常服务全部请求),可是 副本 分片没有所有处在正常状态。 实际上,全部3个副本分片都是 unassigned —— 它们都没有被分配到任何节点。 在同一个节点上既保存原始数据又保存副本是没有意义的,由于一旦失去了那个节点,咱们也将丢失该节点上的全部副本数据。
主分片和对应的副本分片是不会在同一个节点上的。因此副本分片数的最大值是 n -1(其中n 为节点数)。
虽然当前咱们的集群是正常运行的,可是在硬件故障时有丢失数据的风险。
既然单点是有问题的,那咱们只需再启动几个节点并加入到当前集群中,这样就能够提升可用性并实现故障转移,这种方式即 水平扩容。
还以上面的 user 为例,咱们新增一个节点后,新的集群如上图所示。
当第二个节点加入到集群后,3个 副本分片 将会分配到这个节点上——每一个主分片对应一个副本分片。 这意味着当集群内任何一个节点出现问题时,咱们的数据都无缺无损。
全部新近被索引的文档都将会保存在主分片上,而后被并行的复制到对应的副本分片上。这就保证了咱们既能够从主分片又能够从副本分片上得到文档。
cluster-health
如今展现的状态为 green
,这表示全部6个分片(包括3个主分片和3个副本分片)都在正常运行。咱们的集群如今不只仅是正常运行的,而且还处于 始终可用 的状态。
产品不断升级,业务不断增加,用户数也会不断新增,也许咱们以前设计的索引容量(3个主分片和3个副本分片)已经不够使用了,用户数据的不断增长,每一个主分片和副本分片的数据不断累积,达到必定程度以后也会下降搜索性能。那么怎样为咱们的正在增加中的应用程序按需扩容呢?
咱们将以前的两个节点继续水平扩容,再增长一个节点,此时集群状态以下图所示:
为了分散负载,ES 会对分片进行从新分配。Node 1 和 Node 2 上各有一个分片被迁移到了新的 Node 3 节点,如今每一个节点上都拥有2个分片,而不是以前的3个。 这表示每一个节点的硬件资源(CPU, RAM, I/O)将被更少的分片所共享,每一个分片的性能将会获得提高。
分片是一个功能完整的搜索引擎,它拥有使用一个节点上的全部资源的能力。 咱们这个拥有6个分片(3个主分片和3个副本分片)的索引能够最大扩容到6个节点,每一个节点上存在一个分片,而且每一个分片拥有所在节点的所有资源。
可是若是咱们想要扩容超过6个节点怎么办呢?
主分片的数目在索引建立时 就已经肯定了下来。实际上,这个数目定义了这个索引可以 存储 的最大数据量。(实际大小取决于你的数据、硬件和使用场景。) 可是,读操做——搜索和返回数据——能够同时被主分片 或 副本分片所处理,因此当你拥有越多的副本分片时,也将拥有越高的吞吐量。
**索引的主分片数这个值在索引建立后就不能修改了(默认值是 5),可是每一个主分片的副本数(默认值是 1 )对于活动的索引库,这个值能够随时修改的。**至于索引的主分片数为何在索引建立以后就不能修改了,咱们在下面的文档存储原理章节中说明。
既然在运行中的集群上是能够动态调整副本分片数目的 ,那么咱们能够按需伸缩集群。让咱们把副本数从默认的 1 增长到 2 :
PUT /user/_settings
{
"number_of_replicas" : 2
}
复制代码
以下图 所示, user 索引如今拥有9个分片:3个主分片和6个副本分片。 这意味着咱们能够将集群扩容到9个节点,每一个节点上一个分片。相比原来3个节点时,集群搜索性能能够提高 3 倍。
固然,若是只是在相同节点数目的集群上增长更多的副本分片并不能提升性能,由于每一个分片从节点上得到的资源会变少。 你须要增长更多的硬件资源来提高吞吐量。
可是更多的副本分片数提升了数据冗余量:按照上面的节点配置,咱们能够在失去2个节点的状况下不丢失任何数据。
若是咱们某一个节点发生故障,节点服务器宕机或网络不可用,这里假设主节点1发生故障,这时集群的状态为:
此时咱们检查一下集群的健康情况,能够发现状态为 red
,表示不是全部主分片都在正常工做。
咱们关闭的节点是一个主节点。而集群必须拥有一个主节点来保证正常工做,因此发生的第一件事情就是选举一个新的主节点: Node 2 。
在咱们关闭 Node 1 的同时也失去了主分片 1 和 2 ,而且在缺失主分片的时候索引也不能正常工做。
幸运的是,在其它节点上存在着这两个主分片的完整副本, 因此新的主节点当即将这些分片在 Node 2 和 Node 3 上对应的副本分片提高为主分片, 此时集群的状态将会为 yellow
。 这个提高主分片的过程是瞬间发生的,如同按下一个开关通常。
为何咱们集群状态是 yellow
而不是 green
呢? 虽然咱们拥有全部的三个主分片,可是同时设置了每一个主分片须要对应2份副本分片,而此时只存在一份副本分片。 因此集群不能为 green
的状态,不过咱们没必要过于担忧:若是咱们一样关闭了 Node 2 ,咱们的程序 依然 能够保持在不丢任何数据的状况下运行,由于 Node 3 为每个分片都保留着一份副本。
若是咱们从新启动 Node 1 ,集群能够将缺失的副本分片再次进行分配,那么集群的状态又将恢复到原来的正常状态。 若是 Node 1 依然拥有着以前的分片,它将尝试去重用它们,同时仅从主分片复制发生了修改的数据文件。
分布式系统中最麻烦的就是并发冲突,既然 ES 也是分布式的那它是如何处理并发冲突的呢?
一般当咱们使用 索引 API 更新文档时 ,能够一次性读取原始文档,作咱们的修改,而后从新索引 整个文档 。 最近的索引请求将获胜:不管最后哪个文档被索引,都将被惟一存储在 Elasticsearch 中。若是其余人同时更改这个文档,他们的更改将丢失。
不少时候这是没有问题的。也许咱们的主数据存储是一个关系型数据库,咱们只是将数据复制到 Elasticsearch 中并使其可被搜索。也许两我的同时更改相同的文档的概率很小。或者对于咱们的业务来讲偶尔丢失更改并非很严重的问题。
但有时丢失了一个变动就是很是严重的 。试想咱们使用 Elasticsearch 存储咱们网上商城商品库存的数量, 每次咱们卖一个商品的时候,咱们在 Elasticsearch 中将库存数量减小。
有一天,管理层决定作一次促销。忽然地,咱们一秒要卖好几个商品。 假设有两个 web 程序并行运行,每个都同时处理全部商品的销售,那么会形成库存结果不一致的状况。
变动越频繁,读数据和更新数据的间隙越长,也就越可能丢失变动。
在数据库领域中,有两种方法一般被用来确保并发更新时变动不会丢失:
悲观锁 这种方法被关系型数据库普遍使用,它假定有变动冲突可能发生,所以阻塞访问资源以防止冲突。 一个典型的例子是读取一行数据以前先将其锁住,确保只有放置锁的线程可以对这行数据进行修改。
乐观锁 Elasticsearch 中使用的这种方法假定冲突是不可能发生的,而且不会阻塞正在尝试的操做。然而,若是源数据在读写当中被修改,更新将会失败。应用程序接下来将决定该如何解决冲突。例如,能够重试更新、使用新的数据、或者将相关状况报告给用户。
Elasticsearch 中对文档的 index , GET 和 delete 请求时,咱们指出每一个文档都有一个 _version (版本)号,当文档被修改时版本号递增。
Elasticsearch 使用这个 _version 号来确保变动以正确顺序获得执行。若是旧版本的文档在新版本以后到达,它能够被简单的忽略。
咱们能够利用 _version 号来确保应用中相互冲突的变动不会致使数据丢失。咱们经过指定想要修改文档的 version 号来达到这个目的。 若是该版本不是当前版本号,咱们的请求将会失败。
全部文档的更新或删除 API,均可以接受 version
参数,这容许你在代码中使用乐观的并发控制,这是一种明智的作法。
版本号(version)只是其中一个实现方式,咱们还能够借助外部系统使用版本控制,一个常见的设置是使用其它数据库做为主要的数据存储,使用 Elasticsearch 作数据检索, 这意味着主数据库的全部更改发生时都须要被复制到 Elasticsearch ,若是多个进程负责这一数据同步,你可能遇到相似于以前描述的并发问题。
若是你的主数据库已经有了版本号,或一个能做为版本号的字段值好比 timestamp
,那么你就能够在 Elasticsearch 中经过增长 version_type=external
到查询字符串的方式重用这些相同的版本号,版本号必须是大于零的整数, 且小于 9.2E+18
(一个 Java 中 long 类型的正值)。
外部版本号的处理方式和咱们以前讨论的内部版本号的处理方式有些不一样, Elasticsearch 不是检查当前 _version
和请求中指定的版本号是否相同,而是检查当前_version
是否小于指定的版本号。若是请求成功,外部的版本号做为文档的新_version
进行存储。
外部版本号不只在索引和删除请求是能够指定,并且在建立新文档时也能够指定。
例如,要建立一个新的具备外部版本号 5 的博客文章,咱们能够按如下方法进行:
PUT /website/blog/2?version=5&version_type=external
{
"title": "My first external blog entry",
"text": "Starting to get the hang of this..."
}
复制代码
在响应中,咱们能看到当前的 _version 版本号是 5 :
{
"_index": "website",
"_type": "blog",
"_id": "2",
"_version": 5,
"created": true
}
复制代码
如今咱们更新这个文档,指定一个新的 version 号是 10 :
PUT /website/blog/2?version=10&version_type=external
{
"title": "My first external blog entry",
"text": "This is a piece of cake..."
}
复制代码
请求成功并将当前 _version 设为 10 :
{
"_index": "website",
"_type": "blog",
"_id": "2",
"_version": 10,
"created": false
}
复制代码
若是你要从新运行此请求时,它将会失败,并返回像咱们以前看到的一样的冲突错误,由于指定的外部版本号不大于 Elasticsearch 的当前版本号。
建立索引的时候咱们只须要指定分片数和副本数,ES 就会自动将文档数据分发到对应的分片和副本中。那么文件到底是如何分布到集群的,又是如何从集群中获取的呢? Elasticsearch 虽然隐藏这些底层细节,让咱们好专一在业务开发中,可是咱们深刻探索这些核心的技术细节,这能帮助你更好地理解数据如何被存储到这个分布式系统中。
当索引一个文档的时候,文档会被存储到一个主分片中。 Elasticsearch 如何知道一个文档应该存放到哪一个分片中呢?当咱们建立文档时,它如何决定这个文档应当被存储在分片 1 仍是分片 2 中呢?
首先这确定不会是随机的,不然未来要获取文档的时候咱们就不知道从何处寻找了。实际上,这个过程是根据下面这个公式决定的:
shard = hash(routing) % number_of_primary_shards
复制代码
routing
是一个可变值,默认是文档的 _id
,也能够设置成一个自定义的值。 routing
经过 hash
函数生成一个数字,而后这个数字再除以 number_of_primary_shards
(主分片的数量)后获得 余数 。这个分布在 0 到 number_of_primary_shards-1
之间的余数,就是咱们所寻求的文档所在分片的位置。
这就解释了为何咱们要在建立索引的时候就肯定好主分片的数量 而且永远不会改变这个数量:由于若是数量变化了,那么全部以前路由的值都会无效,文档也再也找不到了。
你可能以为因为 Elasticsearch 主分片数量是固定的会使索引难以进行扩容,因此在建立索引的时候合理的预分配分片数是很重要的。
全部的文档 API( get 、 index 、 delete 、 bulk 、 update 以及 mget )都接受一个叫作 routing
的路由参数 ,经过这个参数咱们能够自定义文档到分片的映射。一个自定义的路由参数能够用来确保全部相关的文档——例如全部属于同一个用户的文档——都被存储到同一个分片中。更多路由相关的内容能够访问这里。
上面介绍了一个文档是如何路由到一个分片中的,那么主分片是如何和副本分片交互的呢?
假设有个集群由三个节点组成, 它包含一个叫 user 的索引,有两个主分片,每一个主分片有两个副本分片。相同分片的副本不会放在同一节点,因此咱们的集群看起来以下图所示:
咱们能够发送请求到集群中的任一节点。每一个节点都有能力处理任意请求。每一个节点都知道集群中任一文档位置,因此能够直接将请求转发到须要的节点上。 在下面的例子中,将全部的请求发送到 Node 1 ,咱们将其称为 协调节点(coordinating node)。
当发送请求的时候,为了扩展负载,更好的作法是轮询集群中全部的节点。
对文档的新建、索引和删除请求都是写操做,必须在主分片上面完成以后才能被复制到相关的副本分片。
如下是在主副分片和任何副本分片上面 成功新建,索引和删除文档所须要的步骤顺序:
在客户端收到成功响应时,文档变动已经在主分片和全部副本分片执行完成,变动是安全的。
在处理读取请求时,协调结点在每次请求的时候都会经过轮询全部的副本分片来达到负载均衡。
在文档被检索时,已经被索引的文档可能已经存在于主分片上可是尚未复制到副本分片。在这种状况下,副本分片可能会报告文档不存在,可是主分片可能成功返回文档。一旦索引请求成功返回给用户,文档在主分片和副本分片都是可用的。
我的公众号:JaJian
欢迎长按下图关注公众号:JaJian!
按期为你奉上分布式,微服务等一线互联网公司相关技术的讲解和分析。