分布式 —— 基于Raft算法的KV服务

第一章:

CAP定理:

一致性:同步问题

可用性:高并发、性能高

分区容错性:选择备用

注意:实际情况中,这三个不能同时具备,往往会舍弃一个属性,一般分区容错性是优先考虑的,就只能从可用性和一致性中进行取舍。

Master -- Salve系统(同步、日志复制的时候)

Leader -- Candidate -- Follower (选举的时候称呼)

共识:就是所有节点都达成一致的意见,可以理解为“强一致性”

共识算法:Paxos算法、Raft算法、ZAB算法

分布式优点:

  1. 可以承受高请求量

  2. 防止系统宕机

  3. 提高系统可用性

第二章:

Raft算法的核心:选举和日志复制

选举:实现分区容错性,就是在一台服务器宕机后,选举出新的服务器

日志复制:实现一致性,把日志同步到其他节点中。

Raft必备功能:集群成员管理、日志快照

Raft其他:客户端,其他优化

日志状态机模型:

解决同步操作,数据完整性,发送顺序不一致问题

组成:

1、日志索引

2、操作(同步操作)

3、当前状态

解决问题:将分布式一致性问题转变为“各节点如何顺序写入的问题”

注意:因为他同步的是操作,所以可以通过当前操作找到对应的状态 同步状态也可以,但是没有同步操作快,和便捷。

Quorum机制:

过半写入机制,保证了数据的最新,变更,解决数据延迟产生的问题

作用: 主要通过过半 写入机制,保证了写入的数据都是有效数据,保证了最新性和正确性。

核心: 保证过半的节点是最新数据,主服务器master才会继续往下写入

运行机制: 各节点通过比较各自持有的日志来决定谁成为最新的Leader节点。

注意: 理论上,这个拥有最新数据的节点应该有过半节点的支持

基于日志比较的投票:

就是各节点比较自己持有的日志,然后别人大就投票给别人,自己大就投票给自己

投票原则:

无重复投票:(意思是不能给一个人投两票,并没规定一个节点不能投两票,并不是绝对的公平)

一节点一票制:(避免了一节点多票数,基本满足了公平选举)

拓展:WARO机制(所有的副本都更新完成后,这次写入才算成功,否则失败)

WAL:预写式技术,就是把日志存的东西落实下去,提供原子性和持久性

Raft算法的选举:

投票消息(Candidate)

增加条目消息 (appendEntries)

心跳消息 (follower发出请求获取Leader的心跳)

选举实现流程:

1、Candidate发送请求投票的消息,并附带自己的逻辑时钟,也就是term版本,

2、节点收到请求投票的消息的时候,比较自己本地的term,如果别人大就头票给别人,自己大就投给自己,

3、过半支持,成功写入,最终选出Leader节点。

注意:如果出现split vote,那么就需要再发起一次选举,

选举整体流程:

1、所有节点初始为Follower 节点

2、Leader心跳超时,就是没有心跳,Follower节点自动升级为Candidate 节点(自荐)

3、然后Candidate发起requestvote,经过第一轮选举,投票选出Leader

注意:这里选举规则是Candidate 和 Leader 在收到term 比 自己本地的term大的消息后,就会自己退化为Follower节点(候选者降级为追随者)。

4、要是出现选举超时情况(票数对半 "split vote分割选举"),就再次进入下一轮选 举,

5、出现Leader节点后,Leader节点会发送心跳消息给其他节点,代表选举结束

6、其他节点接收到心跳消息后就会重置tram自己的计时器,并保持Follower角色。

注意:只要Leader持续不断发送有心跳消息,Follower节点就不变成Candidate节点并发起选举。

选举中的角色迁移

1、当Leader宕机后,Candidate节点升级为Leader。

2、当Leader和Candidate发现有比自己节点大的时候,自动降级为Follower

3、当Leader宕机,Follower升级为Candidate

注意:所有的节点初始为Follower节点。 正常情况下,只有Leader和Follower节点,只有宕机了才有Candidate节点

日志条目:

每一个操作都会有一个日志条目,所有来自客户端的变更和请求都会被当作一个日志条目追加到节点日志中

日志条目分类:

已追加但未持久化(就是已经添加的任务,但是没有完成)

已经持久化(已完成)

日志复制:

