Java-分布式架构知识点

一.uuid和雪花id的优缺点

雪花ID

        由64位01数字组成,第一位是符号位,始终为0。接下来的41位是时间戳字段,根据当前时间生成。然后中间的10位表示机房id+机器id,也可以是单独的机器id。最后12位是序列号

优点:

  • 全局唯一: 雪花算法生成的 ID 是全局唯一的,即使在分布式系统中也能保证 ID 的唯一性。
  • 有序: 雪花算法生成的 ID 是有序的,可以根据时间戳进行排序。
  • 高效: 雪花算法的生成速度非常快,可以满足高并发场景的需求。

缺点:

  • 依赖时间: 雪花算法依赖时间戳,如果系统时间出现问题,可能会导致 ID 重复。

解决方案

百度的UidGenerator:Java实现, 基于Snowflake算法的唯一ID生成器。

  • UidGenerator 会在生成 ID 之前对时间戳进行校验,确保时间戳是递增的。
  • 如果发现时间戳出现回拨,则会抛出异常,拒绝生成 ID。

UUID

UUID是一个128位(16字节)长度的标识符,通常以 36 个字符的字符串形式表示。能够在分布式系统中生成全局唯一标识。它可以实现基于随机数生成,不依赖于时间戳或其他信息。

优点

  • 全局唯一
  • 随机无序

不足

  • 占用更多的存储空间uuid 是一个 128 位的二进制数,通常以 36 个字符的字符串形式表示,占用了很多的存储空间,比一般的整数型主键要大得多。这会增加数据库的磁盘占用,降低查询效率,影响性能。
  • 主键是包含索引的,然后mysql的索引是通过b+树来实现的,每一次新的UUID数据的插入,为了查询的优化,都会对索引底层的b+树进行修改,因为UUID数据是无序的。所以每一次UUID数据的插入都会对主键地的b+树进行很大的修改,这一点很不好。 插入完全无序,不但会导致一些中间节点产生分裂。

二.常见的限流算法

1. 计数器法(Fixed Window)

        计数器法是最简单的限流算法。它基于一个固定时间窗口,在窗口内统计请求的数量。如果请求次数超过预设的上限,后续请求将在窗口结束前被拒绝。到达下一个时间窗口时,计数器会重置为 0,重新开始统计。

流程

  1. 在系统中为某个请求路径设置一个计数器和时间窗口(如 1 分钟)。
  2. 每次请求到达时,检查当前时间窗口内的请求数是否已达到限制。
  3. 如果未达到限制,计数器增加 1,请求继续处理。
  4. 如果已达到限制,拒绝请求并返回错误。
  5. 时间窗口结束后,重置计数器为 0。

优点

  • 实现简单,适合不需要复杂限流的场景。
  • 对于稳定且均匀的请求流量效果较好。

缺点:

  • 存在“突发效应”,即时间窗口的边界处可能会出现流量激增的现象。例如,在一个窗口快结束时请求突然增多,下一个窗口又可以立即接受新请求。

场景:
        适用于请求流量较为均匀且系统可承受突发流量的简单场景,如某些 API 请求限流。

2. 滑动窗口法(Sliding Window)

        滑动窗口法通过不断移动时间窗口来限制请求,避免了固定窗口带来的突发效应。它将一个大时间窗口分成多个小的时间片,并实时计算多个时间片内的请求总和。

流程:

  • 将整个时间窗口划分为多个较小的时间段(如 1 分钟划分为 6 个 10 秒)。
  • 每个时间段内记录请求数。
  • 每次有新请求到达时,计算当前时间段和前几个时间段内的总请求数。
  • 如果总请求数未达到限制,请求通过;否则请求被拒绝。
  • 随着时间推移,旧的时间段不断被丢弃,新的时间段被创建,窗口在“滑动”。

优点:

  • 能更精确地限制流量,避免了固定窗口中的突发流量问题。
  • 对突发流量响应更平滑,不会出现固定窗口的边界问题。

缺点:

  • 实现复杂度较高,计算开销相对较大。
  • 需要维护更多的数据结构来存储各个时间段的请求数。

场景:
        适合需要精准限流的场景,尤其是在突发流量较大的环境中,例如支付系统或电商抢购等。

3. 漏桶算法(Leaky Bucket)

        漏桶算法将请求流量视为向桶中注水,桶以固定速率“漏水”(处理请求)。如果水(请求)进入的速度超过漏水的速度,桶会溢出,多余的水(请求)将被丢弃。因此,它通过固定的处理速率来平滑流量。

流程

  1. 当有请求到达时,将请求放入漏桶中。
  2. 桶中的请求以固定速率被处理(类似于恒定的漏水速率)。
  3. 如果桶满了,新请求会被丢弃。
  4. 桶永远保持一个固定大小,无法无限制容纳请求。

