分布式存储入门,看这篇就够了

本文结合工程实践详细阐述分布式一致性协议raft。如果你有一定的编程实践经验,并对分布式相关原理感兴趣,那么希望能有所收获。

什么是分布式

简单点说,分布式(distributed)是指一组机器分工协作一同对外提供服务。在提高服务吞吐量的同时提升服务容灾等级。类比于公司运作,业务发展早期1个人能承担所有。随着业务发展壮大,24h连轴转也忙不过来,这时候就需要招聘员工,满足业务发展需要,也就是分布式的提高吞吐量。同时我们不可能按照刚性需求进行招聘,如个别员工生病请假诸如此类,公司就不能正常运转了。所以需要提供一定的冗余,适当多招聘一点人,这也就是分布式的容灾。

做完类比,大家可能会认为就应该这样啊!难道程序服务不应该就是这样么?分布式为什么会显得如此耀眼夺目?呃,其实就真实世界来说,在很多场景下也不需要多台服务器一同协作提供服务。尤其在互联网发展早期,用户规模有限。相关硬件技术一直在发展,机器的性能越来越好。直到用户规模彻底压垮单机发展曲线,分布式的意义才彻底突显出来。

单机有单机的好处,更好的性能,更低的维护成本。但是也有弊端,高昂的硬件成本(听说oracle搞出来数T内存的数据库),脆弱的容灾,糟糕的横向扩展性。 分布式也有分布式的优缺点,复杂难理解的系统,低效冗余的协议设计。但是分布式通常来说,能够有更好的横向扩展性,更好的容灾。

最后总结下,单机相当于六边形精英战士,分布式则是平庸的人通过一套规则,密切协作合力完成mission impossible。

如何学习分布式

首先对这里的分布式做下限定,特指分布式存储。一组http服务器,通过api网关对外提供服务,也可以认为是分布式(多人通力协作提供服务)。这里感叹下老外玩概念的能力。。。(什么时候我们也能走在前沿,搞出一堆被全世界认可的概念名词啊)千万别被分布式这个名词唬住了。

什么是分布式存储,按照分布式的核心定义来理解,就是一台机器已经不能满足用户规模与容灾需要了,需要多台机器同时提供数据存储的服务。

为了解决容灾问题,我们需要将一份数据写到多台机器,这样在其中一台机器宕机时,其他机器才能有全量数据以供读取。但是这里存在着诸多现实问题。客户端知晓多台服务器的存在,若每次发起数据写入操作,均由客户端管理控制。如果某台机器的数据写入失败了怎么办?机器宕机持续无法写入怎么办?机器重新上线后,数据如何读取,如何恢复?若数据的复制与维护由服务器自动进行,又如何复制与维护数据?

为了解决一份数据,多次写入而产生的数据一致性问题,才有了经典的paxos协议。以及为提高可读性降低工程实践难度的raft协议。paxos协议据说很难理解,而我也不是专门做分布式领域研发的工程师,只是出于好奇心,初窥门径即可,没必要给自己上莫名其妙的难度。后续将以论述raft为主。

raft的相关学习资源在网络上应该存在不少。从简单概念上手(可以看下gif动画),到具体协议实现,依旧少了些许工程实践相关的直观感受。我个人在完成raft论文的精读后,又研究了协议实现的开源项目代码etcd

至于分布式存储在解决容量瓶颈方面,其实在解决了存储多副本一致性问题后,显得特别简单,只是增加了一些工程复杂度而已。本来想画个图的,但是文字描述貌似也可以。以简单的key value来说,比如说我们将服务器3台分为一组,共计3组服务器,每次写入数据,我们根据写入的具体的key,通过一定的路由计算,固定的分配到某一组(当然回头读的时候也通过同样的路由,在同一组进行读取),并对组内每台服务器完成同样的写操作。这样我们不光解决了容量瓶颈问题,也实现了容灾。当然,具体的工程实现会有很多的细节变动与调整优化,但是大方向如此。

由此,我们将的精力放置到分布式一致性协议raft上。

分布式一致性协议

大家可以在阅读完本文后,再完整阅读下raft论文。本文仅对论文核心的部分,尝试添加一些工程实现细节,以降低论文阅读难度。

名词说明

