解剖 Elasticsearch 集群 - 之二
本篇文章是一系列涵盖 Elasticsearch 底层架构和原型示例的其中一篇。在本篇文章中,我们会讨论 Elasticsearch 是如何处理 3C 问题的(共识性、并发性和一致性)以及分片的内部概念如 translog(Write Ahead Log - WAL)以及 Lucene 的分段(segments)。
在之前的文章中,我们谈到了 Elasticsearch 存储模型以及 CRUD 操作。在本篇文章中,我会分享 Elasticsearch 是如何解决一些分布式系统的基本挑战的以及分片的一些内部概念。主要会涵盖以下话题:
- 共识性(Consensus) — 脑裂(split-brain)问题以及选举(quorum)的重要性
- 并发性(Concurrency)
- 一致性(Consistency):保证写和读的一致性
- Translog (Write Ahead Log — WAL)
- Lucene 段
共识性(Consensus) — 脑裂(split-brain)问题以及选举(quorum)的重要性
共识性(Consensus)是分布式系统基本挑战的其中一项。它需要系统的所有进程/节点(processes/nodes)都对某一数据值/状态(value/status)达成一致。有很多达成共识的算法如 Raft、Paxos 等,它们都在数学上被证明是正确的,但是,Elasticsearch 实现了它自己的共识系统(zen discovery),Elasticsearch 的作者 Shay Banon 对此有相关解释。zen discovery 模块有两部分:
- Ping: 进程节点用来发现其他节点
- Unicast: 模块包含了一个主机名的列表来控制 ping 哪个节点
Elasticsearch 是一个对等网络(peer-to-peer)系统,所有节点都可以与其他节点通讯,有一个活动的主节点用来更新和控制集群范围的状态和操作。新 Elasticsearch 集群将选举过程是作为 ping 进程的一部分,从所有合法主节点中选取一个主节点,并将其他节点加入到主节点。默认的 ping_interval 是 1 秒,ping_timeout 为 3 sec 。当节点加入时,它们会向主节点发送加入的请求,默认的 join_timeout 是 ping_timeout 的 20 倍。如果主节点失效,集群的节点会重新开始 ping 并发起新一轮的选举。ping 过程在当一个节点意外误以为主节点失效并通过其他节点发现主节点时也会有帮助。
NOTE: 默认,客户端节点和数据节点并不对选举过程产生任何作用。可以在配置文件 中 elasticsearch.yml 修改配置 discovery.zen.master_election.filter_client 和 discovery.zen.master_election.filter_data 属性为 False 。
为了进行错误检测,主节点会 ping 所有其他节点来检查它们是否处于 alive 状态,所有节点都会回 ping 主节点报告它们处于 alive 状态。
如果用默认设置,Elasticsearch 会在网络隔离时出现 脑裂(split-brain),节点会认为主节点死掉并选举它自己作为主节点,从而导致集群下有多个主节点。这不仅会导致数据丢失还可能会导致数据合并不正确。可以通过设置一下属性来避免这个问题。
discovery.zen.minimum_master_nodes = int(# of master eligible nodes/2)+1
这个属性要求活动合法主节点加入到新选举的主节点标志一次选举的过程的完成,新主节点接受了它主节点的地位。这是个极度重要的属性,它保证集群的稳定性,它会随着继续大小的变化而动态更新。图 a 和 b 表明当在设置或不设置 minimum_master_nodes 属性时,如果网络发生隔离的情况,分别会发生什么。
NOTE: 对于生产环境的集群,推荐是有 3 个专用主节点,不接受任何客户端请求,任意时候有 1 个节点作为活动的主节点。
现在我们了解了 Elasticsearch 中的共识性,我们再来看它是如何处理并发的。
并发(Concurrency)
Elasticsearch 是一个分布式系统并支持并发请求。当一个 create/update/delete 请求发送到主分片时,它同时也会以并行的方式发送到备份分片,不过,这些请求的送达可能是无序的。在这种情况下,Elasticsearch 用 乐观并发控制(optimistic concurrency control) 来保证新版本的文档不会被旧版本覆盖掉。
每个索引的文档都有一个版本号,它会在每次对文档变更后做自增运算。这些版本号用以保证变更是有序的。为了保证在应用里的更新不会导致数据丢失,Elasticsearch 的 API 允许用户指定当前版本号的文档中应该更新的部分。如果请求中指定的版本早于分片里的当前版本,请求会失败,也就是说文档以及被另一进程更新了。可以在应用程序层面来处理失败的请求。Elasticsearch 还提供了一些其他的锁机制,可以在 这里 进一步了解。
当我们并发请求 Elasticsearch 时,另外一个顾虑就是 — 如何确保这些请求的一致?现在,回答 Elasticsearch 是处于 CAP 三角的哪个地方还处于争论中,这个不在本篇文章的讨论范围。
现在,我们来看看 Elasticsearch 是如何达到读写一致的目标的。
一致性 — 保证读写一致
对于写来说,Elasticsearch 对一致性的支持层级与其他大多数数据库不一样,它允许通过使用预检查来看有多少个分片可供写入。可选项有 仲裁集(quorum)、一(one) 和 所有(all)。缺省设置是 仲裁集(quorum),这也就意味着只有当大多数分片可用时,才允许被写入。即使在多数分片可用时,还是会发生向备份分片写入失败的情况,这种情况下,备份分片被认为是有错误的,分片会在另一个节点上重建。
对于读来说,新文档只有在刷新间隔之后才对搜索可见。为了保证搜索请求返回结果是最新版本的文档,备份可以被设置成为 sync(默认值),当写操作在主备分片同时完成之后才会返回请求的结果。这样,无论搜索请求至哪个分片都会返回最新的文档。甚至如果你的应用要求高索引吞吐率(higher indexing rate)时,replication=async,可以为搜索请求设置 _preference 参数为 primary 。这样搜索请求会查询主分片,从而保证结果中的文档是最新版本。
在理解 Elasticsearch 是如何处理共识性、并发性和一致性的问题后,让我们再来看看一些关于分片内部构造的重要概念,这些概念使得 Elasticsearch 具备分布式搜索引擎的特性。
Translog
先写日志(Write Ahead Log - WAL)或事务日志(transaction log - translog)的概念从关系型数据库开始发展时就已经存在。translog 的底层原理可以在失败的情况下保证数据的完整性,即预期更改必须在真实更改保存到磁盘之前被记录保存下来。
当新文档被索引或旧文档被更新后,Lucene 索引会发生更改,这些更改会被保存到磁盘。如果在每次写请求后做这个操作的代价将会很高,所以完成的方式是将多个更改一次写入到磁盘。正像我们在之前的文章中描述的那样,flush 操作(Lucene 提交)默认是每 30 分钟一次,或者是在 translog 过大时(默认 512MB)。在这种情况下,有可能存在丢失两次 Lucene 提交(Luncene commit)之间的所有数据的情况。为了避免这个问题,Elasticsearch 使用 translog 。所有的索引/删除/更新(index/delete/update)操作都会被写入到 translog ,translog 在每次索引/删除/更新(index/delete/update)操作后(或默认每 5 秒钟)都会被同步(fsync)从而保证更改以及持久化。在 translog 在所有主从分片上都被同步后,客户端才会接收到写的应答。
如果在两次 Lucene 提交(Lucene commits)之间出现硬件故障或重启,translog 会进行重放来恢复最近一次 Lucene 提交前丢失的所有数据,所有的更改都会被提交到索引。
NOTE: 在重启 Elasticsearch 实例之前,推荐使用显式地将 translog 进行 flush ,这样需要重放的 translog 会是空的,重启也会更快。POST /_all/_flush 命令可以用来 flush 集群里的所有索引。
有了 translog 的 flush 操作,在文件系统缓存里的分片会被提交到磁盘完成索引的持久化。现在我们再来看看 Lucene 段的工作方式。
Lucene 的段(Segments)
一个 Lucene 的索引是由多个段构成的,一个段又是一个完整的倒排索引。段是不可变的,使 Lucene 可以将新的文档以增量的形式加入到索引,无须对索引进行重建,每个段都会消耗 CPU 时钟,文件句柄和内存。这也意味着段越多,搜索的性能将会越差。
为了解决这个问题,Elasticsearch 会将很多小段合并成大的段(如下图所示),并将新的合并的段提交到磁盘,然后删除老的小段。
这个过程是在后台自动执行的,不会影响索引或搜索。因为段合并会占用资源并影响搜索的性能,Elasticsearch 会对合并进程节流,保证搜索有足够可用的资源。
接下来是什么?
对于搜索请求,Elasticsearch 索引内分片里的所有 Lucene 段都会被搜索,但是,如果要读取所有匹配的文档或者读取搜索结果排名中比较深的文档,对于 Elasticsearch 集群是危险的。在后续的文章中,我们会看看为什么会这样,同时也会看看以下的话题(包括 Elasticsearch 是如何在低延迟与高相关度结果间做权衡的)
- Elasticsearch 的准实时
- 为什么深度分页会很危险?
- 计算搜索相关性的权衡
参考
参考来源:
Anatomy of an Elasticsearch Cluster: Part II