优点

  • 可以有效地平滑突发流量,保持系统处理请求的稳定性。
  • 保证系统不会被突发流量压垮,能够防止超载。

缺点

  • 对突发流量的处理能力较差,如果请求到达速率远超系统的处理速率,超出部分会直接丢弃。

场景

  • 适用于处理必须保持恒定速率的请求,比如银行系统中的转账或交易请求。

4. 令牌桶算法(Token Bucket)

        令牌桶算法与漏桶算法类似,但它允许突发流量。系统以固定速率生成令牌,放入桶中。每个请求必须消耗一个令牌才能被处理,如果没有令牌,请求将被拒绝或等待。桶的大小决定了系统可以承受的突发流量上限。

流程

  1. 系统以固定速率生成令牌,并将令牌放入令牌桶中。
  2. 每个请求到达时,必须先从桶中获取一个令牌,才能被处理。
  3. 如果桶中没有令牌,请求将被拒绝或等待。
  4. 桶中可以积累令牌,从而支持一定的流量突发。

优点

  • 允许一定的突发流量,在突发流量到来时仍能保证部分请求被处理。
  • 适合需要应对偶尔流量高峰的系统。

缺点

  • 实现较为复杂,需要处理令牌生成速率和桶容量的动态调整。

场景

  • 适合那些偶尔有突发流量、但需要大多数时间保持恒定处理速率的场景,比如带宽控制或视频流媒体系统。

5.随机丢弃(Random Early Drop,RED)

        随机丢弃是一种队列管理策略,当系统的请求队列接近满负荷时,随机丢弃部分请求,以防止系统超载。这种机制会在队列未满时就开始丢弃请求,从而在系统达到高负载前缓解压力。

流程:

  1. 监控系统的请求队列。
  2. 当队列长度达到某个阈值时,开始随机丢弃部分请求。
  3. 队列越满,丢弃请求的概率越大。
  4. 在队列完全满之前,系统已经通过丢弃部分请求减轻负载。

优点:

  • 减轻压力:在系统负载接近饱和时主动减轻压力,避免系统完全崩溃。
  • 公平性:通过随机丢弃,可以保证所有请求都有相同的机会被处理,减少某些请求长期被丢弃的情况。

缺点:

  • 不确定性:由于请求被随机丢弃,可能导致某些重要请求被拒绝,不适合对请求有严格时限的场景。
  • 实现复杂度:需要监控队列长度,并设定丢弃概率的阈值,增加了系统的复杂性。

场景:
        适合需要处理大量突发请求的场景,比如网络设备中的流量管理、视频流的实时传输等。

6.基于优先级的限流

        基于优先级的限流算法考虑请求的优先级,根据优先级决定请求的处理顺序。高优先级的请求可以优先处理,而低优先级的请求在系统负载高时可能会被延迟处理或拒绝。

流程

  1. 将请求分为不同的优先级(如高、中、低)。
  2. 高优先级的请求被放在队列的前面,优先处理。
  3. 在处理请求时,检查当前系统负载,根据优先级决定处理哪个请求。
  4. 如果系统负载过高,低优先级的请求可能会被延迟或拒绝。

优点

  • 能根据业务需求灵活地处理不同重要性的请求,提供更好的用户体验。
  • 在高负载情况下,能够保证重要请求不被影响。

缺点

  • 实现复杂度较高,需要定义清晰的优先级策略。
  • 可能导致低优先级请求长期得不到处理。

场景

  • 适用于对请求有不同重要性的场景,比如电商抢购中 VIP 用户和普通用户请求的处理。

三.领域驱动模型

1.领域和子域

        领域(Domain) 是问题的核心,是业务逻辑发生的地方。比如,一个电商系统中,“商品管理”、“订单管理”和“支付管理”可以算作三个不同的领域。

        而 子域(Subdomain) 是领域的更小划分,它们针对具体业务场景提供解决方案。例如,“商品管理”领域下,可能有“库存子域”和“价格子域”。

        领域驱动设计的目标:把业务复杂度与技术实现解耦。通过划分领域与子域,将业务逻辑拆解成可理解、可管理的模块。

        在实际项目中,子域的识别尤为重要。通过上下文地图(Context Map),可以把系统内的各个领域及其交互关系可视化,指导我们进一步划分限界上下文。

2.限界上下文(Bounded Context)

        限界上下文是领域驱动设计的核心概念之一。它定义了领域模型的边界,让每个模型的含义在边界内保持清晰、一致。

        定义:限界上下文是一个业务领域中的逻辑边界。它是领域模型与代码实现的桥梁。

        举个例子,假设我们在做一个订单管理系统。订单中的“状态”可能在不同上下文下含义不同:

  • 订单管理上下文:状态指订单的生命周期(已创建、已支付、已取消)。

  • 物流上下文:状态表示配送的过程(已打包、运输中、已签收)。

        通过划分限界上下文,每个上下文可以独立建模,避免概念混淆。对应到代码中,这些上下文通常落地为一个微服务一个模块

