1.FREE SOLO - 自己动手实现Raft - 开篇
2.一文说透Raft协议
3.万字长文解析raft算法原理
4.FREE SOLO - 自己动手实现Raft - 16 - leveldb源码分析与调试-2
5.FREE SOLO - 自己动手实现Raft - 13 - libuv源码分析与调试-4
6.FREE SOLO - 自己动手实现Raft - 15 - leveldb源码分析与调试-1
FREE SOLO - 自己动手实现Raft - 开篇
欢迎来到我的源码 Raft 自动实现项目,旨在通过个人实践,源码加深对Raft协议的源码理解并分享经验。在大数据和AI时代,源码分布式系统变得至关重要,源码而Raft协议作为一致性保证的源码png 剪裁源码首选,更是源码不可或缺。我在业界有过多次Raft协议的源码开发经验,虽然过程充满挑战,源码但也收获颇丰。源码现在,源码我决定重新实现Raft,源码以MVP形式出发,源码目标是源码创建一个用户友好的开源项目,帮助初学者深入理解协议原理和源码工作原理。源码
项目难点主要体现在理解、测试和应用三个层面。首先,Raft的设计目标是易于理解,但在实际编程时可能会遇到困惑。解决这个问题的关键在于掌握状态机思维,即将分布式系统视为一系列状态转换,遵循明确的规则。状态机模型简单却深具启发,比如图灵机和状态机的区别,将在后续文章中详细介绍。
在测试上,Raft的复杂性导致难以构建精确的自动化测试,尤其是涉及多节点协调的场景。为解决这个问题,我设计了remu工具,它模拟分布式系统的暂停和恢复,以辅助测试。通过gdb扩展,remu允许编写自动化测试用例,并检查系统状态的一致性。
应用方面,我计划提供vmeta、vstore、vectordb、vgraph等多种功能,以适应不同的业务场景。这需要一个灵活的框架,通过插件化设计,如kv引擎、vector引擎等,以简化开发工作。
技术栈的选择上,C/C++是获取邮件源码底层开发的首选,因其对硬件的适应性和功能强大的面向对象编程。我利用C++的智能指针和现代特性,同时避免过度复杂性。第三方库方面,我谨慎选择,并在项目中保持代码的可调试性。
SEDA架构被用于项目,它的分阶段事件驱动设计有助于扩展性。尽管不是最高效,但在分布式系统中,扩展性是首要考虑的。我正在探讨如何让开源更深入,鼓励更多人参与和学习。
未来,我将继续迭代和完善vraft项目,如果你对此感兴趣,可以通过邮件castermode@gmail.com或访问我的网站vectordb.io与我交流,共同进步。感谢你的关注与支持,让我们一起探索Raft的世界!
一文说透Raft协议
分布式一致性共识算法指的是在分布式系统中,使得所有节点对同一份数据的认知能够达成共识的算法。这个命名强调了一致性,但是我们知道在满足强C的情况下,对应的A就会被破坏地支离破碎。所以分布式一致性共识算法一般基于各种精妙的算法机制,能够在尽可能少地牺牲 C 的基础之上,将 A 提升到尽可能高的水平。
在讨论 Raft 算法的可用性A方面,它确保了当分布式系统中多于半数的节点仍然存活时,系统能够稳定运行, 多数派原则是提高分布式系统可用性 A 的关键。此外,请求处理时间是基于大多数节点的最小响应时间,而非整个系统的最小响应时间。
在数据一致性C方面,Raft 算法的标准版本可以确保数据达到最终一致性。但是,在实际工程实践中,我们可以对 Raft 算法进行些许改良,由此实现数据的即时一致性,从而进一步确保强C特性的实现。
Raft协议基于quorum机制,即大多数同意原则,任何的变更都需超过半数的成员确认。
状态机可以被视为节点用来实际存储数据的仓库。每个写入请求的最终步骤都是把结果保存到状态机里面,同时,读取请求也需要从这个状态机中提取数据以便响应。搜源码论坛
预写日志(Write-Ahead Logging, WAL)是一种常用的数据持久化方法,其主要目标是在系统发生故障时保证数据的一致性和可靠性。当进行状态改变的操作时,系统首先将这些操作记录到日志中,然后再实际执行这些操作。只有在操作被成功记录到日志中后,才会修改内存中的状态。如果过程中发生了故障,例如系统崩溃或者电源中断,可以通过重新执行日志中的操作来恢复之前的状态。只有当一个日志(即写入请求)得到了集群大多数派的认同后,它才会被提交并将修改应用到状态机中。
wal日志采用二进制格式,解码后得到的是LogEntry数据结构。其首个字段type包含两个值,0代表Normal,1代表ConfChange(ConfChange用于同步Etcd的配置更改,例如新节点的加入)。其次是term字段,每个term标识一个主节点的任期,任期会在主节点更改时更新。第三个字段index是严格单调增加的序列,代表变更编号。最后一个字段是二进制data,存储了raft request对象的pb结构。在Etcd源代码中有一个工具叫做etcddump-logs,它可以将wal日志转储为文本格式,以便分析Raft协议。
Raft协议并不处理应用数据即data字段的内容,而是通过同步wal日志来保证一致性。每个节点需要将从主节点接收的data应用到其本地存储。在这个过程中,Raft只关心日志的同步状态。因此,如果在应用data到本地存储的过程中存在bug,可能会导致数据不一致,即使所有Raft日志已经同步。
"Term"或"任期"是一个核心概念,它用于标识领导者选举的轮次。
一个Term开始于一个候选人尝试成为领导者,并发起选举。这个候选人会增加他的Term计数,并将其与投票请求一同发送给其他所有节点。如果其他节点收到了带有更高Term的投票请求,那么他们就会更新自己的Term并投票给该候选人。
在 Raft 算法中,所有的节点(无论是领导者、候选人还是现量线源码跟随者)在发送 RPC (Remote Procedure Call) 请求或响应时,都会在消息中带上自己当前的任期号(Term)。这是因为 Term 是 Raft 协议中用于标识消息新旧和避免过期操作的关键信息。
如果一个节点收到了一个包含较新任期(更大的任期号)的消息,那么它会立即更新自己的当前任期到这个新的任期,并将自己的状态切换为跟随者。如果收到的消息的任期号小于自身的任期号,则该消息会被视为过期信息并被忽略。
因此,在 Raft 中,无论是跟随者、候选人还是领导者,所有的 RPC 消息都会带上 Term 信息。
当一个新的日志条目被领导者创建并添加到其日志中时,这个条目会被赋予一个新的索引号,这个索引号等于当前日志列表的长度加一。因为日志索引号是递增的,所以每一条日志都会有一个唯一的索引号。
每一笔日志除了索引号之外,还有一个核心属性term: 用于标识这则日志是哪个任期的 leader写入的。
Raft协议中的领导者承担两种职责: 接收写请求 周期性发送心跳包
对于两阶段提交我们可以分别从单机和系统的两个角度去理解.
从单机层面,一笔写请求会分为添加到预写日志和应用到状态机两个步骤,这可以看做是一种两阶段提交.
在整个系统层面,两阶段提交的流程可拆解如下:
第2步是提议阶段(proposal),第4步是提交阶段(commit),两者构成了所谓的"两阶段提交"的流程.
如果跟随者(follower)发现当前领导者(leader)的任期小于自己记录的最新任期,跟随者会拒绝领导者的这次同步请求,并在响应中告诉领导者当前的最新任期。在感知到新任期的存在后,领导者也会适时地主动退位变为跟随者。
领导者(leader)的任期滞后主要发生在以下几种情况:
即使领导者(leader)的任期是最新的,如果跟随者(follower)在该最新同步日志之前的预写日志数据存在缺失,那么跟随者会拒绝领导者的同步请求。当领导者发现自己的任期与跟随者响应的任期相同,但同步请求被拒绝时,它会试图递归地逐个向跟随者同步预写日志数组中的早期日志,直到补全跟随者缺失的全部日志。一旦所有缺失日志被补全,流程将恢复正常。
当一个领导者试图向跟随者复制一个日志条目时,但在该日志条目还没被提交前,领导者突然宕机,然后新的领导者被选出。这种情况下,已经接收到旧领导者日志条目的节点的日志就会比新领导者的日志更“新”。当新领导者尝试将自己的日志复制给这些节点时,这些节点就需要删除"超前"的日志并同步新领导者的日志,以保持集群状态的一致性。
当跟随者(follower)接收到读请求时,bilibili网页源码它会将这些请求统一转发给领导者处理。只要领导者在处理写请求时,确保先更新状态机,然后再向客户端响应,就能保证状态机的数据实时一致性。然而,这种方式的缺点是领导者的负载可能过高,其他跟随者节点主要作用变成存储备份数据,相对较为被动。
这种强制读主存在另外一个问题,即在网络分区后仍错误地认为自己是领导者,提供的数据都是旧数据(因为多数派原则的存在,领导者写数据都会失败,但是读数据不受影响)。解决方案是在领导者提供读服务时,先广播所有节点以得到多数响应来确认自己的领导者身份。
这种设计可以确保,在任何时刻,所有正常运行的副本节点上的状态机都处于一个一致的状态。即使在发生故障转移的情况下,新选出的领导者也能够完成未完成的日志复制,并保证所有正常运行的副本节点上的状态机达到一致的状态。
在网络分区的情况下,处于小分区的节点可能会触发无效的选举,因为它们不能获得多数派的投票。这将导致多余的领导者选举以及不必要的term增加。
预检查或预投票机制可以有效地解决这个问题。在这种机制下,在真正进行选举之前,候选人会先向其他节点发送预投票请求。只有在收到多数节点的回应,即确认自己能够和集群中的大多数节点进行通信,候选人才会增加term并启动真正的选举流程。
这两种机制配合起来,可以有效地确保客户端请求的不丢失和不重复。
万字长文解析raft算法原理
万字详析raft算法原理:从理论到实践的深入探索
在接下来的两周里,我们将深入探讨分布式一致性算法raft的奥秘,分为理论阐述和实践应用两部分。首先,我们进入第一篇章,深入了解raft协议的核心概念和工作原理。 1. 分布式挑战与共识解决 在扩展系统时,纵向增长受限,横向扩展通过增加节点实现负载均衡,但网络环境对集群规模的影响不容忽视。分布式系统的优势在于数据备份和负载均衡,但一致性保证和秩序维护是关键挑战。CAP理论揭示了系统在一致性、可用性和分区容错性之间常常需要取舍。 2. 理解CAP理论 C代表数据正确性,追求像单机一样确保原子性;A强调服务可用性,快速响应;P是分区容错,是分布式系统的核心考量。在CP与AP之间,raft协议寻求在保证系统稳定性的前提下,提高服务的可用性。 3. 一致性难题与解决方案 即时一致性要求快速响应,但C问题的挑战在于确保数据在更新后的立即一致性。raft通过一主多从模式,主节点负责事务处理,保证写入的顺序性,从而提升系统的即时一致性。 4. raft协议的核心机制 raft协议的核心是leader、follower和candidate的角色以及预写日志、状态机等。多数派原则是关键,通过分散决策降低对单点的依赖,确保在多数节点存活时的可用性和一致性。 5. 协议运作细节一主多从:leader发起事务,follower参与决策,形成多数决定。
读写分离:简化操作,可能导致数据一致性风险,需通过日志同步和两阶段提交策略来解决。
6. 选举与状态同步 raft通过心跳和心跳超时机制进行选举,leader负责事务的提交和同步,保证数据最终一致性。同时,处理如 leader 滞后或 follower 落后等情况,以维持系统的稳定。 7. 实际应用中的挑战 网络分区、心跳问题和配置变更时的同步策略,都需要精心设计,如通过提前试探机制避免无意义选举,确保数据一致性。 8. etcd实践 我们将结合etcd源码,将理论与实践相结合,通过实际案例展示raft算法在现代分布式系统中的应用,包括状态机同步、写请求处理等。 9. 后续更新与资源 关注公众号“小徐先生的编程世界”,获取更多原创编程技术内容,特别是关于Go语言的raft工程化案例。FREE SOLO - 自己动手实现Raft - - leveldb源码分析与调试-2
本文聚焦于leveldb的写入机制,包括log的写入与memtable的写入过程。在深入分析之前,让我们回顾leveldb的核心数据结构,这将为后续的探讨提供直观的参考。
数据写入流程主要包括两个阶段:首先,将数据写入log,紧接着将数据写入memtable以供查询。
在log的写入过程中,数据经由一系列封装,最终通过调用log::Writer::AddRecord实现写入。在这一过程中,数据通过DBImpl::Put和DB::Put进行封装,最终由DBImpl::Write调用实现。
对于memtable的写入,数据同样经历DBImpl::Put和DB::Put的封装,随后由DBImpl::Write和MemTableInserter::Put进行处理,最后调用MemTable::Add完成写入。这一系列操作确保了数据的高效存储与检索。
数据读取方面,主要依赖于DBImpl::Get调用,通过MemTable::Get和SkipList::FindGreaterOrEqual操作在SkipList中进行搜索,实现从memtable中读取数据。同时,数据也可从sorted table中获取。
总结整个流程,本文主要梳理了数据写入与读取的调用栈,以及memtable与log在leveldb中的角色。下一次,我们将深入探讨大量数据写入后,内存与磁盘中数据状态的变化,以进一步理解leveldb的高效与可靠。
期待下次的分享,敬请关注!
FREE SOLO - 自己动手实现Raft - - libuv源码分析与调试-4
深入分析libuv库中的Timer事件处理流程,主要包括初始化、启动、停止以及重启等关键步骤。
初始化Timer事件,使用uv_timer_init函数,该函数仅调用uv__handle_init,将Timer handle添加至loop的handle_queue。
启动Timer事件,通过uv_timer_start函数实现,计算过期时间后将Timer插入loop内部的堆结构中,同时使用timer_less_than比较函数进行排序。
停止Timer事件,执行uv_timer_stop,从堆中移除Timer,uv__handle_stop递减handle引用计数,当loop内无active handle时退出循环。
重启Timer事件,在uv_timer_again函数中判断是否设置repeat参数。若设置,则连续调用uv_timer_stop和uv_timer_start,重启Timer。
Timer事件的回调触发,在loop的uv__run_timers阶段执行,从堆顶取出过期节点,并调用对应的回调函数,同时根据需要重启Timer。
至此,对libuv库中的Timer事件处理有了全面的了解,下期将深入探讨async事件的处理机制。
FREE SOLO - 自己动手实现Raft - - leveldb源码分析与调试-1
leveldb 是由 Google 基础架构工程师 Jeff Dean 所设计的,是一种高效、可靠的键值对存储系统。它基于LSM(Log-Structured Merge)存储引擎,代码简洁精炼,非常适合深入学习与理解。leveldb 不仅可以作为一个简单的键值对引擎使用,而且内部组件如LRU Cache也具有独立的实用性,还能在此基础上封装出其他操作接口,例如vraft中的raftlog和metadata等。
通过理解leveldb,能够对后续学习如rocksdb等更高级的数据库引擎提供坚实基础。本文旨在从状态机的角度解析leveldb,帮助读者深入理解其内部工作原理。
在leveldb中,关键状态包括但不限于内存、磁盘状态以及LRU Cache状态。内存数据与磁盘数据的交互是leveldb的核心,用户的键值对数据通过日志写入到memtable,然后通过immutable memtable最终到达磁盘上的sorted table文件,这些文件按照级别(level)从0到6逐级存储。通过在关键时刻添加ToJson函数,可以记录这些状态的变化,便于分析。
LRU Cache在leveldb中的实现同样值得深入研究。它作为一种缓存机制,有助于优化数据访问效率。通过在LRU Cache中添加ToJson函数并打印状态,可以直观地观察其内部结构和状态的动态变化。
为了更好地理解leveldb,本文将重点分析关键数据结构,并通过观察不同动作导致的状态变化,来深入探究leveldb的内部机制。在后续文章中,将详细展示leveldb内部状态的转换过程,以帮助读者掌握其核心工作原理。
FREE SOLO - 自己动手实现Raft - - leveldb源码分析与调试-3
leveldb的数据流动路径是单向的,从内存中的memtable流向不可变的memtable,最终写入到磁盘上的sorted table文件中。以下是几个关键状态的分析,来了解内存和磁盘上数据的分布。
以下是分析所涉及的状态:
1. 数据全在内存中
随机写入条数据,观察到数据全部存储在memtable中,此时还没有进行compaction操作。
2. 数据全在磁盘中
写入大量数据,并等待数据完全落盘后重启leveldb。此时,数据全部存储在磁盘中,分布在不同的level中。在每个level的sstable文件中,可以看到key的最大值与最小值。
3. 数据部分在内存中,部分在磁盘中
随机写入条数据,发现内存中的memtable已满,触发compaction操作,数据开始写入到sstable文件。同时,继续写入的数据由于还未达到memtable上限,仍然保存在内存中。
4. 总结
通过观察不同数据写入量导致的数据在内存与磁盘间的流动,我们可以看到leveldb内部状态的转换。
下篇文章将分析LRUCache数据状态的变化。敬请期待!
TiKV 源码解析系列文章(十七)raftstore 概览
TiKV,作为分布式 KV 数据库,利用 Raft 算法提供强一致性,但单一 Raft 组无法满足扩展性和均衡需求,因此引入了 MultiRaft 架构。在 TiKV 中,数据通过分片形成多个 Region,每个 Region 由一个 Raft 组管理,形成一对一关系。通过多 Raft 组并行管理,实现高效扩展和均衡。
MultiRaft 与 Region 结构紧密相连,数据在多个副本间分布,一个机器可能承载多个不同 Region 的副本。这种设计允许 Raft 组并行工作,从而提升性能和容错能力。
Batch System 是 raftstore 的核心机制,用于并发驱动状态机。状态机通过 PollHandler 驱动,分为 normal 和 control 两种类型。control 状态机负责全局任务管理,normal 状态机处理特定任务。消息和消息队列绑定在状态机上,PollHandler 负责消费消息,产生落盘或网络交互的副作用。
raftstore 中包含 RaftBatchSystem 和 ApplyBatchSystem 两个 Batch System。RaftBatchSystem 处理 Raft 状态机,包括日志分发、落盘、状态迁移等。ApplyBatchSystem 解析日志并应用到底层 KV 数据库,执行回调函数。写操作遵循此流程,客户端请求序列化为日志后,通过 Raft 提交到 raft。Ready 机制收集副作用,最终由 Batch System 处理。
Region 的分裂和合并是 TiKV 稳定运行的关键。Split 将大范围数据分割,创建新 Raft 组管理;Merge 则合并相邻 Raft 组,优化资源利用。这些操作遵循 Raft 提交/确认流程,并维护版本概念,确保写命令正确分发。
LocalReader 为读操作提供优化,Raft 组 leader 维护 lease 机制,确保在有效期内的读操作即时执行,超出则触发续期。Lease 定义了读操作的时间窗口,允许精度误差,优化性能。
Coprocessor 用于自定义 KV 处理逻辑,如事务一致性、关键数据管理等。TiKV 中包括 SQL 下推、Observer 等 Coprocessor,监听事件并执行自定义逻辑,保证系统正确运行。
综上所述,TiKV 通过 MultiRaft、Batch System、LocalReader 和 Coprocessor 等机制,实现了高效、可靠的分布式 KV 存储。深入理解这些组件的原理与实现细节,有助于优化 TiKV 应用场景与性能。