可以先跳过或简略阅读,在后文阅读过程中如果遇到不解的地方再回头查找

  1. 服务节点:后文简称节点,由ip+端口定位一个节点,节点可以部署在一台服务器上(用于开发测试),也可以部署在多台机器上(用于生成环境),也可以跨机房进行部署。节点通常也是一个正在运行的服务进程。
  2. 集群:若干服务节点通过raft协议整合在一起,一同协作对外提供服务。你可以认为它本质上就是一个商业文明中的公司而已。
  3. 主节点:在raft协议中,服务节点分为主节点和从节点,主节点只有一个,从节点有多个。主节点用于协调数据复制,判定数据修改是否可以提交,并向客户端返回执行结果。
  4. 从节点:被动接收来自主节点的数据副本复制请求与执行请求
  5. 选举周期:后简称term,要解释term会有点麻烦,这是一个核心概念,初期可以先适当跳过,不用过多纠结。这个概念既有本地视角,也有全局视角。本地视角是,每个节点初识term为0,若收到term大于本地term的请求,则将本地term设定更新为这个大的term(最新的term)。term将影响到数据复制,集群选主,数据提交。全局的term,即集群当前主节点的term。
  6. 数据字典:key value结构下,用字典显得更容易理解一些。无非是使用一个值查询这个值相关的信息。raft论文中被称为状态机(state machine),结合raft协议的工作原理也很为形象。每来一个修改,就将原来的数据修改一次,有的时候又会改回去,状态被来回切换。
  7. WAL:Write-Ahead-Logging,我们并不是在收到数据字典修改请求后,立即对字典进行修改。而是将请求本身,进行编号(index)存储。index从0递增,在raft协议通过确认后,再对相关操作进行执行。是否可以提交执行的初始index,在raft中被称为commitIndex。WAL是理解raft协议的关键点之一。WAL有着诸多妙用,在此先不展开说明
  8. log:有了WAL之后,log这个名词也很贴合实际。我们将每次对数据修改的请求,像日志一样一笔一笔的记录下来,以方便后续实际执行与日志比对。raft并不关心实际的数据修改请求。后文中我们将数据修改请求统称为log
  9. 客户端:数据修改操作的发起端

核心流程

相关细节将在后文做进一步详细说明,大家先建立初步全局认识即可

  1. 设定存在S1,S2,S3,S4,S5 5个节点。3,5个为常见场景,raft论文以5个节点为例进行讲解,在这种场景下,可以允许2个节点失联(宕机或机房故障)
  2. 将通过raft选主机制(后文阐述),选出一个节点作为主节点,主节点将主导数据的复制过程(复制过程后议)
  3. 客户端将存储5个节点ip与端口信息,客户端将随机挑选一个节点进行通信,发出数据写操作(数据读操作不在本文讨论范围内,且读操作相对更为简单)
  4. 若选中的通信节点为主节点,则主节点将分发请求至各从节点,从节点在将请求写入WAL后回复主节点。在得到超过半数从节点回复确认后,即可参照请求对数据字典进行实际修改,并回复客户端数据已经写入;若选中通信的节点为从节点,则从节点将写操作转发至主节点并等待主节点回复。etcd是如此实现,当然也可以有很多不同的实现,本文只是引导介绍其中一种实现,先带大家入门而已
  5. 主节点与从节点之间使用心跳机制进行通信,若在设定的超时时间过后,仍然没有收到主节点心跳,从节点将发起选主,以重新提供服务。

初始选主

每个节点均存储的数据 a.term 选举周期数 b.本轮term投票给的节点

  1. 初始状态下每个节点的term为0,节点默认状态为从节点。
  2. 节点在启动后随机一个心跳超时时间,在这个时间过后,若没有收到来自主节点的心跳信息,则将自身状态变更为候选者,并向集群内所有节点发出投票请求。
  3. 节点在收到投票请求后,若请求的term小于自己的term则拒绝。若请求的term等于自己的term,但是自己已经投票则拒绝。若未投票,则投票。若请求的term大于当前term,则直接将本地的term设置为请求的term,并投票给对方。
  4. 1个节点1个term只能投一次票。
  5. 候选人若收集到超过半数的选票,则切换自身状态为主节点,并向其他节点发出心跳。
  6. 任何节点若收到term大于等于自己term的心跳请求则将自己切换为从节点。

从这个选举过程来看,可以看出最终只有一个节点会成为主节点,且通过随机的超时时间,错开了发起选举的时间,从而尽可能避免选举冲突。