3.领域对象

        在限界上下文内部,模型通过 领域对象 来表达。领域对象是领域驱动设计的基本单位,分为三类:

  • 实体(Entity):有唯一标识的对象。

  • 值对象(Value Object):没有唯一标识,表达不变的特性。

  • 服务(Service):封装了无法归属实体或值对象的操作逻辑。

领域对象 vs. 贫血模型和充血模型

  • 贫血模型:只有数据(get/set方法),业务逻辑放在 Service 中。

  • 充血模型:数据和业务逻辑集中在领域对象内,符合面向对象设计思想。

实体(Entity)

        实体是有生命周期的、独一无二的领域对象。它的核心特性是:

  • 有唯一标识(ID)。

  • 状态可以改变(可变性)。

例子:订单(Order)是一个实体,因为每个订单都有唯一的 ID,且其状态(如已支付、已取消)可能变化。

值对象(Value Object)

        值对象用来描述无状态的、不可变的事物。它们通常是“用于计算的参数”或“对象的属性”。

例子:订单的金额(Price)可以建模为一个值对象,因为金额只有数值上的意义,不需要唯一标识。

4.贫血模型与充血模型

这两个模型是领域驱动设计中常见的设计模式:

贫血模型

        在贫血模型中,领域对象只有属性和简单的 get/set 方法,所有的业务逻辑都放在服务(Service)中。虽然实现简单,但容易导致:

  • 领域模型与业务逻辑脱节。

  • 对象的行为和状态分散,不利于维护。

充血模型

        充血模型是面向对象的体现。领域对象除了保存状态外,还负责与自身状态相关的行为。充血模型的优势在于逻辑内聚,但需要适度设计,避免过于复杂。

5.聚合(Aggregate)

聚合 是领域对象的组合体,体现整体与部分的关系。它的主要特点是:

  1. 聚合内有一个根实体,称为 聚合根(Aggregate Root)

  2. 聚合根负责管理聚合内部对象的生命周期。

  3. 只能通过聚合根访问和操作内部对象。

示例:在订单聚合中,订单是聚合根,订单项(OrderItem)是其组成部分。

判断是否是聚合关系:如果聚合根不存在,内部对象也应该失去意义。

6.工厂(Factory)

        工厂负责创建复杂的领域对象,是领域对象生命周期的起点。

示例:订单工厂创建订单。

7.仓库(Repository)

        仓库是领域模型与数据存储的桥梁,负责领域对象的持久化和检索。

示例:订单仓库。

总结

        领域驱动设计提供了一套完整的理论体系,帮助我们应对复杂业务系统的开发。

关键点如下:

  • 领域和子域:明确系统边界,按业务模块划分。

  • 限界上下文:定义领域模型的边界,与微服务对应。

  • 领域对象:实体与值对象结合,贫血与充血模式各有应用场景。

  • 聚合:体现整体与部分的关系,聚合根统一管理。

  • 工厂与仓库:分别负责对象的创建和持久化。

四.如何保障接口的幂等性

        在Java中实现幂等性,通常是指确保某个操作可以被多次执行但只会引起一次实际的变更。幂等性是一个重要的概念,在分布式系统和网络编程中尤为重要,因为它们经常面临重复请求的问题。例如,在支付系统中,用户发起的一次支付请求如果由于网络问题未能得到确认,可能会再次发送同样的支付请求。为了保证系统的正确性和一致性,必须确保这种重复请求不会导致多次扣款。

1.唯一标识符(IDempotency Key)

        每个请求携带一个唯一的标识符,服务端使用这个标识符来判断是否已经处理过该请求。如果是,则直接返回之前的结果而不做任何更改。

2. 状态机

        通过引入状态机来管理操作的状态。比如,订单状态可以是未支付、已支付、取消等。对于支付操作,只有当订单处于未支付状态时才允许执行支付逻辑。

3.使用乐观锁机制

        在数据库中为需要保证幂等性的操作添加版本号或时间戳字段,并使用乐观锁机制进行控制。在更新数据之前,先读取当前数据的版本号或时间戳,并将其与请求中携带的版本号或时间戳进行比较。如果一致,则执行更新操作;如果不一致,则拒绝请求或执行相应的冲突解决策略。

4.使用Token机制

        为每个请求生成一个唯一的Token,并将其返回给客户端。客户端在发送请求时,将Token作为请求头或请求参数的一部分发送到服务端。服务端在处理请求时,首先验证Token的有效性,然后进行幂等性检查。

