参考文档:
http://learnes.net/distributed_crud/bulk_requests.html
一、分布式集群
1.1 空集群
单台机器,其中没有数据,也没有索引。
集群中一个节点会被选举为master节点用于管理所有node。
和MySQL这样的集群架构不同,master在ES中只负责集群范畴的变更,如创建或者删除索引,添加节点或者删除节点,而文档的级别的操作在任何节点都可以进行,因此master不会成为性能瓶颈。
作为用户,我们可以访问包括 master 节点在内的集群中的任一节点。每个节点都知道各个文档的位置,并能够将我们的请求直接转发到拥有我们想要的数据的节点。无论我们访问的是哪个节点,它都会控制从拥有数据的节点收集响应的过程,并返回给客户端最终的结果。这一切都是由 Elasticsearch 透明管理的。
1.2 容错转移
前面介绍过了shard的概念,分为primary shard,replicate shard...
现在我们创建一个索引
PUT /blogs
{
"settings" : {
"number_of_shards" : 3,
"number_of_replicas" : 1
}
}
现在,我们的集群看起来就像下图所示了有索引的单节点集群,这三个主分片都被分配在 Node 1
。
现在我们创建一个新的节点,cluserter.name和第一个相同
当第二个节点加入后,就产生了三个 从分片(replica shards) ,它们分别于三个主分片一一对应。也就意味着即使有一个节点发生了损坏,我们可以保证数据的完整性。
所有被索引的新文档都会先被存储在主分片中,之后才会被平行复制到关联的从分片上。这样可以确保我们的文档在主节点和从节点上都能被检索。
1.3 横向扩展
随着应用需求的增长,我们该如何扩展?如果我们启动第三个节点,集群内会自动重组,这时便成为了三节点集群(cluster-three-nodes)
分片已经被重新分配以平衡负载:
在 Node 1
和 Node 2
中分别会有一个分片被移动到 Node 3
上,这样一来,每个节点上就都只有两个分片了。这意味着每个节点的硬件资源(CPU、RAM、I/O)被更少的分片共享,所以每个分片就会有更好的性能表现。
分片本身就是一个非常成熟的搜索引擎,它可以使用单个节点的所有资源。我们一共有6个分片(3个主分片和3个从分片),因此最多可以扩展到6个节点,每个节点上有一个分片,这样每个分片都可以使用到所在节点100%的资源了。
1.4 故障恢复
前文我们已经提到过 Elasticsearch 可以应对节点故障。让我们来尝试一下。如果我们把第一个节点杀掉,我们的集群就会如下图所示:
(1) 被杀掉的节点是主节点。而为了集群的正常工作必须需要一个主节点,所以首先进行的进程就是从各节点中选择了一个新的主节点:Node 2
。
(2) 主分片 1
和 2
在我们杀掉 Node 1
后就丢失了,我们的索引在丢失主节点的时候是不能正常工作的。如果我们在这个时候检查集群健康状态,将会显示 red
:存在不可用的主节点!
幸运的是,丢失的两个主分片的完整拷贝在存在于其他的节点上,所以新的主节点所完成的第一件事情就是将这些在 Node 2
和 Node 3
上的从分片提升为主分片,然后集群的健康状态就变回至 yellow
。这个提升的进程是瞬间完成了,就好像按了一下开关。
那么为什么集群健康状态依然是是 yellow
而不是 green
呢?是因为现在我们有3个主分片,但是我们之前设定了1个主分片有2个从分片,但是现在却只有1份从分片,所以状态无法变为 green
,不过我们可以不用太担心这里:当我们再次杀掉 Node 2
的时候,我们的程序依旧可以在没有丢失任何数据的情况下运行,因为Node 3
中依旧拥有每个分片的备份。
(3) 如果我们重启 Node 1
,集群就能够重新分配丢失的从分片,这样结果就会与三节点两从集群一致。如果Node 1
依旧还有旧节点的内容,系统会尝试重新利用他们,并只会复制在故障期间的变更数据。
到目前为止,我们已经清晰地了解了 Elasticsearch 的横向扩展以及数据安全的相关内容。接下来,我们将要继续讨论分片的生命周期等更多细节。
二、分布式文档存储
2.1 路由
当你对一个文档建立索引时,它仅存储在一个primary shard上。在拥有多个主分片时候ES是怎么知道一个文档应该属于哪个主shard?当你创建一个新的文档时,ES是怎么知道应该把它存储至shard1还是shard2? 这个过程不能随机无规律的,因为以后我们还要将它取出来,它的路由算法非常简单:
shard = hash(routing) % numberofprimary_shards
routing的值可以是文档的id,也可以是用户自己设置的一个值。hash将会根据routing算出一个数值然后%primaryshards的数量。这也是为什么primary_shards在index创建时就不能修改的原因。
2.2 文档的增删该查
假如我们有以下集群。
我们可以向这个集群的任何一台NODE发送请求,每一个NODE都有能力处理请求。每一个NODE都知道每一个文档所在的位置所以可以直接将请求路由过去。下面的例子,我们将所有的请求都发送到NODE1。
注:最好的实践方式是轮询所有的NODE来发送请求,以达到请求负载均衡。
1. 写操作
创建、索引、删除文档都是写操作,这些操作必须在primary shard完全成功后才能拷贝至其对应的replicas上。见Figure9。
说明:
1.客户端向Node1发送写操作的请求。
2.Node1使用文档的_id进行路由决定shard发现是P0(第0号主分片:Primary 0),于是将请求路由至NODE3。
3.Node3在P0上执行了请求。如果请求成功,则将请求并行的路由至NODE1 NODE2的R0上。当所有的replicas报告成功后,NODE3向请求的node(NODE1)发送成功报告,NODE1再报告至Client。
4.当客户端收到执行成功后,操作已经在Primary shard和所有的replica shards上执行成功了。
当然,有一些请求参数可以修改这个逻辑。见原文。
2. 读操作
读操作步骤:
1.假设客户端发送请求到NODE1,也就是MASTER,不是MASTER也无所谓。
2.NODE1使用文档的_id决定文档属于shard 0.shard 0的所有拷贝存在于所有3个节点上。这次,它将请求路由至NODE2。
3.NODE2将文档返回给NODE1,NODE1将文档返回给客户端。 对于读请求,请求节点(NODE1)将在每次请求到来时都选择一个不同的replica。shard来达到负载均衡。使用轮询策略轮询所有的replica shards。
3. 更新操作
更新操作,结合了以上的两个操作:读、写。见Figure11
步骤:
1.客户端发送更新操作请求至NODE1
2.NODE1将请求路由至NODE3,Primary shard所在的位置
3.NODE3从P0读取文档,改变source字段的JSON内容,然后试图重新对修改后的数据在P0做索引。如果此时这个文档已经被其他的进程修改了,那么它将重新执行3步骤,这个过程如果超过了retryon_conflict设置的次数,就放弃。
4.如果NODE3成功更新了文档,它将并行的将新版本的文档同步到NODE1和NODE2的replica shards重新建立索引。一旦所有的replica
shards报告成功,NODE3向被请求的节点(NODE1)返回成功,然后NODE1向客户端返回成功。
三、索引原理
3.1 per-segment机制
es写到磁盘的倒序索引是不变的,即如果已经建立了倒序索引并且持久化之后就不能更新。
如果索引有更新操作呢?用空间换时间... 通过新的segment来记录更新数据。
ES的这种机制称为动态更新索引。 Lucene引入了per-segment搜索的机制。一个segment(片段)是一个完整倒序索引的片段,即子集,用一系列的segments来分割整个倒序索引,每个segment都包含一些提交点。
新的文档建立时,都是基于内存操作,写入buffer,最后再被写入到磁盘的segment中。每个1s进行同步,这也是es称自己修改操作有1s延迟的原因。
1.操作首先都在内存中进行,再使用定时的同步策略
2.每隔一段时间,buffer将会被提交: 一个新的segment(一个额外的新的倒序索引)将被写到磁盘 一个新的提交点(commit point)被写入磁盘,将包含新的segment的名称。 磁盘fsync,所有在内核文件系统中的数据等待被写入到磁盘,来保障它们被物理写入。
3.新的segment被打开,使它包含的文档可以被索引。
4.内存中的buffer将被清理,准备接收新的文档。
当一个新的请求来时,会遍历所有的segments。词条分析程序会聚合所有的segments来保障每个文档和词条相关性的准确。通过这种方式,新的文档轻量的可以被添加到对应的索引中。
segments是不变的,所以文档不能从旧的segments中删除,也不能在旧的segments中更新来映射一个新的文档版本,逻辑删除即可。取之的是,每一个提交点都会包含一个.del文件,列举了哪一个segmen的哪一个文档已经被删除了。 当一个文档被”删除”了,它仅仅是在.del文件里被标记了一下。被”删除”的文档依旧可以被索引到,但是它将会在最终结果返回时被移除掉。
文档的更新同理:当文档更新时,旧版本的文档将会被标记为删除,新版本的文档在新的segment中建立索引。也许新旧版本的文档都会本检索到,但是旧版本的文档会在最终结果返回时被移除。
手动就行refresh并不推荐,可以使用api显示refresh:POST /blogs/_refresh
虽然刷新比提交更轻量,但是它依然有消耗。人工刷新在测试写的时有用,但不要在生产环境中每写一次就执行刷新,这会影响性能。相反,你的应用需要意识到ES近实时搜索的本质,并且容忍它。
不是所有的用户都需要每秒刷新一次。也许你使用ES索引百万日志文件,你更想要优化索引的速度,而不是进实时搜索。
你可以通过修改配置项refresh_interval减少刷新的频率:PUT /my_logs { "settings": { "refresh_interval": "30s" } }refresh_interval可以在存在的索引上动态更新。你在创建刷新,大索引的时候可以关闭自动在要使用索引的时候再打开它。形如:
PUT /my_logs/_settings { "refresh_interval": -1 } PUT /my_logs/_settings { "refresh_interval": "1s" }
3.2 持久化机制
在上述的per-segment搜索的机制下,新的文档会在分钟级内被索引,但是还不够快。 瓶颈在磁盘。将新的segment提交到磁盘需要fsync来保障物理写入。但是fsync是很耗时的。它不能在每次文档更新时就被调用,否则性能会很低。 现在需要一种轻便的方式能使新的文档可以被索引,这就意味着不能使用fsync来保障。 在ES和物理磁盘之间是内核的文件系统缓存。之前的描述中,Figure19,Figure20,在内存中索引的文档会被写入到一个新的segment。但是现在我们将segment首先写入到内核的文件系统缓存,这个过程很轻量,然后再flush到磁盘,这个过程很耗时。但是一旦一个segment文件在内核的缓存中,它可以被打开被读取。
但是不使用fsync将数据flush到磁盘,我们不能保障在断电后或者进程死掉后数据不丢失。
ES是可靠的,它可以保障数据被持久化到磁盘,方法是使用事务日志。
一个完全的提交会将segments写入到磁盘,并且写一个提交点,列出所有已知的segments。当ES启动或者重新打开一个index时,它会利用这个提交点来决定哪些segments属于当前的shard。 如果在提交点时,文档被修改会怎么样?不希望丢失这些修改:
1.当一个文档被索引时,它会被添加到in-memory buffer,并且添加到Translog日志中,见Figure21.
2.refresh操作会让shard处于Figure22的状态:每秒中,shard都会被refreshed:
- 在in-memory buffer中的文档会被写入到一个新的segment,但没有fsync。
- in-memory buffer被清空
3.这个过程将会持续进行:新的文档将被添加到in-memory buffer和translog日志中,见Figure23
4.一段时间后,当translog变得非常大时,索引将会被flush,新的translog将会建立,一个完全的提交进行完毕。见Figure24
- 在in-memory中的所有文档将被写入到新的segment
- 内核文件系统会被fsync到磁盘。
- 旧的translog日志被删除
即通过事务日志和文件系统缓存来实现持久化机制,称为flush操作
在ES中,进行一次提交并删除事务日志的操作叫做 flush。分片每30分钟,或事务日志过大会进行一次flush操作。flush API可用来进行一次手动flush,形如:flush索引blogs :POST /blogs/_flush
如果要flush所有索引,等待操作结束再返回:POST /_flush?wait_for_ongoing
当然很少需要手动flush,通常自动的就够了。当你要重启或关闭一个索引,flush该索引是很有用的。当ES尝试恢复或者重新打开一个索引时,它必须重放所有事务日志中的操作,所以日志越小,恢复速度越快.
3.3 合并段机制
通过每秒自动刷新创建新的段,用不了多久段的数量就爆炸了。有太多的段是一个问题,每个段消费文件句柄,内存,cpu资源。更重要的是,每次搜索请求都需要依次检查每个段,段越多,查询越慢。
ES通过后台合并段解决这个问题。小段被合并成大段,再合并成更大的段。然后删除旧的文档。这个过程你不必做什么。当你在索引和搜索时ES会自动处理。索引过程中,refresh会创建新的段,并打开它。
合并过程会在后台选择一些小的段合并成大的段,这个过程不会中断索引和搜索。
合并后的操作大致如下:
1:新的段flush到了硬盘
2:新的提交点写入新的段,排除旧的段
3:新的段打开供搜索
4:旧的段被删除
合并大的段会消耗很多IO和CPU,如果不检查会影响到搜素性能。默认情况下,ES会限制合并过程,这样搜索就可以有足够的资源进行。
四、总结
以上ES的所有操作对用户都是透明的
1. ES天然支持扩容,容灾,但是注意primary shards在创建索引后不可变,如果生产环境中,可以考虑使用索引重建的方案: 后面会详述使用索引别名实现热的索引重建
2. ES使用per-segments对倒排索引进行分段,注意是倒排索引不可变,当新的文档需要索引时,通过新的segments操作来避免并发,典型的空间换时间策略
3. 操作首先都是基于内存,即缓冲,同时使用定时同步来让内存的变化同步到segments中,默认的refresh时间是1S
3. ES中segments的持久化操作通过文件系统缓存+事务日志来实现,默认的flush时间是半个小时