就是为了同步操作 为了跟踪各节点之间的复制进度,Leader会纪录各节点的:

 nextIndex(下一个需要日志复制的条目索引)和

 mathchIndex(已经匹配的日志索引)

日志复制流程:

Leader节点收到客户端数据变更请求,

Leader追加日志,然后发送给Follower节点进行日志复制,AppendEntries

Follower节点要是完成后,那么就进行回复

回复成功后,Leader执行日志持久化,

然后Leader节点给各Follower节点发送日志持久化AppendEntries

Follower节点持久化成功后,那么就进行回复

Leader进行计算结果, 然后Leader回复结果给客户端,并且推进commitIndex,

第三章:

Raft算法重点:

核心功能:选举 日志复制

日志快照

集群成员管理(成员表)

客户端

其他优化

状态数据分析:

持久化数据

        1.当前选举的term 2.投过票给谁 3.log日志条目

日志索引可变状态数据 (Leader服务器)

        1.已经提交的最高索引commitIndex 2.已经应用的最高索引 lastApplied

宕机选举可变状态数据 (非Leader服务器)

        1.当前的Leader服务器id 2.投票票数,给谁投票了

Leader主服务器可变状态数据

        1.下一个要复制的日志索引nextIndex 2.和主服务器相匹配的日志索引

服务器id的选择实现代码:(重点 )

注意:创建NodeId的时候建议使用String类型,因为怕长度太长,序列化不成功 这里实现了Serializable接口,这个接口可以让结构化数据转换为二进制数据表示。

public class NodeId implements  Serializable{
    private final String value;//string类型是为了避免序列化的时候长度不够
    //构造函数  (存Id,实例化)
    public NodeId(String value){
         //创建构造函数的时候赋值
         this.value = value;    
    }
    public static NodeId of(String value){ //创建id并返回id  
        return new NodeId(value);
    }
    //重新equals方法,与hashCode一起重载
    @Override
    public boolean equals(Object o){ 
        if(this == o){
              return true;
        }
        if(!(o  instanceof NodeId)){
              return false;
        }
        NodeId id = (NodeId) o;
        return Object.equals(value,id.value);
    }
    //重写hashCode方法,获得哈希吗
    public int hashCode(){
        return Objects.hash(value);
    }
    //重写toString方法
    public String toString(){
        return this.value;
    }
    //获得内部类的标识符
    public String getValue(){
        return value;
    }
}

/* 创建此id方式:

方式1:new NodeId("值") 构造方法实例化

方式2:NodeId.of("值") 调用方法

这两个方法上面都有定义过 */

集群成员列表:

集中管理,为了更好的管理集群里面的成员节点,并且通过指定的NodeId映射管理到集群成员,也可以遍历集群成员。

集群成员与映射表设计:

类:NodeGroup(集群成员表)、GroupMember(集群成员)、NodeEndpoint(详细信息表) 集群成员表核心:

就是map集合,通过NodeId(key) ---> 管理GroupMember(value) (图片在同级目录中)

//集群成员表  NodeGroup
public class NodeGroup{
    private final NodeId selfId;   //当前节点的id
    private Map<NodeId,GroupMamber>  memberMap; //成员表
    //单节点构造函数
    
    //多节点构造函数
    
    
    
}
//成员表
public class GroupMember{
    //成员详细信息表
    private final NodeEndpoint  endpoint;//详细信息
    //复制进度
    private ReplicatingState replicatingState;
    
    
    //构造函数1 (只有成员详细信息构造函数)
    public GroupMember(NodeEndpoint endpoint){
        this(endpoint,null);
    }
    //构造函数2  (有日志复制状态 + 成员节点详细信息)
    GroupMember(NodeEndpoint endpoint,ReplicatingState  replicatingState){
        this.endponit  = endpoint;
        this.replicatingState = replicatingState;
    }
    //get、set方法
    
}
​
//成员详细信息表
public class NodeEndpoint{
    private final NodeId id;  //节点id
    private final Address address;  //host + port  端口 + 地址
    
    //构造函数
    
    //内部类Address
    public calss Address{ 
        private  final String host;
        private  final String port;
        //get、set方法
        
    }
}
​
注意:这样我们就可以通过NodeGroup中的Map中Nodeid,<Nodeid,GroupMenber>,映射到GroupMember表,然后通过GroupMember中的NodeEndPoint又是组合关系,所以我们又能查看到成员的详细信息表。
​
优点:方便了集群成员的管理,map的键值对映射十分灵活。