五.MyCat和ShardingJDBC

        Mycat 和 Apache ShardingSphere JDBC 是两个用于数据库水平扩展和分布式处理的不同解决方案,它们在架构设计、部署方式以及功能特性上有所区别

Mycat

        架构与部署:Mycat 是一个独立部署的数据库中间件,它作为代理服务器存在于应用与数据库之间。应用程序将SQL请求发送给Mycat,Mycat根据预设的分片规则(如分库分表策略)进行路由,并将请求转发到相应的后端数据库。

        MyCat是基于Proxy的,它复写了MySQL协议,使得MyCat Server可以伪装成一个MySQL数据库。这种方式的优势在于能够保证数据库的安全性,并且归并数据结果完全解耦;但缺点是效率可能相对较低。

特点:

  • 不需要修改原始数据库连接代码即可实现分库分表。
  • 提供了强大的SQL解析和执行引擎,支持多种复杂的数据库操作,包括跨库跨表查询等。
  • 支持读写分离,可以灵活配置主从数据库集群的读写策略。
  • 对于非Java环境或者不希望改变现有架构的应用程序来说,是一个很好的选择。

ShardingSphere JDBC

        架构与部署:ShardingSphere JDBC 是一个轻量级的Java框架,以jar包的形式集成到应用中,成为应用程序的一部分,无需额外部署服务器。应用程序通过ShardingSphere JDBC提供的增强版数据源来访问数据库。

        Sharding-JDBC则是基于JDBC的扩展,以jar包的形式提供轻量级服务。它的优势在于效率较高;但缺点是归并数据结果没有实现解耦,可能会影响到业务逻辑代码,且容易内存溢出,因此需要做分页处理。

特点:

  • 需要修改数据库连接代码,引入ShardingSphere JDBC的数据源和相关配置。
  • 内置在应用层,更贴近业务逻辑,能够更好地与应用服务整合,提供细粒度控制。
  • 同样支持分库分表、读写分离等功能,并且由于直接集成在应用中,对于同一数据库内的水平切分支持更好。
  • 支持灵活的分布式治理策略和完善的元数据管理,提供一整套的分布式数据库解决方案,包括数据加密、影子库测试等功能。

比较与优缺点
 

部署复杂性:

Mycat:由于是独立部署的中间件,可能增加运维复杂性,但对已有应用侵入性较小。
ShardingSphere JDBC:集成在应用内部,部署相对简单,但要求修改应用代码。

性能:

Mycat:在网络传输上有额外开销,但由于其独立性,在资源调度和优化方面有较大空间。
ShardingSphere JDBC:减少网络跳数,理论上性能损耗较低,但具体性能取决于JDBC层面的优化程度。


功能兼容性与灵活性:

Mycat:对于复杂的SQL解析能力较强,但在某些特定场景下可能存在兼容性问题。
ShardingSphere JDBC:随着社区的发展,逐渐解决了更多SQL兼容性问题,提供了更多的分布式事务方案(如XA、柔性事务等),并且因为更接近业务层,能够更好地适应复杂多变的业务需求。


维护与升级:

Mycat:作为一个单独的服务,维护和版本升级可能会影响到整个系统链路。
ShardingSphere JDBC:随着应用一起升级,维护更为统一,但也意味着每次升级可能都需要重新构建和部署应用。

六.分布式配置中心

1.Apollo

Apollo是携程框架部门研发的开源配置管理中心,能够集中化管理应用不同环境、不同集群的配置,配置修改后能够实时推送到应用端,并且具备规范的权限、流程治理等特性。

上图简要描述了Apollo的总体设计,我们可以从下往上看:

  • Config Service提供配置的读取、推送等功能,服务对象是Apollo客户端
  • Admin Service提供配置的修改、发布等功能,服务对象是Apollo Portal(管理界面)
  • Config Service和Admin Service都是多实例、无状态部署,所以需要将自己注册到Eureka中并保持心跳
  • 在Eureka之上我们架了一层Meta Server用于封装Eureka的服务发现接口
  • Client通过域名访问Meta Server获取Config Service服务列表(IP+Port),而后直接通过IP+Port访问服务,同时在Client侧会做load balance、错误重试
  • Portal通过域名访问Meta Server获取Admin Service服务列表(IP+Port),而后直接通过IP+Port访问服务,同时在Portal侧会做load balance、错误重试

        为了简化部署,我们实际上会把Config Service、Eureka和Meta Server三个逻辑角色部署在同一个JVM进程中

Apollo发布配置流程

  1. 用户在Portal操作配置发布
  2. Portal调用Admin Service的接口操作发布
  3. Admin Service发布配置后,发送ReleaseMessage(写入数据到mysql)给各个Config Service
  4. Config Service收到ReleaseMessage(单线程每秒扫描mysql)后,通知对应的客户端

