参考文档:
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创建时就不能修改的原因。如果主分片的数量在未来改变了,所有先前的路由值就失效了,文档也就永远找不到了。
所有的文档API(get
、index
、delete
、bulk
、update
、mget
)都接收一个routing
参数,它用来自定义文档到分片的映射。自定义路由值可以确保所有相关文档——例如属于同一个人的文档——被保存在同一分片上。
2.2 分片交互
假如我们有以下集群。
我们可以向这个集群的任何一台NODE发送请求,每一个NODE都有能力处理请求。每一个NODE都知道每一个文档所在的位置所以可以直接将请求路由过去。下面的例子,我们将所有的请求都发送到NODE1。
注:最好的实践方式是轮询所有的NODE来发送请求,以达到请求负载均衡。
2.3 新建、索引和删除
新建、索引和删除请求都是写(write)操作,它们必须在主分片上成功完成才能复制到相关的复制分片上。
下面我们罗列在主分片和复制分片上成功新建、索引或删除一个文档必要的顺序步骤:
- 客户端给
Node 1
发送新建、索引或删除请求。 - 节点使用文档的
_id
确定文档属于分片0
。它转发请求到Node 3
,分片0
位于这个节点上。 Node 3
在主分片上执行请求,如果成功,它转发请求到相应的位于Node 1
和Node 2
的复制节点上。当所有的复制节点报告成功,Node 3
报告成功到请求的节点,请求的节点再报告给客户端。
客户端接收到成功响应的时候,文档的修改已经被应用于主分片和所有的复制分片。你的修改生效了。
有很多可选的请求参数允许你更改这一过程。你可能想牺牲一些安全来提高性能。这一选项很少使用因为Elasticsearch已经足够快,不过为了内容的完整我们将做一些阐述。
replication
复制默认的值是sync
。这将导致主分片得到复制分片的成功响应后才返回。
如果你设置replication
为async
,请求在主分片上被执行后就会返回给客户端。它依旧会转发请求给复制节点,但你将不知道复制节点成功与否。
上面的这个选项不建议使用。默认的sync
复制允许Elasticsearch强制反馈传输。async
复制可能会因为在不等待其它分片就绪的情况下发送过多的请求而使Elasticsearch过载。
consistency
默认主分片在尝试写入时需要规定数量(quorum)或过半的分片(可以是主节点或复制节点)可用。这是防止数据被写入到错的网络分区。规定的数量计算公式如下:
int( (primary + number_of_replicas) / 2 ) + 1
consistency
允许的值为one
(只有一个主分片),all
(所有主分片和复制分片)或者默认的quorum
或过半分片。
注意number_of_replicas
是在索引中的的设置,用来定义复制分片的数量,而不是现在活动的复制节点的数量。如果你定义了索引有3个复制节点,那规定数量是:
int( (primary + 3 replicas) / 2 ) + 1 = 3
但如果你只有2个节点,那你的活动分片不够规定数量,也就不能索引或删除任何文档。
timeout
当分片副本不足时会怎样?Elasticsearch会等待更多的分片出现。默认等待一分钟。如果需要,你可以设置timeout
参数让它终止的更早:100
表示100毫秒,30s
表示30秒。
2.4 检索文档
下面我们罗列在主分片或复制分片上检索一个文档必要的顺序步骤:
- 客户端给
Node 1
发送get请求。 - 节点使用文档的
_id
确定文档属于分片0
。分片0
对应的复制分片在三个节点上都有。此时,它转发请求到Node 2
。 Node 2
返回文档(document)给Node 1
然后返回给客户端。
对于读请求,为了平衡负载,请求节点会为每个请求选择不同的分片——它会循环所有分片副本。
可能的情况是,一个被索引的文档已经存在于主分片上却还没来得及同步到复制分片上。这时复制分片会报告文档未找到,主分片会成功返回文档。一旦索引请求成功返回给用户,文档则在主分片和复制分片都是可用的。
2.5 局部更新
update
API 结合了之前提到的读和写的模式。
下面我们罗列执行局部更新必要的顺序步骤:
- 客户端给
Node 1
发送更新请求。 - 它转发请求到主分片所在节点
Node 3
。 Node 3
从主分片检索出文档,修改_source
字段的JSON,然后在主分片上重建索引。如果有其他进程修改了文档,它以retry_on_conflict
设置的次数重复步骤3,都未成功则放弃。- 如果
Node 3
成功更新文档,它同时转发文档的新版本到Node 1
和Node 2
上的复制节点以重建索引。当所有复制节点报告成功,Node 3
返回成功给请求节点,然后返回给客户端。
update
API还接受《新建、索引和删除》章节提到的routing
、replication
、consistency
和timout
参数。
三、索引原理
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时间是半个小时