一、一致性协议
¥1. CAP理论
CAP理论是分布式系统设计中的一套指导原则,它指出在网络分区的情况下,一个分布式系统最多只能同时满足以下三点中的两点:
- 一致性(Consistency):所有节点在同一时间看到的数据是一致的
- 可用性(Availability):每个请求都能得到响应,即使某些节点出现故障。
- 分区容忍性(Partition tolerance):系统能够处理网络分区的情况,即部分节点之间的通信受阻,而其他节点之间通信正常
2. 单主协议
由一个主节点发出数据,传输给其余从节点,能保证数据传输的有序性
-
Paxos协议:如果存在大部分节点共同接收了该更新内容,可以认为本次更新已经达成一致。过于理论化,难以直接工程化
-
Raft协议
3. Raft协议
数据传输流程(两阶段提交):
- Client向leader发送写请求指令,leader将该请求写入日志(但没提交)
- leader去通知其他follower
- follower收到通知后也将该请求写入日志(但没提交),并回复leader
- 如果 leader收到半数节点的回复,则提交之前没提交的日志,并通知其他follower也提交日志
超过半数同意为什么能保证最终一致性
- 如果原 leader所处的分区节点数超过半数,那么其他分区肯定不能选举出新leader。该leader所处分区的日志都能正常提交,其他分区的日志仅仅被记录而不能提交。
- 这时如果连接恢复正常,其他分区的节点发现正常的节点的term大于它的term,那么它就知道已经选举产生了新的leader,那么它就会将原term中未提交的日志抛弃,而更新至与新term日志一致的状态,从而达到最终一致性。
- 其他follower收到后提交之前没提交的日志
选举流程:
- 如果一个follower在一个随机时间内(150-300ms)没有收到来自leader的heart beat,则其成为一个candidate参与到选举中,并将自己的term值加一
- 然后该candidate投自己一票,然后要求集群中的其他节点给它投票。candidate收到term比自己大的投票请求时将自己的状态修改成follower
- 如果收到集群内半数的投票,那么它就正式成为一个leader
4. 多主协议
从多个主节点出发传输数据,传输顺序具有随机性,因而数据的有序性无法得到保证,只保证最终数据的一致性
- Gossip算法
原理:初始由几个节点发起消息,这几个节点会将消息的更新内容告诉自己周围的节点,收到消息的节点再将这些信息告诉周围的节点。依照这种方式,获得消息的节点会越来越多,总体消息的传输规模会越来越大,消息的传播速度也越来越快。
特点:不是每个节点都能在相同的时间达成一致,但是最终集群中所有节点的信息必然是一致的。确保的是分布式集群的最终一致性。整体上达到一致性的速度是log(n)
Redis集群采用Gossip算法
- Proof-of-work(Pow)算法
原理:大量的节点参与竞争,通过自身的工作量大小来证明自己的能力,最终能力最大的节点获得优胜,其他节点的信息需要与该节点统一
特点:图谋不轨的一方无法篡改或者必须付出与收获完全不等称的代价才有修改的可能。
使用场景:比特币。所有参与者共同求解数学问题,这些数学问题往往需要经过大量枚举才能求解,因此需要参与者消耗大量的硬件算力。成功求解数学问题的参与者将获得记账权,并获得比特币作为奖励。其余所有参与者需要保持和获得记账权节点的区块一致,由此达到最终的一致性。
二、 分布式锁
1. 数据库分布式锁
原理:通过主键或者唯一索性两者都是唯一的特性,如果多个服务器同时请求到数据库,数据库只会允许同一时间只有一个服务器的请求在对数据库进行操作,其他服务器的请求就需要进行阻塞等待或者进行自旋
性能比较差,一般不建议使用
2. Redis分布式锁
通过setnx方法获取锁,并且设置锁的过期时间,需保证setnx和expire两行代码的原子性,借助Lua脚本
3. 基于Zookeeper的分布式锁
实现原理:
- 每个要获取锁的线程都创建顺序临时节点,顺序第一的节点获取锁
- 其他不是第一个节点的节点拿不到锁,需要监听上一个节点
- 业务执行完成后,释放锁,第一个节点被删除
- 第二个节点会监听到第一个节点被删除,成为第一个节点,获取锁
- 后续节点依次类推
4. 分布式锁比较
- 性能:Redis通常比Zookeeper提供更好的性能,因为它是内存中的数据结构,而Zookeeper则是磁盘上的数据结构。因此,对于高并发的场景,Redis可能更适合用作分布式锁
- 功能:Zookeeper提供了更多的功能,如顺序节点、监听机制等,这些功能在某些情况下可能非常有用。但是,如果只需要简单的分布式锁,那么Redis可能是一个更好的选择。
- 复杂性:与Redis相比,Zookeeper的使用更加复杂。你需要处理连接问题、会话超时等问题。而Redis则是一个轻量级的解决方案,易于集成和使用。
Zookeeper分布式锁可以避免羊群效应
三、缓存数据一致性
1. 缓存更新策略
- Cache Aside模式:先更新数据库,再失效缓存。若失败,将需要删除的key发送给消息队列,业务系统接收到消息,根据消息内容重试删除操作。
- 是否可以先更新缓存,再更新数据库?
一旦出现缓存成功而更新数据库失败的情况,会导致缓存错误值,导致业务上无法接受的一致性问题- 是否可以先失效缓存,再更新数据库?
通常数据库的写操作耗时大于读操作,一旦更新数据库期间有读请求并未命中缓存,会导致写回的数据为脏数据- 为什么是失效缓存,而不是更新缓存?
如A、B两个线程都执行写操作,A线程早于B执行(B数据是最新的),但A线程晚于B线程更新缓存,这样就会导致缓存中的数据是A线程更新的旧值,而不是B线程更新的最新值- 先更新数据库,再失效缓存是否存在问题
可能存在更新数据成功,失效缓存失败,导致缓存脏数据的问题。这种场景最常用的解决方案是为缓存设置失效时间,到期自动失效
- Read/Write Through 模式:数据更新时,若没有命中缓存,则直接更新数据库;如命中缓存,则更新缓存,由缓存服务更新数据库(同步更新)。将更新数据库的操作交由缓存代理,从应用的角度看,缓存是主存储,应用层的读写操作均面向缓存,缓存服务代理对数据库的读写
- Write Behind Caching模式:只更新缓存,缓存会异步批量更新数据库。无法保证数据强一致性,且可能会丢失数据
Cache Aside模式可能会存在一种概率极低的读写时序问题:读操作缓存未命中,同时伴随一个并发写操作;读操作在写操作完成数据库更新前读取了旧数据,且在写操作删除缓存后更新了缓存,从而导致数据库中的为最新数据,而缓存中的仍为旧数据
延时双删方案可以解决这一问题:删除缓存、更新数据库、休眠一段时间、再次删除缓存
绝大多数场景 Cache Aside + 自动过期 + 失败补偿 策略已能够满足要求
四、高并发系统设计
1. 设计原则
读操作原则:
- 对于数据量小、变更频率低、接收弱一致性的业务,一般以本地缓存策略为主,辅以分布式缓存策略
- 对于数据量较大、变更频率低、一致性要求高的业务,一般以分布式缓存策略为主,辅以本地缓存策略
- 对于音视频、流媒体、图片、静态资源等内容的分发加速,一般以CDN为主,辅以分布式缓存策略
- 限流策略作为兜底
写操作原则:
- 对于不可降级、时延敏感业务,一般以资源扩展策略为主,辅以流量削峰策略
- 对于不可降级、时延宽松业务,一般以流量削峰策略为主,辅以资源扩展策略
- 对于非核心且占用资源较多的业务,优先考虑服务降级策略
- 限流策略作为兜底
2. 资源扩展策略
- 垂直扩展:增加服务器CPU核数、采用高性能CPU、提升服务器内存、使用固态硬盘,提升硬盘容量、增加网络带宽
- 水平扩展:
- 服务层水平扩展:
- 增加应用服务器数量,采用多个实例对外提供相同的服务。并在客户端和业务服务器集群间增加一个负载均衡层,将来自客户端的请求均匀转发到业务服务器上
- 由于同一用户的每次请求可能被负载均衡中间件转发到不同的实例上,从而导致上下文无法关联,因此业务服务应设计为无状态服务。
- 数据层水平扩展:
- 主从部署、读写分离
- 分库分表
- 服务层水平扩展:
3. 负载均衡
- 四层负载均衡
原理:四层负载均衡服务器在接收到客户端请求后,会通过负载均衡算法选择一个最佳服务器,并对报文中目标IP地址进行修改(改为后端服务器IP),然后直接转发给该服务器。
客户端从DNS服务器获取到的只是服务器集群的虚拟IP(VIP),TCP连接是由客户端和后端业务服务器直接建立的,负载均衡节点只起转发作用
常用方案:LVS、Ali-LVS
- 七层负载均衡
原理:在OSI模型的应用层(HTTP、DNS等)是通过分析报文内容进行负载均衡
相较于四层负载均衡,七层负载均衡的流量路由方式要复杂的多。负载均衡节点需与客户端和服务端分别建立TCP连接(三次握手),才能接收到应用层报文的内容
优点:可以对客户端的请求和服务器的响应进行任意指定的修改,灵活的处理用户需求;并且可以设定多种策略过滤特定报文(如SQL注入),提升系统安全性
常用方案:Tengine(基于Nginx)
- 混合负载均衡
对于一些大型网站,通常会采用“DNS+四层负载+七层负载”
客户端发起请求时,如果使用的是四层协议(TCP/UDP),则LVS集群直接将流量转发给后端服务器,如果使用七层协议(HTTP/HTTPS),LVS会将服务请求转发给Tengine集群,Tengine集群再根据自身的调度算法选择相应的后端服务器建立连接。若使用了HTTPS协议,Tengine集群还需要与KeyServer交互,进行签名验签
4. 流量削峰策略
原理:削弱瞬时的请求高峰,让系统吞吐量在高峰请求下保持可控
- 消息队列削峰:异步化,消息队列在一端承接瞬时的流量洪峰,在另一端平缓地将消息推送出去
- 客户端削峰:核心思想是尽量避免用户同时请求和重复请求
- 答题策略:用户需要先答题才能参与。该策略有损用户体验,通常在采用其他策略后仍无法满足高并发需求时使用
- 限制请求:包括忽略请求(在一定条件下,忽略用户请求),延迟请求(随机算法延迟,分散请求)。该策略存在公平性问题,可能引发舆情,适用场景受限
5. 限流策略
- 单机限流:算法主要有令牌桶、漏桶、计数器算法
- 令牌桶算法:存储固定容量的桶,按照固定的速率往桶里添加令牌。如果桶满了,多余的令牌会被丢弃。当请求到达时,需要从桶中移除一个令牌才能处理请求。如果桶中没有令牌,请求会被缓存或丢弃。
- 漏桶算法:请求进来了就把请求先放进桶里,但是不处理,并以限定的速度出水,出水就相当于处理请求。当水来得过猛而出水不够快时就会导致水直接溢出,即拒绝服务
- 漏桶算法能够强行限制数据的传输速率,令牌桶算法能够在限制数据的平均传输速率的同时还允许某种程度的突发传输。
- 分布式限流:从全局视角实现整体限流
限流手段:
- 客户端限流:如通过浏览器端的JS代码监控点击频率、统计点击次数、或者通过客户端包中的限流逻辑让访问快速失败并返回。缺点是对全局流量无感知,客户端触发限流时,服务端的实际荷载可能还很小,不需要限流
- 接入层限流:Nginx通过漏桶算法实现了限流能力,主要提供两种限流方式:访问频率限制和并发连接数限制。但是接入层限流无法实现接口维度的流量管控
- 应用层限流:在业务代码层面实现精细控制的限流,如Sentinel就可以实现接口级的流量控制。
- Mesh限流:Service Mesh是一个基础设施层,用于处理服务间通信,通常由一系列轻量级的网络代理组成,与应用程序部署在一起,但对应用无感知。Service Mesh可以看做是微服务间的TCP/IP,负责服务之间的网络调用、限流、熔断和监控,所有应用程序间的流量都会通过它。实现如蚂蚁的MSON
6. 降级方案
执行方式:
- 手动降级:通过手动配置(通常是开关变量,通过修改其至来改变执行逻辑)启动降级链路。根据业务场景的需要,降级链路都是提前设计好的,如直接返回错误提示,或使用缓存
- 自动降级:一般有两种策略,所有失败均自动降级走兜底链路,或当频次触发降级阈值时采降级(一般是统计规定时间窗口内服务失败的次数和频率)
降级方案:
- 延迟服务:将非核心服务异步化,如存入消息队列或HBase中,待流量平稳后通过消费消息或定时任务执行
- 关闭/拒绝服务:在流量高峰时关闭低优先级服务
- 有损服务:如为减轻数据库压力而直接读缓存,可能会存在数据不一致问题
五、系统稳定性设计
1. 系统风险识别
- 高并发风险
- 远程调用风险:如网络抖动、依赖的下游系统重启、响应超时。对于未知调用结果,一般只能通过重试解决。同时需要根据具体场景分析重试策略(阶梯延时重试、定时调度重试)、幂等、重试顺序等
2. 系统容量评估
流量模型分析:基于入口核心服务的流量峰值,对全链路进行梳理、分析,推算出传递到下游的应用、中间件、数据库等节点的流量峰值
业务漏斗模型:业务流程的每一个环节可能有用户流失,把每一个环节及其对应的数据串联起来,会形成一个上大下小的漏斗形态
调用链路模型:分析调用链路上各个系统间的流量传递比例,分析得到整个系统链路的容量需求
系统容量验证:在给定QPS或TPS下是否存在性能瓶颈,核心业务链路是否正常运行。通过压力测试进行评估,测试粒度包括接口压测、核心链路压测、全链路压测
六、高可用系统设计
软件可用性是指软件系统在给定的时间间隔内处在可工作状态的时间比例。高可用(HA)用于描述软件系统的可用性程度,对于转账、支付等金融服务通常要求做到99.99%以上
1. 接入层高可用
Keepalived
- 轻量级的高可用解决方案,它基于虚拟路由冗余协议实现服务或网络的高可用。它专门用来监控集群系统中各个服务节点的状态。
- 如果某个服务节点出现故障,Keepalived在探测到后会自动将该节点从集群系统中剔除,在故障节点恢复正常后,Keepalived还可以自动将该节点重新加入集群。
- 健康检查和故障转移是Keepalived的两大核心功能。当检测到主负载均衡器发生故障时,由备负载均衡器承载对应的业务,从而保障可用性。
ECMP
- 等价多路径路由。支持多种路径选择策略,如IP哈希、IP取模、平衡轮询、带权轮询
- 在用户和接入层之间引入交换机,交换机通过ECMP等价路由将请求分发给LVS集群,实现负载均衡。同时,上联交换机与LVS集群间运行OSPF协议,当某台LVS宕机后,交换机会自动发现并将这台机器的路由动态剔除,这样ECMP就不会再给这台机器分发流量,从而保障高可用。
2. 业务层高可用
-
无状态服务
业务逻辑作为无状态的部分,支持平滑的水平扩展,在进行流量分发时,可以根据负载均衡策略将流量分发到非特定的服务器实例。由于服务实例之间完全对等,当任意一台或多台服务器宕机时,可以将请求转发给集群的任意一台可用的机器处理
数据部分作为有状态的部分,根据数据类型分别存储到外部中间件和数据库中,如分布式缓存、分布式消息队列、关系型数据库等。基于中间件和数据库本身的冗余备份、故障转移机制保障高可用 -
集群部署:解决单点问题、如同城双活
-
依赖处理:
少依赖:减少应用之间、业务链路之间的依赖。防止服务雪崩
弱依赖:服务A依赖于服务B,服务B不可用时,服务A仍然可用,只是在A返回的结果中,与服务B相关的信息是缺失的或经过特殊处理的
异步化:对于链路上弱依赖而业务上强依赖的服务,可以采用异步化实现弱依赖,同时采用可靠的处理机制保障服务最终执行成功 -
重试机制、幂等设计、服务降级、服务限流、监控预警
3. 数据层高可用
- 副本机制
通过冗余备份,在多个节点上复制相同的数据,每个节点即副本。主副本(Master):处理读写请求,将写请求同步到从副本,从副本(Slave):处理读请求。从副本和主副本间的数据同步通常存在延迟,无法保证强一致性。
- 数据复制模式
- 同步复制模式:写请求被Master节点处理的同时,还需保证写操作在Slave节点上执行才返回给客户端。
- 异步复制模式:Slave节点异步从Master节点获取更新的数据。
- 半同步复制模式:写请求被Master节点处理的同时,只需保证相应写操作在某个Slave节点上执行成功即可返回
- 组复制:基于分布式一致性算法实现分布式环境下数据的最终一致性,同时提供高可用、高扩展的MySQL集群服务
七、分布式事务
1. X/Open DTP
X/Open DTP 是 X/Open 定义的一套分布式事务标准,包含三种角色:
- Application(AP):指应用程序
- Resource Manager(RM):指资源管理器,比如数据库
- Transaction Manager(TM):指事务管理器,事务协调者,负责协调和管理事务,提供AP编程接口或管理RM.
AP通过资源管理器操作多个资源,AP通过TM接口定义事务边界,TM和RMs交换事务信息
2. 两阶段提交协议
流程:
- 准备阶段:TM 通知 RM 准备分支事务,RM记录事务日志(未提交),并告知 TM 的准备结果
- 提交/回滚阶段:
- 如果所有的 RM 在准备阶段都明确返回成功,则 TM 向所有的 RM 发起事务提交指令,完成数据变更
- 如果任何一个 RM 明确返回失败,则 TM 会向所有 RM 发送事务回滚指令
缺点:
- 同步阻塞:所有参与的 RM 都是事务阻塞型,对于任何一次指令都必须有明确的响应,才能继续进行下一步。否则会处于阻塞状态,占用的资源一直被锁定。
- 过于保守:任何一个节点失败都会导致数据回滚
- TM 的单点故障:如果 TM 在第二阶段出现故障,那其他参与的RM 会一直处于锁定状态
- “脑裂” 导致数据不一致问题:第二阶段中 TM 向所有的 RM 发送 commit 请求后,发生局部网络异常,导致只要一部分 RM 接收到 commit 请求,进而执行了 commit 操作。另一部分 RM 未收到 commit 请求,从而事务无法提交,导致数据不一致问题发生。
3. 三阶段提交协议执行流程
流程:
- CanCommit(询问阶段):TM 向 RM 发送事务执行请求,询问是否可以完成指令,RM 只需要回答是或者不是即可,不需要做真正的事务操作,该阶段会有超时中止机制。
- PreCommit(准备阶段):
- 如果在 CanCommit 阶段所有 RM 都返回可以执行操作,则 TM 会向所有 RM 发送 PreCommit 请求,所有 RM 收到请求后写 redo 和 undo 日志,执行事务操作但不提交事务,然后返回 ACK 响应等待 TM 的下一步通知
- 如果在 CanCommit 阶段,任意 RM 返回不能执行操作的结果,那 TM 会向所有参与者发送事务中断请求
- DoCommit(提交或回滚阶段):
- 如果每个 RM 在 PreCommit 阶段都返回成功,那么 TM 会向所有 RM 发起事务提交指令。
- 如果任何一个 RM 返回失败,那么 TM 会发起中止指令来回滚事务
与二阶段提交相比的优点:
- 增加了 CanCommit 阶段,可以尽早发现无法执行操作而中止后续的行为
- 在准备阶段之后,TM 和 RM 都引入了超时机制,一旦超时TM 和 RM 会继续提交事务,并且认为处于成功状态,因为在超时情况下事务默认成功的可能性比较大。超时机制避免了资源的永久锁定。
4. 分布式事务解决方案
TCC
TCC(Try-Confirm-Cancel)补偿型方案:将事务分为三个阶段:Try、Confirm 和 Cancel,通过业务逻辑的补偿机制实现最终一致性。
- Try 阶段
- 资源预留:尝试执行所有参与者的业务操作,预留资源但不提交。
- 检查与锁定:检查资源可用性并进行锁定,确保后续操作可执行。
- 结果反馈:各参与者返回执行结果,决定是否进入 Confirm 或 Cancel 阶段。
- Confirm 阶段
- 提交操作:如果所有参与者在 Try 阶段成功,执行实际的业务操作并提交事务。
- 不可逆:Confirm 操作必须幂等且不可逆,确保事务最终一致。
- Cancel 阶段
- 回滚操作:如果任一参与者在 Try 阶段失败,执行回滚操作,释放预留资源。
- 幂等性:Cancel 操作也需幂等,确保多次调用不会产生副作用。
优点:
- 最终一致性:通过补偿机制保证数据最终一致。
- 灵活性:适用于多种业务场景,业务逻辑可定制。
- 性能优化:资源预留减少锁冲突,提升系统并发性能。
缺点
- 实现复杂:需为每个服务实现 Try、Confirm、Cancel 逻辑,开发难度大。
- 补偿机制复杂:需处理各种异常情况,确保补偿操作正确执行。
- 一致性延迟:Confirm 或 Cancel 阶段可能存在延迟,导致短暂不一致。
基于可靠性消息的最终一致性方案
流程:
- Producer发送预处理消息成功后,开始执行本地事务。
- 如果本地事务执行成功,发送提交请求提交事务消息,消息会投递给 Consumer.
- 如果本地事务执行失败,发送回滚请求回滚事务消息,消息不会投递给 Consumer.
- 如果本地事务状态未知,网络故障或 Producer 宕机,Broker 未收到二次确认的消息。由 Broker 端发送请求给 Producer 进行消息回查,确认提交或回滚。如果消息状态一直未被确认,需要人工介入处理。
消息队列的可靠性投递机制,保证了消费者如果没有签收消息,消息队列服务器会重复投递,保证最终一致性。
最大努力通知型方案
流程(A -> MQ):
- 业务处理中,操作业务数据入库时,在同一事务中向本地消息表中写一条数据,且数据状态为【未发送】。
- 一般采用定时任务,轮询本地消息表中【未发送】状态的数据,将这部分数据发送到消息中间件。
- 消息中间件收到消息后,通过消息中间件的返回应答状态,修改本地消息表状态为【发送成功】
流程(B -> MQ):
4. 消息中间件收到第一步的消息后,同步投递给相应的下游系统,并触发下游系统的业务执行。
5. 当下游系统业务处理成功后,向消息中间件反馈确认应答,消息中间件便可以将消息删除,从而完成该事务。
6. 对于投递失败的消息,利用重试机制进行重试,对于重试失败的,写入错误消息表。
7. 消息中间件需要提供失败消息的查询接口,下游系统会定期查询失败消息。
最大努力通知也被称为定期校对,是以上【基于可靠性消息的最终一致性方案】的优化版,其引入了本地消息表来记录错误消息,然后加入失败消息的定期校对功能,来进一步保证消息被下游系统消费。