Apollo Client端设计

  • 客户端和服务端保持了一个长连接,从而能第一时间获得配置更新的推送。(通过Http Long Polling实现)
  • 客户端还会定时从Apollo配置中心服务端拉取应用的最新配置。
    1. 这是一个fallback机制,为了防止推送机制失效导致配置不更新
    2. 客户端定时拉取会上报本地版本,所以一般情况下,对于定时拉取的操作,服务端都会返回304 - Not Modified
    3. 定时频率默认为每5分钟拉取一次,客户端也可以通过在运行时指定System Property: apollo.refreshInterval来覆盖,单位为分钟。
  • 客户端从Apollo配置中心服务端获取到应用的最新配置后,会保存在内存中
  • 客户端会把从服务端获取到的配置在本地文件系统缓存一份
  • 在遇到服务不可用,或网络不通的时候,依然能从本地恢复配置
  • 应用程序可以从Apollo客户端获取最新的配置、订阅配置更新通知

优点:

统一管理不同环境、不同集群的配置,Apollo提供了一个统一界面集中式管理不同环境(environment)、不同集群(cluster)、不同命名空间(namespace)的配置。同一份代码部署在不同的集群,可以有不同的配置,比如zk的地址等通过命名空间(namespace)可以很方便的支持多个不同应用共享同一份配置,同时还允许应用对共享的配置进行覆盖,配置界面支持多语言(中文,English)
配置修改实时生效(热发布),用户在Apollo修改完配置并发布后,客户端能实时(1秒)接收到最新的配置,并通知到应用程序。
版本发布管理,所有的配置发布都有版本概念,从而可以方便的支持配置的回滚。
灰度发布,支持配置的灰度发布,比如点了发布后,只对部分应用实例生效,等观察一段时间没问题后再推给所有应用实例。
权限管理、发布审核、操作审计,应用和配置的管理都有完善的权限管理机制,对配置的管理还分为了编辑和发布两个环节,从而减少人为的错误。
所有的操作都有审计日志,可以方便的追踪问题。
客户端配置信息监控,可以方便的看到配置在被哪些实例使用
提供Java和.Net原生客户端,提供了Java和.Net的原生客户端,方便应用集成
支持Spring Placeholder,Annotation和Spring Boot的ConfigurationProperties,方便应用使用(需要Spring 3.1.1+)同时提供了Http接口,非Java和.Net应用也可以方便的使用
提供开放平台API,Apollo自身提供了比较完善的统一配置管理界面,支持多环境、多数据中心配置管理、权限、流程治理等特性。

缺点:

        部署和维护相对复杂,需要依赖 MySQL 和 Eureka。

2.Nacos

        Nacos是一个易于使用、功能强大的配置和服务发现平台,致力于发现、配置和管理微服务。Nacos提供了一组简单易用的特性集,帮助实现动态服务发现、服务配置管理、服务元数据及流量管理,使得构建、交付和管理微服务平台变得更加容易。

3.SpringCloud Config

七.资源隔离

为什么要做资源隔离?

        比如我们现在有3个业务调用分别是查询订单、查询商品、查询用户,且这三个业务请求都是依赖第三方服务-订单服务、商品服务、用户服务。三个服务均是通过RPC调用。当查询订单服务,假如线程阻塞了,这个时候后续有大量的查询订单请求过来,那么容器中的线程数量则会持续增加直致CPU资源耗尽到100%,整个服务对外不可用,集群环境下就是雪崩,如果进行资源隔离仅仅会影响到查询订单服务,其他的服务不影响。

1.信号量隔离

优点
  1. 资源开销小

    • 不需要创建额外的线程,减少了线程创建和上下文切换的开销。

    • 适合 CPU 密集型或低延迟的操作。

  2. 简单轻量

    • 实现简单,不需要管理线程池,适合命令数量较多的场景。

  3. 适合低延迟操作

    • 对于快速完成的命令,信号量隔离的性能更高。

缺点
  1. 隔离性差

    • 所有命令共享调用线程(如 Tomcat 的工作线程),如果某个命令阻塞,会影响整个系统的线程池。

    • 无法实现完全的资源隔离。

  2. 不支持超时控制

    • 信号量隔离无法中断正在执行的命令,因此无法实现超时控制。

  3. 不适合高延迟操作

    • 对于 I/O 密集型或高延迟的操作,信号量隔离可能导致调用线程被长时间占用,影响系统整体性能。

2.线程池隔离

