1. CAP理论
Consistency(一致性):用户访问分布式系统中的任意节点,得到的数据必须一致。
Availability(可用性):用户访问集群中的任意健康节点,必须得到相应,而不是超时或拒绝。
Partition tolerance (分区容忍性):因为网络故障或其他原因导致分布式系统中的部分节点与其他节点失去连接,形成独立分区。在集群出现分区时,整个系统也要持续对外提供服务。
2. 一致性级别
CP和AP之间需要做权衡,其实根据需求不同,也可以将一致性划分为几个级别,在这些级别里面做一个权衡
-
强一致性:系统写入什么,读出来的也会是什么,但实现起来往往对性能影响较大
例如:火车站购票,有就是有,没有就是没有,不能出现不一致的情况
典型算法:
Paxos
、Raft
、ZAB
-
弱一致性:系统写入成功后,不承诺立刻可以读到写入的值,也不承诺具体多久后数据能达到一致,还可以细分为:
- 会话一致性:同一个客户端会话中可以保证一致,其他会话不能保证
- 用户一致性:同一个用户中可以保证一致,其他用户不能保证
例如:网上购物,在商品详情页看到库存还有好多,下单的瞬间才被提示"库存数量不足",实际上商品详情页展示的库存并不是最新的数据,只是在某个流程上才会做准确的检查
-
最终一致性:是弱一致性的特例,保证在一定时间内,能够达到一个一致的状态
例如:转账,转账完成后,会有一个提示,您的转账会在24小时内到账,一般用户也能接受,但最终必须是一致的
典型算法:
Gossip
3. Paxos 算法
3.1 问题引入
集群中有N个节点,如果一个节点写入后要求同步到剩余N - 1个节点后再向客户端返回 OK,虽然看起来最保险,但其中任意一个节点同步失败,势必造成整个集群不可用,能否在此基础上稍微提高可用性呢?
我们可以在写操作的时候,采用多数派
的思想:集群节点设置为奇数,同步超过集群中 N / 2 个节点成功,则向客户端返回 OK。(你可能会疑惑当这 N / 2 个节点是写成功了,如果此时访问到剩余那些没有同步成功的数据怎么办?这里我们先简化一下问题,暂不考虑多数派写成功后的读一致性
)
采用多数派思想其实还存在问题:顺序性问题
如上图,当S1服务器发送一个自增命令以后,此时S5服务器发送了一个set x 0
指令,S2和S3服务器是正常自增了,但S3服务器的 x值 先被设置为 0 ,后又因为顺序性问题自增为1,虽然这两次操作都返回了 OK,但此时就产生了各个服务器之间的数据的不一致。
3.2 角色划分和阶段划分
Paxos是一种共识算法,目的是解决之前提到的强一致性问题。
Paxos角色划分:集群中的每个节点都可以充当
-
Proposer
:负责生成提案注意:Paxos 算法允许有多个 Proposer 同时提案,但可能会引起
活锁
问题 -
Acceptor
:负责批准提案Acceptor 如果只有一个的话,存在单点问题,因此应当有多个
-
Learner:负责获取提案,Acceptor批准提案以后,会将提案发送给所有 Learner
Paxos阶段划分:
执行一个修改操作,不是一上来就能执行
- 准备阶段:Proposer 发起提案以后,必须由多数派 Acceptor 批准通过后才能进入接受阶段
- 接受阶段:Proposer 需要再将要执行的修改操作,广播给 Acceptor,这次仍然多数派通过,此修改才能生效,可以返回响应给客户端
3.3 Basic Paxos 算法描述
要点:
-
整个算法分成两个阶段:预备阶段,前两个箭头,接受阶段,后两个箭头
预备阶段的目的:第一拦截掉旧的方案,第二找到最新的 acceptValue
-
对于 Proposer
- 预备阶段只发送提案号,接受阶段发送提案号 + 值
- 提案号 n 唯一且全局递增,大的提案号有更高的优先级
- 如果见到最新已经接受的值的值,就会替换掉 Proposer 自己的值,保证一致性
-
对于 Acceptor 会持久化以下信息
- minN(最小提案号),会在预备阶段和接受阶段被更新为更大的提案号,会用来决定 Proposer 是否能选中提案
- acceptN(已接受提案号)和acceptValue(已接受值),会在接受阶段被更新,如果 minN > n 则不会更新
3.4 顺序性问题回顾
Paxos 算法通过预备和接受两个阶段来解决一致性问题。
情形一:
情形二:
3.5 缺点
1.效率比较低,两轮操作只能选中一个值
2.难于理解,算法比较复杂难懂
3.活锁问题
例如:
S1 的 1 号提案会被 S5 的 2 号提案所影响,导致 1 号提案作废,同样的 S5 的 2 号提案也会被 S1 的 3 号提案所影响导致 2 号提案被作废,如此循环导致提案都被废弃,这就是 Paxos 的活锁问题。
4. Raft 算法
另一种强一致性算法(共识算法),目的是比 Paxos 更容易理解
Raft 算法演示网站:https://raft.github.io/raftscope/index.html
整个 Raft 算法分解成三不分:
- Leader 选举:
- 只有一个 Server 能作为 Leader
- 一旦此 Server 崩溃,选举新的 Leader
- 执行操作(以日志复制为例,Log Replication)
- 由 Leader 执行自己的日志记录
- 将日志复制到其他 Server,以Leader 的日志为准,会覆盖 Server 中不一致的部分
- 多数派记录日志成功,Leader 才能执行命令,向客户端返回结果
- 确保安全
- 确保日志记录的一致性
- 拥有最新日志的 Server 才能成为 Leader
4.1 Leader 选举
- Leader 会不断向选民发送 AppendEntries 请求,证明自己还活着
- 选民收到 LeaderAppendEntries 请求后会重置自己的 timeout 时间
- 选民收不到 AppendEntries 请求超时后,转换角色为候选者,并将任期加 1,发送 RequestVote 请求,推选自己
- 选民收到第一个 RequestVote,会向该候选者投一票,这样即使有多个候选者,必定会选出一个 Leader,选票过半即当选,落选后变回选民
- 每一任期最多有一个 Leader,但也可能没有(选票数不过半的情况,需要再进行一轮投票,timeout在 T~2T 之间随机,timeout时间过短会导致 Leader仍然存活时重新选举,时间过长会导致选举时间变长,整个系统响应速度变慢,T是指两个节点之间的通信时间)
- 任期由各个 server 自己维护即可,无需全局维护,在超时后 + 1,在接收到任意消息时更新为更新的任期,遇到更旧的任期,视为错误
4.2 执行操作(日志复制为例)
- 客户端发送命令到 Leader
- Leader将命令写入日志
- Leader向所有选民发送 AppendEntries 请求
- 在多数派通过后,执行命令(即提交),向客户端返回结果
- 在后续的 AppendEntries 请求中通知选民
- 选民执行命令(即提交)
4.3 确保安全
Leader 日志的完整性
- Leader 被认为拥有最完整的日志
- 一旦 Leader 完成了某条命令的提交,那么未来的 Leader 也必须有该条命令提交信息
- 投票时,会将候选者最新的 <Term,Index>随 RequestVote 请求发送,如果候选者的日志还没选民的新,则投否决票
选民日志的一致性
- 以 Leader 为例,对选民的日志进行补充或覆盖
- AppendEntries 请求发送时会携带最新的<Term, Index, Commend>以及上一个的<Term, Index>
- 如果选民发现上一个的<Term, Index>能够对应上则成功,否则失败,继续携带更早的信息进行比对
5. Gossip 协议
与 Paxos 和 Raft 目标是强一致性不同,Gossip 达到的是最终一致性
A gossip protocol is a procedure or process of computer peer-to-peer communication that is based on the way epidemics spread.
它可以快速的将信息散播给集群中每个成员,散播速度为log N(实际传播次数可能会高于此结果,因为随机时会随到一些重复的成员)
优点:
- 扩展性高,传播次数不会受集群成员增长而增长过快
- 容错性好,即使某些节点间发生了故障无法通信,也不会影响最终的一致性
- Robust(鲁棒性),即皮实,集群中的节点是对等的,即便一些节点挂了,一些节点添加进来,也不会影响其他节点的信息传播
6. 分布式通用设计
6.1 如何监测节点活着?
向节点周期性发送心跳请求,如果能收到心跳回应,表示该节点还活着
如果收不到心跳回应,却不能证明该节点死了,可能由于网络抖动、回应延时等原因没能及时收到回应。有如下解决思路:
- 如 Redis 哨兵模式中,如果 sentinel 向 master 发送 PING 而没有收到 PONG,整你判断主观下线,必须采用其他 sentinel 的意见,达到多数派后才能判断客观下线,进入主备切换流程
- 将周期心跳检测升级为累计心跳检测机制,即记录统计该节点的历史响应时间,如果超过警戒,则发起有限次数的重试作为进一步判定
6.2 如何保证高可用?
应用层高可用:
关键是做到无状态,即所有节点地位平等,去 session 化。利用负载均衡将请求发送到任意一台节点进行处理,如果有某个节点宕机,把该节点从服务列表中移除,不会影响业务运行。
服务层高可用:
同样要做到无状态,此外还应当考虑
- 核心服务和非核心服务隔离部署,分级管理,方便非核心服务降级
- 对于即时性没有要求的服务可以考虑采用异步调用优化
- 合理设置超时时间,在超时后应当有对应的处理策略,如:重试、转移、降级等
数据层高可用:
需要有数据备份机制与故障转移机制
缓存服务是否需要高可用,两种观点:
- 缓存服务不可用会让数据库失去保护,因此需要保证缓存服务高可用
- 缓存服务不是数据存储服务,缓存宕机应当通过其他手段解决,如扩大缓存规模,一个缓存服务器的宕机只会影响局部
6.3 如何实现全局唯一ID?
Redis:
- 使用 incr 生成 id,由于 Redis 的单线程特性,能保证它不会重复
- 缺点:属于集中式的解决方案,有网络消耗
UUID:
- UUID有多种实现,典型的UUID实现会包含时间信息、MAC地址信息、随机数
- 优点:属于本地解决方案,无网络消耗
- 缺点:MAC地址提供了唯一性的保证,但也带来安全风险,最糟的是它是字符串形式,占用空间大,查询性能低,无法保证趋势递增
Snowflake(推荐):
- 通常的实现是 41 位时间信息、精确到毫秒,10 位的机器标识、12 位的序列号,还有 1 位没有使用,共 8 个字节
- 理解思想以后,可以根据自己实际情况对原有算法做调整
- 优点:本地解决方案,无网络小号。长整型避免了字符串的缺点,并保证了趋势递增
6.4 负载均衡算法
负载均衡算法用于在多个服务器或节点间分发工作负载,以确保各个节点能够有效地处理请求,提高系统的性能和可靠性。以下是一些常见的负载均衡算法:
-
轮询(Round Robin):
- 将请求依次分配给每个节点,循环往复。每个请求按照顺序分发给不同的节点,适用于节点性能相近的情况。
-
加权轮询(Weighted Round Robin):
- 类似于轮询算法,但给不同节点分配不同的权重,使得性能更高的节点能够处理更多的请求。
-
随机算法(Random):
- 随机选择一个节点来处理请求,适用于节点性能相差不大且请求相对均匀的场景。
-
最少连接(Least Connections):
- 将请求分配给当前连接数最少的节点。这种方式可以避免请求集中在少数节点上,适用于长连接或处理时间较长的场景。
-
IP哈希(IP Hash):
- 根据请求的源IP地址进行哈希计算,然后将相同哈希结果的请求发送到同一节点。这种方法可以确保同一客户端的请求始终发送到同一个节点,适用于需要保持会话一致性的场景。
-
最优响应时间(Least Response Time):
- 根据节点的实时负载情况和响应时间选择最优节点。这需要实时监控节点的负载和性能指标来进行决策。
-
最少流量(Least Traffic):
- 分配请求到当前流量最小的节点,确保整个系统的网络流量尽量均衡。
-
一致性哈希(Consistent Hashing):
- 根据请求的键值对将请求映射到节点,通过哈希环的方式确定请求应该由哪个节点处理。这种算法在节点动态变化时能够更好地保持负载均衡。
6.5 数据分片策略
所谓分片就是指数据量较大时,对数据进行水平切分,让数据分布在多个节点上
1. Hash
- 按照 key 的 hash 值将数据映射到不同的节点上
- 优点:实现简洁、数据分布均匀
- 缺点1:如果直接 hash 与节点数取模,节点变动时就会造成数据大规模迁移,可以使用一致性 hash 改进
- 缺点2:查询某一类热点数据时,由于它们是用 hash 分散到了不同的节点上,造成查询效率不高
2. Range
- 可以将 key 按照range 进行划分,让某一范围的数据都存放在同一节点上
- 优点1:按照 range 查询,性能更高
- 优点2:如果配合动态 range 分片,可以将较小的分片合并、将热点数据分散,有很多有用的功能
3. 静态调度和动态调度
- 静态意味着数据分片后分布固定,即使移动也需要人工介入
- 动态意味着通过管理器基于调度算法在各个节点之间自由移动数据
7. 分布式事务解决方案
7.1 两阶段提交(2PC)
两阶段提交(2PC):
-
阶段1 - 准备阶段(Prepare Phase):
- 协调者(Coordinator)向所有参与者(Participants)发送准备请求,并等待它们的响应。
- 参与者收到请求后,执行事务操作,并将准备就绪的消息或“同意”响应返回给协调者。
-
阶段2 - 提交或回滚阶段(Commit or Rollback Phase):
- 如果所有参与者都准备就绪,协调者向所有参与者发送提交请求,并等待它们的响应。
- 参与者收到提交请求后,如果事务执行成功,则提交事务并发送“提交完成”响应;如果执行失败,则回滚事务并发送“回滚完成”响应。
缺点:
- 阻塞型协议:所有参与者在等待接到下一步操作前,都处于阻塞,占用的资源也一直被锁定
- 数据不一致:在阶段二,如果只有部分参与者收到的提交请求,则会造成数据不一致
- 协调者单点问题:如果协调者故障在阶段二出现问题,会导致所有参与者(不会超时)始终处于阻塞状态,资源也被锁定得不到释放
7.2 TCC(事务预留)
TCC模式分成三个阶段:
-
Try:资源检查和预留
-
Confirm:业务执行和提交
-
Cancel:预留资源的释放
优点:
- 一阶段完成直接提交事务,释放数据库资源,性能好
- 不依赖数据库事务,而是依赖补偿操作,可以用于非事务型数据库
缺点:
- 有代码侵入,需要人为编写try、confirm和cancel接口,太麻烦
- 软状态,事务是最终一致
- 需要考虑confirm和cancel的失败情况,做好幂等处理
7.3 基于可靠性消息的最终一致性方案
2PC 和 TCC 都属于同步方案,实际开发中更多采用的是异步方案
我们可以将问题转换为本地事务与消息投递的原子性
例如:下单以后得支付、扣减库存、增加积分等操作对实时性要求并不高。此时将下单成功的消息写入消息中间件,利用消息中间件实现最终一致性