一、分布式系统定义:
《分布式系统概念与设计》一书中对分布式系统的概念定义如下:
分布式系统是一个硬件或者软件组件分布在不同的网络计算机上,彼此之间仅仅通过消息传递进行通信和协调的系统。
分布式系统的设计目标一般包括:
可用性:可用性是分布式系统的核心需求,其用于衡量一个分布式系统持续对外提供服务的能力。
可扩展性:增加机器后不会改变或极少改变系统行为,并且能获得近似线性的性能提升。
容错性:系统发生错误时,具有对错误进行规避以及从错误中恢复的能力。
性能:对外服务的延时响应和吞吐率要能满足客户的需求。
分布式架构集群存在的问题:节点之间的通信是不可靠的,存在网络延时和丢包;存在节点处理错误的情况,节点自身随时可能宕机;同步调用系统使得系统设备不具备可扩展性。
CAP原理:
C:Consistency(一致性)。所有节点上的数据时刻保持同步(强一致性)。一致性严谨的的标书是原子读写,即所有读写都应该看起来是“原子”的,或者串行的。
A:Availability(可用性)。任何非故障节点都应该在有限的时间内给出请求的响应,不论请求是否成功。
P:Tolerance to the partition of network(分区容忍性)。当发生网络分区时(即节点之间无法通信),在丢失任意多消息的情况下,系统仍然能够正常工作。
在任何分布式系统中,可用性、一致性和分区容忍性这三方面都是相互矛盾的,三者不可兼得,最多只能取其二。
分布式系统一致性是一个具备容错能力的分布式系统需要解决的基本问题,通俗地讲,一致性就是不同的副本服务器认可同一份数据。
注意:一致性和正确性没有关系,而是系统对外 呈现的状态是否统一。
一致性协议就是用来解决一致性问题的,它使得一组机器像一个整体一样工作,即使其中的一些机器发生错误也能正常工作。
一致性协议是在复制状态机(Replocated State Machines, RSM)的背景下提出来的,通常也应用于具有复制状态语义的场景。
拜占庭将军问题:是对现实世界的模型化。由于硬件错误、网络拥塞、连接断开或遭到恶意攻击等原因,计算机和网络肯会出现不可预料的行为。
拜占庭错误:在计算机科学领域特指分布式系统中的某些恶意节点扰乱系统的正常运行,包括选择性不传递消息,选择性伪造消息等。(这种错误在实际环境很罕见)。
在分布式系统中,‘异步通信’’与“同步通信”的最大区别是没有时钟、不能时间同步、不能使用超时、不能探测失败、消息可任意延迟、消息可乱序等。
在分布式系统的协议设计中,不能简单地认为基于TCP的所有通信都是可靠的。一方面,尽管TCP保证了两个TCP栈之间的可靠通信,但无法保证两个上层应用之间的可靠通信。另一方面,TCP只能保证同一个TCP连接内网络报文不乱序,而无法保证不同TCP连接之间的网络报文顺序。在分布式系统中,节点之间的通信,可能会先后使用多个TCP连接,也可能并发建立多个TCP连接。
二、Raft算法:
1、Raft算法把问题分解成了领袖选举(leader election)、日志复制(log replication)、安全性(saftey)和成员关系变化(membership changes)四个子问题。
领袖选举:在一个灵虚节点发生故障后必须重新给出一个新的领袖节点。
日志复制:leader节点从客户端接收请求,然后将操作日志复制到集群中的其他服务器上,并强制要求其他服务器跟自己保存一致。
安全性:如果一个服务器已经将给定索引位置的日志条目应用到状态机中,则所有其他服务器不会在该该索引位置应用不同的条目。
成员关系变化:配置发生变化的时候,集群能够继续工作。
Raft算法基于复制状态机模型推导的。
2、Raft算法的基本概念:
Raft算法采用非对称节点关系模型:基于选主模型,只有主节点拥有决策权。任意时刻有且仅有一个主节点,客户端只与主节点进行交互。
一个由Raft协议组织的集群中,一共包含3个角色:Leader(领袖)、Candidate(候选人)、Follower(群众)。
Raft的世界里,每一个任期(Term)的开始都是一次领导人的选举。任期在raft中起着逻辑时钟的作用,也可用于在Raft节点中检测过期信息--过期的领导人。每个Raft节点各自都在本地维护一个当前任期值,触发这个数字变化主要有两个 场景:开始选举和与其他节点交换信息。当节点之间进行通信时,会相互交换当前的任期号。
如果一个节点收到的请求所携带的任期号是过时的,那么该节点就会拒绝响应本次请求。
Raft会强制使用较新的任期(Term)更新旧的Term。
3、领导人选:
Leader从客户端接收日志条目,再把日志条目复制到其他服务器上,并且在保证安全性的前提下,告诉其他服务器将日志条目应用到他们的状态机中。数据都是单向地从领导人流向其他服务器。
在Raft选举中,有两个非常重要的概念:心跳和选举定时器。每个Raft节点都有一个选举定时器,所有的Raft节点最开始以Follwer角色运行,都会启动这个定时器。每个节点选举定时器时长均不相等。
Leader在任期内必须定期向集群内的其他节点广播心跳包,昭告自己的存在。Follwer每次收到心跳包后主动将自己的选举定时器清零重置。Follwer选举定时器超时意味着在Raft规定的一个选举超时时间周期内,leader的心跳包并没有发送给Follwer(或在网络传输过程中延迟或丢了),于是Follwer就假定Leader已经不存在或发送故障,发起一次新的选举。
注意:要求Leader的广播心跳的周期必须要短于选举定时器的超时时间,否则会频繁的发生选举,切换Leader。
如果一个Follower决定开始参加选举,他会执行如下步骤:
- 将自己本地维护的当前任期号(current_term_id)加1
- 将自己的状态切换到候选人(Candidate) ,并为自己投票。也就是每个候选人第一张投票来源于自己。
- 向其所在的集群中的其他节点发送RequestVote RPC(RPC消息会携带“current_term_id”值),要求他们投票给自己。(在一个任期内,一个Raft节点最多只能为一个候选人投票,先到先得的原则,投给最早来拉票的候选人)
一个候选人有三种状态迁移的可能性:
1)得到大多数节点的选票(包括自己)成为Leader
2)发现其他节点赢得了选举,主动切回Follower
3)过一段时间发现没有人赢得选举,重新发起一次选举。
为避免选票被多个候选人平分且无线循环该状态,Raft采用随机重试的方法。
候选人的拉票过程使用Raft算法预定义的RPC---RequestVote RPC描述。RequestVote RPC的发起/调用方是候选人,接收方是集群内所有的其他节点(包括Leader、Follwer、Candidate)。
RequestVote RPC有4个参数两个返回值。具体如下表:
参数 | 描述 |
term | 候选人的任期号 |
candidateId | 请求投票的候选人id |
lastLogIndex | 候选人最新日志条目的索引值 |
lastLogTerm | 候选人最新日志条目对应的任期号 |
返回值 | 描述 |
term | 当前任期号,用于候选人更新自己本地的term值 |
voteGranted | 如果候选人得到了Follwer的这张选票,则为true,否则为false |
4、日志复制
一旦某个领导人赢得选举,那么它就会开始接收客户端的请求。每个客户端请求都将被解析成一条需要复制状态机执行的指令。领导人将该指令作为一条新的日志条目加入他的日志文件, 然后向其他节点发起AppendEntries RPC,要求其他节点复制该日志条目。如果Follower发生错误,运行缓慢没有及时响应AppendEntries RPC,或者发生了网络丢包,领导人会无限重试AppendEntries RPC,直到所有Follower存储了和leader一样的日志条目。
日志由有序编号的日志条目组成。每一个日志条目一般均包含三个属性:
*整数索引(log index):该条目在日志文件中的槽位、
*任期号(term):指其被领导人创建时所在的任期号。
*指令(command):即用于被状态机执行的外部命令。如果日志条目能够被状态机安全执行,就任务可以被提交了。
一但领导人创建的条目被复制到半数以上的节点,那么这个条目就称为可被提交的。领导人日志中的所有条目都是可被提交的,包括之前的领导人创建的日志条目。通常情况下,一次AppendEntries RPC就下能完成一条新的日志条目在集群内大多数节点的复制。
Raft算法设计了以下日志机制来保证不同节点上日志的一致性。
1)如果在不同的日志中两个条目有着相同索引和任期号,则他们所存储的命令是相同的。
2)如果在不同的日志中两个条目有着相同索引和任期号,则他们之前的索引条目完全一样。
一次正常的Raft日志的复制流程:
1)客户端向Leader发送写请求。
2)Leader将写请求解析成操作指令追加到本地日志文件中
3)Leader为每个Follwer广播AppendEntries RPC
4) Follwer通过一致性检查,选择从哪个位置开始追加Leader的日志条目。
5)一旦日志项提交成功,Leader就将该日志条目对应的指令应用(apply)到本地状态机,并向客户端返回操作结果。
6)Leader后续通过AppendEntries RPC将已经成功(在大多数节点上)提交(commit)的日志项告知Follower
7)Follower收到提交的日志项之后,将其应用(apply)到本地状态机。
AppendEntries RPC的调用方是Leader,接收方是Follwer。AppendEntries RPC有6个参数,2个返回值,AppendEntries RPC除了用于日志复制,还可广播leader的心跳。
参数 | 描述 |
term | 领导人的任期号 |
leaderId | 领导人的ID,为了其他raft节点能够重定向客户端请求 |
prevLogIndex | 领导人最新日志前一个位置日志的索引值 |
prevLogTerm | 领导人最新日志前一个位置日志的任期号 |
entries[] | 将要追加到Follwer上的日志条目。发生心跳包时为空,有时会为了效率而向多个节点并发发送 |
leaderCommit | 领导人会为每个Follwer都维护一个leaderCommit,表示领导人认为Follwer已经提交的日志条目索引值 |
返回值 | 描述 |
term | 当前的任期号,即AppendEntries RPC参数中term(领导人的)与Follwer本地维护的当前任期号的较大值。用于领导人更新自己的任期号。一旦领导人发现当前任期号比自己的大,表示自己“过时”了,便停止发送AppendEntries RPC,主动切换回Follwer |
success | 如果其他服务器包含能够匹配prevLogIndex和prevLogTerm的日志,则为真。 |
5、安全性Q&A
Q:怎样成为领导人的资格?
A:在所有以领导人选举为基础的一致性算法中,领导人最终必须要存储全部已经提交的日志条目。
隐含了:没有包含全部已经提交日志条目的节点无法成为Leader;日志条目只有一个流向:从Leader到Follwer。领导人永远不会覆盖已经存在的日志条目。 \
Q:如何判断日志已经提交?
A:1、只要一个日志条目被存在了大多数的服务器上,领导人就知道当前任期可以提交该条目了。
2、如果领导人在提交日志之前就崩溃了,之后的领导人会试着继续完成对日志的复制。但是,新任领导人无法断定存储在大多数服务器上的日志条目一定在之前的任期中被提交了(即使日志保存在大部分服务器上也有可能没来得及提交)。
然而,一个领导人不能因为由之前领导人创建的的某条日志存储在大多数节点上了,就笃定该日志条目已经被提交了。-----因为一旦某条日志被提交,那么它将永远没法被删除或修改。
领导人无法单纯地依靠前任期的日志条目信息判断它的提交状态。
针对这种情况,raft算法对日志提交增加了一个额外的限制:要求leader在当前任期至少有一条日志 被提交,即超过板书的节点写盘。
异常情况:
网络分区导致的脑裂情况,出现双leader
网络分区将原先的Leader节点和Follwer节点分隔开,Follwer收不到Leader的心跳将发起选举产生新的Leader。这时就产生了双Leader,原先的Leader独自在一个区,向它提交数据不可能复制到大多数节点上,所以永远都是提交不成功。向新的Leader提交数据可以成功,网络回复后旧的Leader发现集群中有更新任期的新Leader,则自动降级为Follwer并从新的Leader处同步数据达成集群数据一致。
分布式ID生成算法
分库分表后需要有一个唯一ID来标识一条数据,数据库的自增ID显然不能满足需求;特别一点的如订单、优惠券也都需要有唯一ID
做标识。那么这个全局唯一ID
就叫分布式ID
。
分布式ID一般需要满足以下条件:
- 全局唯一:必须保证ID是全局性唯一的,基本要求
- 高性能:高可用低延时,ID生成响应要块,否则反倒会成为业务瓶颈
- 高可用:100%的可用性是骗人的,但是也要无限接近于100%的可用性
- 好接入:要秉着拿来即用的设计原则,在系统设计和实现上要尽可能的简单
- 趋势递增:最好趋势递增,这个要求就得看具体业务场景了,一般不严格要求
分布式ID的生成方式:
- UUID 优点:生成足够简单,本地生成无网络消耗,具有唯一性。 缺点:无序的字符串,不具备趋势自增特性;没有具体的业务含义;长度过长
- 数据库自增ID 优点:实现简单,ID单调自增,数值类型查询速度快 缺点:DB单点存在宕机风险,无法扛住高并发场景
- 数据库多主模式 优点:解决DB单点问题 缺点:不利于后续扩容,而且实际上单个数据库自身压力还是大,依旧无法满足高并发场景。
-
号段模式 优点:号段模式是当下分布式ID生成器的主流实现方式之一,号段模式可以理解为从数据库批量的获取自增ID,每次从数据库取出一个号段范围,例如 (1,1000] 代表1000个ID,具体的业务服务将本号段,生成1~1000的自增ID并加载到内存
- Redis: 用
redis
实现需要注意一点,要考虑到redis持久化的问题。redis
有两种持久化方式RDB
和AOF。
RDB
会定时打一个快照进行持久化,假如连续自增但redis
没及时持久化,而这会Redis挂掉了,重启Redis后会出现ID重复的情况。
RDB持久化既可以手动执行也可以根据服务器配置选项定期执行,该功能可以将某个时间点上的数据库状态保存到一个RDB文件中。RDB文件是一个经过压缩的二进制文件。RDB文件保存在硬盘。根据RDB文件可还原
AOF
会对每条写命令进行持久化,即使Redis
挂掉了也不会出现ID重复的情况,但由于incr命令的特殊性,会导致Redis
重启恢复的数据时间过长。- 雪花算法(SnowFlake):
雪花算法(Snowflake)是twitter公司内部分布式项目采用的ID生成算法。
Snowflake
生成的是Long类型的ID,一个Long类型占8个字节,每个字节占8比特,也就是说一个Long类型占64个比特。
Snowflake ID组成结构:正数位
(占1比特)+ 时间戳
(占41比特)+ 机器ID
(占5比特)+ 数据中心
(占5比特)+ 自增值
(占12比特),总共64比特组成的一个Long类型。
- 滴滴出品(TinyID)
Tinyid
是基于号段模式原理实现的与Leaf
如出一辙,每个服务获取一个号段(1000,2000]、(2000,3000]、(3000,4000]。
Tinyid
提供http
和tinyid-client
两种方式接入
- 百度 (Uidgenerator)
uid-generator
是基于Snowflake
算法实现的,与原始的snowflake
算法不同在于,uid-generator
支持自定义时间戳
、工作机器ID
和 序列号
等各部分的位数,而且uid-generator
中采用用户自定义workId
的生成策略。
- 美团(Leaf)
Leaf
同时支持号段模式和snowflake
算法模式,可以切换使用。
二、分布式锁
首先,使用分布式锁性能会有所下降。
在有分布式锁的情况下,能保证多机多进程、多线程访问资源的一致性,这个时候也是需要进程内部的jvm锁的.
client 分布式肯定比单机要慢。
分布式的意义:分治可以间接速度提升。
中间件IO吞吐对性能影响较大。
两大类分布式锁:
1、类CAS自旋式分布式锁询问的方式尝试加锁:MySQL、Redis
2、event事件通知后续锁的变化,轮询向外的过程:zookeeper、etcd