优点
  1. 完全隔离

    • 每个服务或命令使用独立的线程池,避免了资源竞争和故障扩散。

    • 即使某个服务出现故障(如超时、阻塞),也不会影响其他服务的线程池。

  2. 支持异步调用

    • 线程池隔离天然支持异步调用,适合需要异步执行的场景。

  3. 超时控制

    • 可以方便地设置超时时间,超时后可以中断线程。

  4. 适合高延迟操作

    • 对于 I/O 密集型或高延迟的操作(如网络请求),线程池隔离可以更好地控制资源。

缺点
  1. 资源开销大

    • 每个命令都需要独立的线程池,线程的创建和上下文切换会带来额外的开销。

    • 如果命令数量较多,线程池的数量和线程数会显著增加,可能导致系统资源耗尽。

  2. 复杂性高

    • 需要合理配置线程池的大小、队列大小等参数,配置不当可能导致性能问题。

  3. 不适合低延迟操作

    • 对于 CPU 密集型或低延迟的操作,线程池隔离的开销可能得不偿失。

特性线程池隔离(THREAD)信号量隔离(SEMAPHORE)
隔离性强隔离,每个命令有独立的线程池弱隔离,所有命令共享调用线程
资源开销高(线程创建、上下文切换)低(无额外线程开销)
超时控制支持不支持
适合场景高延迟操作(如网络请求、I/O 操作)低延迟操作(如 CPU 密集型任务)
异步支持支持不支持
复杂性高(需要配置线程池参数)低(实现简单)
并发支持支持(最大线程池大小)支持(最大信号量上限)

八.Zaft协议和Zab协议

        分布式系统设计中,在极大提高可用性、容错性的同时,带来了一致性问题(CAP理论)。Raft协议和ZAB协议是在分布式系统中为保证一致性而设计的协议,旨在处理CAP理论中的一致性问题,同时在面对分区容忍性和可用性方面进行权衡。  

Raft协议

   1.Raft协议是什么?

   Raft协议是一种分布式一致性算法(共识算法),它是为了替代复杂难懂的 Paxos 算法而生的。共识就是多个节点对某一个事件达成一致的算法,即使出现部分节点故障,网络延时等情况,也不影响各节点,进而提高系统的整体可用性。

   Raft算法将分布式一致性分解为多个子问题,包括 Leader选举(Leader election)、日志复制(Log replication)、安全性(Safety)、日志压缩(Log compaction)等。

   2.系统中的角色

   Raft 协议将系统中的角色分为领导者(Leader)、跟从者(Follower)和候选者(Candidate)。

   1)领导者(Leader)

   Leader是Raft协议中协调集群一致性、处理客户端请求和管理日志复制的核心节点。Leader 负责处理客户端请求并将操作日志复制到 Followers(跟随者)上,只有 Leader 才能处理写操作。

   2)跟从者(Follower)

   接受并持久化Leader同步的日志,在Leader告知日志可以提交后,提交日志。当Leader出现故障时,主动推荐自己为候选人(Candidate)。

   3)候选者(Candidate)

   Leader选举过程中的临时角色。向其他节点发送请求投票信息,如果获得大多数选票,则晋升为Leader。

 3.任期选举

        Raft 要求系统在任意时刻最多只有一个Leader,正常工作期间只有Leader和Follower,Raft算法将时间划分为任意不同长度的任期(Term),每一任期的开始都是一次选举,一个或多个候选人会试图成为Leader,在成功选举Leader后,Leader会在整个任期内管理整个集群:负责处理所有的客户端交互,日志复制等,Follower 仅仅响应领导者的请求。如果Leader选举失败,该任期就会因为没有Leader而结束,开始下一任期,并立刻开始下一次选举。

      

4.Leader选举

   Raft协议的Leader选举流程是其核心部分之一,目的是确保集群中始终只有一个领导者(Leader),并且在发生故障时能快速选举出新的领导者。这个过程能够保证日志的顺序性和一致性。

   选举流程大致如下

   Raft使用心跳机制来触发领导者选举,当服务器启动时,初始化都是Follower身份,由于没有Leader,Followers无法与Leader保持心跳,因此,Followers会认为Leader已经下线,进而转为Candidate状态,然后Candidate向集群其他节点请求投票,同意自己成为Leader,如果Candidate收到超过半数节点的投票(N/2 +1),它将获胜成为Leader。

        Leader 向所有Follower周期性发送心跳包,如果Follower在选举超时时间内没有收到Leader的心跳包,就会等待一段随机的时间后发起一次Leader选举。

 1)唯一投票

   在Raft协议中,一个节点在一次任期(term)内只能投一次票。这是为了防止同一候选者在一轮选举中获得多次投票,或者多个候选者在同一轮选举中都获得同一节点的投票,从而确保选举的公正性。

   在进入选举投票时,每个节点都会先给自己投票,那会不会出现所有的节点都给自己投票呢?注意上述中 Candidate 和 Follower 的区别。

   Follower 在选举超时期间没有接收到 Leader 的心跳,它就会把自己标记为 Candidate,成为 Candidate 后才会给自己投票,并给其他节点发起请求投票。但每个节点的超时时间都是随机的(通常在150ms到300ms之间),所以所有 Follower 节点进入 Candidate 状态的时间是不一样的。当第一个 Candidate 发送请求投票给其他节点时,其他节点基本还是 Follower 状态,还没有把票投给自己。

   同样如果第一轮选举失败,要发起下一轮选举。每个节点还是经过随机的不同超时时间后,再进入 Candidate 状态。

   2)任期

   在Raft协议中,任期被用作逻辑时钟,并用于解决冲突。每当选举开始时,候选者都会增加它们的任期。这样可以确保每一轮选举都有一个唯一的标识,同时也能防止过期的选举结果影响新的选举。

   如果第一轮选举失败了,第二轮选举开始时,候选者依然会首先增加它们的任期。例如,假设一个节点在第一轮选举中没有获得足够的投票,然后开始第二轮选举。此时,如果它没有增加其任期,那么就可能出现一个问题:这个节点可能同时在第一轮和第二轮选举中都是候选者。这将导致混乱,因为其他节点可能不知道他们应该对哪一轮选举进行投票。

