一.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,请求继续处理。
- 如果已达到限制,拒绝请求并返回错误。
- 时间窗口结束后,重置计数器为 0。
优点:
- 实现简单,适合不需要复杂限流的场景。
- 对于稳定且均匀的请求流量效果较好。
缺点:
- 存在“突发效应”,即时间窗口的边界处可能会出现流量激增的现象。例如,在一个窗口快结束时请求突然增多,下一个窗口又可以立即接受新请求。
场景:
适用于请求流量较为均匀且系统可承受突发流量的简单场景,如某些 API 请求限流。
2. 滑动窗口法(Sliding Window)
滑动窗口法通过不断移动时间窗口来限制请求,避免了固定窗口带来的突发效应。它将一个大时间窗口分成多个小的时间片,并实时计算多个时间片内的请求总和。
流程:
- 将整个时间窗口划分为多个较小的时间段(如 1 分钟划分为 6 个 10 秒)。
- 每个时间段内记录请求数。
- 每次有新请求到达时,计算当前时间段和前几个时间段内的总请求数。
- 如果总请求数未达到限制,请求通过;否则请求被拒绝。
- 随着时间推移,旧的时间段不断被丢弃,新的时间段被创建,窗口在“滑动”。
优点:
- 能更精确地限制流量,避免了固定窗口中的突发流量问题。
- 对突发流量响应更平滑,不会出现固定窗口的边界问题。
缺点:
- 实现复杂度较高,计算开销相对较大。
- 需要维护更多的数据结构来存储各个时间段的请求数。
场景:
适合需要精准限流的场景,尤其是在突发流量较大的环境中,例如支付系统或电商抢购等。
3. 漏桶算法(Leaky Bucket)
漏桶算法将请求流量视为向桶中注水,桶以固定速率“漏水”(处理请求)。如果水(请求)进入的速度超过漏水的速度,桶会溢出,多余的水(请求)将被丢弃。因此,它通过固定的处理速率来平滑流量。
流程:
- 当有请求到达时,将请求放入漏桶中。
- 桶中的请求以固定速率被处理(类似于恒定的漏水速率)。
- 如果桶满了,新请求会被丢弃。
- 桶永远保持一个固定大小,无法无限制容纳请求。
优点:
- 可以有效地平滑突发流量,保持系统处理请求的稳定性。
- 保证系统不会被突发流量压垮,能够防止超载。
- 对突发流量的处理能力较差,如果请求到达速率远超系统的处理速率,超出部分会直接丢弃。
- 适用于处理必须保持恒定速率的请求,比如银行系统中的转账或交易请求。
4. 令牌桶算法(Token Bucket)
令牌桶算法与漏桶算法类似,但它允许突发流量。系统以固定速率生成令牌,放入桶中。每个请求必须消耗一个令牌才能被处理,如果没有令牌,请求将被拒绝或等待。桶的大小决定了系统可以承受的突发流量上限。
流程:
- 系统以固定速率生成令牌,并将令牌放入令牌桶中。
- 每个请求到达时,必须先从桶中获取一个令牌,才能被处理。
- 如果桶中没有令牌,请求将被拒绝或等待。
- 桶中可以积累令牌,从而支持一定的流量突发。
优点:
- 允许一定的突发流量,在突发流量到来时仍能保证部分请求被处理。
- 适合需要应对偶尔流量高峰的系统。
缺点:
- 实现较为复杂,需要处理令牌生成速率和桶容量的动态调整。
场景:
- 适合那些偶尔有突发流量、但需要大多数时间保持恒定处理速率的场景,比如带宽控制或视频流媒体系统。
5.随机丢弃(Random Early Drop,RED)
随机丢弃是一种队列管理策略,当系统的请求队列接近满负荷时,随机丢弃部分请求,以防止系统超载。这种机制会在队列未满时就开始丢弃请求,从而在系统达到高负载前缓解压力。
流程:
- 监控系统的请求队列。
- 当队列长度达到某个阈值时,开始随机丢弃部分请求。
- 队列越满,丢弃请求的概率越大。
- 在队列完全满之前,系统已经通过丢弃部分请求减轻负载。
优点:
- 减轻压力:在系统负载接近饱和时主动减轻压力,避免系统完全崩溃。
- 公平性:通过随机丢弃,可以保证所有请求都有相同的机会被处理,减少某些请求长期被丢弃的情况。
缺点:
- 不确定性:由于请求被随机丢弃,可能导致某些重要请求被拒绝,不适合对请求有严格时限的场景。
- 实现复杂度:需要监控队列长度,并设定丢弃概率的阈值,增加了系统的复杂性。
场景:
适合需要处理大量突发请求的场景,比如网络设备中的流量管理、视频流的实时传输等。
6.基于优先级的限流
基于优先级的限流算法考虑请求的优先级,根据优先级决定请求的处理顺序。高优先级的请求可以优先处理,而低优先级的请求在系统负载高时可能会被延迟处理或拒绝。
流程:
- 将请求分为不同的优先级(如高、中、低)。
- 高优先级的请求被放在队列的前面,优先处理。
- 在处理请求时,检查当前系统负载,根据优先级决定处理哪个请求。
- 如果系统负载过高,低优先级的请求可能会被延迟或拒绝。
优点:
- 能根据业务需求灵活地处理不同重要性的请求,提供更好的用户体验。
- 在高负载情况下,能够保证重要请求不被影响。
- 实现复杂度较高,需要定义清晰的优先级策略。
- 可能导致低优先级请求长期得不到处理。
场景:
- 适用于对请求有不同重要性的场景,比如电商抢购中 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)
聚合 是领域对象的组合体,体现整体与部分的关系。它的主要特点是:
-
聚合内有一个根实体,称为 聚合根(Aggregate Root)。
-
聚合根负责管理聚合内部对象的生命周期。
-
只能通过聚合根访问和操作内部对象。
示例:在订单聚合中,订单是聚合根,订单项(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发布配置流程
- 用户在Portal操作配置发布
- Portal调用Admin Service的接口操作发布
- Admin Service发布配置后,发送ReleaseMessage(写入数据到mysql)给各个Config Service
- Config Service收到ReleaseMessage(单线程每秒扫描mysql)后,通知对应的客户端
Apollo Client端设计
- 客户端和服务端保持了一个长连接,从而能第一时间获得配置更新的推送。(通过Http Long Polling实现)
- 客户端还会定时从Apollo配置中心服务端拉取应用的最新配置。
- 这是一个fallback机制,为了防止推送机制失效导致配置不更新
- 客户端定时拉取会上报本地版本,所以一般情况下,对于定时拉取的操作,服务端都会返回304 - Not Modified
- 定时频率默认为每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.信号量隔离
优点
-
资源开销小:
-
不需要创建额外的线程,减少了线程创建和上下文切换的开销。
-
适合 CPU 密集型或低延迟的操作。
-
-
简单轻量:
-
实现简单,不需要管理线程池,适合命令数量较多的场景。
-
-
适合低延迟操作:
-
对于快速完成的命令,信号量隔离的性能更高。
-
缺点
-
隔离性差:
-
所有命令共享调用线程(如 Tomcat 的工作线程),如果某个命令阻塞,会影响整个系统的线程池。
-
无法实现完全的资源隔离。
-
-
不支持超时控制:
-
信号量隔离无法中断正在执行的命令,因此无法实现超时控制。
-
-
不适合高延迟操作:
-
对于 I/O 密集型或高延迟的操作,信号量隔离可能导致调用线程被长时间占用,影响系统整体性能。
-
2.线程池隔离
优点
-
完全隔离:
-
每个服务或命令使用独立的线程池,避免了资源竞争和故障扩散。
-
即使某个服务出现故障(如超时、阻塞),也不会影响其他服务的线程池。
-
-
支持异步调用:
-
线程池隔离天然支持异步调用,适合需要异步执行的场景。
-
-
超时控制:
-
可以方便地设置超时时间,超时后可以中断线程。
-
-
适合高延迟操作:
-
对于 I/O 密集型或高延迟的操作(如网络请求),线程池隔离可以更好地控制资源。
-
缺点
-
资源开销大:
-
每个命令都需要独立的线程池,线程的创建和上下文切换会带来额外的开销。
-
如果命令数量较多,线程池的数量和线程数会显著增加,可能导致系统资源耗尽。
-
-
复杂性高:
-
需要合理配置线程池的大小、队列大小等参数,配置不当可能导致性能问题。
-
-
不适合低延迟操作:
-
对于 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 等更为通用和成熟的协议。