每一个shard都需要有个家

这是一篇译文,原文(Every shard deserves a home)于2016-11-11发布在elastic官方博客。译文稍有更改

阅读提示

  1. 文章包含很多gif动图,你可以使用“2345看图王”查看/暂停/回放gif动图的每一帧
  2. 所有图片都可以在新标签页中查看大图
  3. “索引”有时作动词,有时作名词。例如“当索引第一个文档到新的索引中时…”,第一个索引是动词,第二个索引是名词
  4. 术语及翻译。有些术语不翻译,直接使用英文原词
rebalance 重新平衡
relocation 重新安置。即:集群已经选定了目标shard,需要从primary shard向这个目标shard复制数据
reallocation 重新分配。即:集群把某个/ 些索引的shard分布到集群中节点上
shard allocation shard分配
shard copy 分片副本(即可以是primary shard也可以是replica shard)
master master(主节点。使用英文原词,不再翻译)
shard shard(分片。使用英文原词,不再翻译)
primary shard primary shard(主分片。使用英文原词,不再翻译)
replica shard replica shard(副分片。使用英文原词,不再翻译)
segment segment(段,Lucene中的概念。使用英文原词,不再翻译)

文章正文开始

文中这些优秀的幻灯片来自于Core Elasticsearch: Operations课程,它们有助于解释shard分配(shard allocation)的概念。我们推荐您参加完整课程以更好的理解这些概念,但,我会在此列出培训的梗概

Shard分配(shard allocation)是把shard分配给节点的过程。 当初始恢复(initial recovery)、副分片分配(replica allocation)、重新平衡(rebalancing)或向集群中加入/移除节点时就会发生shard分配。大部分时间,你无需挂心它,它在后台由elasticsearch完成。如果你发现自己对这些细节感到好奇,这篇博客将探索几种不同场景下的shard分配

本文的集群由4个节点组成,如下图所示。文中的例子都使用此集群完成

我们将覆盖四种不同的场景

场景一、 创建索引

如上图所示,这是最简单的用例。我们创建了索引c,于是我们必须得为它分配新的shard。当索引第一个文档到这个新的索引时,就会为它分配shard。上图使用Kinaba中的Console插件(之前称为Sense)来执行灰色高亮的命令,索引一个文档到索引中

对于索引c,我们正在创建一个primary shard和一个replica shard。master需要创建索引c,并为它分配2个shard,即一个primary shard和一个replica shard。集群会通过以下方式来平衡集群

  1. 考察集群中每个节点所包含shard的平均数量,然后,尽可能使得每个节点上的此数字保持一致
  2. 基于集群中每一个索引来做评估,使得shard跨所有索引而保持平衡

Shard分配过程中存在一些限制,分配决定器(allocation decider)在做分配时会遵从这些限制。分配决定器会评估集群要做的每一个决定,并给出yes/no的回复。分配决定器运行在master上。你可以认为是master给出修改提议,分配决定器则告知master此修改提议是否能通过。

关于此最简单的一个例子就是,你不能把同一个shard的primary shard和replica shard放到同一个节点上

关于此还有一些其他例子

1. 基于Hot/Warm配置作分配过滤

这允许你把shard只放到具有特定属性的节点上,分配决定器会根据Hot/Warm配置接受或拒绝集群所作的决定。这是用户决定直接控制分配决定器的例子