总结

        Raft协议会先选举出Leader,Leader完全负责日志复制的管理和负责接受所有客户端更新请求,然后复制到Follower,并在“安全”的时候执行这些请求,如果Leader故障,Follower会重新选举出新的Leader,保证一致性。

ZAB协议

  1. ZAB协议是什么?

  ZAB 协议主要是用在 ZooKeeper 集群中。同 Raft协议一样,都是为了替代复杂难懂的 Paxos 算法,二者的相似度很高。

  ZAB 协议是专为 Zookeeper 的分布式协调和强一致性要求而设计的。尽管与 Raft、Paxos 等协议在实现上有相似之处,但 ZAB 更专注于处理事务日志的广播、Leader 选举和快速故障恢复。 

  2. ZAB 协议核心

  在 Zookeeper 中只有一个 Leader,并且只有 Leader 可以处理外部客户端的事务请求,并将其转换成一个事务 Proposal(写操作),然后 Leader 服务器再将事务 Proposal 操作的数据同步到所有 Follower(数据广播/数据复制)。

  Zookeeper 采用 Zab 协议的核心就是只要有一台服务器提交了 Proposal,就要确保所有服务器最终都能正确提交 Proposal,这也是 CAP/BASE 最终实现一致性的体现。

ZAB 两种模式

   ZAB 协议有两种模式:一种是消息广播模式,另一种是崩溃恢复模式。

   1)消息广播模式

   在系统正常运行时,Zookeeper 集群中数据副本的传递策略就是采用消息广播模式,Zookeeper 中的数据副本同步方式与2PC方式相似但却不同,2PC是要求协调者必须等待所有参与者全部反馈ACK确认消息后,再发送 commit 消息,要求所有参与者要么全成功要么全失败,2PC方式会产生严重的阻塞问题。

   而 Zookeeper 中 Leader 等待 Follower 的 ACK 反馈是指:只要半数以上的 Follower 成功反馈即可,不需要收到全部的 Follower 反馈。

 Zookeeper 中广播消息步骤:

  • 客户端发起一个写操作请求
  • Leader 服务器处理客户端请求后将请求转换为 Proposal,同时为每个 Proposal 分配一个全局唯一 ID,即 ZXID
  • Leader 服务器与每个 Follower 之间都有一个队列,Leader 将消息发送到该队列
  • Follower 机器从队列中取出消息处理完(写入本地事务日志中)后,向 Leader 服务器发送 ACK 确认
  • Leader 服务器收到半数以上的 Follower 的 ACK 后,即认为可以发送 Commit
  • Leader 向所有的 Follower 服务器发送 Commit 消息

   补充说明:ZXID

   类似于 RDBMS 中的事务ID,用于标识一个 Proposal ID,为了保证顺序性,ZXID 必须单调递增,因此 Zookeeper 使用一个 64 位的数来表示,高 32 位是 Leader 的 epoch(选举纪元),从 1 开始,每次选出新的 Leader,epoch 加 1,低 32 位为该 epoch 内的序号,每次 epoch 变化,都将低 32 位的序号重置,这样保证了 ZXID 的全局递增性。

   2)崩溃恢复模式

   一旦 Leader 服务器出现崩溃或者由于网络原因导致 Leader 服务器失去了与过半 Follower 的联系,那么就会进入崩溃恢复模式。

   Zookeeper 集群中为保证任何进程能够顺序执行,只能是 Leader 服务器接收写请求,其他服务器接收到客户端的写请求,也会转发至 Leader 服务器进行处理。

   Zab 协议崩溃恢复需满足以下2个请求:

  •   确保已经被 Leader 提交的 proposal 必须最终被所有的 Follower 服务器提交
  •   确保丢弃已经被 Leader 提出的但没有被提交的 Proposal

   也就是新选举出来的 Leader 不能包含未提交的 Proposal,必须都是已经提交了的 Proposal 的 Follower 服务器节点,新选举出来的 Leader 节点中含有最高的 ZXID,所以,在 Leader 选举时,将 ZXID 作为每个 Follower 投票时的信息依据。这样做的好处是避免了 Leader 服务器检查 Proposal 的提交和丢弃工作。

   3. Leader 选举

   ZAB协议的选举过程主要发生在恢复模式中,一般在系统启动或者领导者节点故障时触发,主要分以下几步:

   1)开始选举

   每个节点首先投给自己一票,并将自己的编号和最后一条已经提交的事务的ZXID(Zookeeper Transaction ID,包含了事务的epoch和计数器)发送给其他所有节点,表示它推举自己成为领导者。

   2)收集选票

   接收到其他节点的投票信息后,每个节点会比较其他节点的ZXID和自己的ZXID。如果其他节点的ZXID更高(epoch或者计数器更大),或者ZXID相同但是节点编号更大,那么就会将票投给这个节点。

   3)计票

   每个节点收集到超过半数的投票(包括自己的)后,就会认为选举完成,选出的领导者就是得票最多的节点。如果有多个节点的票数相同,就选择ZXID最大的节点,如果还是相同,就选择节点编号最大的。(比较优先级:票数最多>ZXID最大>节点编号最大)

   4)领导者确认

   选举完成后,新的领导者会向所有节点发送领导者确认消息,其他节点收到确认消息后,会向领导者发送已经接受领导者的消息。

   5)新领导者工作

   当领导者收到大多数节点的确认消息时,就可以开始提供服务,处理客户端的请求,从而结束了选举过程。

   总结:ZAB协议的选举过程是为了在领导者节点故障时快速选出新的领导者,恢复系统的正常运行,同时还确保了系统的一致性。

