微服务
微服务
nacos配置 swcmlrelecsb
nacos注册 craosnrnbs ngshsuf
eureka 服注表同 客注续剔下获调 保缓
hystrix ACEQ断资超M
网关 入路负限鉴监跨聚
zuul proec tosr 生命周期
gateway RPF QPABC2H2M PR5S4
feign EFJRFSC
ribbon LoadBalancerFeignClient 轮权随最重敏
seata TTR ATSX
基础概念
拆分原则
- 微服务设计四个原则
- AKF拆分原则
AKF 立方体也叫做scala cube,它在《The Art of Scalability》一书中被首次提出,旨在提供一个系统化的扩展思路。AKF 把系统扩展分为以下三个维度- X 轴:直接水平复制应用进程来扩展系统。
- Y 轴:将功能拆分出来扩展系统。
- Z 轴:基于用户信息扩展系统。
- 前后端分离
- 无状态服务
- Restful通信风格
- AKF拆分原则
- 微服务设计目标
- 架构必须稳定
- 服务必须高内聚,服务应该实现一小组强相关的功能
- 服务必须符合开闭原则,将一同变更的内容打包在一起,以确保每个更改仅影响一个服务
- 服务必须松耦合,每个服务都可以在不影响客户端的情况下更改实现
- 微服务划分方法
- 纵向拆分
从业务维度进行拆分。标准是按照业务的关联程度来决定,关联比较密切的业务适合拆分为一个微服务,而功能相对比较独立的业务适合单独拆分为一个微服务 - 横向拆分
从公共且独立功能维度拆分。标准是按照是否有公共的被多个其他服务调用,且依赖的资源独立不与其他业务耦合
- 纵向拆分
优缺点
- 优点
- 每一个服务足够内聚,代码容易理解
- 开发效率提高,一个服务只做一件事
- 微服务能够被小团队单独开发
- 微服务是松耦合的,是有功能意义的服务
- 可以用不同的语言开发,面向接口编程
- 易于与第三方集成
- 微服务只是业务逻辑的代码,不会和HTML,CSS或者其他界面组合
- 可以灵活搭配,连接公共库/连接独立库
- 缺点
- 分布式系统的复杂性
- 多服务运维难度,随着服务的增加,运维的压力也在增大
- 系统部署依赖
- 服务间通信成本
- 数据一致性
- 系统集成测试
- 性能监控
配置中心
Zookeeper
Nacos
NacosConfigService

SpringCloud中几个关键类
- MapPropertySource<T>: 这个是spring中属性配置的数据源,所有的配置文件都要转成这样的形式。
- RefreshEvent: 发送该事件,可以类似调用RefreshEndpoint#refresh,也就是通知spring刷新配置文件。
- EnvironmentChangeEvent: 发送该事件,会通知环境已经发生变化。
- ContextRefresher: 刷新管理类。
- NacosContextRefresher: nacos配置上下文管理类。
- RefreshScope: 该注解会把bean加入到'refresh'的scope中。
- ClientWorker: nacos配置中心客户端,会定时http请求服务器。
处理流程: swcmlrelecsb
S(server)W(ClientWorker)C(checkConfigInfo/checkLocalConfig)M(checkListenerMd5)L(listener.receiveConfigInfo)R(NacosContextRefresher)E(RefershEvent)L(RefreshEventListener)E(Environment)C(ConfigurationProperties)S(RefreshScope)B(Bean)
- 在nacos上修改配置。
- nacos客户端中ClientWorker会每隔10ms异步读取一次配置中心文件md5值 checkConfigInfo->checkLocalConfig。
- 和本地md5值比较,有变化的从服务器拉取 checkListenerMd5 。
- 将文件保存/缓存到本地。
- 通知NacosContextRefresher配置文件有变化 listener.receiveConfigInfo。
- NacosContextRefresher判断是否需要更新配置 registerNacosListener。
- 发送事件通知ContextRefresher去更新 RefreshEvent。
- 这里是更新配置的关键步骤 RefreshEventListener->ContextRefresher.refresh。
- 准备一份before配置,然后通过构建新的Environment的方式拿到新的配置, 接着比较变化,得到有变化的keys builder.run(重载容器)。
- 构建Environment时会去读取配置文件,文件优先读本地,如果本地没有通过Http请求服务商。
- 构建NacosPropertiesSource,并重新生成ConfigurationProperties对象。
- 通知RefreshScope去更新。
- 销毁scope='refresh'的bean。
- 通知bean容器去构建新的bean(懒加载)。
- 将属性(@Value注解)注入到新的bean。
注册中心
Zookeeper
Nacos
NacosNamingService

