原文出处:iceman1952
这是一篇译文,原文(Every shard deserves a home)于2016-11-11发布在elastic官方博客。译文稍有更改
阅读提示
- 文章包含很多gif动图,你可以使用“2345看图王”查看/暂停/回放gif动图的每一帧
- 所有图片都可以在新标签页中查看大图
- “索引”有时作动词,有时作名词。例如“当索引第一个文档到新的索引中时…”,第一个索引是动词,第二个索引是名词
- 术语及翻译。有些术语不翻译,直接使用英文原词
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。集群会通过以下方式来平衡集群
- 考察集群中每个节点所包含shard的平均数量,然后,尽可能使得每个节点上的此数字保持一致
- 基于集群中每一个索引来做评估,使得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”后,会进行如下动作。如下面动图所示
- 被选中的节点探测到它自己被分配了一个新的shard
- 在被选中的节点上,将创建一个空的lucene索引(译注:每一个shard都是一个独立的lucene索引),创建完成后被选中的节点向master发送“shard已经就绪”的通知
- master收到通知后,master需要把被选中的节点上shard的状态标注”started”,为了做到这一点,master发送一个modified ClusterState
- 被选中的节点收到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
超过低水位阈值时,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)命令时,会做两件事情
- 把indexing buffer中的数据写入磁盘,从而生成一个新的segment
- 遍历所有的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。全部步骤如下
- master向node5发送了一个modified ClusterState,master要求node5初始化一个新的shard
- node5探测到自己被分配了一个新的shard
- node5向node1(node1上有primary shard)发送请求,请求开始恢复过程
- node1收到node5的请求,然后,node1验证它自己知道node5发送的请求
- node1验证通过,于是在node1上,elasticsearch固定transaction log以防止其被删除并捕获索引的segment快照,确保我们捕获了shard中的所有数据
- node1将segment数据发送到node5上的目标文件
- 在node5重放node1的transaction log,这会确保数据复制期间新索引进来的文档也能复制进入到node5上的目标文件
- node1发送“数据恢复已完成”给node5
- node5告知master“node5上的shard已经就绪”
- 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。如下图所示
完