Raft协议和ZAB协议的区别

   1. leader选举的区别

   1)  选举触发条件

   在Raft协议中,如果 Follower 在一段时间内没有收到 Leader 的心跳信息,就会主动进入候选人状态并启动新一轮选举,原因可能只是 Leader 节点繁忙而无法及时响应等情况。而在ZAB协议除了系统启动时,只有等到 Leader 故障,无法提供服务时才会进行选举。Raft更偏向于积极选举,只要有可能就尝试选举新的领导者,而ZAB更倾向于保持现状,只有在确定领导者无法提供服务时才触发选举。

   2) 投票规则

   Raft协议中,每个节点只能投票一次,并且投票给第一个请求投票的候选人。而在 ZAB 协议中,节点可能会改变自己的投票,投给 ZXID 更大的节点。所以 ZAB 不存在随机超时时间,虽然第一票都会投给自己,但后续如果发现其他节点的 ZXID 更大,就修改自己的选票。

   3) 票数要求

   在Raft 协议中,候选人在获得超过半数的投票后成为 Leader。在 ZAB 协议中,新的领导者被选出后,还需要得到超过半数的节点的确认才能开始工作。

   4) 选举leader决定因素

   在Raft协议中,选举过程严格按照轮次(任期)进行,每轮选举只能选出一位Leader,选出 Leader 很大程度取决于随机的超时时间和日志最新。而在 ZAB 协议中,选举的结果主要取决于节点的 ZXID 以及节点编号。ZXID最大的节点通常是最近处理过事务,日志较新。

   简而言之,Raft协议更侧重于通过随机的超时时间和日志一致性来进行选举,而ZAB协议则通过ZXID(事务的处理顺序)和节点编号来选举出Leader。

   2. 日志复制机制的区别

   ZAB 协议 的日志复制机制中由于多了一个预提交步骤,因此相比 Raft 协议会有更高的延迟,特别是在高负载和大规模集群的情况下。Raft 通过简单的多数节点确认就可以完成日志提交,因此它通常具有更低的延迟。

   3.  使用场景 

   1)Raft 协议

   较低的延迟,适用于实时要求较高的应用。Raft是使用较为广泛的分布式协议,我们熟悉的etcd注册中心就采用了这个算法。Redis中Sentinel 系统选举领头Sentinel的方法也是采用了raft协议。

   2)ZAB 的优势

  更强的容错性,适用于对日志顺序和一致性有严格要求的场景,如 Zookeeper 提供的分布式协调服务。由于 ZAB 协议是为 Zookeeper 的需求量身定制的,其他系统可能更倾向于使用 Raft 或 Paxos 等更为通用和成熟的协议。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值