数据复制

  1. log index:节点在收到数据修改请求后,将请求打包存储在一个index依次递增的数组中(实际也就是WAL)
  2. 后5行代表主节点、从节点的WAL
  3. 后5行内每行的方块内有两行,上一行代表数据修改请求发生时主节点的term。下一行代表实际的数据修改请求的请求内容。如 x<-3 代表将x设置为3。

主节点在收到数据修改请求后,向从节点发出请求,同步数据修改信息。从节点在收到信息并实际写入到磁盘后,回复主节点。主节点在收到半数(不含半数)以上的节点回复后即可写入磁盘数据修改数据字典,并回复客户端。已经回复客户端的修改请求,可以保证在集群中持续生效(哪怕主节点宕机)。这主要依赖于raft协议的选主规则,这里也是raft协议,我个人认为最巧妙和精巧的地方

这个规则就是,节点在收到投票请求时,如果对方的WAL末尾log内的term,比自己的小则不给投票。如果term等于自己的,而log index小于自己的,也不投票。而选举需要成功,需要拿到的票数需要超过一半,这恰好和日志复制提交需要拿到的响应节点数一致。这样也就保证了选举出的节点必定含有最新的,超过半数节点均已写入的log。

宕机选主

与初始状态下描述的选主过程大致相同,但是在进行一段时间的复制后,各节点上的WAL已经有了不同的数据,而这些数据会决定节点是否能被选举为主节点,也就是得到其他节点的投票

一点细节

在这个图中 S1-S5代表5个集群节点。第一行的 1,2,3 代表各节点的log index。最后一行的(a)(b)..(e)代表了各个时间节点(d、e为虚构),时间顺序从前到后。各染色的方框代表一次数据修改操作。方框内的数字表示此次修改发生的term。方块边缘黑色加粗代表其对应的节点当前为主节点

  1. a时刻:当前主节点为S1,发生一次数据写操作,但是操作仅同步给到S2,S1就宕机了
  2. b时刻:通过选主,S5胜出(按照规则它可以收到 S3 S4 S5的选票),这时来了一次新的数据修改操作,这次操作只在S5的WAL内记录,但是没有同步给到其他任何节点
  3. c时刻:S5宕机,通过选举S1再次胜出。在S1胜出后,S1开启日志数据复制,将方框内为2的日志传播给到S3。同时再来了一次新的写操作(方框内数字为4),此时集群的term已经来到了4。这里的问题是,如果数据2成功复制到了集群的多数节点,数据2是否可以提交?答案是不能,让我们考虑下下面的场景
  4. d时刻:S1再次宕机,S5发起了选举操作并最终胜出(获得S5 S4 S3三张选票),这时,S5可以将方框3代表的数据修改操作,同步至其他节点。从而这里就违背了在c中,方框2已经提交的事实
  5. e时刻:那在c时刻的方框2代表的数据修改操作,需要在什么条件才能提交了(修改数据字典)?需要其为主节点阶段,新的数据修改操作,复制超过半数节点,此时,这个操作及之前的操作才可以统一提交

小结

还有诸多raft细节并未在本文披露,比如集群新增或移除节点,WAL压缩等。有兴趣可以查看论文,阅读etcd源码。本文仅阐述了一种分布式一致性的实现方式,实践中诸多细节可以结合具体的业务场景进行调整或优化,本文仅用于入门之用。

相关推荐
Estar.Lee2 小时前
查手机号归属地免费API接口教程
android·网络·后端·网络协议·tcp/ip·oneapi
2401_857610034 小时前
SpringBoot社团管理:安全与维护
spring boot·后端·安全
凌冰_4 小时前
IDEA2023 SpringBoot整合MyBatis(三)
spring boot·后端·mybatis
码农飞飞5 小时前
深入理解Rust的模式匹配
开发语言·后端·rust·模式匹配·解构·结构体和枚举
一个小坑货5 小时前
Rust 的简介
开发语言·后端·rust
monkey_meng5 小时前
【遵守孤儿规则的External trait pattern】
开发语言·后端·rust
Estar.Lee5 小时前
时间操作[计算时间差]免费API接口教程
android·网络·后端·网络协议·tcp/ip
新知图书6 小时前
Rust编程与项目实战-模块std::thread(之一)
开发语言·后端·rust
盛夏绽放6 小时前
Node.js 和 Socket.IO 实现实时通信
前端·后端·websocket·node.js
Ares-Wang7 小时前
Asp.net Core Hosted Service(托管服务) Timer (定时任务)
后端·asp.net