组件分析:

定时器组件、日志组件、一致性算法组件、成员表组件、通信组件

组件依赖问题:”一致性关联四大组件,核心组件依赖四大组件“

解决方法:把核心组件的依赖都放到另一个类中,让核心组件通过访问这个类,间接访问其他组件的引用,也就是中间添加一个间接层类。

如何解耦组件间的双向调用关系:

耦合关系:高内聚,低耦合

解决方法4:使用Pub-Sub,增加中间件,中间系统,中间类,让A -B之间的强依赖变为弱依赖, 解决方法5:延迟设置

多模块maven工程:

Maven:项目对象模型,可以用来管理和构建java项目的一些依赖,

Maven的运行机制:

自己下载依赖 ---> 应用的时候先在本地仓库寻找 ---> 如果没有的话,去中央仓库寻找和下载。

第四章

角色建模

前言:如果每个角色变化的时候,一个一个去修改相关数据,很容易出错,所以。。。

角色建模就是为每个角色建模(Leader、Candidate、Follower),抽取相同的相同的属性,然后作为父类数据,然后给每个角色创建单独的子类。

角色分类:

        Leader角色

        Follower角色

        Candidate角色

为了避免在角色变化的时候数据设置的出错率可以采用以下方法:

1、把角色变化的修改全部放在一个方法中,

2、为每一个角色建模,即抽取相同的字段作为父类数据,然后为每一个角色单独创建一个子类。 代码实现:

abstract class AbstractNodeRole{
    private  final  RoleName name ; //角色名称
    producted final int term; //版本信息
​
}
//注意:这里的RoleName是自定义的枚举类,
public enum RoleName{ //角色名称枚举类
    FOLLOWER,
    CANDIDATE,
    LEADER,
}

定时器组件:选举超时定时器 + 日志复制定时器

消息建模:

RequestVote(请求投票) + AppendEntries(追加条目)

注意:这两种消息都需要有一个响应,所以实际的类应该是再加上两个响应类Result,

1. RequestVote()
​
2. RequestVoteResult()
​
3. AppendEntries() 
​
4. AppendEntriesResult()

第五章

日志实现分析:

1.日志条目的内容

普通日志 + 空日志NO_OP

一个直接区分日志类型的方法就是给日志加上KIND类型

类型:

KIND_NO_OP = 0; (空日志)

KIND_CENERAL = 1;(普通日志)

2.日志存储的选型 使用什么介质存储日志也是一个技术选型的问题

主要类型:

1、文件型

2、内嵌式KV数据库,最终还是存在磁盘中(例如QQ相册)

3、内嵌式RDBMS数据库,普通关系型数据库

3.日志条目的序列化

就是把日志条目进行序列化,才能进行网络上的传输和具体写入文件中,常见的序列化方式有结构化文本和二进制两种。

4.日志接口

日志接口就是把日志实现的一些功能放在一个接口里面定义好具体的功能,然后让别的类实现接口,重新里面的方法就好,这样方便了管理

主要功能:
1、getLastEntryMeta() 获取最后一条日志条目的term,index等信息。
2、createAppendENtriesRpc()  创建日志复制消息
3、appendEntry()  增加日志条目(又分为空日志 +普通日志)
4、appendEntriesFormLeader()  追加从Leader服务器过来的日志条目序列
5、advanceCommitIndex()  推进commitIndex,提交和应用

代码实现:

public interface Log{
    int ALL_ENTRIES = -1;  //当前没有日志
    //获取最后一条数据的原信息
    EntryMeta getLastEntryMeta();
​
    //创建日志复制的消息
    AppendEntryRpc createAppendEntriesRpc(int term,NodeId selfId,int nextIndex,int maxEntries);
​
    //增加空日志条目
    NoOpEntry appendEntry(int term);
    //增加普通日志条目
    GeneralEntry  appendEntry(int term,byte[] command);
​
    //追加来自Leader的日志条目
    boolean appendEntriesFromLeader(int prevLogIndex, int prevLogTerm,List<Entry> entries);
​
    //推进commitIndex
    void advanceCommitIndex(int  newCommitIndex,int currentTrem);
​
    //关闭
    void close();
}

5.日志元信息:

        下标:Index

        版本:term

        类型:Kind

6.消息去向:

收到RequestVote消息: 是Candidate发送请求,Follower收到投票请求,