- AutoServiceRegistrationAutoConfiguration -> AutoServiceRegistration -> AbstractAutoServiceRegistration -> onApplicationEvent -> bind -> start -> register -> serviceRegistry.register -> NacosAutoServiceRegistration
- ServiceRegistry -> NacosServiceRegistry -> register -> namingService.registerInstance -> beatReactor.addBeatInfo -> serverProxy.registerService
说明:
- 通过自动装配加载 AutoServiceRegistrationAutoConfiguration, 其中注入了 AutoServiceRegistration 实例
- 实际注入的为 AutoServiceRegistration 实现类 NacosAutoServiceRegistration, 它继承抽象类 AbstractAutoServiceRegistration
- AbstractAutoServiceRegistration 实现了 ApplicationListener 接口,并且传入了 WebServerInitializedEvent 作为泛型
- 在 onApplicationEvent 方法中, 调用 register 方法进行服务注册,最终调用的是 namingService.registerInstance
- 注册过程中,主要做了2件事:
- 1.通过 beatReactor.addBeatInfo 检测心跳,每5s一次,请求地址
/nacos/v1/ns/instance/beat - 2.通过 serverProxy.registerService 注册服务,请求地址
/nacos/v1/ns/instance
- 1.通过 beatReactor.addBeatInfo 检测心跳,每5s一次,请求地址
服务发现
NacosServerList.getServers -> namingService.selectInstances -> hostReactor.getServiceInfo
说明:
- 以调用远程接口(OpenFeign)为例,当执行远程调用时,需要经过服务发现的过程。
- 服务发现先执行NacosServerList类中的getServers()方法,根据远程调用接口上@FeignClient中的属性作为serviceId传入NacosNamingService.selectInstances()方法中进行调用。
- 根据subscribe的值来决定服务是从本地注册列表中获取还是从Nacos服务端中获取。
- 以本地注册列表为例,通过调用HostReactor.getServiceInfo()来获取服务的信息(serviceInfo),Nacos本地注册列表由3个Map来共同维护。
- 本地Map–>serviceInfoMap,
- 更新Map–>updatingMap
- 异步更新结果Map–>futureMap,
- 最终的结果从serviceInfoMap当中获取。
- HostReactor.getServiceInfo()方法通过this.scheduleUpdateIfAbsent() 方法和updateServiceNow()方法实现服务的定时更新和立刻更新。
- 而对于scheduleUpdateIfAbsent()方法,则通过线程池来进行异步的更新,将回调的结果(Future)保存到futureMap中,并且发生提交线程任务时,还负责更新本地注册列表中的数据。
Eureka
核心概念
registry: 注册表
ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>> registry = new ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>>();
说明:
- 第一层Map的Key就是服务名称,第二层Map的key是服务的实例id
- InstanceInfo就代表了服务实例的具体信息,比如机器的ip地址、hostname以及端口号。
- Lease里面则会维护每个服务最近一次发送心跳的时间
EurekaServer:服务端
注册中心服务端主要对外提供了三个功能:
- 服务注册
服务提供者启动时,会通过 EurekaClient 向 EurekaServer 注册信息,EurekaServer 会存储该服务的信息,EurekaServer 内部有二层缓存机制来维护整个注册表 - 提供注册表
服务消费者在调用服务时,如果 EurekaClient 没有缓存注册表的话,会从 EurekaServer 获取最新的注册表 - 同步状态
EurekaClient 通过注册、心跳机制和 EurekaServer 同步当前客户端的状态。
EurekaClient:客户端
EurekaClient 是一个 Java 客户端,用于简化与 EurekaServer 的交互。EurekaClient 会拉取、更新和缓存 EurekaServer 中的信息。因此当所有的 EurekaServer 节点都宕掉,服务消费者依然可以使用缓存中的信息找到服务提供者,但是当服务有更改的时候会出现信息不一致。
Register: 服务注册
服务的提供者,将自身注册到注册中心,服务提供者也是一个 EurekaClient。当 EurekaClient 向 EurekaServer 注册时,它提供自身的元数据,比如 IP 地址、端口,运行状况指示符 URL,主页等。Renew: 服务续约
EurekaClient 会每隔 30 秒发送一次心跳来续约。 通过续约来告知 EurekaServer 该 EurekaClient 运行正常,没有出现问题。 默认情况下,如果 EurekaServer 在 90 秒内没有收到 EurekaClient 的续约,Server 端会将实例从其注册表中删除,此时间可配置,一般情况不建议更改。# 服务续约任务的调用间隔时间,默认为30秒 eureka.instance.lease-renewal-interval-in-seconds=30 # 服务失效的时间,默认为90秒。 eureka.instance.lease-expiration-duration-in-seconds=90Eviction 服务剔除
当 EurekaClient 和 EurekaServer 不再有心跳时,EurekaServer 会将该服务实例从服务注册列表中删除,即服务剔除。Cancel: 服务下线
EurekaClient 在程序关闭时向 EurekaServer 发送取消请求。 发送请求后,该客户端实例信息将从 EurekaServer 的实例注册表中删除。该下线请求不会自动完成,它需要调用以下内容:DiscoveryManager.getInstance().shutdownComponent();GetRegisty: 获取注册列表信息
EurekaClient 从服务器获取注册表信息,并将其缓存在本地。客户端会使用该信息查找其他服务,从而进行远程调用。该注册列表信息定期(每30秒钟)更新一次。每次返回注册列表信息可能与 EurekaClient 的缓存信息不同,EurekaClient 自动处理。如果由于某种原因导致注册列表信息不能及时匹配,EurekaClient 则会重新获取整个注册表信息。 EurekaServer 缓存注册列表信息,整个注册表以及每个应用程序的信息进行了压缩,压缩内容和没有压缩的内容完全相同。EurekaClient 和 EurekaServer 可以使用 JSON/XML 格式进行通讯。在默认情况下 EurekaClient 使用压缩 JSON 格式来获取注册列表的信息。
获取服务是服务消费者的基础,所以必有两个重要参数需要注意:
# 启用服务消费者从注册中心拉取服务列表的功能 eureka.client.fetch-registry=true # 设置服务消费者从注册中心拉取服务列表的间隔 eureka.client.registry-fetch-interval-seconds=30Remote Call: 远程调用
当 EurekaClient 从注册中心获取到服务提供者信息后,就可以通过 Http 请求调用对应的服务;服务提供者有多个时,EurekaClient 客户端会通过 Ribbon 自动进行负载均衡。
自我保护机制
默认情况下,如果 EurekaServer 在一定的 90s 内没有接收到某个微服务实例的心跳,会注销该实例。但是在微服务架构下服务之间通常都是跨进程调用,网络通信往往会面临着各种问题,比如微服务状态正常,网络分区故障,导致此实例被注销。
固定时间内大量实例被注销,可能会严重威胁整个微服务架构的可用性。为了解决这个问题,Eureka 开发了自我保护机制,那么什么是自我保护机制呢?
EurekaServer 在运行期间会去统计心跳失败比例在 15 分钟之内是否低于 85%,如果低于 85%,EurekaServer 即会进入自我保护机制。EurekaServer 触发自我保护机制后,页面会出现提示: EMERGENCY! EUREKA MAY BE INCORRECTLY CLAIMING INSTANCES ARE UP WHEN THEY’RE NOT. RENEWALS ARE LESSER THAN THRESHOLD AND HENCE THE INSTANCES ARE NOT BEING EXPIRED JUST TO BE SAFE.
EurekaServer 进入自我保护机制,会出现以下几种情况:
- (1 Eureka 不再从注册列表中移除因为长时间没收到心跳而应该过期的服务
- (2 Eureka 仍然能够接受新服务的注册和查询请求,但是不会被同步到其它节点上(即保证当前节点依然可用)
- (3 当网络稳定时,当前实例新的注册信息会被同步到其它节点中
Eureka 自我保护机制是为了防止误杀服务而提供的一个机制。当个别客户端出现心跳失联时,则认为是客户端的问题,剔除掉客户端;当 Eureka 捕获到大量的心跳失败时,则认为可能是网络问题,进入自我保护机制;当客户端心跳恢复时,Eureka 会自动退出自我保护机制。
如果在保护期内刚好这个服务提供者非正常下线了,此时服务消费者就会拿到一个无效的服务实例,即会调用失败。对于这个问题需要服务消费者端要有一些容错机制,如重试,断路器等。
通过在 EurekaServer 配置如下参数,开启或者关闭保护机制,生产环境建议打开:
eureka.server.enable-self-preservation=true
Eureka 集群原理
EurekaServer 集群相互之间通过 Replicate 来同步数据,相互之间不区分主节点和从节点,所有的节点都是平等的。在这种架构中,节点通过彼此互相注册来提高可用性,每个节点需要添加一个或多个有效的 serviceUrl 指向其他节点。
如果某台 EurekaServer 宕机,EurekaClient 的请求会自动切换到新的 EurekaServer 节点。当宕机的服务器重新恢复后,Eureka 会再次将其纳入到服务器集群管理之中。当节点开始接受客户端请求时,所有的操作都会进行节点间复制,将请求复制到其它 EurekaServer 当前所知的所有节点中。
另外 EurekaServer 的同步遵循着一个非常简单的原则:只要有一条边将节点连接,就可以进行信息传播与同步。所以,如果存在多个节点,只需要将节点之间两两连接起来形成通路,那么其它注册中心都可以共享信息。每个 EurekaServer 同时也是 EurekaClient,多个 EurekaServer 之间通过 P2P 的方式完成服务注册表的同步。
EurekaServer 集群之间的状态是采用异步方式同步的,所以不保证节点间的状态一定是一致的,不过基本能保证最终状态是一致的。
Eureka 分区
Eureka 提供了 Region 和 Zone 两个概念来进行分区,这两个概念均来自于亚马逊的 AWS:
- region:可以理解为地理上的不同区域,比如亚洲地区,中国区或者深圳等等。没有具体大小的限制。根据项目具体的情况,可以自行合理划分 region。
- zone:可以简单理解为 region 内的具体机房,比如说 region 划分为深圳,然后深圳有两个机房,就可以在此 region 之下划分出 zone1、zone2 两个 zone。
Zone 内的 EurekaClient 优先和 Zone 内的 EurekaServer 进行心跳同步,同样调用端优先在 Zone 内的 EurekaServer 获取服务列表,当 Zone 内的 EurekaServer 挂掉之后,才会从别的 Zone 中获取信息。
Eurka 保证 AP
EurekaServer 各个节点都是平等的,几个节点挂掉不会影响正常节点的工作,剩余的节点依然可以提供注册和查询服务。而 EurekaClient 在向某个 Eureka 注册时,如果发现连接失败,则会自动切换至其它节点。只要有一台 EurekaServer 还在,就能保证注册服务可用(保证可用性),只不过查到的信息可能不是最新的(不保证强一致性)。
工作流程
- EurekaServer 启动成功,等待服务端注册。在启动过程中如果配置了集群,集群之间定时通过 Replicate 同步注册表,每个 EurekaServer 都存在独立完整的服务注册表信息;
- EurekaClient 启动时根据配置的 EurekaServer 地址去注册中心注册服务;
- EurekaClient 会每 30s 向 EurekaServer 发送一次心跳请求,证明客户端服务正常;
- 当 EurekaServer 90s 内没有收到 EurekaClient 的心跳,注册中心则认为该节点失效,会注销该实例;
- 单位时间内 EurekaServer 统计到有大量的 EurekaClient 没有上送心跳,则认为可能为网络异常,进入自我保护机制,不再剔除没有上报心跳的客户端;
- 当 EurekaClient 心跳请求恢复正常之后,EurekaServer 自动退出自我保护模式;
- EurekaClient 定时全量或者增量从注册中心获取服务注册表,并且将获取到的信息缓存到本地;
- 服务调用时,EurekaClient 会先从本地缓存找寻调取的服务。如果获取不到,先从注册中心刷新注册表,再同步到本地缓存;
- EurekaClient 获取到目标服务器信息,发起服务调用;
- EurekaClient 程序关闭时向 EurekaServer 发送取消请求,EurekaServer 将实例从注册表中删除。
多级缓存机制
在eurekaClient拉取注册表的时候,就会用到所谓的多级缓存机制,多级缓存机制中有两个缓存, 一个叫只读缓存 ReadOnlyCacheMap, 一个叫读写缓存 ReadWriteCacheMap 。
eurekaClient 拉取注册表的时候, 会先从 ReadOnlyCacheMap 中去获取注册表数据,如果获取不到的话再去 ReadWriteCacheMap 中找, 如果还是找不到的话,那就只能重新从注册表中 registry 拉取了
ReadOnlyCacheMap就是一个普通的ConcurrentHashMap,而ReadWriteCacheMap是guava cache,如果ReadWriteCacheMap读不到数据,就会通过ClassLoader的 load方法直接从注册表获取数据再返回
多级缓存机制有多种过期策略:
- 主动过期:当服务实例发生注册、下线、故障的时候,ReadWriteCacheMap中所有的缓存过期掉
- 定时过期:readWriteCacheMap在构建的时候,指定了一个自动过期的时间,默认值就是180秒,所以你往readWriteCacheMap中放入一个数据,180秒过后,就将这个数据给他过期了
- 被动过期 : 默认是每隔 30 秒 , 执行一个定时调度的线程任务, 对 readOnlyCacheMap和readWriteCacheMap中的数据进行一个比对,如果两块数据是不一致的,那么就将 readWriteCacheMap中的数据放到readOnlyCacheMap中来
通过过期的机制,可以发现一个问题,就是如果ReadWriteCacheMap发生了主动过期或定时过期, 此时里面的缓存就被清空或部分被过期了, 但是在此之前 readOnlyCacheMap刚执行了被动过期,发现两个缓存是一致的,就会接着使用里面的缓存数据所以可能会存在30秒的时间,readOnlyCacheMap和ReadWriteCacheMap的数据不一致
服务熔断
Hystrix
- 容错限流的需求
在复杂的分布式系统中通常有很多依赖,如果一个应用不能对来自依赖故障进行隔离,那么应用本身就处于被拖垮的风险中。在一个高流量的网站中,某一个单一后端一旦发生延迟,将会在数秒内 导致所有的应用资源被耗尽,这也就是我们常说的雪崩效应。
比如在电商系统的下单业务中,在订单服务创建订单后同步调用库存服务进行库存的扣减,假如库存服务出现了故障,那么会导致下单请求线程会被阻塞,当有大量的下单请求时,则会占满应用连 接数从而导致订单服务无法对外提供服务。 - 容错限流的原理
对于基本的容错限流模式,主要有以下几点需要考量:- 主动超时:在调用依赖时尽快的超时,可以设置比较短的超时时间,比如2s,防止长时间的等待
- 限流:限制最大并发数
- 熔断:错误数达到阈值时,类似于保险丝熔断隔离:隔离不同的依赖调用
- 服务降级:资源不足时进行服务降级
- 容错模式
- 断路器模式
实现流程为:当断路器的开关为关闭时,每次请求进来都是成功的,当后端服务出现问题,请求出现的错误数达到一定的阈值,则会触发断路器为打开状态;在断路器为打开状态时,进来的所有请求都会被拒绝,当然也不是一直会拒绝请求,而是弹性的,过了特定的时间后,断路器会进入半打开状态,这是会让一部分请求通过进行尝试,如果尝试还是有问题,则继续进入打开状态,如果尝试没有问题了,则会进入关闭状态 - 舱壁隔离模式
舱壁隔离模式可以对资源进行隔离,类似于船的船舱都是被隔离开来的,当其中一个或者几个船舱出现问题,比如漏水,是不会影响到其他的船舱的,从而实现一种资源隔离的效果
- 断路器模式
- 容错理念
- 凡是依赖都有可能会失败
- 凡是资源都有限制,比如CPU、Memory、Threads、Queue
- 网络并不可靠,可能存在网络抖动等其他问题
- 延迟是应用稳定的杀手,延迟会占据大量的资源
- Hystrix工作流程
- 对于一次依赖调用,会被封装在一个HystrixCommand对象中,调用的执行有两种方式,一种是调用execute()方法同步调用,另一种是调用queue()方法进行异步调用。
- 执行时会判断断路器开关是否打开,如果断路器打开,则进入getFallback()降级逻辑;如果断路器关闭,则判断线程池/信号量资源是否已满,如果资源满了,则进入 getFallback()降级逻辑;如果没满,则执行run()方法。再判断执行run()方法是否超时,超时则进入getFallback()降级逻辑,run()方法执行失败,则进入getFallback()降级逻辑,执行成功则报告Metrics。Metrics中的数据包括执行成功、超时、失败等情况的数据,Hystrix会计算一个断路器的健康值,也就是失败率,当失败率超过阈值后则会触发断路器开关打开。
- getFallback()逻辑为:如果没有实现fallback()方法,则直接抛出异常,另外fallback降级也是需要资源的,在fallback时需要获取一个针对fallback的信号量,只有获取成功才能fallback,获取信号量失败,则抛出异常,获取信号量成功,才会执行fallback方法并且会响应fallback方法中的内容
Sentinel
限流条件
流量控制(Flow Control),原理是监控应用流量的QPS或并发线程数等指标,当达到指定阈值时对流量进行控制,避免系统被瞬时的流量高峰冲垮,保障应用高可用性。
一条限流规则主要由下面几个因素组成,我们可以组合这些元素来实现不同的限流效果:
- resource:资源名,即限流规则的作用对象;唯一名称,默认请求路径
- count: 限流阈值
- grade: 限流阈值类型(QPS 或并发线程数)
- limitApp: 流控针对的调用来源,若为 default 则不区分调用来源
- strategy: 调用关系限流策略
1.直接:当api大达到限流条件时,直接限流
2.关联:当关联的资源到达阈值,就限流自己
3.链路:只记录指定路上的流量,指定资源从入口资源进来的流量,如果达到阈值,就进行限流,api级别的限流 - controlBehavior: 流量控制效果(直接拒绝、Warm Up、匀速排队)
核心组件
- Resource
resource是sentinel中最重要的一个概念,sentinel通过资源来保护具体的业务代码或其他后方服务。sentinel把复杂的逻辑给屏蔽掉了,用户只需要为受保护的代码或服务定义一个资源,然后定义规则就可以了,剩下的通通交给sentinel来处理了。定义完资源后,就可以通过在程序中埋点来保护你自己的服务了,埋点的方式有两种:- try-catch 方式(通过 SphU.entry(...)),当 catch 到BlockException时执行异常处理(或fallback)
- if-else 方式(通过 SphO.entry(...)),当返回 false 时执行异常处理(或fallback)
- Context
Context是对资源操作时的上下文环境,每个资源操作(针对Resource进行的entry/exit)必须属于一个Context,如果未指定会使用默认的Context。。一个Context生命周期内可能有多个资源操作,Context生命周期内的最后一个资源exit时会清理该Context,这也预示这整个Context生命周期的结束。Context主要属性如下:public class Context { // context名字,默认名字 "sentinel_default_context" private final String name; // context入口节点,每个context必须有一个entranceNode private DefaultNode entranceNode; // context当前entry,Context生命周期中可能有多个Entry,所有curEntry会有变化 private Entry curEntry; // The origin of this context (usually indicate different invokers, e.g. service consumer name or origin IP). private String origin = ""; private final boolean async; } - Entry
每次执行 SphU.entry() 或 SphO.entry() 都会返回一个Entry,Entry表示一次资源操作,内部会保存当前invocation信息。在一个Context生命周期中多次资源操作,也就是对应多个Entry,这些Entry形成parent/child结构保存在Entry实例中 - Node
默认实现类DefaultNode,该类还有一个子类EntranceNode- EntranceNode:该类的创建是在初始化Context时完成的(ContextUtil.trueEnter方法),注意该类是针对Context维度的,也就是一个context有且仅有一个EntranceNode。
- DefaultNode:该类的创建是在NodeSelectorSlot.entry完成的,当不存在context.name对应的DefaultNode时会新建(new DefaultNode(resourceWrapper, null),对应resouce)并保存到本地缓存(NodeSelectorSlot中private volatile Map<String, DefaultNode> map);获取到context.name对应的DefaultNode后会将该DefaultNode设置到当前context的curEntry.curNode属性,也就是说,在NodeSelectorSlot中是一个context有且仅有一个DefaultNode。
- Slot
slot是另一个sentinel中非常重要的概念,sentinel的工作流程就是围绕着一个个插槽所组成的插槽链来展开的。需要注意的是每个插槽都有自己的职责,他们各司其职完好的配合,通过一定的编排顺序,来达到最终的限流降级的目的。默认的各个插槽之间的顺序是固定的,因为有的插槽需要依赖其他的插槽计算出来的结果才能进行工作。
默认生成ProcessorSlotChain为DefaultProcessorSlotChain, 这里大概的介绍下每种Slot的功能职责:- NodeSelectorSlot 负责收集资源的路径,并将这些资源的调用路径,以树状结构存储起来,用于根据调用路径来限流降级;
- ClusterBuilderSlot 则用于存储资源的统计信息以及调用者信息,例如该资源的 RT, QPS, thread count 等等,这些信息将用作为多维度限流,降级的依据;
- StatisticsSlot 则用于记录,统计不同维度的 runtime 信息;
- SystemSlot 则通过系统的状态,例如 load1 等,来控制总的入口流量;
- AuthoritySlot 则根据黑白名单,来做黑白名单控制;
- FlowSlot 则用于根据预设的限流规则,以及前面 slot 统计的状态,来进行限流;
- DegradeSlot 则通过统计信息,以及预设的规则,来做熔断降级;
服务调用
Feign
调用流程:
@FeignClient -> JDK Proxy动态代理 -> ReflectiveFeign.FeignInvocationHandler -> SynchronousMethodHandler -> feign.Client
从源码的角度上说明 Feign 的底层原理:
- 通过 @EnableFeignCleints 注解启动 Feign Starter 组件
- Feign Starter 在项目启动过程中注册全局配置,扫描包下所有的 @FeignClient 接口类,并进行注册 IOC 容器
- @FeignClient 接口类被注入时,通过 FactoryBean#getObject 返回动态代理类
- 接口被调用时被动态代理类逻辑拦截,将 @FeignClient 请求信息通过编码器生成 Request
- 交由 Ribbon 进行负载均衡,挑选出一个健康的 Server 实例
- 继而通过 Client 携带 Request 调用远端服务返回请求响应
- 通过解码器生成 Response 返回客户端,将信息流解析成为接口返回数据
Dubbo
RestTemplate
网关路由
网关: 入路负限鉴监跨聚 入口、路由、负载、限流、鉴权、监控、跨域、聚合api文档
Zuul
Zuul网关过滤器类型:
- 前置过滤(pre filter):是请求进入Zuul之后,立刻执行的过滤逻辑。
- 路由后过滤(routing filter):是请求进入Zuul之后,并Zuul实现了请求路由后执行的过滤逻辑,路由后过滤,是在远程服务调用之前过滤的逻辑。
- 后置过滤(post filter):远程服务调用结束后执行的过滤逻辑。
- 异常过滤(error filter):是任意一个过滤器发生异常或远程服务调用无结果反馈的时候执行的过滤逻辑。无结果反馈,就是远程服务调用超时。
- 异常过滤(custom filter):自定义过滤器。
ZuulFilter 四个抽象方法:
- filterType: 方法返回字符串数据,代表当前过滤器的类型。可选值有 pre, route, post, error。
- filterOrder: 返回int数据,用于为同filterType的多个过滤器定制执行顺序,返回值越小,执行顺序越优先。
- shouldFilter: 返回boolean数据,代表当前filter是否生效。
- run: 具体的过滤执行逻辑。如pre类型的过滤器,可以通过对请求的验证来决定是否将请求路由到服务上;如post类型的过滤器,可以对服务响应结果做加工处理(如为每个响应增加footer数据)。
过滤器的生命周期:
app client -> zuul -> pre filter -> routing filter -> app service -> post filter(error filter) -> return
Zuul默认过滤器:
| 过滤器 | 类型 | 顺序 | 作用 |
|---|---|---|---|
| ServletDetectionFilter | pre | -3 | 标记处理Servlet的类型 |
| Servlet30WrapperFilter | pre | -2 | 包装HttpServletRequest请求 |
| FormBodyWrapperFilter | pre | -1 | 包装请求体 |
| DebugFilter | pre | 1 | 标记调试标志 |
| PreDecorationFilter | pre | 5 | 处理请求上下文供后续使用 |
| RibbonRoutingFilter | route | 10 | serviceId请求转发 |
| SimpleHostRoutingFilter | route | 100 | url请求转发 |
| SendForwardFilter | route | 500 | forward请求转发 |
| LocationRewriteFilter | post | 900 | 请求地址重写 |
| SendResponseFilter | post | 1000 | 处理正常的请求响应 |
| SendErrorFilter | error | 0 | 处理错误的请求响应 |
Gateway
Route配置
Route 配置 Predicate 和 Filter
spring:
cloud:
gateway:
enabled: true
routes:
- id: blog1
uri: https://blog.csdn.net/
predicates:
- Path=/blog1/**
filters:
- RewritePath=/blog1/(?<segment>.*), /$\{segment}
# 代理前 http://192.168.68.1:9999/blog1/crazymakercircle/article/details/80208650
# 代理后 https://blog.csdn.net/crazymakercircle/article/details/80208650
- id: service_provider_demo_route_filter
uri: lb://service-provider-demo
predicates:
- Path=/filter/**
filters:
- RewritePath=/filter/(?<segment>.*), /provider/$\{segment}
- UserIdCheck # 自定义filter(UserIdCheckGateWayFilter)
- id: service_consumer_demo_route
uri: lb://service-consumer-demo
predicates:
- Path=/consumer/**
核心对象
- Route: 路由是网关的基本单元,由ID、URI、一组Predicate、一组Filter组成,根据Predicate进行匹配转发
- Predicate: 路由转发的判断条件,目前SpringCloud Gateway支持多种方式,常见如:Path、Query、Method、Header等,写法必须遵循 key=vlue的形式
- Filter: 过滤器是路由转发请求时所经过的过滤逻辑,可用于修改请求、响应内容
路由匹配规则
Predicate断言规则
| 规则 | 实例 | 说明 |
|---|---|---|
| Query | - Query=keep, pu. | 当请求中包含 keep 属性并且参数值是以 pu 开头的长度为三位的字符串才会进行匹配和路由 |
| Path | - Path=/gate/,/rule/ | 当请求的路径为gate、rule开头的时,转发到http://localhost:9023服务器上 |
| Before | - Before=2017-01-20T17:42:47 | 在某个时间之前的请求才会被转发到 http://localhost:9023服务器上 |
| After | - After=2017-01-20T17:42:47 | 在某个时间之后的请求才会被转发 |
| Between | - Between=2017-01-20T17:42:47,2017-01-21T17:42:47 | 在某个时间段之间的才会被转发 |
| Cookie | - Cookie=chocolate, ch.p | 名为chocolate的表单或者满足正则ch.p的表单才会被匹配到进行请求转发 |
| Header | - Header=X-Request-Id, \d+ | 携带参数X-Request-Id或者满足\d+的请求头才会匹配 |
| Host | - Host=www.hd123.com | 当主机名为 www.hd123.com 的时候直接转发到http://localhost:9023服务器上 |
| Method | - Method=GET | 只有GET方法才会匹配转发请求,还可以限定POST、PUT等请求方式 |
| ip | - RemoteAddr=192.168.1.1/24 | 如果请求的远程地址是 192.168.1.x,则此路由将匹配 |
各种 Predicates 同时存在于同一个路由时,请求必须同时满足所有的条件才被这个路由匹配。
一个请求满足多个路由的断言条件时,请求只会被首个成功匹配的路由转发
Filter过滤器规则
| 过滤规则 | 实例 | 说明 |
|---|---|---|
| PrefixPath | - PrefixPath=/app | 在请求路径前加上app |
| RedirectTo | - RedirectTo=302, https://acme.org | 重定向 |
| RemoveRequestHeader | - RemoveRequestHeader=X-Request-Foo | 去掉请求头信息X-Request-Foo |
| RemoveResponseHeader | - RemoveResponseHeader=X-Request-Fo | 去掉响应头信息X-Request-Foo |
| RemoveRequestParameter | - RemoveRequestParameter=red | 去掉某个请求参数信息 |
| RewritePath | - RewritePath=/test, /app/test | 访问localhost:9022/test,请求会转发到localhost:8001/app/test |
| SetPath | - SetPath=/app/ | 修改请求路径 |
| SetRequestHeader | - SetRequestHeader=X-Request-Red, Blue | 设置请求头信息 |
| SetStatus | - SetStatus=401 | 设置回执状态码 |
| StripPrefix | - StripPrefix=2 | 跳过指定路径 |
当配置多个filter时,优先定义的会被调用,剩余的filter将不会生效
工作原理
GatewayClient -> HttpWebHandlerAdapter(组装成网关上下文) -> DispatcherHandler(循环遍历Mapping,获取Handler) -> RoutePredicateHandlerMapping(匹配路由信息,通过路由判断断言是否可用) -> 断言失败(执行下一个Mapping)&断言成功 -> FilteringWebHandler(创建过滤器链,调用过滤器) -> pre filter ->post filter -> proxy service
Spring Cloud Gateway 的核心处理流程如下图,Gateway的客户端回向Spring Cloud Gateway发起请求,请求首先会被HttpWebHandlerAdapter进行提取组装成网关的上下文,然后网关的上下文会传递到DispatcherHandler。DispatcherHandler是所有请求的分发处理器,DispatcherHandler主要负责分发请求对应的处理器,比如将请求分发到对应RoutePredicateHandlerMapping(路由断言处理器映射器)。路由断言处理映射器主要用于路由的查找,以及找到路由后返回对应的FilteringWebHandler。FilteringWebHandler主要负责组装Filter链表并调用Filter执行一系列Filter处理,然后把请求转到后端对应的代理服务处理,处理完毕后,将Response返回到Gateway客户端。
在Filter链中,通过虚线分割Filter的原因是,过滤器可以在转发请求之前处理或者接收到被代理服务的返回结果之后处理。所有的Pre类型的Filter执行完毕之后,才会转发请求到被代理的服务处理。被代理的服务把所有请求完毕之后,才会执行Post类型的过滤器。
消息队列
Kafka
RabbitMQ
RocketMQ
各种队列对比
| 分类 | 功能项 | Kafka | RocketMQ | RabbitMQ |
|---|---|---|---|---|
| 功能 | 推拉模式 | pull | pull | push |
| 延迟队列 | × | √ | √ | |
| 死信队列 | × | √ | √ | |
| 优先级队列 | × | × | √ | |
| 消息回溯 | √ | √ | × | |
| 消息持久化 | √ | √ | √ | |
| 确认机制 | offset | offset | 单条 | |
| 消息TTL | × | √ | √ | |
| 多租户隔离 | × | × | × | |
| 消息顺序性 | 分区有序 | 消费者加锁 | × | |
| 消息查询 | × | √ | √ | |
| 消息模式 | 流模式 | 广播+集群模式 | 队列模式 | |
| 消息可靠性 | request.required.acks | 与kafka类似 | 镜像模式 | |
| 性能 | 单机吞吐量 | 605MB/s | 与kafka类似 | 38MB/s |
| 消息延迟 | 5ms | 毫秒级 | 微秒级 | |
| 支持主题数 | 几十到几百 | 几百到几千 | 数千 | |
| 运维 | 高可用 | 分布式架构 | 主从架构 | 主从架构 |
| 集群扩容 | 增加节点,通过复制数据均衡 | 增加节点 | 增加节点 |
服务总线
Bus
负载均衡
Ribbon
工作流程
- 微服务之间通过Feign调用,最后通过LoadBalancerFeignClient发送请求
- LoadBalancerFeignClient端从client端服务的上下文环境中找到负载均衡器,并把提取到的服务名称交给负载均衡器
- 负载均衡器提到选到 server 实例 , 将 client 端的请求包装成调用请求LoadBalancerCommand
- 根据封装的信息,发送远程调用到具体的服务实例
- 和Feign的集成模式:
在使用Feign作为客户端时,最终请求会转发成http://<服务名称>/的格式,通过 LoadBalancerFeignClient, 提取出服务标识 <服务名称> ,然后根据服务名称在上下文中查找对应服务的负载均衡器FeignLoadBalancer,负载均衡器负责根据既有的服务实例的统计信息,挑选出最合适的服务实例
负载算法
- RoundRobinRule: 轮询策略,按照一定的顺序依次调用服务实例。
- WeightedResponseTimeRule: 权重策略,根据每个服务提供者的响应时间分配一个权重,响应时间越长,权重越小,被选中的可能性也就越低。
- RandomRule: 随机策略,从服务提供者的列表中随机选择一个服务实例。
- BestAvailableRule: 最小连接数策略,也叫最小并发数策略,它是遍历服务提供者列表,选取连接数最小的⼀个服务实例。如
- RetryRule: 重试策略,按照轮询策略来获取服务,如果获取的服务实例为 null 或已经失效,则在指定的时间之内不断地进行重试来获取服务,如果超过指定时间依然没获取到服务实例则返回 null。
- AvailabilityFilteringRule: 可用敏感性策略,先过滤掉非健康的服务实例,然后再选择连接数较小的服务实例。
- ZoneAvoidanceRule: 区域敏感策略,根据服务所在区域(zone)的性能和服务的可用性来选择服务实例,在没有区域的环境下,该策略和轮询策略类似。
分布式事务
Seata
Seata 分三大模块 :
- TC :事务协调者。负责我们的事务ID的生成,事务注册、提交、回滚等。
- TM:事务发起者。定义事务的边界,负责告知 TC,分布式事务的开始,提交,回滚。
- RM:资源管理者。管理每个分支事务的资源,每一个 RM 都会作为一个分支事务注册在 TC。
在Seata的AT模式中,TM和RM都作为SDK的一部分和业务服务在一起,我们可以认为是Client。TC是一个独立的服务,通过服务的注册、发现将自己暴露给Client们。
Seata 中有三大模块中, TM 和 RM 是作为 Seata 的客户端与业务系统集成在一起,TC 作为 Seata 的服务端独立部署。
在 Seata 中,分布式事务的执行流程:
- TM 开启分布式事务(TM 向 TC 注册全局事务记录);
- 按业务场景,编排数据库、服务等事务内资源(RM 向 TC 汇报资源准备状态 );
- TM 结束分布式事务,事务一阶段结束(TM 通知 TC 提交/回滚分布式事务);
- TC 汇总事务信息,决定分布式事务是提交还是回滚;
- TC 通知所有 RM 提交/回滚 资源,事务二阶段结束;
AT
Seata AT模式是最早支持的模式。AT模式是指Automatic (Branch) Transaction Mode自动化分支事务。
Seata AT 模式是增强型2pc模式,或者说是增强型的XA模型。
总体来说,AT 模式,是 2pc两阶段提交协议的演变,不同的地方,Seata AT 模式不会一直锁表。
执行流程
- 第一阶段流程:
- 1)余额服务中的TM,向TC申请开启一个全局事务,TC会返回一个全局的事务ID。
- 2)余额服务在执行本地业务之前,RM会先向TC注册分支事务。
- 3)余额服务依次生成undo log、执行本地事务、生成redo log,最后直接提交本地事务。
- 4)余额服务的RM向TC汇报,事务状态是成功的。
- 5)余额服务发起远程调用,把事务ID传给积分服务。
- 6)积分服务在执行本地业务之前,也会先向TC注册分支事务。
- 7)积分服务次生成undo log、执行本地事务、生成redo log,最后直接提交本地事务。
- 8)积分服务的RM向TC汇报,事务状态是成功的。
- 9)积分服务返回远程调用成功给余额服务。
- 10)余额服务的TM向TC申请全局事务的提交/回滚。
- 第二阶段流程:
- Client和TC之间是有长连接的,如果是正常全局提交,则TC通知多个RM异步清理掉本地的redo和undo log即可。如果是回滚,则TC通知每个RM回滚数据即可。
TCC
TCC 与 Seata AT 事务一样都是两阶段事务,它与 AT 事务的主要区别为:
- TCC 对业务代码侵入严重
每个阶段的数据操作都要自己进行编码来实现,事务框架无法自动处理。 - TCC 性能更高
不必对数据加全局锁,允许多个事务同时操作数据。
Seata TCC 整体是 两阶段提交 的模型。一个分布式的全局事务,全局事务是由若干分支事务组成的,分支事务要满足 两阶段提交 的模型要求,即需要每个分支事务都具备自己的:
- 一阶段 prepare 行为
- 二阶段 commit 或 rollback 行为
根据两阶段行为模式的不同,我们将分支事务划分为 Automatic (Branch) Transaction Mode 和 TCC (Branch) Transaction Mode.
AT 模式(参考链接 TBD)基于 支持本地 ACID 事务 的 关系型数据库:
- 一阶段 prepare 行为:在本地事务中,一并提交业务数据更新和相应回滚日志记录。
- 二阶段 commit 行为:马上成功结束,自动 异步批量清理回滚日志。
- 二阶段 rollback 行为:通过回滚日志,自动 生成补偿操作,完成数据回滚。
相应的,TCC 模式,不依赖于底层数据资源的事务支持:
- 一阶段 prepare 行为:调用 自定义 的 prepare 逻辑。
- 二阶段 commit 行为:调用 自定义 的 commit 逻辑。
- 二阶段 rollback 行为:调用 自定义 的 rollback 逻辑。
所谓 TCC 模式,是指支持把 自定义 的分支事务纳入到全局事务的管理中。
SAGA
Saga模式是SEATA提供的长事务解决方案,在Saga模式中,业务流程中每个参与者都提交本地事务,当出现某一个参与者失败则补偿前面已经成功的参与者,一阶段正向服务和二阶段补偿服务都由业务开发实现。
适用场景:
- 业务流程长、业务流程多
- 参与者包含其它公司或遗留系统服务,无法提供 TCC 模式要求的三个接口
XA
在 Seata 定义的分布式事务框架内,利用事务资源(数据库、消息服务等)对 XA 协议的支持,以 XA 协议的机制来管理分支事务的一种 事务模式。
- 执行阶段:
- 可回滚:业务 SQL 操作放在 XA 分支中进行,由资源对 XA 协议的支持来保证 可回滚
- 持久化:XA 分支完成后,执行 XA prepare,同样,由资源对 XA 协议的支持来保证 持久化(即,之后任何意外都不会造成无法回滚的情况)
- 完成阶段:
- 分支提交:执行 XA 分支的 commit
- 分支回滚:执行 XA 分支的 rollback
4种模式对比
- AT:
- 基于sql层面,进行sql解析,并记录sql执行前后的数据,持久化到 undo.log(修改前数据)和redo.log(修改后数据)
- AT模式的RM在本地事务执行完成后就会提交事务,不会占用本地资源
- 最终一致性,可能会出现脏读
- 业务无侵入
- XA:
- 基于XA协议,全局一致性,没有数据日志的记录,由 XA 分支来执行 sql 进入准备状态,通知 TC ,由进行对各个(RM)分支事务处理
- XA模式则是在所有的本地事务执行完成后,TC才发送指令让所有的RM提交事务、释放本地锁
- 强一致性,因为都是它一直占着,不全部执行完就不放。因此XA模式是不会出现脏读
- 业务无侵入
- XA 和 AT 共同点:基于 支持本地 ACID 事务 的 关系型数据库
- TCC:
- 不依赖于底层数据资源的事务支持
- 侵入性较强,需自己编写事务控制逻辑
- 全局基本没有锁,性能较强
- 业务侵入
- Saga:
- 基于事件来驱动的,各个参与者之间的是异步执行的,Saga模式是一种长事务解决方案
- 业务侵入
身份认证
OAuth2
Shiro
SpringSecurity
链路追踪
服务监控
参考:
- Eureka工作原理
- Eureka 缓存机制详细配置
- Feign原理(图解)
- Spring Cloud学习(四)Zuul过滤器详解
- SpringCloud gateway(史上最全)
- SpringCloud Gateway的工作原理
- sentinel(史上最全+入门教程)
- 分布式事务( 图解 + 秒懂 + 史上最全 )
- seata 的 4大事务模式对比
- Nacos配置中心工作原理(超简单)
- SpringCloud-Nacos注册中心实现原理
- nacos注册中心的注册原理深度解析
- 一张图搞定OAuth2.0 - 闪客sun - 博客园 (cnblogs.com)
- 理解OAuth 2.0 - 阮一峰的网络日志 (ruanyifeng.com)
- Spring Security -- Spring Boot中开启Spring Security - 大奥特曼打小怪兽 - 博客园 (cnblogs.com)