2. 磁盘使用情况分配器(Disk usage allocator

master监控集群中磁盘的使用情况,并根据高水位/低水位阈值控制shard分配(见下面的:“场景二、 是时候移动shard了”)

3. 抑制(Throttles

这意味着,理论上我们可以把shard分配到某节点,但,此节点上有太多正在进行中的恢复(recovery)。为了保护节点并且也允许恢复进行,分配决定器让集群进行等待,然后在下一个迭代中再重试把shard分配给同一个节点

Shard初始化

一旦我们做出了primary shard将分配到哪个节点的决定,这个shard的状态就被标注为”initializing”(正在初始化),并且这个决定会通过一个modified ClusterState广播到集群中所有节点,然后集群中所有节点都将应用这个ClusterState

在shard状态被标注为”initializing”后,会进行如下动作。如下面动图所示

  1. 被选中的节点探测到它自己被分配了一个新的shard
  2. 在被选中的节点上,将创建一个空的lucene索引(译注:每一个shard都是一个独立的lucene索引),创建完成后被选中的节点向master发送“shard已经就绪”的通知
  3. master收到通知后,master需要把被选中的节点上shard的状态标注”started”,为了做到这一点,master发送一个modified ClusterState
  4. 被选中的节点收到master发送的modified ClusterState,于是被选中的节点激活此shard,并把shard的状态标注为”started”

因为这是一个primary shard,自此,我们就可以向其索引文档了

正如你所见,所有的通信都是通过modified ClusterState进行的。一旦这个周期结束,master会执行re-route,重新评估shard分配,有可能对先前迭代中被抑制的内容做出决定

现在,master要分配剩下的replica shard c0了,这也是由分配决定器来作决定的。分配决定器必须得等到包含primary shard的节点把primary shard的状态标注为”started”后,才能开始分配replica shard c0。如下图所示,primary shard c0已经在node2上分配完成且状态已经被标注为”started”,现在master需要分配剩下的replica shard c0了,replica shard c0的状态是unassigned

 

此时,会进行重新平衡,过程就和前面所描述的一样,重新平衡的目的是使数据在集群中是平衡的。在当前例子中,集群将把replica shard c0分配到node3,以使得集群是平衡的。最终,集群中每个节点包含3个shard。如下面两幅图所示,重新平衡把replilca shard c0分配给了node node3

 

 

上例子中,我们只是创建了一个空的replica shard,这比,假设说已经存在某个状态为”started”且包含数据的primary shard,要简单。对于这种情况,我们必须得确保新的replica shard包含有和primary shard同样的数据。如下面两幅图所示,第一幅,master把需要初始化replica shard c0的ClusterState广播到整个集群;第二幅,node2探测到自己被分配了一个新的shard


当replica shard分配完成后,需要理解的很重要的一点是,我们会从primary shard复制所有缺失的数据到replica shard,数据复制完成后,master才会把replica shard的状态标注为”started”,并且向集群中广播一个新的ClusterState。如下面动图所示

场景二、 是时候移动shard了

有时你的集群可能需要在集群内部移动已经存在的shard。这可能会有很多原因

1. 用户配置

这方面最常见的一个例子就是Hot/Warm配置,当数据老化时,会根据Hot/Warm配置把数据移动到访问速度较慢的磁盘上。如下图所示

 

2. 用户使用命令显式移动shard

用户通过cluster re-route命令来使得elasticsearch将shard从一个地方移到另一个地方

3. 磁盘相关的配置

存在与磁盘使用空间相关的以下两个设置,分配决定器会根据这些设置的阈值来移动shard

  1. cluster.routing.allocation.disk.watermark.low
  2. cluster.routing.allocation.disk.watermark.high

超过低水位阈值时,elasticsearch将阻止我们写入新的shard。同样,超过高水位阈值时,elasticsearch会把此节点上shard重新分配到其他节点上,直到当前节点的磁盘占用低于高水位阈值。如下图所示

4. 集群添加节点

可能你的集群已经达到最大容量,于是你添加了一个新的节点,此时elasticsearch会重新平衡(rebalance)整个集群。如下图所示

 

Shard可能会包含很多G的数据,因此,在集群间移动它们可能产生极大的性能影响。为使这个过程对用户透明,移动shard必须在后台运行。也就是尽可能的降低移动shard对elasticsearch其他方面的影响。为此,引入了一个抑制参数(indices.recovery.max_bytes_per_sec/cluster.routing.allocation.node_concurrent_recoveries) ),以保证移动shard期间依然可以继续向这些shard索引数据。如下图所示

记住:elasticsearch的所有数据都是通过Lucene存储的。Lucene使用被称为segment的一组文件来存储一组倒排索引。给定的tokens/words时,倒排索引结构可以方便的告诉你这些tokens/words包含在哪些文档中,出现在文档中的什么位置。当Lucene索引文档时,文档暂存于内存中的indexing buffer。当indexing buffer满或,elasticsearch发出refresh操作(从而引发lucene flush)时,indexing buffer中的数据就被强制写入被称为segment的倒排索引中。如下图所示

随着我们继续索引文档,我们会用同样的方式创建新的segment。关于segment,一个重要的事情就是segment是不可变的(immutable)。这意味着,一旦写了一个segment,这个segment就永远不会改变了。如果你发出删除或任何改变,这些动作将发生在新的segment上,在新的segment上同样发生合并过程。如下图所示

既然数据是存储在内存的,理论上在数据提交到segment文件之前(译注:即使已写入segment也可能会丢失,因为segment写入filesystem时,只是写入了内存即filesystem cache,只有调用filesystem的fsync后,内容才真正写入了磁盘。而出于性能考虑,filesystem是周期性而不是实时的调用fsync的),数据是有可能丢失的。elasticsearch使用transaction log来缓解这种情况。每当文档索引进Lucene时,文档也会被写入transaction log。如下图所示


Transaction log是顺序写入的,最后一个请求位于文件的末尾。借助transaction log,我们就可以恢复尚未写入Lucene中的文档。elasticsearch的持久化模型如下图所示


生成segment时可能并未执行fsync,此时segment会暂存于filesystem cache内存中,OS会暂缓刷新数据到磁盘。这么作是出于性能原因,因此,必须要把filesystem cache内存中的segment写入到磁盘,同时清空transaction log,这个工作是通过elasticsearch flush来完成的
当发出elasticsearch flush(从而引发lucene commit)命令时,会做两件事情

  1. 把indexing buffer中的数据写入磁盘,从而生成一个新的segment
  2. 遍历所有的segment文件,请求filesystem使用fsync将所有segment写入磁盘

执行elasticsearch flush就把内存中所有数据(即indexing buffer中的数据以及filesystem cache内存中的segment),统统写入了磁盘,并且清空了transaction log,这确保我们不会丢失任何数据。对于重新安置(relocation)shard,如果我们捕获并保存一组给定的segment,则我们得到一个时间点一致且数据不可变的数据快照

译注:参考Elasticsearch: 权威指南–>持久化变更了解文档写入过程。梗概总结如下图所示

 

以下面动图为例,集群想要把node4上的a0移动到node5,于是master标注a0为正在从node4重新安置到node5,node5收到请求后在node5上初始化一个shard。对这个行为,有一个非常重要的事情需要注意,当进行重新平衡时,看起来replica shard正在从node4移动到node5,但事实上,重新安置shard时总是从primary shard复制数据的(即:node1上的a0)

 

以下面动图为例,我们来演示“把node1上的primary shard重新安置到node5”。记住我们前面所说的两种数据存储机制,transaction log和lucene segment
此例中node5是空节点,node1上有primary shard。全部步骤如下

  1. master向node5发送了一个modified ClusterState,master要求node5初始化一个新的shard
  2. node5探测到自己被分配了一个新的shard
  3. node5向node1(node1上有primary shard)发送请求,请求开始恢复过程
  4. node1收到node5的请求,然后,node1验证它自己知道node5发送的请求
  5. node1验证通过,于是在node1上,elasticsearch固定transaction log以防止其被删除并捕获索引的segment快照,确保我们捕获了shard中的所有数据
  6. node1将segment数据发送到node5上的目标文件
  7. 在node5重放node1的transaction log,这会确保数据复制期间新索引进来的文档也能复制进入到node5上的目标文件
  8. node1发送“数据恢复已完成”给node5
  9. node5告知master“node5上的shard已经就绪”
  10. master发送modified ClusterState到node5,激活shard,将shard状态标注为”started”。同时,master删除掉node1上的源shard

上面这一切都在后台发生,因此整个过程中你依然可以向primary shard中索引数据。在这个过程中,如果确实又向primary shard中索引了新的数据,那么,这些新的数据并不包含在步骤5所捕获快照中,但这没有关系,因为通过步骤7重放node1的transaction log就可以确保这些新的数据也被复制进入了新的shard

现在问题来了,何时才能停下?复制过程中可能依然有新索引的文档进入primary shard,这意味着transaction log是一直增长的。在1.x中,我们的措施是锁定transaction log,从锁定点开始,所有再进来的请求都被阻塞,直到重放transaction log完成

在2.x/5.x中,我们作的更好。一旦我们开始重新安置(relocation),primary shard会把所有的索引操作发送到新的primary shard(位于node5上)。因为我们知道我们何时捕获的lucene快照,我们也知道shard是何时被初始化的,于是我们就确切的知道需要重放transaction log中的哪些数据

一旦恢复完成,目标节点(target node)给master发送通知,告知master“shard都已就绪”。master处理请求,复制剩余的primary shard(译注:因为一个索引可能存储多个primary shard),并激活shard。然后,源shard可以被移除了,这个过程一直重复,直到重新平衡(rebalancing)完成

场景三、 重启整个集群

我们要考察的下一个场景是重启整个集群。在这个场景中,我们并不处理激活的segment,而是在每个节点上找到本地数据。重启整个集群可能会发生在 维护周期,升级以及与计划中维护相关的任何事情

这里,master被选举出来,然后会新建一个ClusterState或者从磁盘恢复一个ClusterState。现在我们有了一个待分配的shard的列表,这些shard第一次被分配时,分配决定器可以把它们分配到任何一个节点上,但,现在不能再随便分配了。这意味着,我们需要找到这些数据,并确保我们能打开这些我们之前创建的lucene索引。如下图所示

为了做到这点(找到数据并打开之前创建的索引),master在集群中每一个节点上分配一个primary shard,且要求此primary shard返回磁盘上的所有内容。这意味着,我们物理上打开segment,然后通过确认一个shard副本来响应master。这时,master决定哪个节点将得到primary shard。在5.x中,我们会优先选择之前的primary shard(这是一个优化)。如下图所示

在下面的例子中,我们可以看到,node1上的a0之前是primary shard,但,其他任何副本都可能变成primary shard。在这个例子中,node4上的shard被标注为”initializing”,但,不同之处在于,这一次我们要使用已经存在的数据,并且可以检查节点上的lucene索引来验证lucene索引有效且可以打开。master会收到“shard已经就绪”、“shard已被分配”的通知,然后,master把这些shard的分配结果加入到集群状态中。如下面动图所示

为了验证两个shard上数据是一样的,我们会有一个复制过程,这个过程和重新安置非常类似,不同之处在于因为所有shard副本都是从磁盘恢复得来的,这些shard可能已经是匹配的了,因此可能无需传输shard。此链接详细描述了这个过程。如下图所示

因为segment就是独立的lucene索引,大量索引文档之后,非常可能磁盘上的segment和其他节点上相同的segment并不一致。有些segment用了更多资源,有些segment拥有烦人的邻居(nosy neighbors)。如下图所示

在v1.6之前,必须得复制所有segment。正是由于这些,v1.6之前的恢复是很慢的。我们必须得同步primary shard和replic shard,却又不能使用本地数据。为了解决这个问题,我们添加了sync_flush和sync_id。取不发生索引文档的某个时刻,使用一个唯一标识符来表示捕获的信息,确保同一个shard的各个副本是完全一致的。因此,当我们进行恢复时,我们发送sync_id标识符,如果标识符一致,则无需复制文件了,就可以重用老的副本。因为lucene中的segment是不可变的,这只对非激活的shard有效。注意:下图展示的是不同节点上的同一个shard,复制的数字就是发生的变化

 

场景四、 单个节点丢失(node3丢失)

在下图中,node3从集群中被移除了,node3上存储有b1的primary shard。此时,首先立即采取的步骤是master把当前位于node1上的b1的replica shard提升为primary shard。b1所在索引的健康状态以及集群的健康状态变成黄色,因为存在某个shard备份并没有全部被分配(shard所有备份的数量是用户在定义索引时指定的)。因此,master要尝试在剩下的某个节点上再分配一个新的b1的replica shard。如果node3是由于暂时的网络故障(或JVM GC引起的长时间STW)所导致,且,在网络故障恢复和节点再回到集群前,并未向shard中索引任何文档,则在node3缺席的这段时间内,在某个剩下的节点上重新复制一个新的b1的replica shard就纯属是浪费资源。如下面动图所示

在v1.6中,引入了基于index(per-index)设置来解决这个问题(index.unassigned.node_left.delayed_timeout,默认1分钟)。当node3离开时,会先延迟此处指定的时长再进行重新分配shard。如果在此时长之前node3回来了,则考察primary shard较node3上的shard是否发生了变化,若在此期间,primary shard并未有任何变化,则node3上的shard就被指定为replica shard;若primary shard发生了变化,则丢弃node3上的shard,并且重新从primary shard复制shard到node3

在v2.0中,引入了一个改进,如果node3在延迟超时之后才回来,对于位于node3上且依然匹配primary shard的任何shard(使用sync_id标识符来决定匹配与否),即使重新复制已经开始了,这些重新复制过程也会被停止,然后,node3上的这些shard被指定为replica shard。如下图所示



相关文章

发表评论

Comment form

(*) 表示必填项

还没有评论。

跳到底部
返回顶部