发送AppendEntries消息: 是Leader发送给Follower的,一般里面有日支复制请求和Leader自己的心跳消息、

收到AppendEntries消息:是Follower收到的,会进行日志复制和确认心跳消息

收到AppendEntries响应:当Follower日志复制完成后,返回成功的消息给Leader,Leader计算结果和推进CommitIndex。

第六章:

通信实现分析:

        1、网络协议

        2、数据的传输(序列、反序列化)

        3、通信组件的选择

        4、应用实现

阻塞式io 和 非阻塞式io:

传统IO:同步,需等待,好比单线程,用户交出CPU控制权,等待数据就绪

非阻塞式IO:异步读写,速度很快,用户线程能在不同的线程之间来回跳转,无需等待,性能很高,但是容易造成线程安全问题。

TCP和UDP区别:

Tcp:是面向连接,基于流的网络传输协议,保证了数据的完整性,丢包会重传,但是速度没有UDP快,

UDP:是一种无连接,基于数据包的不太可靠的网络传输协议,数据不完整性,丢包不会重传,不存在沾包丢包的情况,速度快。类似广播,大小有限制

 

TCP三次握手:

        客户端发起请求

        服务端回传确认

        客户端回传确认

Nio和Netty:

Nio:非阻塞式io,无序等待,可以实现高性能的多线程程序,但是学习成本高,难度较大,比较复杂,要很好的掌握锁机制,和同步机制。

Netty:是一个网络应用框架,用于开发网络应用程序,它具有异步处理,事件驱动,还提供nio,但是他相对简单,学习成本低,Api简单,社区活跃,功能还很强大。

第七章:

基于Raft算法的KV服务设计图:

KV服务功能:

查询:GetCommand
​
查询结果响应:GetCommandResponse
​
修改请求:SetCommand
​
响应是否成功:Success
​
异常处理:Failure
​
请求重定向:Redirect
​
增加:
​
删除:

请求转发和请求重定向

重定向:跨域,多次请求,域数据会丢失,实际情况一般是,当前服务器解决不了的问题,然后交给其他服务器节点解决,。

请求转发:当前域,一次请求,域数据可保留,服务器地址不变,还是内部服务器进行转发。

KV组件处理模型两部分:

1、命令作为日志追加回调的处理:异步单线程处理模型

2、KV服务的通信部分到KV服务的处理:分层调用模型,直接多线程调用

注意:异步必须使用回调函数,这样才能保证客户端发送的命令一定会回复结果给客户端。所以我们可以把回复结果写在异步函数里面。

命令的序列化和反序列化:

命令的序列化:KV客户端和服务端之间的通信

命令的反序列化:命令到日志中二进制部分的转换

注意:结构化文本会比二进制相对复杂,二进制会比结构化文本效率高、

组件依赖

第八章

基于命令行的客户端:

        Dos命令窗口,Git命令窗口等

命令列表

客户端命令使用前缀来归类

        以kvstore开头是KV服务相关命令

        以client开头是服务器集群列表相关的命令

        exit是windows自带dos命令

kvstore-get KV服务的GET命令,

kvstore-set KV服务的SET命令

client-add-server 添加节点

client-remove-server 移除节点

client-list-server 列出所有节点

client-set-leader 手动设置Leader节点

exit 退出命令行

注意:这边的移除节点并不会移除系统中的数据,因为这是分布式的系统,别的服务器节点上面还有备份的,但是通过key - value 方式移除的数据,那么就是真的没有了,因为节点之间的操作会同步。

客户端必备特征:

         交互式

        自动处理重定向

        自动同步集群成员或者支持手动修改。

单机模式和集群模式

单机模式:只有一台服务器组成,无需通信,无需选举和日志复制,服务器默认端口是2333.

集群模式:有多台服务器组成,需要通信保持数据的同步,有选举Leader和日志复制,服务器端口是3333

单机模式下的修改:

单机模式下的选举:选举超时后立马设置自己为Leader节点。

单机模式下的日志复制:单机模式下不需要进行日志,但是还需要推进commitIndex。

单机模式下的启动:不需要进行集群成员解析。

单机模式下的测试:测试中遇到了问题,建议先使用单机模式。

第九章

日志快照:就是快速把日志数据保存到内存中,并删除旧的日志。

注意:

在集群模式下,只有leader才能生成日志快照,然后其他节点去同步。

