分布式
2024-03-18 12:34:31 0 举报
AI智能生成
分布式
作者其他创作
大纲/内容
CAP 理论/定理起源于 2000 年,由 Eric Brewer 教授在分布式计算原理研讨会(PODC)上提出,因此又被称作 布鲁尔定理(Brewer’s theorem)。
2 年后,麻省理工学院的 Seth Gilbert 和 Nancy Lynch 发表了布鲁尔猜想的证明,CAP 理论正式成为分布式领域的定理。
CAP 也就是 Consistency(一致性)、Availability(可用性)、Partition Tolerance(分区容错性) 这三个单词首字母组合。
定义
一致性(Consistency) : 所有节点访问同一份最新的数据副本。
可用性(Availability): 非故障的节点在合理的时间内返回合理的响应(不是错误或者超时的响应)。
分区容错性(Partition Tolerance) : 分布式系统出现网络分区的时候,仍然能够对外提供服务。
CAP 理论中分区容错性 P 是一定要满足的,在此基础上,只能满足可用性 A 或者一致性 C。
组成
分布式系统中,多个节点之间的网络本来是连通的,但是因为某些故障某些节点之间不连通了,整个网络就分成了几块区域,这就叫 网络分区。
网络分区
ZooKeeper、HBase
CP 架构
Cassandra、Eureka
AP 架构
Nacos
CP + AP 架构
分布式系统理论上不可能选择 CA 架构,只能选择 CP 或者 AP 架构。
在进行分布式系统设计和开发时,我们不应该仅仅局限在 CAP 问题上,还要关注系统的扩展性、可用性等等。
如果系统发生“分区”,我们要考虑选择 CP 还是 AP。如果系统没有发生“分区”的话,我们要思考如何保证 CA 。
总结
CAP 理论
BASE 理论起源于 2008 年, 由 eBay 的架构师 Dan Pritchett 在 ACM 上发表。
BASE 是 Basically Available(基本可用)、Soft-state(软状态) 和 Eventually Consistent(最终一致性) 三个短语的缩写。
BASE 理论是对 CAP 中一致性 C 和可用性 A 权衡的结果,其来源于对大规模互联网系统分布式实践的总结,是基于 CAP 定理逐步演化而来的。
即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。
牺牲数据的一致性来满足系统的高可用性,系统中一部分数据不可用或者不一致时,仍需要保持系统整体“主要可用”。
对 CAP 的延伸和补充,更具体地说,是对 CAP 中 AP 方案的一个补充。
本质
分布式系统在出现不可预知故障的时候,允许损失部分可用性。但是,这绝不等价于系统不可用。
响应时间上的损失:正常情况下,处理用户请求需要 0.5s 返回结果,但是由于系统出现故障,处理用户请求的时间变为 3 s。
系统功能上的损失:正常情况下,用户可以使用系统的全部功能,但是由于系统访问量突然剧增,系统的部分非核心功能无法使用。
允许损失部分可用性
基本可用
允许系统中的数据存在中间状态(CAP 理论中的数据不一致),并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时。
软状态
系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。
需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。
强一致性:系统写入了什么,读出来的就是什么。
弱一致性:不一定可以读取到最新写入的值,也不保证多少时间之后读取到的数据是最新的,只是会尽量保证某个时刻达到数据一致的状态。
最终一致性:弱一致性的升级版,系统会保证在一定时间内达到数据一致的状态。(推荐)
分布式一致性级别
读时修复 : 在读取数据时,检测数据的不一致,进行修复。比如 Cassandra 的 Read Repair 实现,具体来说,在向 Cassandra 系统查询数据的时候,如果检测到不同节点的副本数据不一致,系统就自动修复数据。
写时修复 : 在写入数据,检测数据的不一致时,进行修复。比如 Cassandra 集群的节点之间远程写数据的时候,如果写失败 就将数据缓存下来,然后定时重传,修复数据的不一致性。(推荐)
异步修复 : 这个是最常用的方式,通过定时对账检测副本数据的一致性,并修复。
实现方式
最终一致性
三要素
BASE 理论
ACID 是数据库事务完整性的理论,CAP 是分布式系统设计理论,BASE 是 CAP 理论中 AP 方案的延伸。
理论
Paxos 算法是第一个被证明完备的分布式系统共识算法。
让分布式系统中的多个节点之间对某个提案(Proposal)达成一致的看法。
作用
Basic Paxos 算法:描述的是多节点之间如何就某个值(提案 Value)达成共识。
Multi-Paxos 思想:描述的是执行多个 Basic Paxos 实例,就一系列值达成共识。Multi-Paxos 说白了就是执行多次 Basic Paxos ,核心还是 Basic Paxos 。
比 Paxos 算法更易理解和实现的共识算法。
Raft 是 Multi-Paxos 的一个变种,其简化了 Multi-Paxos 的思想,变得更容易被理解以及工程实现。
Raft 算法
除了 Raft 算法之外,当前最常用的一些共识算法比如 ZAB 协议、 Fast Paxos 算法都是基于 Paxos 算法改进的。
没有
一般使用的是 工作量证明(POW,Proof-of-Work)、 权益证明(PoS,Proof-of-Stake ) 等共识算法。
区块链
应用
有
恶意节点
提议者(Proposer):也可以叫做协调者(coordinator),提议者负责接受客户端的请求并发起提案。提案信息通常包括提案编号 (Proposal ID) 和提议的值 (Value)。
接受者(Acceptor):也可以叫做投票员(voter),负责对提议者的提案进行投票,同时需要记住自己的投票历史。
学习者(Learner):若有超过半数接受者就某个提议达成了共识,学习者就需要接受这个提议,并就该提议作出运算,然后将运算结果返回给客户端。
角色
为了减少实现该算法所需的节点数,一个节点可以身兼多个角色。
一个提案被选定需要被半数以上的 Acceptor 接受。
Basic Paxos 算法具备容错性,在少于一半的节点出现故障时,集群仍能正常工作。
Basic Paxos 算法
Multi-Paxos 只是一种思想。
通过多个 Basic Paxos 实例就一系列值达成共识。也就是说,Basic Paxos 是 Multi-Paxos 思想的核心,Multi-Paxos 就是多执行几次 Basic Paxos。
核心
Multi Paxos 思想
Paxos 算法
假设多位拜占庭将军中没有叛军,信使的信息可靠但有可能被暗杀的情况下,将军们如何达成是否要进攻的一致性决定?
先在所有的将军中选出一个大将军,用来做出所有的决定。
解决
拜占庭将军
可容错系统中的一个基本问题:即使面对故障,服务器也可以在共享状态上达成一致。
允许一组节点像一个整体一样一起工作,即使其中的一些节点出现故障也能够继续工作下去,其正确性主要是源于复制状态机的性质:一组Server的状态机计算相同状态的副本,即使有一部分的Server宕机了它们仍然能够继续运行。
一般通过使用复制日志来实现复制状态机。因此共识算法的工作就是保持复制日志的一致性。
安全。确保在非拜占庭条件下的安全性,包括网络延迟、分区、包丢失、复制和重新排序。
高可用。只要大多数服务器都是可操作的,并且可以相互通信,也可以与客户端进行通信,那么这些服务器就可以看作完全功能可用的。
一致性不依赖时序。错误的时钟和极端的消息延迟,在最坏的情况下也只会造成可用性问题,而不会产生一致性问题。
在集群中大多数服务器响应,命令就可以完成,不会被少数运行缓慢的服务器来影响整体系统性能。
特性
共识算法
背景
Leader:负责发起心跳,响应客户端,创建日志,同步日志。
Candidate:Leader 选举过程中的临时角色,由 Follower 转化而来,发起投票参与竞选。
Follower:接受 Leader 的心跳和日志同步数据,投票给 Candidate。
在正常的情况下,只有一个服务器是 Leader,剩下的服务器是 Follower。Follower 是被动的,它们不会发送任何请求,只是响应来自 Leader 和 Candidate 的请求。
规则
节点类型
raft 算法将时间划分为任意长度的任期(term),任期用连续的数字表示,看作当前 term 号。
每一个任期的开始都是一次选举,在选举开始时,一个或多个 Candidate 会尝试成为 Leader。
如果一个 Candidate 赢得了选举,它就会在该任期内担任 Leader。
如果没有选出 Leader,将会开启另一个任期,并立刻开始下一次选举。
raft 算法保证在给定的一个任期最少要有一个 Leader。
过程
每个节点都会存储当前的 term 号,当服务器之间进行通信时会交换当前的 term 号。
如果有服务器发现自己的 term 号比其他人小,那么他会更新到较大的 term 值。
如果一个 Candidate 或者 Leader 发现自己的 term 过期了,他会立即退回成 Follower。
如果一台服务器收到的请求的 term 号是过期的,那么它会拒绝此次请求。
term 号
任期
log:由 entry 构成的数组,每一个 entry 都有一个表明自己在 log 中的 index。只有 Leader 才可以改变其他节点的 log。entry 总是先被 Leader 添加到自己的 log 数组中,然后再发起共识请求,获得同意后才会被 Leader 提交给状态机。Follower 只能从 Leader 获取新日志和当前的 commitIndex,然后把对应的 entry 应用到自己的状态机中。
日志
基础
raft 使用心跳机制来触发 Leader 的选举。
如果一台服务器能够收到来自 Leader 或者 Candidate 的有效信息,那么它会一直保持为 Follower 状态,并且刷新自己的 electionElapsed,重新计时。
Leader 会向所有的 Follower 周期性发送心跳来保证自己的 Leader 地位。如果一个 Follower 在一个周期内没有收到心跳信息,就叫选举超时,它就会认为此时没有可用的 Leader,并且开始进行一次选举以选出一个新的 Leader。
为了开始新的选举,Follower 会自增自己的 term 号并且转换状态为 Candidate。然后他会向所有节点发起 RequestVoteRPC 请求, Candidate 的状态会持续到以下情况发生:赢得选举、其他节点赢得选举、一轮选举结束,无人胜出。
赢得选举的条件是:一个 Candidate 在一个任期内收到了来自集群内的多数选票(N/2+1),就可以成为 Leader。
在 Candidate 等待选票的时候,它可能收到其他节点声明自己是 Leader 的心跳,此时有两种情况:该 Leader 的 term 号大于等于自己的 term 号,说明对方已经成为 Leader,则自己回退为 Follower。该 Leader 的 term 号小于自己的 term 号,那么会拒绝该请求并让该节点更新 term。
由于可能同一时刻出现多个 Candidate,导致没有 Candidate 获得大多数选票,如果没有其他手段来重新分配选票的话,那么可能会无限重复下去。
raft 使用了随机的选举超时时间来避免上述情况。每一个 Candidate 在发起选举后,都会随机化一个新的选举超时时间,这种机制使得各个服务器能够分散开来,在大多数情况下只有一个服务器会率先超时,它会在其他服务器超时之前赢得选举。
领导人选举
一旦选出了 Leader,它就开始接受客户端的请求。每一个客户端的请求都包含一条需要被复制状态机(Replicated State Machine)执行的命令。
如果 Follower 接受该 entry,则会将 entry 添加到自己的日志后面,同时返回给 Leader 同意。
如果 Leader 收到多数的成功响应,Leader 会将这个 entry 应用到自己的状态机中,之后可以成为这个 entry 是 committed 的,并且向客户端返回执行结果。
raft 保证以下两个性质:在两个日志里,有两个 entry 拥有相同的 index 和 term,那么它们一定有相同的 cmd。在两个日志里,有两个 entry 拥有相同的 index 和 term,那么它们前面的 entry 也一定相同。
通过“仅有 Leader 可以生成 entry”来保证第一个性质,第二个性质需要一致性检查来进行保证。
一般情况下,Leader 和 Follower 的日志保持一致,然后,Leader 的崩溃会导致日志不一样,这样一致性检查会产生失败。Leader 通过强制 Follower 复制自己的日志来处理日志的不一致。这就意味着,在 Follower 上的冲突日志会被领导者的日志覆盖。
为了使得 Follower 的日志和自己的日志一致,Leader 需要找到 Follower 与它日志一致的地方,然后删除 Follower 在该位置之后的日志,接着把这之后的日志发送给 Follower。
Leader 给每一个Follower 维护了一个 nextIndex,它表示 Leader 将要发送给该追随者的下一条日志条目的索引。当一个 Leader 开始掌权时,它会将 nextIndex 初始化为它的最新的日志条目索引数+1。如果一个 Follower 的日志和 Leader 的不一致,AppendEntries 一致性检查会在下一次 AppendEntries RPC 时返回失败。在失败之后,Leader 会将 nextIndex 递减然后重试 AppendEntries RPC。最终 nextIndex 会达到一个 Leader 和 Follower 日志一致的地方。这时,AppendEntries 会返回成功,Follower 中冲突的日志条目都被移除了,并且添加所缺少的上了 Leader 的日志条目。一旦 AppendEntries 返回成功,Follower 和 Leader 的日志就一致了,这样的状态会保持到该任期结束。
日志复制
Leader 需要保证自己存储全部已经提交的日志条目。使日志条目只有一个流向:从 Leader 流向 Follower,Leader 永远不会覆盖已经存在的日志条目。
每个 Candidate 发送 RequestVoteRPC 时,都会带上最后一个 entry 的信息。所有节点收到投票信息时,会对该 entry 进行比较,如果发现自己的更新,则拒绝投票给该 Candidate。
判断日志新旧的方式:如果两个日志的 term 不同,term 大的更新;如果 term 相同,更长的 index 更新。
选举限制
如果 Leader 崩溃,集群中的节点在 electionTimeout 时间内没有收到 Leader 的心跳信息就会触发新一轮的选主,在选主期间整个集群对外是不可用的。
如果 Follower 和 Candidate 崩溃,处理方式会简单很多。之后发送给它的 RequestVoteRPC 和 AppendEntriesRPC 会失败。由于 raft 的所有请求都是幂等的,所以失败的话会无限的重试。如果崩溃恢复后,就可以收到新的请求,然后选择追加或者拒绝 entry。
节点崩溃
raft 的要求之一就是安全性不依赖于时间:系统不能仅仅因为一些事件发生的比预想的快一些或者慢一些就产生错误。
broadcastTime << electionTimeout << MTBF
broadcastTime:向其他节点并发发送消息的平均响应时间。
electionTimeout:选举超时时间。
MTBF(mean time between failures):单台机器的平均健康时间。
时间条件
broadcastTime应该比electionTimeout小一个数量级,为的是使Leader能够持续发送心跳信息(heartbeat)来阻止Follower开始选举。
electionTimeout也要比MTBF小几个数量级,为的是使得系统稳定运行。当Leader崩溃时,大约会在整个electionTimeout的时间内不可用;我们希望这种情况仅占全部时间的很小一部分。
由于broadcastTime和MTBF是由系统决定的属性,因此需要决定electionTimeout的时间。
一般来说,broadcastTime 一般为 0.5~20ms,electionTimeout 可以设置为 10~500ms,MTBF 一般为一两个月。
时间与可用性
安全性
算法
在分布式系统中,不同的节点进行数据/信息共享是一个基本的需求。
一个主节点同时共享最新信息给其他所有节点,比较适合中心化系统。(简单粗暴)
节点多的时候不光同步消息的效率低,还太依赖与中心节点,存在单点风险问题。
缺点
集中式发散消息
Gossip 协议。
分散式发散消息
共享方式
Gossip 协议 也叫 Epidemic 协议(流行病协议)或者 Epidemic propagation 算法(疫情传播算法)。
Gossip 协议是一种允许在分布式系统中共享状态的去中心化通信协议,通过这种通信协议,我们可以将信息传播给网络或集群中的所有成员。
随机传播特性 (联想一下病毒传播、癌细胞扩散等生活中常见的情景)。
特点
NoSQL 数据库 Redis 和 Apache Cassandra、服务网格解决方案 Consul 等。
一个典型的分布式系统,分布式系统中的各个节点需要互相通信。
其中各个节点基于 Gossip 协议 来进行通信共享信息,每个 Redis 节点都维护了一份集群的状态信息。
MEET:在 Redis Cluster 中的某个 Redis 节点上执行 CLUSTER MEET ip port 命令,可以向指定的 Redis 节点发送一条 MEET 信息,用于将其添加进 Redis Cluster 成为新的 Redis 节点。
PING/PONG:Redis Cluster 中的节点都会定时地向其他节点发送 PING 消息,来交换各个节点状态信息,检查各个节点状态,包括在线状态、疑似下线状态 PFAIL 和已下线状态 FAIL。
FAIL:Redis Cluster 中的节点 A 发现 B 节点 PFAIL ,并且在下线报告的有效期限内集群中半数以上的节点将 B 节点标记为 PFAIL,节点 A 就会向集群广播一条 FAIL 消息,通知其他节点将故障节点 B 标记为 FAIL 。
消息分类
有了 Redis Cluster 之后,不需要专门部署 Sentinel 集群服务了。Redis Cluster 相当于是内置了 Sentinel 机制,Redis Cluster 内部的各个 Redis 节点通过 Gossip 协议互相探测健康状态,在故障时可以自动切换。
Redis Cluster
熵的概念最早起源于物理学,用于度量一个热力学系统的混乱程度。
熵最好理解为不确定性的量度而不是确定性的量度,因为越随机的信源的熵越大。
理解为节点之间数据的混乱程度/差异性。
反熵中的熵
熵
反熵就是指消除不同节点中数据的差异,提升节点间数据的相似度,从而降低熵值。
集群中的节点,每隔段时间就随机选择某个其他节点,然后通过互相交换自己的所有数据来消除两者之间的差异,实现数据的最终一致性。
推方式,就是将自己的所有副本数据,推给对方,修复对方副本中的熵。
拉方式,就是拉取对方的所有副本数据,修复自己副本中的熵。
推拉式,就是同时修复自己副本和对方副本中的熵。
方式
实际应用场景中,一般不会采用随机的节点进行反熵,而是需要可以的设计一个闭环。
能够在一个确定的时间范围内实现各个节点数据的最终一致性,而不是基于随机的概率。
闭环优势
实现
反熵很简单实用,但节点过多或者节点动态变化的话,反熵就不太适用了。
优缺点
反熵(Anti-entropy)
分布式系统中的一个节点一旦有了新数据之后,就会变为活跃节点,活跃节点会周期性地联系其他节点向其发送新数据,直到所有的节点都存储了该新数据。
比较适合节点数量比较多或者节点动态变化的场景。
要尽量避免传播的信息包不能太大,避免网络消耗太大。
注意
传谣(Rumor-Mongering)
消息传播模式
相比于其他分布式协议/算法来说,Gossip 协议理解起来非常简单。
能够容忍网络上节点的随意地增加或者减少,宕机或者重启,因为 Gossip 协议下这些节点都是平等的,去中心化的。新增加或者重启的节点在理想情况下最终是一定会和其他节点的状态达到一致。
速度相对较快。节点数量比较多的情况下,扩散速度比一个主节点向其他节点传播信息要更快(多播)。
优势
消息需要通过多个传播的轮次才能传播到整个网络中,因此,必然会出现各节点状态不一致的情况。毕竟,Gossip 协议强调的是最终一致,至于达到各个节点的状态一致需要多长时间,谁也无从得知。
由于拜占庭将军问题,不允许存在恶意节点。
可能会出现消息冗余的问题。由于消息传播的随机性,同一个节点可能会重复收到相同的消息。
缺陷
Gossip 协议
协议
API网关(API Gateway)是一种架构模式,它是将一些服务共有的功能整合在一起,独立部署为单独的一层,用来解决一些服务治理的问题。
请求转发 + 请求过滤。
请求转发:将请求转发到目标微服务。
负载均衡:根据各个微服务实例的负载情况或者具体的负载均衡策略配置对请求实现动态的负载均衡。
安全认证:对用户请求进行身份验证并仅允许可信客户端访问 API,并且还能够使用类似 RBAC 等方式来授权。
参数校验:支持参数映射与校验逻辑。
日志记录:记录所有请求的行为日志供后续使用。
监控告警:从业务指标、机器指标、JVM 指标等方面进行监控并提供配套的告警机制。
流量控制:对请求的流量进行控制,也就是限制某一时刻内的请求数。
熔断降级:实时监控请求的统计信息,达到配置的失败阈值后,自动熔断,返回默认值。
响应缓存:当用户请求获取的是一些静态的或更新不频繁的数据时,一段时间内多次请求获取到的数据很可能是一样的。对于这种情况可以将响应缓存起来。这样用户请求可以直接在网关层得到响应数据,无需再去访问业务服务,减轻业务服务的负担。
响应聚合:某些情况下用户请求要获取的响应内容可能会来自于多个业务服务。网关作为业务服务的调用方,可以把多个服务的响应整合起来,再一并返回给用户。
灰度发布:将请求动态分流到不同的服务版本(最基本的一种灰度发布)。
异常处理:对于业务服务返回的异常响应,可以在网关层在返回给用户之前做转换处理。这样可以把一些业务侧返回的异常细节隐藏,转换成用户友好的错误提示返回。
API 文档: 如果计划将 API 暴露给组织以外的开发人员,那么必须考虑使用 API 文档,例如 Swagger 或 OpenAPI。
协议转换:通过协议转换整合后台基于 REST、AMQP、Dubbo 等不同风格和实现技术的微服务,面向 Web Mobile、开放平台等特定客户端提供统一服务。
证书管理:将 SSL 证书部署到 API 网关,由一个统一的入口管理接口,降低了证书更换时的复杂度。
功能
Zuul 是 Netflix 开发的一款提供动态路由、监控、弹性、安全的网关服务,基于 Java 技术栈开发,可以和 Eureka、Ribbon、Hystrix 等组件配合使用。
Zuul 主要通过过滤器(类似于 AOP)来过滤请求,从而实现网关必备的各种功能。
Zuul 1.x 基于同步 IO,性能较差。Zuul 2.x 基于 Netty 实现了异步 IO,性能得到了大幅改进。
Netflix Zuul
SpringCloud Gateway 属于 Spring Cloud 生态系统中的网关,其诞生的目标是为了替代老牌网关 Zuul。
为了提升网关的性能,SpringCloud Gateway 基于 Spring WebFlux 。
Spring WebFlux 使用 Reactor 库来实现响应式编程模型,底层基于 Netty 实现同步非阻塞的 I/O。
原理
路由判断:客户端的请求到达网关后,先经过 Gateway Handler Mapping 处理,这里面会做断言(Predicate)判断,看下符合哪个路由规则,这个路由映射后端的某个服务。
请求过滤:然后请求到达 Gateway Web Handler,这里面有很多过滤器,组成过滤器链(Filter Chain),这些过滤器可以对请求进行拦截和修改,比如添加请求头、参数校验等等,有点像净化污水。然后将请求转发到实际的后端服务。这些过滤器逻辑上可以称作 Pre-Filters,Pre 可以理解为“在...之前”。
服务处理:后端服务会对请求进行处理。
响应过滤:后端处理完结果后,返回给 Gateway 的过滤器再次做处理,逻辑上可以称作 Post-Filters,Post 可以理解为“在...之后”。
响应返回:响应经过过滤处理后,返回给客户端。
客户端的请求先通过匹配规则找到合适的路由,就能映射到具体的服务。然后请求经过过滤器处理后转发给具体的服务,服务处理后,再次经过过滤器处理,最后返回给客户端。
工作流程
一种编程术语,说白了它就是对一个表达式进行 if 判断,结果为真或假,如果为真则做这件事,否则做那件事。
在 Gateway 中,如果客户端发送的请求满足了断言的条件,则映射到指定的路由器,就能转发到指定的服务上进行处理。
常见的路由断言配置规则
断言
一对多:一个路由规则可以包含多个断言。
同时满足:如果一个路由规则中有多个断言,则需要同时满足才能匹配。
第一个匹配成功:如果一个请求可以匹配多个路由,则映射第一个匹配成功的路由。
路由和断言关系
Pre 类型:在请求被转发到微服务之前,对请求进行拦截和修改,例如参数校验、权限校验、流量监控、日志输出以及协议转换等操作。
Post 类型:微服务处理完请求后,返回响应给网关,网关可以再次进行处理,例如修改响应内容或响应头、日志输出、流量监控等。
按请求和响应
GatewayFilter:局部过滤器,应用在单个路由或一组路由上的过滤器。标红色表示比较常用的过滤器。
GlobalFilter:全局过滤器,应用在所有路由上的过滤器。
按照作用范围
对应的接口是 RateLimiter,RateLimiter 接口只有一个实现类 RedisRateLimiter (基于 Redis + Lua 实现的限流),提供的限流功能比较简易且不易使用。
限流过滤器
过滤器分类
只需要在项目中配置 @RestControllerAdvice和 @ExceptionHandler就可以了。
SpringBoot 项目
提供了多种全局处理的方式,比较常用的一种是实现 ErrorWebExceptionHandler 并重写其中的 handle 方法。
Spring Cloud Gateway
自定义全局异常处理
不仅提供统一的路由方式,并且基于 Filter 链的方式提供了网关基本的功能,例如:安全,监控/指标,限流。
差别不大,也是通过过滤器来处理请求。不过,目前更加推荐使用 Spring Cloud Gateway 而非 Zuul,Spring Cloud 生态对其支持更加友好。
与 Zuul 2.x
简单易用、成熟稳定、与 Spring Cloud 生态系统兼容、Spring 社区成熟等等。
优点
一般还需要结合其他网关一起使用比如 OpenResty。并且,其性能相比较于 Kong 和 APISIX,还是差一些。
基于 Nacos 注册中心。(推荐)
动态路由实现
一个基于 Nginx 与 Lua 的高性能 Web 平台,其内部集成了大量精良的 Lua 库、第三方模块以及大多数的依赖项。
用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。
基于 Nginx,主要还是看中了其优秀的高并发能力。
需要编写 C 语言的模块,并重新编译 Nginx。
在 Nginx 内部里嵌入 Lua 脚本,使得可以通过简单的 Lua 语言来扩展网关的功能,比如实现自定义的路由规则、过滤器、缓存策略等。
Lua 是一种非常快速的动态脚本语言,它的运行速度接近于 C 语言。
LuaJIT 是 Lua 的一个即时编译器,它可以显著提高 Lua 代码的执行效率。
LuaJIT 将一些常用的 Lua 函数和工具库预编译并缓存,这样在下次调用时就可以直接使用缓存的字节码,从而大大加快了执行速度。
Lua
自定义
OpenResty
一款基于 OpenResty (Nginx + Lua)的高性能、云原生、可扩展、生态丰富的网关系统。
Kong Server:基于 Nginx 的服务器,用来接收 API 请求。
Apache Cassandra/PostgreSQL:用来存储操作数据。
Kong Dashboard:官方推荐 UI 管理工具,当然,也可以使用 RESTful 方式 管理 Admin api。
默认使用 Apache Cassandra/PostgreSQL 存储数据,Kong 的整个架构比较臃肿,并且会带来高可用的问题。
Kong 提供了插件机制来扩展其功能,插件在 API 请求响应循环的生命周期中被执行。
Kong 本身就是一个 Lua 应用程序,并且是在 Openresty 的基础之上做了一层封装的应用。归根结底就是利用 Lua 嵌入 Nginx 的方式,赋予了 Nginx 可编程的能力,这样以插件的形式在 Nginx 这一层能够做到无限想象的事情。
Kong
APISIX 是一款基于 OpenResty 和 etcd 的高性能、云原生、可扩展的网关系统。
etcd 是使用 Go 语言开发的一个开源的、高可用的分布式 key-value 存储系统,使用 Raft 协议做分布式共识。
与传统 API 网关相比,APISIX 具有动态路由和插件热加载,特别适合微服务系统下的 API 管理。
APISIX 与 SkyWalking(分布式链路追踪系统)、Zipkin(分布式链路追踪系统)、Prometheus(监控系统) 等 DevOps 生态工具对接都十分方便。
作为 Nginx 和 Kong 的替代项目,APISIX 目前已经是 Apache 顶级开源项目,并且是最快毕业的国产开源项目。国内目前已经有很多知名企业(比如金山、有赞、爱奇艺、腾讯、贝壳)使用 APISIX 处理核心的业务流量。
APISIX 同样支持定制化的插件开发。开发者除了能够使用 Lua 语言开发插件,还能通过其它方式开发来避开 Lua 语言的学习成本。
通过 Plugin Runner 来支持更多的主流编程语言(比如 Java、Python、Go 等等)。通过这样的方式,可以让后端工程师通过本地 RPC 通信,使用熟悉的编程语言开发 APISIX 的插件。这样做的好处是减少了开发成本,提高了开发效率,但是在性能上会有一些损失。
使用 Wasm(WebAssembly) 开发插件。Wasm 被嵌入到了 APISIX 中,用户可以使用 Wasm 去编译成 Wasm 的字节码在 APISIX 中运行。
基于堆栈的虚拟机的二进制指令格式,一种低级汇编语言,旨在非常接近已编译的机器代码,并且非常接近本机性能。Wasm 最初是为浏览器构建的,但是随着技术的成熟,在服务器端看到了越来越多的用例。
Wasm
避开 Lua
APISIX
一款基于 WebFlux 的可扩展、高性能、响应式网关,Apache 顶级开源项目。
Shenyu 通过插件扩展功能,插件是 ShenYu 的灵魂,并且插件也是可扩展和热插拔的。不同的插件实现不同的功能。Shenyu 自带了诸如限流、熔断、转发、重写、重定向、和路由监控等插件。
Shenyu
网关系统
最常用的是 Spring Cloud Gateway、Kong、APISIX 这三个。
对于公司业务以 Java 为主要开发语言的情况下,Spring Cloud Gateway 通常是个不错的选择。
APISIX 基于 etcd 来做配置中心,不存在单点问题,云原生友好;而 Kong 基于 Apache Cassandra/PostgreSQL ,存在单点风险,需要额外的基础设施保障做高可用。
APISIX 支持热更新,并且实现了毫秒级别的热更新响应;而 Kong 不支持热更新。
APISIX 的性能要优于 Kong 。
APISIX 支持的插件更多,功能更丰富。
Kong 和 APISIX
选择
API网关
分布式 ID 是分布式系统下的 ID。
全局唯一:ID 的全局唯一性肯定是首先要满足的!
高性能:分布式 ID 的生成速度要快,对本地资源消耗要小。
高可用:生成分布式 ID 的服务要保证可用性无限接近于 100%。
方便易用:拿来即用,使用方便,快速接入!
安全:ID 中不包含敏感信息。
有序递增:如果要把 ID 存放在数据库的话,ID 的有序性可以提升数据库写入速度。并且,还很有可能会直接通过 ID 来进行排序。
有具体的业务含义:生成的 ID 如果能有具体的业务含义,可以让定位问题以及开发更透明化(通过 ID 就能确定是哪个业务)。
独立部署:也就是分布式系统单独有一个发号器服务,专门用来生成分布式 ID。这样就生成 ID 的服务可以和业务相关的服务解耦。不过,这样同样带来了网络调用消耗增加的问题。总的来说,如果需要用到分布式 ID 的场景比较多的话,独立部署的发号器服务还是很有必要的。
要求
通过关系型数据库的自增主键产生来唯一的 ID。
实现起来比较简单、ID 有序递增、存储消耗空间小
支持的并发量不大。
存在数据库单点问题(可以使用数据库集群解决,不过增加了复杂度)。
没有具体业务含义。
安全问题(比如根据订单 ID 的递增规律就能推算出每天的订单量,商业机密啊! )。
每次获取 ID 都要访问一次数据库(增加了对数据库的压力,获取速度也慢)。
数据库主键自增
基于数据库的号段模式来生成分布式 ID。
ID 有序递增、存储消耗空间小,数据库的访问次数更少,数据库压力更小。
和数据库主键自增方案的缺点类似。
数据库号段模式
一般情况下,NoSQL 方案使用 Redis 多一些。我们通过 Redis 的 incr 命令即可实现对 id 原子顺序递增。
性能不错并且生成的 ID 是有序递增的。
除了 Redis 之外,MongoDB ObjectId 经常也会被拿来当做分布式 ID 的解决方案。
NoSQL
数据库
UUID 是 Universally Unique Identifier(通用唯一标识符) 的缩写。UUID 包含 32 个 16 进制数字(8-4-4-4-12)。
版本 1 : UUID 是根据时间和节点 ID(通常是 MAC 地址)生成。
版本 2 : UUID 是根据标识符(通常是组或用户 ID)、时间和节点 ID 生成。
版本 3、版本 5 : 版本 5 - 确定性 UUID 通过散列(hashing)名字空间(namespace)标识符和名称生成。
版本 4 : UUID 使用随机性或伪随机性生成。
版本
生成速度比较快、简单易用。
存储消耗空间大(32 个字符串,128 位)。
不安全(基于 MAC 地址生成 UUID 的算法会造成 MAC 地址泄露)。
无序(非自增)。
需要解决重复 ID 问题(当机器时间不对的情况下,可能导致会产生重复 ID)。
UUID
Snowflake 是 Twitter 开源的分布式 ID 生成算法。Snowflake 由 64 bit 的二进制数字组成。
sign(1bit):符号位(标识正负),始终为 0,代表生成的 ID 为正数。
timestamp (41 bits):一共 41 位,用来表示时间戳,单位是毫秒,可以支撑 2 ^41 毫秒(约 69 年)。
datacenter id + worker id (10 bits):一般来说,前 5 位表示机房 ID,后 5 位表示机器 ID。这样就可以区分不同集群/机房的节点。
sequence (12 bits):一共 12 位,用来表示序列号。 序列号为自增值,代表单台机器每毫秒能够产生的最大 ID 数(2^12 = 4096)。
生成速度比较快、生成的 ID 有序递增、比较灵活(可以对 Snowflake 算法进行简单的改造比如加入业务 ID)。
需要解决重复 ID 问题(ID 生成依赖时间,在获取时间的时候,可能会出现时间回拨的问题)。
依赖机器 ID 对分布式环境不友好(当需要自动启停或增减机器时,固定的机器 ID 可能不够灵活)。
Snowflake(雪花算法)
UidGenerator 是百度开源的一款基于 Snowflake(雪花算法)的唯一 ID 生成器。
delta seconds (28 bits):当前时间,相对于时间基点\"2016-05-20\"的增量值,单位:秒,最多可支持约 8.7 年。
worker id (22 bits):机器 id,最多可支持约 420w 次机器启动。内置实现为在启动时由数据库分配,默认分配策略为用后即弃,后续可提供复用策略。
sequence (13 bits):每秒下的并发序列,13 bits 可支持每秒 8192 个并发。
UidGenerator
Leaf 是美团开源的一个分布式 ID 解决方案 。
Leaf 提供了 号段模式 和 Snowflake(雪花算法) 这两种模式来生成分布式 ID。
支持双号段,还解决了雪花 ID 系统时钟回拨问题。不过,时钟问题的解决需要弱依赖于 Zookeeper。
在 4C8G VM 基础上,通过公司 RPC 方式调用,QPS 压测结果近 5w/s,TP999 1ms。
性能
Leaf
Tinyid 是滴滴开源的一款基于数据库号段模式的唯一 ID 生成器。
双号段缓存:为了避免在获取新号段的情况下,程序获取唯一 ID 的速度比较慢。Tinyid 中的号段在用到一定程度的时候,就会去异步加载下一个号段,保证内存中始终有可用号段。
增加多 db 支持:支持多个 DB,并且,每个 DB 都能生成唯一 ID,提高了可用性。
增加 tinyid-client:纯本地操作,无 HTTP 请求消耗,性能和可用性都有很大提升。
优化
Tinyid
和 UidGenerator、Leaf 一样,IdGenerator 也是一款基于 Snowflake(雪花算法)的唯一 ID 生成器。
生成的唯一 ID 更短。
兼容所有雪花算法(号段模式或经典模式,大厂或小厂)。
原生支持 C#/Java/Go/C/Rust/Python/Node.js/PHP(C 扩展)/SQL等语言,并提供多线程安全调用动态库(FFI)。
解决了时间回拨问题,支持手工插入新 ID(当业务需要在历史时间生成新 ID 时,用本算法的预留位能生成 5000 个每秒)。
不依赖外部存储系统。
默认配置下,ID 可用 71000 年不重复。
IdGenerator
开源框架
方案
分布式ID
当多个进程不在同一个系统中,用分布式锁控制多个进程对资源的访问。
互斥:任意一个时刻,锁只能被一个线程持有。
高可用:锁服务是高可用的,当一个锁服务出现问题,能够自动切换到另外一个锁服务。
可重入:一个节点获取了锁之后,还可以再次获取锁。
基本
高性能:获取和释放锁的操作应该快速完成,并且不应该对整个系统的性能造成过大影响。
非阻塞:如果获取不到锁,不能无限期等待,避免对系统正常运行造成影响。
优秀
条件
基于关系型数据库比如 MySQL 实现分布式锁。
基于分布式协调服务 ZooKeeper 实现分布式锁。
基于分布式键值存储系统比如 Redis 、Etcd 实现分布式锁。
在 Redis 中, SETNX 命令是可以帮助我们实现互斥。SETNX 即 SET if Not eXists (对应 Java 中的 setIfAbsent 方法)。
实现方式比较简单,性能也很高效。
释放锁的逻辑突然挂掉,可能会导致锁无法被释放,进而造成共享资源无法再被其他线程/进程访问。
问题:过期时间具体数值较难界定。
过期时间
一个开源的 Java 语言 Redis 客户端,提供了很多开箱即用的功能,不仅仅包括多种分布式锁的实现。
Redisson 还支持 Redis 单机、Redis Sentinel、Redis Cluster 等多种部署架构。
Redisson 中的分布式锁自带自动续期机制,使用起来非常简单,原理也比较简单,其提供了一个专门用来监控和续期锁的 Watch Dog( 看门狗),如果操作共享资源的线程还未执行完成的话,Watch Dog 会不断地延长锁的过期时间,进而保证锁不会因为超时而被释放。
Redisson
锁释放/续期
在一个线程中可以多次获取同一把锁。Java 中的 synchronized 和 ReentrantLock 都属于可重入锁。
不可重入的分布式锁基本可以满足绝大部分业务场景了,一些特殊的场景可能会需要使用可重入的分布式锁。
线程在获取锁的时候判断是否为自己的锁,如果是的话,就不用再重新获取了。
推荐 Redisson ,其内置了多种类型的锁,比如可重入锁(Reentrant Lock)、自旋锁(Spin Lock)、公平锁(Fair Lock)、多重锁(MultiLock)、 红锁(RedLock)、 读写锁(ReadWriteLock)。
可重入锁
让客户端向 Redis 集群中的多个独立的 Redis 实例依次请求申请加锁,如果客户端能够和半数以上的实例成功地完成加锁操作,那么我们就认为,客户端成功地获得分布式锁,否则加锁失败。
即使部分 Redis 节点出现问题,只要保证 Redis 集群中有半数以上的 Redis 节点可用,分布式锁服务就是正常的。
Redlock 是直接操作 Redis 节点的,并不是通过 Redis 集群操作的,这样才可以避免 Redis 集群主从切换导致的锁丢失问题。
Redlock 实现比较复杂,性能比较差,发生时钟变迁的情况下还存在安全性隐患。(不推荐)
Redlock 算法
Redis
ZooKeeper 分布式锁是基于 临时顺序节点 和 Watcher(事件监听器) 实现的。
首先我们要有一个持久节点/locks,客户端获取锁就是在locks下创建临时顺序节点。
假设客户端 1 创建了/locks/lock1节点,创建成功之后,会判断 lock1是否是 /locks 下最小的子节点。
如果 lock1是最小的子节点,则获取锁成功。否则,获取锁失败。
如果获取锁失败,则说明有其他的客户端已经成功获取锁。客户端 1 并不会不停地循环去尝试加锁,而是在前一个节点比如/locks/lock0上注册一个事件监听器。这个监听器的作用是当前一个节点释放锁之后通知客户端 1(避免无效自旋),这样客户端 1 就加锁成功了。
获取锁
成功获取锁的客户端在执行完业务流程之后,会将对应的子节点删除。
成功获取锁的客户端在出现故障之后,对应的子节点由于是临时顺序节点,也会被自动删除,避免了锁无法被释放。
我们前面说的事件监听器其实监听的就是这个子节点删除事件,子节点删除就意味着锁被释放。
释放锁
Curator 是 Netflix 公司开源的一套 ZooKeeper Java 客户端框架。
相比于 ZooKeeper 自带的客户端 zookeeper 来说,Curator 的封装更加完善,各种 API 都可以比较方便地使用。
实际项目中,推荐使用 Curator 来实现 ZooKeeper 分布式锁。
InterProcessMutex:分布式可重入排它锁。
InterProcessSemaphoreMutex:分布式不可重入排它锁。
InterProcessReadWriteLock:分布式读写锁。
InterProcessMultiLock:将多个锁作为单个实体管理的容器,获取锁的时候获取所有锁,释放锁也会释放所有锁资源(忽略释放失败的锁)。
主要实现了四种锁
整个可重入锁的实现逻辑非常简单,直接在客户端判断当前线程有没有获取锁,有的话直接将加锁次数加 1 就可以了。
Curator
每个数据节点在 ZooKeeper 中被称为 znode,它是 ZooKeeper 中数据的最小单元。
持久(PERSISTENT)节点:一旦创建就一直存在即使 ZooKeeper 集群宕机,直到将其删除。
临时(EPHEMERAL)节点:临时节点的生命周期是与客户端会话(session)绑定的,会话消失则节点消失 。临时节点只能做叶子节点 ,不能创建子节点。
持久顺序(PERSISTENT_SEQUENTIAL)节点:除了具有持久(PERSISTENT)节点的特性之外, 子节点的名称还具有顺序性。
临时顺序(EPHEMERAL_SEQUENTIAL)节点:除了具备临时(EPHEMERAL)节点的特性之外,子节点的名称还具有顺序性。
分类
临时节点相比持久节点,最主要的是对会话失效的情况处理不一样,临时节点会话消失则对应的节点消失。这样的话,如果客户端发生异常导致没来得及释放锁也没关系,会话失效节点自动被删除,不会发生死锁的问题。
使用顺序节点之后,只需要监听前一个节点就好了,对性能更友好。
znode
ZooKeeper 中的一个很重要的特性。ZooKeeper 允许用户在指定节点上注册一些 Watcher,并且在一些特定事件触发的时候,ZooKeeper 服务端会将事件通知到感兴趣的客户端上去,该机制是 ZooKeeper 实现分布式协调服务的重要特性。
当前一个节点对应的客户端释放锁之后(也就是前一个节点被删除之后,监听的是删除事件),通知获取锁失败的客户端(唤醒等待的线程,Java 中的 wait/notifyAll ),让它尝试去获取锁,然后就成功获取锁了。
Watcher(事件监听器)
ZooKeeper
如果对性能要求比较高的话,建议使用 Redis 实现分布式锁(优先选择 Redisson 提供的现成的分布式锁,而不是自己实现)。
如果对可靠性要求比较高的话,建议使用 ZooKeeper 实现分布式锁(推荐基于 Curator 框架实现)。但 ZooKeeper 使用率低,单因分布式锁,不可取。
分布式锁
微服务架构下,系统被拆分为多个微服务,而每个微服务都可能有单独的机器或数据库,这时,⼀组操作可能会涉及到多个微服务以及多个数据库。
分布式事务的终极⽬标就是保证系统中多个相关联的数据库中的数据的⼀致性!
业界⽐较推崇是 最终⼀致性,但是某些对数据⼀致要求⼗分严格的场景⽐如银⾏转账还是要保证强⼀致性。
CAP 理论和 BASE 理论
BASE 理论 + 业务实践,追求最终⼀致性。
根据⾃身业务特性,通过适当的⽅式来保证系统数据的最终⼀致性。 像 TCC、Saga、MQ 事务 、本地消息表 就属于柔性事务。
目标
柔性事务
刚性事务追求的是强⼀致性。像2PC 、3PC 就属于刚性事务。
刚性事务
2PC(Two-Phase Commit)两阶段提交协议
2 -> 指代事务提交的 2 个阶段。
P-> Prepare (准备阶段)。
C ->Commit(提交阶段)。
准备阶段的核⼼是“询问”事务参与者执⾏本地数据库事务操作是否成功。
测试 事务参与者 能否执⾏ 本地数据库事务 操作(!!!注意:这⼀步并不会提交事务)。
目的
事务协调者/管理者 向所有参与者发送消息询问:“你是否可以执⾏事务操作呢?”,并等待其答复。
事务参与者 接收到消息之后,开始执⾏本地数据库事务预操作⽐如写 redolog/undo log ⽇志。但是 ,此时并不会提交事务!
事务参与者 如果执⾏本地数据库事务操作成功,那就回复:“就绪”,否则就回复:“未就绪”。
准备阶段(Prepare)
提交阶段的核⼼是“询问”事务参与者提交事务是否成功。
事务协调者/管理者 会根据 准备阶段 中 事务参与者 的消息来决定是执⾏事务提交还是回滚操作。
事务协调者/管理者 向所有参与者发送消息:“你们可以提交事务啦!”(commit 消息)
事务参与者 接收到 commit 消息 后执⾏ 提交本地数据库事务 操作,执⾏完成之后 释放整个事务期间所占⽤的资源。
事务参与者 回复:“事务已经提交” (ack 消息)。
事务协调者/管理者 收到所有 事务参与者 的 ack 消息 之后,整个分布式事务过程正式结束。
就绪
事务协调者/管理者 向所有参与者发送消息:“你们可以执⾏回滚操作了!”(rollback 消息)。
事务参与者 接收到 rollback 消息 后执⾏ 本地数据库事务回滚 执⾏完成之后 释放整个事务期间所占⽤的资源。
事务参与者 回复:“事务已经回滚” (ack 消息)。
事务协调者/管理者 收到所有 事务参与者 的 ack 消息 之后,取消事务。
未就绪
提交阶段(Commit)
提交阶段 之后⼀定会结束当前的分布式事务。
阶段
实现起来⾮常简单,各⼤主流数据库⽐如 MySQL、Oracle 都有⾃⼰实现。
针对的是数据强⼀致性。不过,仍然可能存在数据不⼀致的情况。
同步阻塞 :事务参与者会在正式提交事务之前会⼀直占⽤相关的资源。
数据不⼀致 :由于⽹络问题或者事务协调者/管理者宕机都有可能会造成数据不⼀致的情况。
单点问题 : 事务协调者/管理者在其中也是⼀个很重要的⻆⾊,如果在准备(Prepare)阶段完成之后挂掉,事务参与者就会⼀直卡在提交(Commit)阶段。
2PC
3PC 是⼈们在 2PC 的基础上做了⼀些优化。把 2PC 中的 准备阶段(Prepare) 做了进⼀步细化。
询问阶段(CanCommit) :这⼀步 不会执⾏事务操作,只会询问事务参与者能否执⾏本地数据库事操作。
准备阶段(PreCommit) :当所有事物参与者都返回“可执⾏”之后, 事务参与者才会执⾏本地数据库事务预操作⽐如写 redo log/undo log ⽇志。
细化内容
除此之外,3PC 还引⼊了 超时机制 来避免事务参与者⼀直阻塞占⽤资源。
3PC
TCC 是 Try、Confirm、Cancel 三个词的缩写,属于⽬前⽐较⽕的⼀种柔性事务解决⽅案。
Try(尝试)阶段 : 尝试执⾏。完成业务检查,并预留好必需的业务资源。
Confirm(确认)阶段 :确认执⾏。当所有事务参与者的 Try 阶段执⾏成功就会执⾏ Confirm ,Confirm 阶段会处理 Try 阶段预留的业务资源。否则,就会执⾏ Cancel 。
Cancel(取消)阶段 :取消执⾏,释放 Try 阶段预留的业务资源。
TCC 模式不需要依赖于底层数据资源的事务⽀持,但是需要我们⼿动实现更多的代码,属于 侵⼊业务代码 的⼀种分布式解决⽅案。
ByteTCC : ByteTCC 是基于 Try-Confirm-Cancel(TCC)机制的分布式事务管理器的实现。
Seata : Seata 是⼀款开源的分布式事务解决⽅案,致⼒于在微服务架构下提供⾼性能和简单易⽤的分布式事务服务。
Hmily : ⾦融级分布式事务解决⽅案。
TCC
RocketMQ 、 Kafka、Pulsar 、QMQ都提供了事务相关的功能。事务允许事件流应⽤将消费,处理,⽣产消息整个过程定义为⼀个原⼦操作。
MQ 发送⽅(⽐如物流服务)在消息队列上开启⼀个事务,然后发送⼀个“半消息”给 MQ Server/Broker。事务提交之前,半消息对于 MQ 订阅⽅/消费者(⽐如第三⽅通知服务)不可⻅。
“半消息”发送成功的话,MQ 发送⽅就开始执⾏本地事务。
MQ 发送⽅的本地事务执⾏成功的话,“半消息”变成正常消息,可以正常被消费。MQ 发送⽅的本地事务执⾏失败的话,会直接回滚。
MQ 的事务消息使⽤的是两阶段提交(2PC),简单来说就是咱先发送半消息,等本地事务执⾏成功之后,半消息才变为正常消息。
RocketMQ 中的 Broker 会定期去 MQ 发送⽅上反查这个事务的本地事务的执⾏情况,并根据反查结果决定提交或者回滚这个事务。
消息消费失败的话,RocketMQ 会⾃动进⾏消费重试。如果超过最⼤重试次数这个消息还是没有正确消费,RocketMQ 就会认为这个消息有问题,然后将其放到死信队列。
MQ 事务
Saga 属于⻓事务解决⽅案,其核⼼思想是将⻓事务拆分为多个本地短事务(本地短事务序列)。
简介:如果 Ti 短事务提交失败,则补偿所有已完成的事务(⼀直执⾏ Ci 对 Ti 进⾏补偿)。
执⾏顺序:T1,T2,…,Ti(失败),Ci(补偿),…,C2,C1。
反向恢复
简介:如果 Ti 短事务提交失败,则⼀直对 Ti 进⾏重试,直⾄成功为⽌。
执⾏顺序:T1,T2,…,Ti(失败),Ti(重试)…,Ti+1,…,Tn。
正向恢复
TCC 类似,Saga 正向操作与补偿操作都需要业务开发者⾃⼰实现,因此也属于 侵⼊业务代码 的⼀种分布式解决⽅案。
和 TCC 很⼤的⼀点不同是 Saga 没有“Try” 动作,它的本地事务 Ti 直接被提交。因此,性能⾮常⾼!
与 TCC
因为 Saga 没有进⾏“Try” 动作预留资源,所以不能保证隔离性。
理论上,补偿操作⼀定能够执⾏成功。不过,当⽹络出现问题或者服务器宕机的话,补偿操作也会执⾏失败。这种情况下,往往需要我们进⾏⼈⼯⼲预。
ServiceComb Pack :微服务应⽤的数据最终⼀致性解决⽅案。
Seata :Seata 是⼀款开源的分布式事务解决⽅案,致⼒于在微服务架构下提供⾼性能和简单易⽤的分布式事务服务。
Saga
分布式事务
属于 Spring Cloud ⽣态组件,可以和 Spring Cloud 体系⽆缝整合。由于基于 Git 存储配置,因此 Spring Cloud Config 的整体设计很简单。
Spring Cloud Config
Nacos 阿里开源,社区活跃,使⽤起来⽐较简单,并且还可以直接⽤来做服务发现及管理。
Apollo 携程开源,社区活跃,只能⽤来做配置管理,使⽤相对复杂⼀些。
Apollo
技术选型是 Kubernetes 的话,可以考虑使⽤ K8s ConfigMap 来作为配置中⼼。
K8s ConfigMap
已经没有维护,⽣态也并不活跃,并不建议使⽤,在做配置中⼼技术选型的时候可以跳过。
Disconf 和 Qconf
配置中心
权限控制 :配置的修改、发布等操作需要严格的权限控制。
⽇志记录 : 配置的修改、发布等操需要记录完整的⽇志,便于后期排查问题。
配置推送 : 推送模式通常为推、拉、推拉结合。
灰度发布 :⽀持配置只推给部分应⽤。
易操作 : 提供 Web 界⾯⽅便配置修改和发布。
版本跟踪 :所有的配置发布都有版本概念,从⽽可以⽅便的⽀持配置的回滚。
⽀持配置回滚 : 我们⼀键回滚配置到指定的位置,这个需要和版本跟踪结合使⽤。
功能需求
分布式配置
RPC(Remote Procedure Call) 即远程过程调用,通过名字我们就能看出 RPC 关注的是远程调用而非本地调用。
通过 RPC 可以帮助我们调用远程计算机上某个服务的方法,这个过程就像调用本地方法一样简单。
客户端(服务消费端):调用远程方法的一端。
客户端 Stub(桩):这其实就是一代理类。代理类主要做的事情很简单,就是把你调用方法、类、方法参数等信息传递到服务端。
网络传输:网络传输就是要把调用的方法的信息传输到服务端,然后服务端执行完之后再把返回结果通过网络传输传输回来。网络传输的实现方式有很多种,比如最近基本的 Socket 或者性能以及封装更加优秀的 Netty(推荐)。
服务端 Stub(桩):这个桩就不是代理类了。这里的服务端 Stub 实际指的就是接收到客户端执行方法的请求后,去执行对应的方法然后返回结果给客户端的类。
服务端(服务提供端):提供远程方法的一端。
服务消费端(client)以本地调用的方式调用远程服务。
客户端 Stub(client stub) 接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体(序列化):RpcRequest。
客户端 Stub(client stub) 找到远程服务的地址,并将消息发送到服务提供端。
服务端 Stub(桩)收到消息将消息反序列化为 Java 对象: RpcRequest。
服务端 Stub(桩)根据RpcRequest中的类、方法、方法参数等信息调用本地的方法。
服务端 Stub(桩)得到方法执行结果并将组装成能够进行网络传输的消息体:RpcResponse(序列化)发送至消费方。
客户端 Stub(client stub)接收到消息并将消息反序列化为 Java 对象:RpcResponse ,这样也就得到了最终结果。
Apache Dubbo 是一款微服务框架,为大规模微服务实践提供高性能 RPC 通信、流量治理、可观测性等解决方案,涵盖 Java、Golang 等多种语言。
Dubbo 提供了从服务定义、服务发现、服务通信到流量管控等几乎所有的服务治理能力,支持 Triple 协议(基于 HTTP/2 之上定义的下一代 RPC 通信协议)、应用级服务发现、Dubbo Mesh (Dubbo3 赋予了很多云原生友好的新特性)等特性。
Dubbo 是由阿里开源,后来加入了 Apache 。正是由于 Dubbo 的出现,才使得越来越多的公司开始使用以及接受分布式架构。
Dubbo 除了能够应用在分布式系统中,也可以应用在现在比较火的微服务系统中。但由于 Spring Cloud 在微服务中应用更加广泛,所以,一般提 Dubbo,大部分是分布式系统的情况。
面向接口代理的高性能 RPC 调用。
智能容错和负载均衡。
服务自动注册和发现。
高度可扩展能力。
运行期流量调度。
可视化的服务治理与运维。
Dubbo 不光可以调用远程服务,还提供了一些其他开箱即用的功能,比如智能负载均衡。
核心能力
把整个系统拆分成不同的服务然后将这些服务放在不同的服务器上减轻单体服务的压力提高并发量和性能。
分布式
Container: 服务运行容器,负责加载、运行服务提供者。必须。
Provider: 暴露服务的服务提供方,会向注册中心注册自己提供的服务。必须。
Consumer: 调用远程服务的服务消费方,会向注册中心订阅自己所需的服务。必须。
Registry: 服务注册与发现的注册中心。注册中心会返回服务提供者地址列表给消费者。非必须。
Monitor: 统计服务的调用次数和调用时间的监控中心。服务消费者和提供者会定时发送统计数据到监控中心。 非必须。
核心角色
Invoker 是 Dubbo 领域模型中非常重要的一个概念,就是 Dubbo 对远程调用的抽象。
服务提供 Invoker
服务消费 Invoker
Invoker
proxy 服务代理层:调用远程方法像调用本地的方法一样简单的一个关键,真实调用过程依赖代理类,以 ServiceProxy 为中心。
registry 注册中心层:封装服务地址的注册与发现。
cluster 路由层:封装多个提供者的路由及负载均衡,并桥接注册中心,以 Invoker 为中心。
monitor 监控层:RPC 调用次数和调用时间监控,以 Statistics 为中心。
transport 网络传输层:抽象 mina 和 netty 为统一接口,以 Message 为中心。
serialize 数据序列化层:对需要在网络传输的数据进行序列化。
整体设计分层
SPI(Service Provider Interface) 机制被大量用在开源项目中,它可以帮助我们动态寻找服务/功能(比如负载均衡策略)的实现。
将接口的实现类放在配置文件中,在程序运行过程中读取配置文件,通过反射加载实现类。这样可以在运行时,动态替换接口的实现类。
Java 本身就提供了 SPI 机制的实现。不过,Dubbo 没有直接用,而是对 Java 原生的 SPI 机制进行了增强,以便更好满足自己的需求。
创建对应的实现类 XxxLoadBalance 实现 LoadBalance 接口或者 AbstractLoadBalance 类。
将这个实现类的路径写入到 resources 目录下的 META-INF/dubbo/org.apache.dubbo.rpc.cluster.LoadBalance 文件中即可。
自定义负载均衡策略
SPI
微内核架构模式(有时被称为插件架构模式)是实现基于产品应用程序的一种自然模式。可让用户添加额外的应用如插件,到核心应用,继而提供了可扩展性和功能分离的用法。
微内核架构包含两类组件:核心系统(core system) 和 插件模块(plug-in modules)。
核心系统提供系统所需核心能力,插件模块可以扩展系统的功能。因此, 基于微内核架构的系统,非常易于扩展功能。
我们常见的一些 IDE,都可以看作是基于微内核架构设计的。绝大多数 IDE 比如 IDEA、VSCode 都提供了插件来丰富自己的功能。
Dubbo 采用 微内核(Microkernel) + 插件(Plugin) 模式,简单来说就是微内核架构。微内核只负责组装插件。
通常情况下,微核心都会采用 Factory、IoC、OSGi 等方式管理插件生命周期。Dubbo 不想依赖 Spring 等 IoC 容器,也不想自己造一个小的 IoC 容器(过度设计),因此采用了一种最简单的 Factory 方式管理插件:JDK 标准的 SPI 扩展机制 (java.util.ServiceLoader)。
微内核架构
旨在优化资源使用,最大化吞吐量,最小化响应时间,并避免任何单个资源的过载。
负载均衡
在集群负载均衡时,Dubbo 提供了多种均衡策略,默认为 random 随机调用。
在 Dubbo 中,所有负载均衡实现类均继承自 AbstractLoadBalance,该类实现了 LoadBalance 接口,并封装了一些公共的逻辑。
策略
根据权重随机选择(对加权随机算法的实现)。这是 Dubbo 默认采用的一种负载均衡策略。
RandomLoadBalance
最小活跃数负载均衡
Dubbo 就认为谁的活跃数越少,谁的处理速度就越快,性能也越好,这样就优先把请求给活跃数少的服务提供者处理。
如果有多个服务提供者的活跃数相等,那就再走一遍 RandomLoadBalance 。
LeastActiveLoadBalance
一致性 Hash 负载均衡策略
没有权重的概念,具体是哪个服务提供者处理请求是由你的请求的参数决定的,也就是说相同参数的请求总是发到同一个服务提供者。
为避免数据倾斜问题(节点不够分散,大量请求落到同一节点),还引入虚拟节点的概念。通过虚拟节点可以让节点更加分散,有效均衡各个节点的请求量。
ConsistentHashLoadBalance
加权轮询负载均衡
轮询就是把请求依次分配给每个服务提供者。加权轮询就是在轮询的基础上,让更多的请求落到权重更大的服务提供者上。
RoundRobinLoadBalance
负载均衡策略
Dubbo 支持多种序列化方式:JDK 自带的序列化、hessian2(默认)、JSON、Kryo、FST、Protostuff,ProtoBuf 等等。
序列化协议
Dubbo
Motan 是新浪微博开源的一款 RPC 框架,据说在新浪微博正支撑着千亿次调用。不过很少看到有公司使用,而且网上的资料也比较少。
Motan 更像是一个精简版的 Dubbo,可能是借鉴了 Dubbo 的思想,Motan 的设计更加精简,功能更加纯粹。
Motan
gRPC 是 Google 开源的一个高性能、通用的开源 RPC 框架。
面向移动应用开发并基于 HTTP/2 协议标准而设计(支持双向流、消息头压缩等功能,更加节省带宽),基于 ProtoBuf 序列化协议,支持众多开发语言。
一种更加灵活、高效的数据格式,可用于通讯协议、数据存储等领域,基本支持所有主流编程语言且与平台无关。
不过,通过 ProtoBuf 定义接口和数据类型还挺繁琐的,这是一个小问题。
ProtoBuf( Protocol Buffer)
gRPC 的通信层的设计还是非常优秀的,Dubbo-go 3.0 的通信层改进主要借鉴了 gRPC。
gRPC 的设计导致其几乎没有服务治理能力。如果要解决这个问题,就需要依赖其他组件,比如腾讯的 PolarisMesh(北极星)。
gRPC
Apache Thrift 是 Facebook 开源的跨语言的 RPC 通信框架,目前已经捐献给 Apache 基金会管理。
由于其跨语言特性和出色的性能,在很多互联网公司应用,有能力的公司甚至会基于 thrift 研发一套分布式服务框架,增加诸如服务注册、服务发现等功能。
Thrift 支持多种不同的编程语言,包括C++、Java、Python、PHP、Ruby等(相比于 gRPC 支持的语言更多 )。
Thrift
框架
RPC
ZooKeeper 是一个开源的分布式协调服务。设计目标是将那些复杂且容易出错的分布式一致性服务封装起来,构成一个高效可靠的原语集,并以一系列简单易用的接口提供给用户使用。
操作系统或计算机网络用语范畴。由若干条指令组成,用于完成一定功能的一个过程。具有不可分割性,即原语的执行必须是连续的,在执行过程中不允许被中断。
原语
ZooKeeper 为我们提供了高可用、高性能、稳定的分布式数据一致性解决方案。
ZooKeeper 将数据保存在内存中,性能是不错的。 在“读”多于“写”的应用程序中尤其地高性能,因为“写”会导致所有的服务器间同步状态。
数据发布/订阅、负载均衡、命名服务、分布式协调/通知、集群管理、Master 选举、分布式锁和分布式队列等功能。
这些功能的实现主要依赖于 ZooKeeper 提供的 数据存储+事件监听 功能。
顺序一致性: 从同一客户端发起的事务请求,最终将会严格地按照顺序被应用到 ZooKeeper 中去。
原子性: 所有事务请求的处理结果在整个集群中所有机器上的应用情况是一致的。
单一系统映像: 无论客户端连到哪一个 ZooKeeper 服务器上,其看到的服务端数据模型都是一致的。
可靠性: 一旦一次更改请求被应用,更改的结果就会被持久化,直到被下一次更改覆盖。
实时性: 每个客户端的系统视图都是最新的。
采用层次化的多叉树形结构,每个节点上都可以存储数据,这些数据可以是数字、字符串或者是二进制序列。
每个节点还可以拥有 N 个子节点,最上层是根节点以“/”来代表。
每个数据节点在 ZooKeeper 中被称为 znode,它是 ZooKeeper 中数据的最小单元。每个 znode 都有一个唯一的路径标识。
ZooKeeper 主要是用来协调服务的,而不是用来存储业务数据的,所以不要放比较大的数据在 znode 上,每个节点的数据大小上限是 1M 。
Data model(数据模型)
stat:状态信息。
data:节点存放的数据的具体内容。(包括版本信息)
znode(数据节点)
dataVersion:当前 znode 节点的版本号
cversion:当前 znode 子节点的版本
aclVersion:当前 znode 的 ACL 的版本。
版本(version)
ZooKeeper 采用 ACL(AccessControlLists)策略来进行权限控制,类似于 UNIX 文件系统的权限控制。
CREATE : 能创建子节点。
READ:能获取节点数据和列出其子节点。
WRITE : 能设置/更新节点数据。
DELETE : 能删除子节点。
ADMIN : 能设置节点 ACL 的权限。
CREATE 和 DELETE 这两种权限都是针对 子节点 的权限控制。
znode 操作权限
world:默认方式,所有用户都可无条件访问。
auth:不使用任何 id,代表任何已认证的用户。
digest:用户名/密码认证方式:username:password 。
ip : 对指定 ip 进行限制。
身份认证
ACL(权限控制)
Watcher(事件监听器)是 ZooKeeper 中的一个很重要的特性。ZooKeeper 允许用户在指定节点上注册一些 Watcher,并且在一些特定事件触发的时候,ZooKeeper 服务端会将事件通知到感兴趣的客户端上去,该机制是 ZooKeeper 实现分布式协调服务的重要特性。
Session 可以看作是 ZooKeeper 服务器与客户端的之间的一个 TCP 长连接。通过连接,客户端能够通过心跳检测与服务器保持有效的会话,也能向服务器发送请求并接受响应,还能通过该连接接收来自服务器的 Watcher 事件通知。
在为客户端创建会话之前,服务端首先会为每个客户端都分配一个 sessionID。由于 sessionID是 ZooKeeper 会话的一个重要标识,许多与会话相关的运行机制都是基于这个 sessionID 的,因此,无论是哪台服务器为客户端分配的 sessionID,都务必保证全局唯一。
会话(Session)
重要概念
为了保证高可用,最好是以集群形态来部署 ZooKeeper,这样只要集群中大部分机器是可用的(能够容忍一定的机器故障),那么仍然是可用的。
集群间通过 ZAB 协议(ZooKeeper Atomic Broadcast)来保持数据的一致性。
最典型集群模式:Master/Slave 模式(主备模式)。通常 Master 服务器作为主服务器提供写服务,其他的 Slave 服务器从服务器通过异步复制的方式获取 Master 服务器最新的数据提供读服务。
在 ZooKeeper 中没有选择传统的 Master/Slave 概念,而是引入了 Leader、Follower 和 Observer 三种角色。
为客户端提供读和写的服务,负责投票的发起和决议,更新系统状态。
Leader
为客户端提供读服务,如果是写服务则转发给 Leader。参与选举过程中的投票。
Follower
为客户端提供读服务,如果是写服务则转发给 Leader。不参与选举过程中的投票,也不参与“过半写成功”策略。在不影响写性能的情况下提升集群的读性能。此角色于 ZooKeeper3.3 系列新增的角色。
Observer
集群角色
当 Leader 服务器出现网络中断、崩溃退出与重启等异常情况时,就会进入 Leader 选举过程,这个过程会选举产生新的 Leader 服务器。
触发
Leader election(选举阶段):节点在一开始都处于选举阶段,只要有一个节点得到超半数节点的票数,它就可以当选准 leader。
Discovery(发现阶段):在这个阶段,followers 跟准 leader 进行通信,同步 followers 最近接收的事务提议。
Synchronization(同步阶段):主要是利用 leader 前一阶段获得的最新提议历史,同步集群中所有的副本。同步完成之后准 leader 才会成为真正的 leader。
Broadcast(广播阶段):这个阶段才能正式对外提供事务服务,并且 leader 可以进行消息广播。同时如果有新的节点加入,还需要对新节点进行同步。
LOOKING:寻找 Leader。
LEADING:Leader 状态,对应的节点为 Leader。
FOLLOWING:Follower 状态,对应的节点为 Follower。
OBSERVING:Observer 状态,对应节点为 Observer,该节点不参与 Leader 选举。
服务器状态
选举过程
ZooKeeper 集群在宕掉几个 ZooKeeper 服务器之后,如果剩下的 ZooKeeper 服务器个数大于宕掉的个数的话整个 ZooKeeper 才依然可用。
假如我们有 3 台,那么最大允许宕掉 1 台 ZooKeeper 服务器,如果我们有 4 台的的时候也同样只允许宕掉 1 台。
奇数台
保证可用性的同时,会发生一种机房间网络线路故障,导致机房间网络不通,而集群被割裂成几个小集群,这时候子集群各自选主导致“脑裂”的情况。
ZooKeeper 的过半机制导致不可能产生 2 个 leader,因为少于等于一半是不可能产生 leader 的,这就使得不论机房的机器如何分配都不可能发生脑裂。
防止
脑裂
集群
Paxos 算法是 ZooKeeper 的灵魂。但 ZooKeeper 并没有完全采用 Paxos 算法 ,而是使用 ZAB 协议作为其保证数据一致性的核心算法。
ZAB(ZooKeeper Atomic Broadcast 原子广播) 协议是为分布式协调服务 ZooKeeper 专门设计的一种支持崩溃恢复的原子广播协议。
ZooKeeper 主要依赖 ZAB 协议来实现分布式数据一致性,基于该协议实现了一种主备模式的系统架构来保持集群中各个副本之间的数据一致性。
崩溃恢复:当整个服务框架在启动过程中,或是当 Leader 服务器出现网络中断、崩溃退出与重启等异常情况时,ZAB 协议就会进入恢复模式并选举产生新的 Leader 服务器。当选举产生了新的 Leader 服务器,同时集群中已经有过半的机器与该 Leader 服务器完成了状态同步之后,ZAB 协议就会退出恢复模式。其中,所谓的状态同步是指数据同步,用来保证集群中存在过半的机器能够和 Leader 服务器的数据状态保持一致。
消息广播:当集群中已经有过半的 Follower 服务器完成了和 Leader 服务器的状态同步,那么整个服务框架就可以进入消息广播模式了。 当一台同样遵守 ZAB 协议的服务器启动后加入到集群中时,如果此时集群中已经存在一个 Leader 服务器在负责进行消息广播,那么新加入的服务器就会自觉地进入数据恢复模式:找到 Leader 所在的服务器,并与其进行数据同步,然后一起参与到消息广播流程中去。
模式
ZAB协议
Kafka : ZooKeeper 主要为 Kafka 提供 Broker 和 Topic 的注册以及多个 Partition 的负载均衡等功能。不过,在 Kafka 2.8 之后,引入了基于 Raft 协议的 KRaft 模式,不再依赖 Zookeeper,大大简化了 Kafka 的架构。
Hbase : ZooKeeper 为 Hbase 提供确保整个集群只有一个 Master 以及保存和提供 regionserver 状态信息(是否在线)等功能。
Hadoop : ZooKeeper 为 Namenode 提供高可用支持。
命名服务:可以通过 ZooKeeper 的顺序节点生成全局唯一 ID。
数据发布/订阅:通过 Watcher 机制 可以很方便地实现数据发布/订阅。当你将数据发布到 ZooKeeper 被监听的节点上,其他机器可通过监听 ZooKeeper 上节点的变化来实现配置的动态更新。
分布式锁:通过创建唯一节点获得分布式锁,当获得锁的一方执行完相关代码或者是挂掉之后就释放锁。分布式锁的实现也需要用到 Watcher 机制。
这些功能的实现基本都得益于 ZooKeeper 可以保存数据的功能,但是 ZooKeeper 不适合保存大量数据,这一点需要注意。
0 条评论
回复 删除
下一页