但是在Raft算法中,并没有规定非要谁来生成日志快照,只要满足日志快照生成条件的节点都能生成。

第十章

集群成员的变更:

对于基于集群的服务来说,难免会因为某些原因需要增加或者移除服务器,

最简单的方法:

就是停止所有服务器,修改集群成员配置,然后重启集群。

注意:但是这样的方法难免会有一段时间内不能提供服务,必须要仔细选择维护的时间。

所以为了减少无法提供服务的时间,以及夜间维护的次数,Raft算法给出了安全的集群成员变更方法 ---- 单服务器变更,这里的安全是指不会出现多个Leader节点的情况。

单节点变更的具体流程是:

  1. 节点 D 向领导者申请加入集群;
  2. 领导者 A 向新节点 D 同步数据;
  3. 领导者 A 将新配置 [A、B、C、D] 作为一个日志项,复制到配置中的所有节点,然后应用新的配置项(这里需要注意的是,每个节点接收到新的配置项肯定是有时差的);
  4. 如果新的日志项应用成功(被大多数节点复制成功),那么新节点添加成功。

单服务器变更:

第十一章

Raft优化

启动优化 、维护优化

拓展:

上下文和命令上下文:

上下文:就是一个操作的前置条件和后置条件,

命令上下文:包含集群成员列表和KV客户端实际命令的实现,就是上下文聊天记录类似

注意:通信超时和更新延迟都是属于一致性问题。

分布式共识算法:Raft、Paxos、ZAB算法

作用:保证所有参与者达成共识,可以理解为强一致性。

Paxos角色:

提出决策者

参与决策者

不参与决策者

Paxos算法运行机制:

不定时的情况下达成共识

提议者:向接受者提出意见

接收者:接收意见,并表达自己的意见

运行机制:在提议者中选出一个领袖,以领袖的意见为准,如果说大多数的接收者都认可这个提议,那么就通过

选举问题:

选举谁为最新的服务器? Leader服务器。

选举的时候数据写入不一致怎么办?日志状态机模型 + Quorum过半写入机制。

集群和分布式:

集群:是一种物理形态,其本质是将很多的计算机集合在一起进行工作,

分布式:是一种工作思想,是一种工作方式,其本质是将业务分布在不同的地方进行工作。

注意:分布式中的每一个节点都能做集群,但是集群就不一定是分布式的。

非关系型数据库:Redis、MongDB、Memcached、

KV数据库和关系型数据的区别:

作用不同:

关系型数据库一般用作储存数据

KV数据库一般用作缓存,提高数据库系统的速度

储存方式不同:

关系型数据库一般利用行和列表格化方式储存数据;

非关系型数据库一般通过数据块,数据集合,通过键值对的方式进行存储。

实现中,需关注日志条目类型有几种,存储引擎能选几种?

日志条目类型:普通日志条目,空日志条目

存储引擎:

        本地文件

        内嵌式KV数据库

        内嵌关系型数据库

日志接口中五个重要的方法有哪几个?

  1. 获取最后一条日志条目的trem,index等信息    getlastEntryMeta()
  2. 创建日志复制消息  createAppendEntriesRpc()
  3. 增加日志条目  AppendEntry()
  4. 追击从Leader服务器上过来的日志条目序列    AppendEntriesFromLeader()
  5. 推进commitIndex    advancecommitIndex()

日志序列的作用是什么?

日志序列的作用就是方便日后相关的日志优化操作,便于日志快照,把日志序列的操作 抽取出来,提前做好日志组件的拆分,这样以后就能快速的加入日志快照。

什么是日志条目序列化?

日志条目序列化:就是日志条目进行写入文件之前,需要先将其序列化,转换为计算机二进制。

Raft算法所需的日志系统和数据库WAL有什么区别?

区别:

WAL是预写式日志技术,提供原子性和持久性,就是他会把日志存的东西落实下 来,只会追加日志的系统。

Raft算法所需要的系统是,需要能考虑到在运行中写入的日志可能会因为冲突而被 丢弃或者说被覆盖,然后这样的日志是不能写入的。

master/slave 和 leader/follower的区别:

他们两个都是主从模型,本质上没有任何区别,

因为master/slave 的意思是主人和奴隶,有一点人道上的不尊重,所以改名为leader/follower ,这种模型的意思是:一个主要服务器,多个从属服务器。

  • 4
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

芒果果。

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值