微服务的基本概念

  1. 服务调用(远程调用)

    ​ 将一个系统拆分成各个微服务后,各个微服务之间协同工作才能对外提供完整的服务,这就涉及到各个 微服务之间的调用问题。目前各个微服务之间一般会采用Restful接口或者RPC协议的方式进行调用。

    (1)Restful接口 Restful接口一般是基于HTTP协议实现的,这种协议使用上比较广泛,几乎所有的编程语言都支持HTTP 协议。

    (2)RPC协议 RPC是一种远程过程调用,能够做到像调用本地服务一样调用远程服务。RPC框架在底层屏蔽了数据的 传输方式,序列化方式和交互的细节信息,让使用RPC框架开发微服务的人员觉得更加简单,实现起来 更加容易。

  2. 服务治理(服务注册、服务发现、服务剔除)

    (1) 服务注册

    ​ 各个微服务实例在启动时,能够将自身提供的服务注册到某个注册中心。

    (2) 服务发现

    ​ 当某个微服务将自身提供的服务注册到注册中心时,其他微服务实例能够通过注册中心感知到这个微服 务提供的服务,并且能够获取到这个微服务的实例信息,通过这个微服务的实例信息就能够调用这个微 服务的方法,来进行相应的读写操作。

    (3) 服务剔除

    ​ 如果某个微服务实例出现故障,或者连接一直超时,则注册中心会认为当前微服务实例不可用,就会将 这个微服务实例剔除出注册中心,使其不再被其他微服务感知到和调用到。

  3. 注册中心

    ​ 提供微服务注册、发现和剔除功能的服务组件。

  4. 服务网关

    ​ 服务网关是所有微服务的入口,客户端在访问各个微服务时,首先需要经过服务网关。接入服务网关 后,会将所有API的调用统一接入到API的网关层,由网关层统一接收参数进行路由转发,将返回的结果 数据返回给客户端。

    ​ 通常情况下,一个服务网关最基本的功能包括:统一接入、限流、熔断、降级、安全防护、协议适配、 容错等等。主要专注的是对系统安全、流量和路由等的管理。这样,业务开发人员就可以专注于开发业 务逻辑啦。

  5. 服务限流

    ​ 在高并发大流量场景下,经常会出现某个服务或者接口因为调用的流量过大而导致不可用的情况,由于 某个服务或者接口的不可用,可能还会导致整个系统崩溃。此时,就会对系统采取限流的手段来进行防 护,当请求达到一定的频率或者速率时,对这些请求采取排队、等待、降级等策略,甚至是拒绝服务。

  6. 服务熔断

    ​ 如果某个服务出现故障不可用,或者调用超时,为了不让其他服务受到牵累而导致整个系统不可用,则 断开与这个服务的连接,暂停对这个服务的调用。

  7. 服务降级

    ​ 服务降级主要是从整个系统的负载情况进行考虑,如果某些服务的负载情况比较高,则为了预防某些功 能出现负载过高而导致响应慢的问题,会在提供这些功能的方法内部暂时舍弃对一些非核心功能接口的 调用,直接返回一个提前准备好的错误处理信息。

    ​ 服务降级是有损服务,但是能够保证整个系统的稳定性和可用性。

  8. 服务容错

    ​ 服务容错指的是微服务能够容纳一定错误情况的发生。从某种意义上说,服务限流、服务熔断和服务降级都是服务容错的措施。

  9. 链路追踪

    ​ 当系统被拆分成各个微服务后,一次请求往往会涉及到多个服务之间的调用关系。如果系统出现问题, 则会增加定位问题的难度。为了解决这个问题,就需要对一次请求涉及到的多个服务链路的日志进行追 踪和记录,一方面可以记录调用的链路,另一方面还可以监控系统中各个调用环节的性能,这就是链路 追踪。

SpringCloud Alibaba

为什么要开发SpringCloud Alibaba

​ 单纯就是因为Netfix摆烂了,不想继续更新,然后alibaba看到这是个宣传自己、让自己的技术出名的机会,就去搞了,当然她确实有这个钱和资本。

什么是维护模式:spring cloud团队将不会再向模块添加新功能,我们将修复block级别的bug以及安全问题,我们也会考虑并审查社区的小型pull request。我们打算继续支持这些模块,直到Greenwich版本被普遍采用至少一年,然后就开始彻底摆烂,不搞了。

维护的组件

  1. Spring-Cloud-Netflix-archaius
  2. hystrix [断路器] (hystrix-contract、hystrix-dashboard、hystrix-stream、hystrix)
  3. ribbon [远程调用] (OpenFeign内部封装的那个)
  4. trubine-stream
  5. trubine
  6. zuul (网关,我没学只学了Gateway)

​ 总的来说,就是网飞感觉没必要,打算开摆,然后就让位置让给未来人了。

SpringCloud Alibaba功能

  1. **服务限流降级:**默认支持 WebServlet、WebFlux, OpenFeign、RestTemplate、Spring Cloud Gateway, Zuul, Dubbo 和 RocketMQ 限流降级功能的接入,可以在运行时通过控制台实时修改限流降级规则,还支持查看限流降级 Metrics 监控。

  2. **服务注册与发现:**适配 Spring Cloud 服务注册与发现标准,默认集成了 Ribbon 的支持。 分布式配置管理:支持分布式系统中的外部化配置,配置更改时自动刷新。

  3. **消息驱动能力:**基于 Spring Cloud Stream 为微服务应用构建消息驱动能力。

  4. **分布式事务:**使用 @GlobalTransactional 注解, 高效并且对业务零侵入地解决分布式事务问题。。

  5. **阿里云对象存储:**阿里云提供的海量、安全、低成本、高可靠的云存储服务。支持在任何应用、任何时间、任何地点存储和访问任意类型的数据。

  6. **分布式任务调度:**提供秒级、精准、高可靠、高可用的定时(基于 Cron 表达式)任务调度服务。同时提供分布式的任务执行模型,如网格任务。网格任务支持海量子任务均匀分配到所有 Worker(schedulerx-client)上执行。

  7. **阿里云短信服务:**覆盖全球的短信服务,友好、高效、智能的互联化通讯能力,帮助企业迅速搭建客户触达通道。

    ​ 除了上述所具有的功能外,针对企业级用户的场景,Spring Cloud Alibaba 配套的企业版微服务治理方 案 微服务引擎 MSE 还提供了企业级微服务治理中心,包括全链路灰度、服务预热、无损上下线和离群 实例摘除等更多更强大的治理能力,同时还提供了企业级 Nacos 注册配置中心,企业级云原生网关等多 种产品及解决方案。

SpringCloud Alibaba组件

  1. Sentinel:(代替了Hystrix)把流量作为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。
  2. Nacos:(代替了Eruka以及配置中心Config和Bus)把流量作为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。
  3. RocketMQ:(代替了RabbitMQ / Kafka)把流量作为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。
  4. Dubbo:(RPC远程调用) Apache Dubbo™ 是一款高性能 Java RPC 框架。
  5. Seata:(分布式事务)阿里巴巴开源产品,一个易于使用的高性能微服务分布式事务解决方案。
  6. Alibab Cloud OSS:(就是OSS,图床、文件管理)阿里云对象存储服务(Object Storage Service,简称 OSS),是阿里云提供的海量、安全、低成本、高可靠的云存储服务。您可以在任何应用、任何时间、任何地点存储和访问任意类型的数据。
  7. Alibaba Cloud SchedulerX:(分布式的调度)阿里中间件团队开发的一款分布式任务调度产品,提供秒级、精准、高可靠、高可用的定时(基于 Cron 表达式)任务调度服务。
  8. Alibaba Cloud SMS:(全球发送短信)覆盖全球的短信服务,友好、高效、智能的互联化通讯能力,帮助企业迅速搭建客户触达通道。

版本对应关系

SpringCloud alibaba中组件对应关系

Spring Cloud Alibaba VersionSentinel VersionNacos VersionRocketMQ VersionDubbo VersionSeata Version
2021.0.1.0*1.8.31.4.24.9.22.7.151.4.2

SpringCloud与SpringBoot、Spring等对应关系

Spring Cloud Alibaba VersionSpring Cloud VersionSpring Boot Version
2021.0.1.0Spring Cloud 2021.0.12.6.3

SpringCloud Alibaba项目完整架构

青色 代表当前暂未找到相关的资料学习使用在SpringCloudAlibaba项目中的组件

作为一个完整的微服务项目,往往需要具有以下几种的微服务组件:

总体来说,需要使用到的架构设计包含有:

  1. 消息中间件:RocketMQ / Kafka / RabbitMQ (各SpringBoot服务之中内嵌依赖、调用即可)

  2. 服务治理与服务配置:Nacos (各SpringBoot服务之中内嵌依赖、配置、下载相关的Jar包启动即可)

  3. 负载均衡组件:Ribbon(OpenFeign内部包含着Ribbon) (各SpringBoot服务之中内嵌依赖、配置即可)

  4. 远程服务调用:Openfeign (各SpringBoot服务之中内嵌依赖、配置、调用即可)

  5. 服务限流与容错:Sentinel (各SpringBoot服务之中内嵌依赖、配置即可,控制台需要下载jar包启动)

  6. 服务网关:SpringCLoud - GateWay (独立一个SpringBoot项目,并对外提供服务)

    ​ 当然也有Nginx + Lua/Kong/Zuul/Apache Shenyu等其他技术

  7. 服务链路追踪:Seluth + ZipKin (Seluth需要下载相关Jar包启动)

  8. 分布式事务处理:Seata

具体到每一个SpringBoot项目来说,还需要考虑以下内容:

  1. 持久层框架:Mybatis / Mybatis-plus
  2. 数据存储:MySQL + ElasticSearch
  3. 缓存:Redis

对于项目的可用性,我们可以使用Jmeter对项目资源服务进行压力测试,打开jmeter中bin / jemeter.bat即可打开Jmeter的GUI,配置相关的设置即可进行压力测试。

怎么使用SpringCloud Alibaba

Spring官网:https://spring.io/projects/spring-cloud-alibaba

GitHub:https://github.com/alibaba/spring-cloud-alibaba

GitHub中文文档:https://github.com/alibaba/spring-cloud-alibaba/blob/master/README-zh.md

Spring Cloud Alibaba参考文档:https://spring-cloud-alibaba-group.github.io/github-pages/greenwich/spring-cloud-alibaba.html

**官网步骤:**https://spring.io/projects/spring-cloud-alibaba#learn

父项目配置

​ 虽然官方文档有这个具体的步骤啊,但是还是自己写一下好,毕竟在学东西,还是谦虚点。

在父工程之中添加锁定信息

<dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>2021.0.1.0</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

完整配置文件,解决了SpringBoot版本和java版本的问题

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.leticiafeng</groupId>
    <artifactId>LF-SpringCloud-Alibaba</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <!--spring boot 环境 -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.3</version>
        <relativePath/>
    </parent>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>2021.0.1.0</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
</project>

Nacos基本介绍

Nacos目前为止已有两个版本,Nacos1.0 和 Nacos2.0。在Nacos2.0之中为了方便相关程序的调用及使用,Nacos2.0添加了长链接gRPC,因此为了实现长链接gRPC的链接,除去了Nacos本身端口的8848端口以外,还添加了一个新端口9848端口的使用。(完整的升级步骤可以参考官网)

​ Nacos2.0之中除了Nacos1.0提供的功能以外,还提供了像1. 长链接 2.鉴权功能 3.配置文件加密(配置中心) 存入mysql之中 / 磁盘文件之中

Nacos的一些基本概念的介绍

服务 (Service)

服务是指一个或一组软件功能(例如特定信息的检索或一组操作的执行),其目的是不同的客户端可以为不同的目的重用(例如通过跨进程的网络调用)。Nacos 支持主流的服务生态,如 Kubernetes Service、gRPC|Dubbo RPC Service 或者 Spring Cloud RESTful Service。

服务注册中心 (Service Registry)

服务注册中心,它是服务,其实例及元数据的数据库。服务实例在启动时注册到服务注册表,并在关闭时注销。服务和路由器的客户端查询服务注册表以查找服务的可用实例。服务注册中心可能会调用服务实例的健康检查 API 来验证它是否能够处理请求。

服务元数据 (Service Metadata)

服务元数据是指包括服务端点(endpoints)、服务标签、服务版本号、服务实例权重、路由规则、安全策略等描述服务的数据。

服务提供方 (Service Provider)

是指提供可复用和可调用服务的应用方。

服务消费方 (Service Consumer)

是指会发起对某个服务调用的应用方。

配置 (Configuration)

在系统开发过程中通常会将一些需要变更的参数、变量等从代码中分离出来独立管理,以独立的配置文件的形式存在。目的是让静态的系统工件或者交付物(如 WAR,JAR 包等)更好地和实际的物理运行环境进行适配。配置管理一般包含在系统部署的过程中,由系统管理员或者运维人员完成这个步骤。配置变更是调整系统运行时的行为的有效手段之一。

配置管理 (Configuration Management)

在数据中心中,系统中所有配置的编辑、存储、分发、变更管理、历史版本管理、变更审计等所有与配置相关的活动统称为配置管理。

名字服务 (Naming Service)

提供分布式系统中所有对象(Object)、实体(Entity)的“名字”到关联的元数据之间的映射管理服务,例如 ServiceName -> Endpoints Info, Distributed Lock Name -> Lock Owner/Status Info, DNS Domain Name -> IP List, 服务发现和 DNS 就是名字服务的2大场景。

配置服务 (Configuration Service)

在服务或者应用运行过程中,提供动态配置或者元数据以及配置管理的服务提供者。

Nacos领域模型

​ Nacos数据模型Key由三元组唯一确认,Namespace默认是空串,公告命名空间(public),分组默认是DEFAULT_GROUP。

​ 为什么需要这个领域模型,往往在大型的互联网项目之中,我们需要为不同地区建立多个服务器集群来提供相关服务,这就导致了我们而在之前Config之中或者是注册中心提供服务的时候,需要考虑到区分隔离的问题,所以Nacos之中存在该模型。

uTools_1659794652805

Nacos的其他作用

​ Nacos除去了注册中心的作用以外,还包含了配置中心,还有总线的作用,我们可以依靠Nacos很自然的将配置中心和Bus统一管理配置等一系列SpringCloud老一套的组件拼接在一起。(Eureka / Zookeeper + SpringCloud-Config + SpringCloud-Bus)

Nacos的配置文件

​ 如果我们想要修改Nacos的一些基本的配置文件信息,我们可以在Nacos的目录config下找到application.properties下打开对Nacos的一些配置信息项进行修改,包括有像Nacos的启动端口号等信息。

Nacos注册中心部分

Nacos注册中心与其他注册中心对比

eureka和Zookeeper作为注册中心来说,属于是建立临时节点的注册中心。

不管是eureka还是Zookeeper,两者都可以提供客户端上报健康状态、摘除不健康实例、非持久化等功能

而consul与CoreDNS作为注册中心来说,属于是持久化实例的

不管是consul还是CoreDNS,两者都可以提供服务端的探索健康状态、保留不健康实例、持久化等功能

​ 需要注意的是,Nacos实现负载均衡的真正方式已经弃用了Ribbon,而使用了SpringCloud中的loadBalance组件来实现负载均衡。

Nacos横向对比其他注册中心

服务注册与服务框架CAP模型控制台管理社区活跃度
EurekaAP高可用支持低(2.x版本闭源)
ZookeeperCP一致支持
ConsulCP/AP支持

Nacos与其他注册中心特性对比

NacosEurkaConsulCoreDNSZookeeper
一致性协议CP + APApCp/CP
健康检查TCP/HTTP/MYSQL/Client BeatClient BeatTCP/HTTP/gRPC/Cmd/Client Beat
负载均衡权重/DSL/meatadata/CMDBRibbonFabioRR/
雪崩保护支持支持不支持不支持不支持
自动注销实例支持支持不支持不支持支持
访问协议HTTP/DNS/UDPHTTPHTTP/DNSDNSTCP
监听支持支持支持支持不支持支持
多数据中心支持支持支持不支持不支持
跨注册中心支持不支持支持不支持不支持
SpringCloud集成支持支持支持不支持不支持
Dubbo集成支持不支持不支持不支持支持
k8s支持不支持支持支持不支持

Nacos与CAP原则

CAP原则

A:可用性

C:一致性

P:分区容错性

​ AP会丢失C,可以简单的考虑一个场景,当我们一些实例,如果当机了下线了,eurka还会暂时保留这些provider,这样做就会提供了分布式的一个可用性还有分区容错性,但是也因此会丢失一定的一致性。

​ 而如果要保留一致性,那就需要进行数据信息的同步,而如果涉及到数据信息的一致性的时候,那就会出现注册中心不可用的情况出现。

Nacos默认为AP模式,当然可以通过命令来实现切换。

Nacos注册中心配置

Nacos下载启动

​ Nacos如果要在windows下进行安装使用,首先需要在Nacos官网到nacos快速入门之中,跳转到Nacos官方的Github上才可到正确的页面下载Nacos,下载成功后,需要进入到Nacos文件夹下的bin文件夹,通过cmd,使用命令,即可实现服务启动。

单机模式启动Nacos

startup .cmd -m standalone

使用该命令之后,只需要在浏览器输入 localhost:8848/nacos 等待跳转完毕,即可看到nacos相关页面信息。默认账号密码都是nacos

Nacos CAP配置

​ Nacos1.0 -> Nacos2.0 版本之间的切换只需要,在使用的时候,向Nacos使用的服务器调用某个接口进行查询即可。

切换为CP模式命令

curl -X PUT '$NACOS_SERVER:8848/nacos/v1/ns/operator/switches?entry=serverMode&value=CP'

AP模式命令

curl -X PUT '$NACOS_SERVER:8848/nacos/v1/ns/operator/switches?entry=serverMode&value=AP'

Nacos服务方和消费方搭建(因为"技术歧视",只想用OpenFeign)

如果要实现负载均衡,两个或多个的服务提供商必须名字一致,在消费方的搭建时,遇到了一些问题,发现当我们如果想要将nacos和openfeign一起进行使用的时候,就会出现异常报错。

Caused by: java.lang.IllegalStateException: No Feign Client for loadBalancing defined. Did you forget to include spring-cloud-starter-netflix-ribbon?

最终发现是一个版本冲突的问题,可以通过在parent之中添加相关的配置信息,来解决该问题

​ 为了避免出现该版本冲突的问题,首先需要在SpringCloudAlibaba的父工程中的pom.xml之中添加SpringCloud的版本锁定信息,并且需要将SpringCloud版本锁定为与SpringCloudAlibaba所使用版本对应的版本号,该部分可以参考本文档中提供的相关信息。

[版本对应关系](###版本对应关系)

父工程之中添加配置

<groupId>com.leticiafeng</groupId>
<artifactId>LF-SpringCloud-Alibaba</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>

<modules>
    <module>nacos-provider8000</module>
    <module>nacos-provider8001</module>
    <module>nacos-consumer9000</module>
    <module>nacos-consumer9001</module>
</modules>


<properties>
    <maven.compiler.source>8</maven.compiler.source>
    <maven.compiler.target>8</maven.compiler.target>
    <spring.cloud.version>2021.0.1</spring.cloud.version>
    <spring.cloud.alibaba>2021.0.1.0</spring.cloud.alibaba>
</properties>

<!--spring boot 环境 -->
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.6.3</version>
    <relativePath/>
</parent>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>${spring.cloud.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-dependencies</artifactId>
            <version>2021.0.1.0</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

​ 然后需要只需要在服务的调用方上,添加一些配置信息,就可以实现配置需求。

极速搭建项目

  1. 首先需要在项目之中新建一个模块,模块nacos-provider8000

    pom.xml文件

    ​ 将pom.xml文件之中的parent标签内的父工程配置进行替换,替换为微服务于中父项目的信息

    <parent>
            <groupId>com.leticiafeng</groupId>
            <artifactId>LF-SpringCloud-Alibaba</artifactId>
            <version>1.0-SNAPSHOT</version>
        </parent>
    

    ​ 然后为了控制父子项目版本信息的一致性,需要将子项目的一些配置信息也删掉,需要删除版本信息等

        <artifactId>nacos-provider8000</artifactId>
        <name>nacos-provider8000</name>
        <description>Nacos-provider-Template</description>
    

    yml文件

    ​ 需要在服务的提供方,添加相关的配置信息,使得服务能够顺利的注册到nacos之中

    server:
      port: 8000
    
    
    spring:
      application:
        name: nacos-provider
      cloud:
        nacos:
          discovery:
            server-addr: localhost:8848
    

    主启动类

    @SpringBootApplication
    @EnableDiscoveryClient      //配置为注册中心的客户端   (开启注册中心的一个注册发现)
    public class NacosProvider8000Application {
    
        public static void main(String[] args) {
            SpringApplication.run(NacosProvider8000Application.class, args);
        }
    
    }
    

    提供服务接口定义

    @RestController
    @RequestMapping("/goods")
    public class GoodsController {
        
        @Value("${server.port}")
        private String serverPort;
        
        @GetMapping("/findById/{id}")
        public String findById(@PathVariable("id") Integer id){
            // 模拟业务逻辑
            return "nacos-provider" + serverPort + ": return information - 114514";
        }
    }
    

    当然如果想要体现出调用的差异等效果,那就需要建两个服务提供方,才能体现出差异

  2. 构建一个服务的调用方服务,模块名等可以设置为nacos-consumer9000 / nacos-consumer9001

    pom.xml文件

      <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
    
            <!--SpringCloud Alibaba nacos-->
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
                <exclusions>
                    <exclusion>
                        <groupId>org.springframework.cloud</groupId>
                        <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
                    </exclusion>
                </exclusions>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-loadbalancer</artifactId>
            </dependency>
            
            
            <!-- 添加 openfeign 框架依赖 -->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-openfeign</artifactId>
            </dependency>
    
        </dependencies>
    

    yml文件

    server:
      port: 9001
    
    spring:
      application:
        name: nacos-consumer
      cloud:
        nacos:
          discovery:
            server-addr: localhost:8848
    

    主启动类

    @SpringBootApplication
    @EnableFeignClients   //开启远程Feign的调用
    public class NacosConsumer9001Application {
    
        public static void main(String[] args) {
            SpringApplication.run(NacosConsumer9001Application.class, args);
        }
    
    }
    

    远程调用Feign接口

    @FeignClient(value = "nacos-provider")
    public interface GoodFeign {
        //需要远程调用的方法 (注意url需要将restController的前缀和      后续方法的url都写上)
        @GetMapping("/goods/findById/{id}")
        public String findById(@PathVariable Integer id);
    }
    

    服务调用

    @RestController
    @RequestMapping("/order")
    public class OrderController {
    
        //使用Feign的方式实现远程调用
        @Autowired
        private GoodFeign goodFeign;
    
        @Value("${server.port}")
        private String serverPort;
    
        @GetMapping("/getGoodsInfo/{id}")
        public String getGoodsInfo(@PathVariable("id") Integer id){
            // 业务逻辑 实现远程调用
            return goodFeign.findById(id);
        }
    }
    

到此为止,就可以实现Nacos + Feign的远程调用服务配置了

Nacos配置中心部分

Nacos配置中心的特性

Nacos配置中心之中如果在实际运行期间对Nacos的配置文件的配置项进行了修改的话,那么Nacos会动态的将相关的配置更新到当前读取配置的项目之中,这点说明Nacos已经整合了SpringCloud之中的消息流Stream流驱动机制

​ 首先,Nacos除了注册中心以外,还可以提供配置中心还有配置实时更新的功能。为了实现该功能,我们首先需要创建一个子模块,来作为Nacos配置中心部分的调用Nacos配置中心的调用端

以下示例模块名:nacos-config-client7777

  1. 首先需要在pom之中引入1. nacos的依赖信息 2. nacos-config的依赖信息

        <artifactId>nacos-config-client7777</artifactId>
        <name>nacos-config-client7777</name>
        <description>SpringCloud-Alibaba-Nacos-Config-Template</description>
    
        <parent>
            <groupId>com.leticiafeng</groupId>
            <artifactId>LF-SpringCloud-Alibaba</artifactId>
            <version>1.0-SNAPSHOT</version>
        </parent>
        
        
        <properties>
            <java.version>1.8</java.version>
        </properties>
        
        
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
            </dependency>
    
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
            </dependency>
    
            <!--SpringCloud Alibaba nacos-->
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-bootstrap</artifactId>
            </dependency>
        </dependencies>
    

    相比于Nacos注册中心普通使用还需要额外添加两个配置信息

    <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-bootstrap</artifactId>
    </dependency>
    <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
    </dependency>
    
  2. 第二步,我们需要向nacos表明我们是什么环境,进而使用哪个环境的配置进行使用

    spring:
      profiles:
        active: dev #表示要用作用于什么环境的配置文件 (配置当前服务应当为什么环境提供配置服务)
    
  3. 除此以外,我们还需要在resources下添加用于实现配置配置文件:bootstrap.yml 文件指明配置文件在哪里,在哪个Nacos服务上,叫啥名字等

    # nacos配置
    server:
      port: 7777
    
    spring:
      application:
        name: nacos-config-client
      cloud:
        nacos:
          discovery:
            server-addr: localhost:8848 #Nacos服务注册中心地址
          config:      # 配置客户端nacos配置文件寻找点
            server-addr: localhost:8848 #Nacos作为配置中心地址
            file-extension: yaml #指定yaml格式的配置
            
    #${spring.application.name}-${spring.profile.active}.${spring.cloud.nacos.config.file-extension}
    
  4. 第四步我们则需要在启动类上添加相关的注解信息 @EnableDiscoveryClient,使得我们的配置可以被识别,进而自动的实现配置中心的功能

    @SpringBootApplication
    @EnableDiscoveryClient
    public class NacosConfigClient7777Application {
        public static void main(String[] args) {
            SpringApplication.run(NacosConfigClient7777Application.class, args);
        }
    }
    
  5. 然后,我们需要在该子模块下创建Controller层,并添加相关的接口实现配置去验证我们成功的调用到了Nacos配置中心的服务

    (这一步在SpringCloudConfig的配置中心之中是没有的,因为本质上SpringCloudConfig的配置文件并不是在本地,而是在第三方应该提供的服务上的)

    @RestController
    @RequestMapping("/config")
    @RefreshScope
    public class ConfigClientController {
        @Value("${name}")
        private String name;
        
        @GetMapping("/name")
        public String name(){
            return name;
        }
    }
    
  6. 至此,所有的客户端的配置已经结束,接下来的只剩下是对服务器端的配置,对于服务器端的配置,我们仅仅需要在nacos提供的可视化网站上将配置文件上传即可实现配置

    在Nacos的网页上可通过点击 配置管理 -> 配置列表 -> + -> 新建配置 -> 填写Data ID、Group、描述、格式、内容等 -> 发布即可

    Data ID的填写格式如:

    官方描述:

    ${prefix}-${spring.profiles.active}.${file-extension}
    

    因为该描述又设计到一些基本的配置规则

    1. prefix:默认为 spring.application.name 的值,也可以通过配置项 spring.cloud.nacos.config.prefix 来进行配置。

    2. spring.profiles.active 即为当前环境对应的profile,详情可以参考SpringBoot文档。需要注意的是当 spring.profiles.active为空的时候,对应的连接符 - 也将不存在,dataId的拼接格式将变成 ${prefix}.${file-tension}

    3. file-extension为配置内容的数据格式,可以通过配置项spring.cloud来进行配置,而目前仅仅支持propertiesyaml类型

    ​ 可以简单的认为,Nacos的配置功能首先不需要像SpringCloud-Config一样需要一个提供相关配置文件获取的接口,再经由客户端调用接口来实现向第三方获取文件,而仅仅需要一些配置即可。

    当然虽然Nacos可以做到这一点,但我们找到相关的配置文件还是需要在客户端之中通过boostrap.yml文件的方式来指定获取的配置文件位置,而application.yml还需要指定获取哪一个环境下的配置文件,如果yml之中的profiles配置的是dev则会到nacos之中找 bootstrap.yml之中的spring-application-name的配置值 + application.yml之中 spring-profiles-active 中的配置值 + yml/properties

    实际可以认为是:

    ${spring.application.name}-${spring.profile.active}.${spring.cloud.nacos.config.file-extension}
    

    而在实际的配置过程之中又有一个较好的例子:

    项目名为nacos-config-client项目作为 配置中心时,并且将配置文件作为dev环境下的文件时,我们可以使用以下代码将其配置文件作为dev的配置文件进行发放配置

    nacos-config-client-dev.yaml
    
  7. 这个时候,只要我们打开客户端,并且调用相关设置配置文件的接口即可实现自动配置,另外由于nacos的封装性,还可以自动的实现消息的一个实时更新。

DGN方案

(对应就是在Nacos哪一个namespace下的哪一个group的哪一个文件)

namesapce + group + data

data方案:所谓的data方案,其实就是通过nacos的文件名的方式,指定spring.profile.avtive和配置文件的dataID的方式来使得不同环境下读取不同的配置

配置空间 + 配置分组 + 新建dev和test两个dataId:就是配置后不同的两个温家明nacos-config-client-dev.yaml、nacos-config-client-prod.yaml

只需要修改data方案对应项目中IDEA之中application.yml文件的active即可

spring:
  profiles:
    active: prod #表示要用作用于什么环境的配置文件 (配置当前服务应当为什么环境提供配置服务)

通过IDEA之中的application指定寻找的配置文件的profiles,以及依靠于bootstrap文件来寻找对应的配置文件中的相关配置找到nacos

**group方案:**所有的group方案,其实就是在nacos创建配置文件的时候,给文件指定分组,并且利用IDEA客户端之中的配置文件来指定从哪一个group之中读取配置文件

在nacos之中建立了配置文件之后,只需要修改IDEA之中bootstrap.yml文件的配置信息即可

# nacos配置
server:
  port: 7777

spring:
  application:
    name: nacos-config-client
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848 #Nacos服务注册中心地址
      config:
        server-addr: localhost:8848 #Nacos作为配置中心地址
        file-extension: yaml #指定yaml格式的配置
        group: GZ_GROUP   #增加group名字

#${spring.application.name}-${spring.profile.active}.${spring.cloud.nacos.config.file-extension}

namespace方案:(默认是public)

其实就是直接在nacos之中新建一个命名空间,自动可以出现一个命名空间的序列ID,并且在IDRA客户端项目之中指定命名空间的ID,即可使其读取某一个指定namespace中的配置文件

如果要使用不同命名的namespace**在nacos之中建立了配置文件之后,只需要修改IDEA之中bootstrap.yml文件的配置信息即可**

# nacos配置
server:
  port: 7777

spring:
  application:
    name: nacos-config-client
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848 #Nacos服务注册中心地址
      config:
        server-addr: localhost:8848 #Nacos作为配置中心地址
        file-extension: yaml #指定yaml格式的配置
        namespace: ea87b5e1-2e52-4481-b47f-d9a3fda880ba  # 必须为ID
        group: GZ_GROUP

#${spring.application.name}-${spring.profile.active}.${spring.cloud.nacos.config.file-extension}

Nacos如何确定准确的某一个配置文件?

​ 在Nacos的基本介绍之中我们已经介绍过了Nacos的一些基本属性,它由外层的namesapce包含内层的groupId再包含内部的dataId的方式来唯一的确定配置文件。

实际环境如何保证读取配置文件是我们想要的?

场景一: 一个系统往往在实际开发的时候,会存在有 dev /test/ prod多种环境,为了保证环境启动的时候,服务能够准确的读取到对应nacos上的环境配置文件,我们往往需要使用 namespace 来实现区分。

场景二: 一个发行的微服务又会有相应的开发环境会有多个微服务子项目,每一个微服务项目又会有相应的开发环境、测试环境、预发环境、正式环境,那怎么对微服务配置进行管理呢?我们往往可以通过使用nacos的文件管理中的group的方式,将不同的微服务划分到一个分组里面去。

Service就是微服务,一个service可以包含多个cluster集群,nacos默认cluster是DEFAULT,Cluster是对指定微服务的一个虚拟划分。

问题: 这么多个机房,如何避免服务调用了不同机房的服务,导致出现舍近求远调用的情况出现效率更低的情况出现。

可以通过namespace之中建立不同的group,便可以使得不同机房下的机器读取到的配置文件是有所区别的。

配置文件位置

​ 当前Nacos的一切配置需要建立在Nacos相关服务启动才能进行相关操作,那么如果我们配置后关闭Nacos配置文件会丢失吗?

​ 结果是不会的,Nacos会将配置文件的相关配置信息存储在Nacos本地服务相关的文件夹下nacos / data下,但又有一个问题,我们实际的生产、测试、开发环境下,如果Nacos出现异常,那我们的远程调用岂不是用不了了吗?那我们该怎么办呢?

​ 那如果是这样,我们的配置文件就不能仅仅是存在于单个具体的Nacos服务之中,而是应该将配置文件信息配置在集群数据库之中,集群部署的Mysql之中。所以就涉及到了Nacos的部署问题,以下是介绍Nacos的集中部署方式。

Nacos集群部署

Nacos作为IDC(数据中心内部的数据组件),我们应该在内部网络之中隔离部署。

Nacos部署模式

  • 单机模式 (以往我们所使用的部署方式)
  • 集群模式
  • 多集群模式
单机模式

​ 在这种部署方式下,nacos将会默认的将我们的配置文件等信息放到内嵌的一个DB数据库之中保存起来,以下是教程教导如何放到指定的Mysql数据库之中保存起来。

​ 单机模式下,我们的配置文件可以通过使用数据库即使用Mysql的方式来存储配置文件信息。具体的操作步骤如下:

  1. 安装数据库,要求Mysql的版本要在5.65+以上才可以

  2. 初始化mysql数据库,数据库初始化文件:nacos-mysql.sql

  3. 修改Nacos目录下的conf/application.properties文件,增加支持mysql数据源配置(目前只支持mysql),添加mysql数据源的url、用户名以及密码

    spring.datasource.platform=mysql
    
    ### Count of DB:
    db.num=1
    
    ### Connect URL of DB:
    db.url.0=jdbc:mysql://127.0.0.1:3306/nacos_stand_alone_config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
    db.user.0=root
    db.password.0=root
    

    最后再以单机模式的方式来启动nacos,nacos所有写嵌入式数据库的数据都写到mysql即可

  4. 虽然官方文档在这里就说可以直接使用了,但是我自己用的时候到这里倒是直接报错...最后靠百度更改了配置信息(已更新到上面)

集群部署

​ 开源时推荐用户把所有的服务列表放到一个vip下面,然后挂一个域名下,其实主要就三种方式:

http://ip1open in new window:port/openAPI 直连ip模式,机器挂则需要修改ip才可以使用。

http://SLBopen in new window:port/openAPI 挂载SLB模式(内网SLB,不可暴露到公网,以免带来安全风险),直连SLB即可,下面挂server真实ip,可读性不好。

http://nacos.comopen in new window:port/openAPI 域名 + SLB模式(内网SLB,不可暴露到公网,以免带来安全风险),可读性好,而且换ip方便,推荐模式

deployDnsVipMode.jpg

image-20220420182030318

​ 总的来说,其实就是当用户的请求过来的时候,需要先通过DNS解析为IP地址之后,就会通过使用SLB即是通过使用负载均衡的方式来确定最终决定使用的Nacos提供相关服务的准确服务器。

通过SLB以H5硬件或者软件的方式来实现对负载均衡的处理。

(因为现在电脑真的不大行,所以我决定用云服务器ECS来做这个部署的具体实验 -> 事实证明,穷人不配用服务器测试,还是回过头来尝试虚拟机吧)

集群部署(服务器)

  1. 首先,需要将nacos的服务器端的安装包或是源码上传服务器上。

  2. 将安装包/程序安装到服务器上

    2.1 使用命令在服务器的链接程序/网站上打开对应的安装包位置(这里只介绍使用安装包安装的方式)

    2.2 然后对Nacos安装包使用命令,将其进行解压

    tar -zxvf nacos-server-2.0.4.tar.gz
    cd nacos/bin
    
  3. 配置Nacos集群启动的集群信息

    打开Nacos的解压目录之后,在conf目录下,会有配置文件cluster.conf,将每一行都配置成ip:config的形式即可。

    这里的ip必须是当前机器的ip地址 + nacos对应的端口号

    [如果是虚拟机的IP,则必须先去配置虚拟机的网络使得宿主机与虚拟机的网络都可以使用的前提下才能保证宿主机可以访问到启动后的Nacos集群]

    192.168.246.128:6666
    192.168.246.128:7777
    192.168.246.128:8888
    
  4. 确定数据源

    在服务器的nacos配置之中配置,使得他们的配置的配置文件存放地址即数据源指定来源于某个位置。

    4.1 在服务器之中指定数据来源即在服务器的Mysql之中创建所需的数据库

    4.2 对服务器之中的Nacos的配置文件信息即application.properties进行修改,使得其可以读取自身Mysql的一些配置文件信息,保证其可用。

    // 配置文件的修改和单机版的一模一样这里就不赘述了
    ### If use MySQL as datasource:
    spring.datasource.platform=mysql
    
    ### Count of DB:
    db.num=1
    
    ### Connect URL of DB:
    db.url.0=jdbc:mysql://192.168.246.128:3306/nacos_cluster_config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
    db.user.0=root
    db.password.0=root 
    

    (为了真正的实现三份的Nacos集群服务,这里需要复制三份的Nacos,并且将application.properties中的端口号改为6666、7777、8888)

  5. 启动服务器中Nacos服务

    需要进入到bin中使用命令才可以正确启动服务

    sh startup.sh
    

    [当然机器存在防火墙,我们需要还需要考虑防火墙的问题,因此我们还需要在虚拟机之中打开相应的端口保证宿主机/外部计算机可以访问到对应的页面]

    [到这里就已经可以在宿主机上通过访问相关的URL,访问到部署了Nacos的服务器,但是到这里仍然存在一个问题,我们在实际使用的时候,不可能准确的确定好要调用的目标是谁,这就引发了一个新的需求]

  6. 引入Nginx(也就是负责SLB部分的功能)

    6.1 首先可以去Nginx官网,打开Nginx官网后进去到下载页面,下载Linux版本的Nginx,然后将Nginx上传到虚拟机/对应的机器上,然后通过tar -zvxf [nginx压缩包全名],将Nginx解压出来

    6.2 打开nginx的文件夹,通过cd命令进入到nginx / conf ,并通过vim nginx.conf对nginx的配置文件进行修改。

    6.3 在nginx.conf之中对nginx的配置进行修改

    1. 在server - listen之中配置为,虚拟的项目端口号(用于访问)

    2. 并在server - location之中将以下部分注释,在后加上

      #root html;
      
      #index index.html index.htm
      proxy_pass http://cluster;   //这个代理的具体名可以随便写(虚拟URL)
      
    3. 最后在server上方加入cluster的指定访问ip + 端口

      upstream cluster{
      	server 192.168.1.102:6666;
      	server 192.168.1.102:7777;
      	server 192.168.1.102:8888;
      }
      

      到此即可利用Nginx通过访问5555端口,让Nginx帮助我们负载均衡Nacos的集群。

    6.4 然后我们应当先对nginx进行安装

    1. 首先需要先安装其他依赖

      yum -y install make zlib zlib-devel gcc-c++ libtool  openssl openssl-devel
      
    2. 进入nginx的根目录,执行./configure

    3. 再执行make && make install

    4. 到sbin文件夹下启动nginx

      cd /usr/local/nginx/
      
    5. (我们在nginx下修改的信息会自动的跟过来)

    6. 到此,我们就可以通过访问被nginx监控的端口来实现对Nacos集群的负载均衡调用。

怎么使用Sentinel实现熔断限流

前提概念

什么是熔断

​ 如果一个微服务自己限流,那他就不可用了。此时调用只会返回报错信息,这就给客户带来了很差的体验。

什么是降级

​ 熔断自然保护了某个资源服务本身。但是在实际的环境之中,微服务的模块之间是相互调用的。当被依赖的服务当掉之后,依赖的服务为了能够保证整体服务的正常,需要知道、识别到最后这种情况存在的问题,基于客户一个特殊的对象。这个过程就叫做降级。

因为错误有可能出现在服务方提供方,即被依赖方还能够是识别的情况下。也可能出现在服务的提供方已经挂掉了,只能让服务的消费方自己解决的两种情况。(可以参考Hystrix)因此降级有两种降级,一种是定义服务提供方的降级方法,另外一种是定义服务的消费方的降级方法。

两者的关系

​ 所以说降级其实是熔断的基础上做了一定的优化。

概述

(千万别用Sentinel 1.8.5)

​ 总的来说,其实就只有一句话,Sentinel其实就是Hystrix+GateWay的替代,但是确实Sentinel的功能远比Hystrix要强得多。

Sentinel官网:https://github.com/alibaba/sentinel

中文版官网:https://github.com/alibaba/Sentinel/wiki/%E4%BB%8B%E7%BB%8D

Sentinel介绍:https://github.com/alibaba/Sentinel/wiki/

随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。

Sentinel 具有以下特征:

  • 丰富的应用场景:Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。
  • 完备的实时监控:Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况。
  • 广泛的开源生态:Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Apache Dubbo、gRPC、Quarkus 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。同时 Sentinel 提供 Java/Go/C++ 等多语言的原生实现。
  • 完善的 SPI 扩展机制:Sentinel 提供简单易用、完善的 SPI 扩展接口。您可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等。

Sentinel 的主要特性:

image-20220424114427413.95cea440

Sentinel 的开源生态:

image-20220424114442619.a6e4b456

Sentinel的组成

Sentinel由两个部分组成,第一个部分是核心库部分,第二个部分是控制台部分。

核心库部分之中:他完全脱离出来不依赖于任意的框架/库,能够运行于所有Java运行时环境,同时对 Dubbo / Spring Cloud等框架也有较好的支持。

控制台部分之中:Sentinel该部分由一个SpringBoot项目来实现监控控制台的具体功能,不需要像Hystrix之中一样引入一个运维监控模块才能完成监控中心的功能

​ 也就是说在目前的Sentinel之中,我们使用不需要使用其他的项目,只需要在相互调用的任意两个项目之中引入Sentinel的Jar包即可实现两者之间调用活动的监控,并且监控信息将会被收集到Sentinel的控制台之中(dashbord),我们如果想查看调用信息,只需要到Sentinel的控制台的页面进行访问即可以查看到相关的信息。

1663857362065

​ 如上图所示,在Sentinel之中,存在着A、B两个相互调用的服务,我们只需要在A、B之中引入Sentinel,A、B两个服务就会定期的往dashbord项目运行端口即8719的控制台发送相关的调用信息,而我们如果想要查看调用的记录信息,只需要访问8080的页面端口,就可以在页面上看到相关的调用信息。

启动运行Sentinel控制台

​ 在Sentinel官网,即从Github上下载Sentinel的相关Jar包,等下载完毕,即可在项目所在的机器上,通过使用command命令台上通过使用java命令java -jar + jar包完整名,即可成功启动相关的服务。

如果想要修改日志的输出路径,可以使用如下的代码

java -jar -DJM.LOG.PATH=../../Logs -DJM.SNAPSHOT.PATH=../../Log sentinel-dashboard-1.8.4.jar

另外Java17启动需要添加参数

java -jar --add-exports=java.base/sun.net.util=ALL-UNNAMED sentinel-dashboard-1.8.4.jar
java -jar -DJM.LOG.PATH=../../Logs -DJM.SNAPSHOT.PATH=../../Log --add-exports=java.base/sun.net.util=ALL-UNNAMED sentinel-dashboard-1.8.4.jar

默认的Sentinel控制台的账号和密码都是Sentinel

Sentinel监控项目搭建

​ 搭建Sentinel项目也是简单的,只需要在构建SpringBoot项目的时候,在pom.xml文件之中添加相关的依赖信息,并使用一定的配置即可。

  1. pom.xml文件

    在pom文件之中需要引用Nacos + Sentinel的包一方面我们需要使用Nacos来实现远程调用,另一方面我们需要使用Sentinel来实现Hystrix的流量控制、熔断、隔离等功能。

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
        
        <artifactId>sentinel-servce-8002</artifactId>
        <name>sentinel-servce-8002</name>
        <description>Demo-for-Sentinel</description>
    
    
        <parent>
            <groupId>com.leticiafeng</groupId>
            <artifactId>LF-SpringCloud-Alibaba</artifactId>
            <version>1.0-SNAPSHOT</version>
        </parent>
        
        
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
            </dependency>
    
            <!-- SpringCloud ailibaba nacos-->
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
            </dependency>
            <!-- SpringCloud ailibaba sentinel-datasource-nacos 持久化需要用到-->
            <dependency>
                <groupId>com.alibaba.csp</groupId>
                <artifactId>sentinel-datasource-nacos</artifactId>
            </dependency>
            <!-- SpringCloud ailibaba sentinel-->
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
            </dependency>
            
        </dependencies>
    
        <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                </plugin>
            </plugins>
        </build>
    
    </project>
    
  2. 在项目的yml或者yaml文件之中添加相关的配置,如下:

    server:
      port: 8002
      
    spring:
      application:
        name: sentinel-service-8002
      cloud:
        nacos:
          discovery:
            server-addr: localhost:8848
        sentinel:
          transport:
            dashboard: 8080
            # 默认8719端口,假如被占用了会自动从8719端口+1进行扫描,直到找到未被占用的 端口
            port: 8719
    
  3. 在启动类上添加相关的注解配置,使得Nacos可以在该项目上提供相应的注册服务

    @EnbaleDiscoveryClient
    
  4. 最后在Controller层之中添加需要被调用的方法,并且使用另外一个项目之中进行调用 / 我们直接在浏览器之中对其进行调用。

    ​ 为了方便自己复习Nacos,此处我是通过直接写了一个调用程序的方式来实现的调用。

  5. 登录Sentinel的控制台便可以查看相关信息

    uTools_1664019797690

Sentinel控制台dashbord的其中提供了众多的功能供使用。

Sentinel功能基本使用

需要注意的是,Sentinel的流控规则的添加或者修改都是可以动态的更新到当前正在运行的项目的,这一点说明在Sentinel之中他是提供了类似于nacos具有的Stream消息流机制的。

流量的基本控制

流量控制面板介绍

1664024395700

  • **资源名:**唯一名称,默认请求路径

  • **针对来源:**sentinel可以针对调用者进行限流,填写微服务名,默认default(不区分来源)

    在Sentinel之中,可以对访问的来源进行限制,使得某一个资源只有某个服务器或者是来自于某个地方的服务才可以进行调用

阈值类型-阈值

​ (这里先不讨论他的实现方式,毕竟先学怎么用)

​ Sentinel的流量控制很简单,我们可以直接在Sentinel的控制台上进行操作即可,无关在代码上进行配置(dashbord配置GUI)

QPS

​ 当我们使用阈值类型为QPS的时候,该资源在1s内只允许有QPS阈值设置值的访问次数,超过该次数后来的请求将由Sentinel的流控结果来决定返回的信息是什么。

并发线程数

​ 当我们使用的阈值类型为并发线程数的时候,该资源是否可以成功请求取决于资源设置的并发线程数设置的阈值(该阈值其实应该设置为该资源处理的代码中所使用的线程数)。如果超过该限制的阈值,那么将会由Sentinel的流控结果来决定返回的信息是什么。

示例

​ 当然在实际的项目中除了QPS的限制以外,当出现较大的高并发的问题的时候,我们还需要考虑到一个我们的项目某个资源往往还会是由线程数数量决定的性能。 在Sentinel之中提供了让我们可以通过对线程数进行限制的方法来对资源被调用的流量进行了限制。

1664089403186

只需要在Sentinel的的,流控规则之中对规则进行配置,配置为阈值类型为开发线程数,并且根据提供资源的服务的线程数量来进行配置即可。

错误信息

​ 如果出现了超出线程数的限制的访问数量的时候,那么就会返回相应的报错信息(直接错误Blocked by Sentinel (flow limiting))。

流控模式

直接

QPS-直接快速失败

​ QPS直接快速失败最简单,快捷的配置方式,效果也非常的简单,就是限制某个服务器资源在1s的访问数量,当1s内访问的数量超过QPS限制的阈值之后,就会通过Sentinel的流控效果的设置来决定最终返回、打印出来的是什么信息。

关联

QPS-关联-快速失败

​ 当我们某个业务需求出现相互依赖的时候,就需要使用关联模式,例如A依赖于B,如果B出现错误,那么A在执行的时候也会返回快速失败。若非完全依赖的话,那就需要分情况进行处理。 相同的是同样会导致返回信息为:Blocked by Sentinel (flow limiting),但是前提是被关联的资源出现失败。

(测试这个的时候,需要注意的是APIPost在测试这个部分的时候,会无法产生占用QPS的情况,我也不知道为什么,但是换成Postman就好了)

链路

QPS链路限制

​ 在我们的项目之中,往往会存在链路调用的情况出现。即可能存在着两个或者多个资源共同调用某个资源的情况出现,而这种时候,我们又往往可能会出现只需要对这些共同都有调用的服务进行限流的操作。这个时候就需要链路限制出现了。

uTools_1664092530735

在链路限制的情况下我们的限制就可以变得更加的精细化,更加准确的来说,我们可以对调用同一个Service的方法的Controller接口进行不同的策略限制。

测试步骤

  1. 新增GoodService接口和impl层,在Impl层之中新添加方法,并使用注解对方法声明为Sentinel资源(在接口之中添加所需被调用的方法)

    @Service
    public class SentinelServiceImpl implements SentinelService {
    
        @SentinelResource("querySentinel")
        @Override
        public String getSentinelTestInfo() {
            return "SentinelTest -- Success";
        }
    }
    
  2. 由于版本迭代,在1.6.3版本之后就不再支持使用链路限制了,因为Sentinel Web filter默认收敛所有URL的入口context,导致链路限流不生效。 从1.7.0版本开始,官方在CommonFilter引入了WEB_CONTEXT_UNIFY参数,用于控制是否收敛context,将其配置为false即可根据不同的URL进行链路限流

    spring:
      application:
        name: cloudalibaba-sentinal-service
      cloud:
        nacos:
          discovery:
            server-addr: localhost:8848
        sentinel:
          transport:
            dashboard: localhost:8080
            # 默认8719端口,假如被占用了会自动从8719端口+1进行扫描,直到找到未被占用的端口
            port: 8719
          web-context-unify: false
    

    这里需要注意,如果我们想要将Sentinel的配置信息放到Nacos之中进行配置,而不是在本地进行配置的话,我们在Nacos之中的配置文件必须完整的补充Sentienl所在的spring级到sentinel的配置,不能只添加只关于Sentinel的相关配置信息,否则将会失效。

  3. 后续便可以在Sentinel之中发现刚使用的注解有效了,在Sentinel控制台之中查看相关的信息。在Sentinel控制台之中添加相应的配置之后,便可以正常的对资源进行限制

    uTools_1664103374603

    1664103343637

  4. 后续在浏览器之中进行进一步的测试便可以检测相应的效果。

    uTools_1664103454480

    uTools_1664103466215

流控结果

1664088738424

快速失败

​ 当我们设置为快速失败的时候,Sentinel就会直接给我们返回Blocked by Sentinel (flow limiting)

预热Warm up

uTools_1664285456935

​ 当系统处于长期的处于低QPS的时候,突然出现了流量突然增加,直接把系统拉升到高水位就可以能突然导致系统压垮。通过“冷启动”,就是让通过的流量缓慢耳朵增加,在一定的时间内逐渐的增加到阈值上线,给冷系统一个预热的时间,避免处于低性能下的系统直接就被冲垮了。

image

​ (QPS随时间增长示意图)

​ 在SpringCloud Alibaba提供的Sentinel之中,为了避免这一现象带来的严重后果,提供了一整套理论 - 自动化配置实现的解决方案。

理论知识

​ Sentinel官方的介绍是,Warm up是根据codeFactor(冷负载因子,默认大小为3)的值,即刚从冷状态被激活的时候,接口的QPS是从QPS阈值 / codeFactor 3 开始,逐渐增长,经过预热时长,才达到设置的QPS阈值。

​ 预热的时间长度由用户在Sentinel控制台之中输入相应值来控制。

示例

​ 对某个资源设置了Warm up的流控模式之后,可以在浏览器之中通过不停的点击的方式来测试出,QPS阈值的变化。

uTools_1664285888056

uTools_1664285916446

排队等待

​ 匀速排队等待的情况适合用于大批量必须执行的请求,但资源处理能力无法一次处理完毕,需要形成队列,在一定时间能缓慢完成的情况。

uTools_1664287384503

基础理论

​ 匀速排队指的是严格的控制请求通过的时间,也即是让请求以均匀的速度同,对应的是漏桶算法

uTools_1664287527289

从而将大批量当前无法完成的请求,留存在系统较为缓和的时候再去执行处理。

这种方式主要用于处理间隔性突发的流量,例如消息队列。想象一下这样的场景,在某一秒有大量的请求到来,而接下来的几秒则处于空闲状态,我们希望系统能够在接下来的空闲期间逐渐处理这些请求,而不是在第一秒直接拒绝多余的请求。(相当于在请求获取之中加了一个消息队列)

注意:匀速排队模式暂时不支持 QPS > 1000 的场景。

示例

uTools_1664287716950

熔断与降级

基本概述

​ 除去了流量控制以外,对于调用链路之中不稳定的资源进行熔断降级也是保障高可用的重要措施之一,一个服务常常会调用别的模块,可能是另外一个远程服务、数据库、或者是第三方API等等。例如,支付的时候可能就会需要远程调用银联提供的API,查询某个商品的价格,可能就需要进行数据库的查询,然后如果被依赖的服务不可能就会导致前者的服务也不可用,这就导致了可能会出现不稳定的情况出现。 ----->>>>> 服务雪崩

​ 熔断降级往往是一个保护的手段,他一般会用在客户端进行配置。

值得注意的是,往往在实际的微服务开发之中我们的熔断时间设置得应当更大一些,方便我们去发现以及检测出错误的信息,后续好生成日志来检查错误。

以下的所有的介绍信息,是基于Sentinel1.8.0及以上的版本,作为介绍基础进行介绍的,因为在Sentinel1.8.0版本之后对熔断机制做了全新的更新升级,所以在实际学习使用的时候,会以1.8.0版本作为基础进行介绍。

熔断

熔断策略

Sentinel提供以下几种的熔断策略:

慢调用比例
  • 慢调用比例 (SLOW_REQUEST_RATIO):选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大的响应时间),请求的响应时间大于该值则统计为慢调用。当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断。

    简单来说就是,当在某个时间段之中(时间段长度可设置[统计时长]),当请求的数目大于设置的最小的请求数目[最小请求数]、并且当出现调用时间超过了某个阈值的时候(调用时间阈值可被设置[最大RT]、出现超时的比例可被设置[比例阈值]),就会出现熔断。经过一定的熔断时长(熔断时长可以被设置[熔断时长])的时候,就会进入到半熔断状态。此时如果出现一个请求成功的在规定的调用时间之中完成调用,那么就将该接口资源的状态重新改为全开放状态。

    image-20220425190913439

    a. 代码(与普通的可访问的接口代码一致即可)

    @GetMapping("/testC/{id}")
    public String textC(@PathVariable("id")Integer id){
        if (id == 10){
            //模拟复杂的业务逻辑
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e){
                e.printStackTrace();
            }
        }
        
        return "itlils testC";
    }
    

    b. 设置策略

    1665112195672

    c. 当接口配置完毕后,一直点击,尝试访问接口资源

    uTools_1665112136124

    d. 过了一会之后,资源重新被开放访问,就可以成功访问相关资源

    uTools_1665112164223

异常比例

(其实和慢调用比例有点像,只有一点是有所不同的,就是异常比例触发熔断的关键是出现异常)

  • 异常比例 (ERROR_RATIO):当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常比率的阈值范围是 [0.0, 1.0],代表 0% - 100%。

    image-20220426115525060

    a. 代码

    @GetMapping("/testD/{id}")
    public String textD(@PathVariable("id")Integer id){
        if (id == 10){
            //模拟复杂的业务逻辑
            try {
                throw new InterruptedException("114514");
            } catch (InterruptedException e){
                e.printStackTrace();
            }
        }
        
        return "itlils testD";
    }
    

    b. 设置策略

    uTools_1665124771387

    c. 测试

    d. 不断点击访问设置了策略的资源接口

    uTools_1665118413962

    e. 过一段时间后

    uTools_1665114502198

异常数
  • 异常数 (ERROR_COUNT):当单位统计时长内的异常数目超过阈值之后会自动进行熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。

    image-20220426160648474

    a. 代码

    @GetMapping("/testE/{id}")
    public String textE(@PathVariable("id")Integer id){
        if (id == 10){
            //模拟复杂导致异常的业务代码
            int a = 1 / 0;
        }
        return "itlils testE";
    }
    

    b. 策略

    uTools_1665131836621

    c. 测试

    d. 正常情况下的访问结果

    uTools_1665132139368

    e. Sentinel熔断生效后的访问结果

    uTools_1665132158105

热点参数限流

概述

为什么叫做热点?热点就是经常会访问的数据,很多时候我们都希望统计某个热点数据之中访问频次最高的TopK的数据,并且对其访问进行限制。比如:

  • 商品 ID 为参数,统计一段时间内最常购买的商品 ID 并进行限制

    限制某一个资源,在一段时间内被成功访问的次数(用来保证资源访问时长[避免一下子抢完])

  • 用户 ID 为参数,针对一段时间内频繁访问的用户 ID 进行限制

    限制某一个资源,被某些高频访问ID的次数(防止脚本盗刷)

​ 热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流。热点参数限流可以看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效。

image-20220426121224000

Sentinel 利用 LRU 策略统计最近最常访问的热点参数,结合令牌桶算法来进行参数级别的流控。热点参数限流支持集群模式。

举例:

​ 类似于一个商品的抢购,限制用户的访问次数以及限制该资源在某个时间段内成功访问的次数。进行访问限制。

如何实现?

​ 在用户访问某个资源的时候,可以参考将该资源所在的参数信息,以URL后缀/POST中JSON的形式传递到后端之中。依靠此作为依据进行设置即可。

uTools_1665134000169

基本使用
  1. 添加热点限流所需依赖

            <dependency>
                <groupId>com.alibaba.csp</groupId>
                <artifactId>sentinel-parameter-flow-control</artifactId>
                <version>1.8.4</version>
            </dependency>
    
  2. 接口业务代码

    @GetMapping("/order")
    @SentinelResource(value = "hotKeys")
    public String order(@RequestParam("goodsId")String goodsId,@RequestParam("userId")String userId){
        //业务逻辑
        return "用户下单成功";
    }
    
  3. 设置规则

    ​ 在规则设置之中,如果我们想要对第一个参数进行限制,那就把参数索引的值设置为0,如果我们想对第二个参数的统计窗口市场进行限制,那就把参数索引的值设置为1

    uTools_1665136005609

  4. 测试

  5. 访问成功的记录

    uTools_1665147328808

  6. 超过在规定的统计窗口时长的单机阈值数后

    uTools_1665147240063

    (关于如何保证确实是对单个参数的限制有效,可以将Controller接口中方法的参数设置为required= false,然后以相同的方式不断刷新请求访问,如果没有出现异常报错,那就说明和其他参数请求无关[这说明了Sentinel热点参数限流的限制参数索引是根据源代码之中参数的位置来决定的,而不是根据用户请求的时候用户参数的位置来决定的])

    限制后会出现报错,报错信息为:com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowException


​ 现在,我们的服务可以对用户的请求进行限制了,但目前返回的信息是程序的报错信息,这对于用户来说,太不友好了。

  1. 在Sentinel之中,也提供了类似于Hystrix的类似处理方法,对于出现服务限制的情况的时候,可以设置错误处理方法,在出现现在的时候调用该方法来实现兜底。

    7.1 代码 (在SentinelResource注解之中添加blockHandler属性,并配置为某个方法)

    @GetMapping("/order")
    @SentinelResource(value = "hotKeys",blockHandler = "blockHandlerMethod")
    public String order(@RequestParam(value = "goodsId" , required = false)String goodsId, @RequestParam(value = "userId" , required =  false)String userId){
        //业务逻辑
    
        return "用户下单成功";
    }
    
    public String blockHandlerMethod(@RequestParam(value = "goodsId" , required = false)String goodsId, @RequestParam(value = "userId" , required =  false)String userId, BlockException blockException){
        //exception可以进行用来做日志记录
        return "用户下单失败,请稍后重试";
    }
    

    7.2 测试结果

    uTools_1665147968654

例外项目

需要注意的是,热点Key的取值例外限制的,只支持基本数据类型和String,自定义的类型是无法提供支持的

​ 在实际的产品之中,我们除了需要对某个参数的请求加以限制以外,我们往往还需要对某个参数的具体取值为某个值的时候的情况加以限制,这个时候我们就需要使用到Sentinel的例外限制条件。

​ 例如:我在某个时间点想要开放某个商品大量购买的权限(例如goodsId为100的,我想要这个在出现抢购的热烈情况,或者限制某个产品的购买数为更小的值,我们都可以通过使用例外项目的条件加以限制)

该部分不需要额外的代码进行配置,只需要对重新配置Sentinel的配置项即可。

a. 配置过滤项

uTools_1665236106709

b. 测试其他取值

​ 当取值为其他普通取值时,短时间内刷新多次就会导致访问资源失败

uTools_1665236170911

c. 测试例外取值

​ 当取值为规定的例外取值时,无论刷新多少次都能成功访问资源

uTools_1665235955734

系统自适应限流(GateWay)

​ 从整个系统的入口流量来实现控制,结合应用的Load、CPU使用率、总体平均RT、入口QPS和并发线程数等几个纬度的监控指标,通过自适应的流控策略,让系统的入口流量和系统的负载到达一个平衡,让系统尽可能的跑在大吞吐量的同时保证系统整体的稳定性。

目的

在开始之前,我们先了解一下系统保护的目的:

  • 保证系统不被拖垮
  • 在系统稳定的前提下,保持系统的吞吐量

长期以来,系统保护的思路是根据硬指标,即系统的负载 (load1) 来做系统过载保护。当系统负载高于某个阈值,就禁止或者减少流量的进入;当 load 开始好转,则恢复流量的进入。这个思路给我们带来了不可避免的两个问题:

  • load 是一个“结果”,如果根据 load 的情况来调节流量的通过率,那么就始终有延迟性。也就意味着通过率的任何调整,都会过一段时间才能看到效果。当前通过率是使 load 恶化的一个动作,那么也至少要过 1 秒之后才能观测到;同理,如果当前通过率调整是让 load 好转的一个动作,也需要 1 秒之后才能继续调整,这样就浪费了系统的处理能力。所以我们看到的曲线,总是会有抖动。
  • 恢复慢。想象一下这样的一个场景(真实),出现了这样一个问题,下游应用不可靠,导致应用 RT 很高,从而 load 到了一个很高的点。过了一段时间之后下游应用恢复了,应用 RT 也相应减少。这个时候,其实应该大幅度增大流量的通过率;但是由于这个时候 load 仍然很高,通过率的恢复仍然不高。

TCP BBRopen in new window 的思想给了我们一个很大的启发。我们应该根据系统能够处理的请求,和允许进来的请求,来做平衡,而不是根据一个间接的指标(系统 load)来做限流。最终我们追求的目标是 在系统不被拖垮的情况下,提高系统的吞吐率,而不是 load 一定要到低于某个阈值。如果我们还是按照固有的思维,超过特定的 load 就禁止流量进入,系统 load 恢复就放开流量,这样做的结果是无论我们怎么调参数,调比例,都是按照果来调节因,都无法取得良好的效果。

Sentinel 在系统自适应保护的做法是,用 load1 作为启动自适应保护的因子,而允许通过的流量由处理请求的能力,即请求的响应时间以及当前系统正在处理的请求速率来决定。

系统规则

​ 系统保护规则是从应用级别的入口流量进行控制,从单台机器的Load、CPU使用率,平均RT、入口QPS和并发线程数等几个纬度监控应用指标,让系统尽可能跑在最大的吞吐量的同时保证系统整体的稳定性。

​ 系统保护规则是应用整体纬度的,而不是资源纬度的,并且仅对入口流量生效,入口流量指的是进入应用的流量(EntryType.IN),比如Web服务或者Dubbo服务端接收的请求,都属于是入口流量。

系统规则支持以下的模式:

  • Load自适应 (仅对Linux/Unix-like机器生效):系统的load1作为启发指标,进行自适应的系统保护。当系统load1超过设定耳朵启发值,并且系统当前的并发线程数超过估算的系统容量的时候才会处罚系统保护(BBR阶段)。系统容量由系统的 maxQps * minRt 估算得出。设定参考值一般是 CPU cores * 2.5
  • CPU usage[CPU使用率](1.5.0+ 版本):当系统 CPU 使用率超过阈值即触发系统保护(取值范围 0.0-1.0),比较灵敏。
  • 平均 RT:当单台机器上所有入口流量的平均 RT 达到阈值即触发系统保护,单位是毫秒。
  • 并发线程数:当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护。
  • 入口 QPS:当单台机器上所有入口流量的 QPS 达到阈值即触发系统保护
配置系统规则

​ 配置系统规则的时候,只需要在Sentinel的控制台上添加规则即可。

a. 在Sentinel控制台上点击系统规则,便可对系统所有的接口加以一定规则的限制

uTools_1665322683532

b. 测试,即可以发现所有的接口都会出现被Sentinel限流的情况

uTools_1665322704071

SentinelResource注解

@SentinelResource 用于定义资源,并提供可选的异常处理和 fallback 配置项。 @SentinelResource 注解包含以下属性:

  • value:资源名称,必需项(不能为空)

    简单的来说SentinelResource可以帮助我们对某个接口的资源名进行修改 

    (因为在实际的环境之中某个资源的URL可能会存在很多级,如果还是以往的那种直接将URL设置为资源名,存在着不妥)

  • entryType:entry 类型,可选项(默认为 EntryType.OUT

  • blockHandler / blockHandlerClass: blockHandler 对应处理 BlockException 的函数名称,可选项。blockHandler 函数访问范围需要是 public,返回类型需要与原方法相匹配,参数类型需要和原方法相匹配并且最后加一个额外的参数,类型为 BlockException。blockHandler 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 blockHandlerClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。

    blockHandler可以用来设置热点Key的流速限制报错后的执行方法,使得返回到客户观看的额信息更加人性化一些  (异常错误兜底)

    ----> 按照blockHandler的说法,我们每写一个业务可能出现异常的接口,我们就需要写一个异常处理方法,那我们的代码量将会大大的激增,为了解决这个问题,就出现了blockHandlerClass来对可能会出现的需写多个异常处理方法的情况进行处理

    使用blockHandlerClass的指定类对象进行处理之后,我们还需要对需要调用的函数使用static静态函数进行使用,否则会导致无法解析。

    blockHandlerClass作为一个类似于AOP面向切面的思想一样,我们就可以通过写一个公共的类,提供相应所需的方法来提供blockHandler的方式来实现所需的效果。

    但实际情况下,虽然有blockHandlerClass的存在我们仍然需要写大量的blockHandler方法来实现限流后出现的异常报错,因为接口的参数列表必须一致才能触发@SentinelResource之中的blockHandler指定的方法。

  • fallback/fallbackClass:fallback 函数名称,可选项,用于在抛出异常的时候提供 fallback 处理逻辑。fallback 函数可以针对所有类型的异常(除了

    exceptionsToIgnore里面排除掉的异常类型)进行 处理。fallback 函数签名和位置要求:

    • 返回值类型必须与原函数返回值类型一致;
    • 方法参数列表需要和原函数一致,或者可以额外多一个 Throwable 类型的参数用于接收对应的异常。
    • fallback 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 fallbackClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。
  • defaultFallback(since 1.6.0):默认的 fallback 函数名称,可选项,通常用于通用的 fallback 逻辑(即可以用于很多服务或方法)。默认 fallback 函数可以针对所有类型的异常(除了exceptionsToIgnore

    里面排除掉的异常类型)进行处理。若同时配置了 fallback 和 defaultFallback,则只有 fallback 会生效。defaultFallback 函数签名要求:

    • 返回值类型必须与原函数返回值类型一致;
    • 方法参数列表需要为空,或者可以额外多一个 Throwable 类型的参数用于接收对应的异常。
    • defaultFallback 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 fallbackClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。
  • exceptionsToIgnore(since 1.6.0):用于指定哪些异常被排除掉,不会计入异常统计中,也不会进入 fallback 逻辑中,而是会原样抛出。

1.8.0 版本开始,defaultFallback 支持在类级别进行配置。

注:1.6.0 之前的版本 fallback 函数只针对降级异常(DegradeException)进行处理,不能针对业务异常进行处理

特别地,若 blockHandler 和 fallback 都进行了配置,则被限流降级而抛出 BlockException 时只会进入 blockHandler 处理逻辑。若未配置 blockHandlerfallbackdefaultFallback,则被限流降级时会将 BlockException 直接抛出(若方法本身未定义 throws BlockException 则会被 JVM 包装一层 UndeclaredThrowableException)。

blockHandler熔断测试

降级

概述

​ 为了能够准确的描述和介绍降级的相关功能及其实现,我们需要为降级准备三个微服务模块。

三台微服务的配置及降级配置步骤

(LF-SpringCloud-Alibaba中Sentinel-p     rovider-demotion[7001]、Sentinel-provider-demotion2[7002]为服务提供方;Sentinel-consumer-demotion[7101]为服务消费方)

三台微服务配置

(使用Sentinel之后,需要在yaml之中配置让Sentinel打开对OpenFeign的支持,才可以正常使用OpenFeign)

  1. 创建提供资源服务的Sentinel-provider7001

    1.1. 在Sentinel-provider-demotion 7001的 pom.xml文件之中引入相关的依赖

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
        
        <groupId>com.leticiafeng</groupId>
        <artifactId>sentinel-provider-demotion</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <name>sentinel-provider-demotion</name>
        <description>Sentinel Provider Demotion Server</description>
    
        <parent>
            <groupId>com.leticiafeng</groupId>
            <artifactId>LF-SpringCloud-Alibaba</artifactId>
            <version>1.0-SNAPSHOT</version>
        </parent>
        
        <properties>
            <java.version>1.8</java.version>
        </properties>
        
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
            </dependency>
    
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
            </dependency>
            
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
            </dependency>
            
            <!-- SpringCloud ailibaba sentinel-->
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
            </dependency>
            
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-openfeign</artifactId>
            </dependency>
    
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-bootstrap</artifactId>
            </dependency>
    
            <dependency>
                <groupId>com.alibaba.csp</groupId>
                <artifactId>sentinel-parameter-flow-control</artifactId>
                <version>1.8.4</version>
            </dependency>
        </dependencies>
    
        <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                </plugin>
            </plugins>
        </build>
    
    </project>
    

    1.2 bootstrap.yml和application配置

    server:
      port: 7001
    
    spring:
      application:
        name: sentinel-provider-demotion
      cloud:
        nacos:
          discovery:
            server-addr: localhost:8848 #Nacos服务注册中心地址
          config:
            server-addr: localhost:8848 #Nacos作为配置中心地址
            file-extension: yaml #指定yaml格式的配置
            namespace: c6a23619-dd30-41ad-8435-2ae302982816  # 必须为ID
            group: GZ_GROUP
    
    spring:
      profiles:
        active: prod #表示要用作用于什么环境的配置文件 (配置当前服务应当为什么环境提供配置服务)
    

    1.3 启动类配置

    @SpringBootApplication
    @EnableDiscoveryClient
    public class SentinelProviderDemotionApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(SentinelProviderDemotionApplication.class, args);
        }
    
    }
    

    1.4 controller配置

    @RestController
    @RequestMapping("/goods")
    public class GoodsController {
        
        @Value("${server.port}")
        private Integer port;
        
        @GetMapping("/findById/{id}")
        public Goods findById(@PathVariable("id") Integer id){
            //servive-->dao/mapper
            Goods goods=new Goods();
            goods.setGoodId(id);
            goods.setPrice(123);
            goods.setTitle("手机.name:" + port);
            goods.setStock(10);
    
            return goods;
        }
        
    }
    

    1.5 domain配置

    public class Goods implements Serializable {
        private Integer goodId;
        private String title;
        private double price;
        private Integer stock;
    
        public Goods() {
        }
    
    
        public Goods(Integer goodId, String title, double price, Integer stock) {
            this.goodId = goodId;
            this.title = title;
            this.price = price;
            this.stock = stock;
        }
    
        @Override
        public String toString() {
            return "Goods{" +
                    "goodId=" + goodId +
                    ", title='" + title + '\'' +
                    ", price=" + price +
                    ", stock=" + stock +
                    '}';
        }
    
        public Integer getGoodId() {
            return goodId;
        }
    
        public void setGoodId(Integer goodId) {
            this.goodId = goodId;
        }
    
        public String getTitle() {
            return title;
        }
    
        public void setTitle(String title) {
            this.title = title;
        }
    
        public double getPrice() {
            return price;
        }
    
        public void setPrice(double price) {
            this.price = price;
        }
    
    
        public Integer getStock() {
            return stock;
        }
    
        public void setStock(Integer stock) {
            this.stock = stock;
        }
    }
    
  2. 创建提供资源服务的Sentinel-provider-demotion2[7002]的配置除了server.port的设置不一致以外其他的全部一致,在此就不多赘述了

  3. 创建消费资源服务的Sentinel-consumer-demotion[7101]

    3.1 pom.xml文件

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
        
        <groupId>com.leticiafeng</groupId>
        <artifactId>sentinel-consumer-denotion</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <name>sentinel-consumer-denotion</name>
        <description>Sentinel-Consumer-Denotion</description>
    
        <parent>
            <groupId>com.leticiafeng</groupId>
            <artifactId>LF-SpringCloud-Alibaba</artifactId>
            <version>1.0-SNAPSHOT</version>
        </parent>
        
        <properties>
            <java.version>1.8</java.version>
        </properties>
        
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
            </dependency>
    
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
                <exclusions>
                    <exclusion>
                        <groupId>org.springframework.cloud</groupId>
                        <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
                    </exclusion>
                </exclusions>
            </dependency>
            
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-loadbalancer</artifactId>
                <version>3.1.1</version>
            </dependency>
            
            <!-- SpringCloud ailibaba sentinel-->
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
            </dependency>
            
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-openfeign</artifactId>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-bootstrap</artifactId>
            </dependency>
    
            <dependency>
                <groupId>com.alibaba.csp</groupId>
                <artifactId>sentinel-parameter-flow-control</artifactId>
                <version>1.8.4</version>
            </dependency>
    
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
            </dependency>
            
        </dependencies>
    
        <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                </plugin>
            </plugins>
        </build>
    
    </project>
    

    3.2 bootstrap.yml以及application.yml配置文件如下

    server:
      port: 7101
    
    spring:
      application:
        name: sentinel-consumer-demotion
      cloud:
        nacos:
          discovery:
            server-addr: localhost:8848 #Nacos服务注册中心地址
          config:
            server-addr: localhost:8848 #Nacos作为配置中心地址
            file-extension: yaml #指定yaml格式的配置
            namespace: c6a23619-dd30-41ad-8435-2ae302982816  # 必须为ID
            group: GZ_GROUP
    
    spring:
      profiles:
        active: prod #表示要用作用于什么环境的配置文件 (配置当前服务应当为什么环境提供配置服务)
    

    3.3 启动类配置

    @SpringBootApplication
    @EnableFeignClients   //开启远程Feign的调用
    @EnableDiscoveryClient
    public class SentinelConsumerDenotionApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(SentinelConsumerDenotionApplication.class, args);
        }
    
    }
    

    3.4 Openfeign接口类配置

    @FeignClient(value = "sentinel-provider-demotion")
    public interface GoodFeign {
        //需要远程调用的方法 (注意url需要将restController的前缀和      后续方法的url都写上)
        @GetMapping("/goods/findById/{id}")
        public Goods findById(@PathVariable Integer id);
    }
    

    3.5 实体类配置如下

    public class Goods implements Serializable {
        private Integer goodId;
        private String title;
        private double price;
        private Integer stock;
    
        public Goods() {
        }
    
    
        public Goods(Integer goodId, String title, double price, Integer stock) {
            this.goodId = goodId;
            this.title = title;
            this.price = price;
            this.stock = stock;
        }
    
        @Override
        public String toString() {
            return "Goods{" +
                    "goodId=" + goodId +
                    ", title='" + title + '\'' +
                    ", price=" + price +
                    ", stock=" + stock +
                    '}';
        }
    
        public Integer getGoodId() {
            return goodId;
        }
    
        public void setGoodId(Integer goodId) {
            this.goodId = goodId;
        }
    
        public String getTitle() {
            return title;
        }
    
        public void setTitle(String title) {
            this.title = title;
        }
    
        public double getPrice() {
            return price;
        }
    
        public void setPrice(double price) {
            this.price = price;
        }
    
    
        public Integer getStock() {
            return stock;
        }
    
        public void setStock(Integer stock) {
            this.stock = stock;
        }
    }
    

    3.6 调用OpenFeign的接口,配置信息如下

    @RestController
    @RequestMapping("/test")
    public class OpfeignController {
        
        @Autowired
        private GoodFeign goodFeign;
        
        @GetMapping("/getFeignController/{id}")
        public String getFeignController(@PathVariable Integer id) {
            Goods byId = goodFeign.findById(id);
            return JSON.toJSONString(byId);
        }
    }
    

测试结果

uTools_1665669503730

uTools_1665669491187

降级的使用场景

被调用方的降级 - SentinelSource的fallback

出现业务不应该出现的问题(用户恶意输入非法信息调用资源)

​ (比如说访问的商品页面是编号为负数的商品、超过现有所有商品数量的时候),如果我们不配置任何的设置,那么程序将会正常的进入到异常报错,并将报错信息返回给前端显示给用户的情况,这种处理方式会给用户带来非常不友好的体验。因此在实际使用Sentinel开发的时候,往往需要设置fallback方法来对错误信息进行拦截处理。

​ 根据上述对blockHandler的描述之中,我们可以知道blockHandler只可以限制住不符合热点 、 流控限制条件的访问,而对于业务上出现的报错,blockHandler则无能为力,所以我们需要使用fallback来实现业务异常的处理。(需要注意的是发生了业务错误的请求仍然是触发了请求的。因此,出现业务错误的请求仍然会被流控规则所限制)。

添加blockHandler处理步骤如下:

  1. 在相应的Controller之中添加异常处理方法 (参数必须与Controller接口方法的一致,并添加一个BlockException参数)

        @GetMapping("/findById/{id}")
        @SentinelResource(value = "find" , blockHandler = "fail_add")
        public Goods findById(@PathVariable("id") Integer id){
            //servive-->dao/mapper
            Goods goods=new Goods();
            goods.setGoodId(id);
            goods.setPrice(123);
            goods.setTitle("手机.name:" + port);
            goods.setStock(10);
    
            if (id < 0){
                throw new IllegalArgumentException("非法参数!!");
            }else if (id > 100){
                throw new NullPointerException("查不到该商品");
            }
    
            return goods;
        }
     	 public Goods fail_add(@PathVariable("id")Integer id, BlockException ex) {
           Goods goods = new Goods();
           goods.setGoodId(-1);
           goods.setPrice(-1);
           goods.setStock(-1);
           goods.setTitle("限流之后的特殊对象");
    
           return goods;
       }
    
  2. 在Sentinel上加上配置信息,对接口进行流量管控即可实现流量的限制访问。

    uTools_1665896748437

    到这一步为止,我们的服务已经可以实现流量限制访问的效果,但是还是会在遇到业务异常的时候,将相关的错误信息打印到浏览器的前端页面上,仍然是不够友好的结果,所以我们需要使用fallback方法

添加fallback处理步骤如下

  1. 首先,在Controller之中添加相关的fallback处理方法,fallback方法负责给予用户一个较好的体验的返回对象,避免出现异常错误的信息页面因为业务问题出现在返回的页面上。

    @GetMapping("/findById/{id}")
        @SentinelResource(value = "find", blockHandler = "fail_find", fallback = "fall_business")
        public Goods findById(@PathVariable("id") Integer id){
            //servive-->dao/mapper
            Goods goods=new Goods();
            goods.setGoodId(id);
            goods.setPrice(123);
            goods.setTitle("手机.name:" + port);
            goods.setStock(10);
                                                                                                                      
            if (id < 0){
                throw new IllegalArgumentException("非法参数!!");
            }else if (id > 100){
                throw new NullPointerException("查不到该商品");
            }
                                                                                                                      
            return goods;
        }
    
    
        public Goods fail_find(@PathVariable("id")Integer id, BlockException ex) {
            Goods goods = new Goods();
            goods.setGoodId(-1);
            goods.setPrice(-1);
            goods.setStock(-1);
            goods.setTitle("限流之后的特殊对象");
                                                                                                                                                            
            return goods;
        }
                                                                                                                                                                
        public Goods fall_business(@PathVariable("id")Integer id, Throwable ex) {
            Goods goods = new Goods();
            goods.setGoodId(-114514);
            goods.setPrice(-114.514);
            goods.setStock(-114514);
            goods.setTitle("业务出现异常,异常处理方法被触发");
                                                                                                                                                                    
            return goods;
        }
    
  2. 效果测试

    7002返回结果

    uTools_1665906150159

    7001返回结果

    uTools_1665906150159

    出现业务异常时(触发fallback方法处理方法)

    uTools_1665906323118

    多次访问,触发熔断降级时,就会出现Sentienl熔断提供的支持页面

    uTools_1665906359898

很简单就能想到,fallback这种拦截的处理肯定是基于将所有的异常都进行拦截并且处理的方式来实现的,但如果我们并不希望他将全部的错误都拦截的话,那我们应该怎么做呢?

SentinelSource注解还可以添加exceptionsToIgnore 对特定的异常错误进行忽略

@SentinelResource(value = "find", blockHandler = "fail_find", fallback = "fall_business", exceptionsToIgnore = {IllegalArgumentException.class})

完整代码

@GetMapping("/findById/{id}")
@SentinelResource(value = "find", blockHandler = "fail_find", fallback = "fall_business", exceptionsToIgnore = {IllegalArgumentException.class})
public Goods findById(@PathVariable("id") Integer id){
    //servive-->dao/mapper
    Goods goods=new Goods();
    goods.setGoodId(id);
    goods.setPrice(123);
    goods.setTitle("手机.name:" + port);
    goods.setStock(10);

    if (id < 0){
        throw new IllegalArgumentException("非法参数!!");
    }else if (id > 100){
        throw new NullPointerException("查不到该商品");
    }

    return goods;
}


public Goods fail_find(@PathVariable("id")Integer id, BlockException ex) {
    Goods goods = new Goods();
    goods.setGoodId(-1);
    goods.setPrice(-1);
    goods.setStock(-1);
    goods.setTitle("限流之后的特殊对象");

    return goods;
}

public Goods fall_business(@PathVariable("id")Integer id, Throwable ex) {
    Goods goods = new Goods();
    goods.setGoodId(-114514);
    goods.setPrice(-114.514);
    goods.setStock(-114514);
    goods.setTitle("业务出现异常,异常处理方法被触发");
    
    return goods;
}

效果

对特定报错,将会显示到前端页面

uTools_1665908503432

其他报错仍然会被fallback方法进行处理

uTools_1665908564252

调用方降级 - OpenFeign的fallback异常处理方法

(因为Sentinel之中可以对部分的异常放开处理,但我们又不能将这些放开的异常直接显示到前端页面上,这个时候我们就可以使用OpenFeign来处理这些异常)

​ 当然除了SentinelSource的fallback可以提供异常的一个处理,还有Openfeign的fallback,(除此以外,Openfeign还提供了日志的一个记录方法[让我们可以在调用方查看到错误的异常信息],这里回顾一下)

只需要在OpenFeign的对应接口之中添加属性fallback,并且创建远程调用的日志记录文件Log以及异常处理的impl方法,对OpenFeign的接口进行实现即可实现解决。

实现步骤

  1. OpenFeign添加fallback属性以及configuration属性

    @FeignClient(value = "sentinel-provider-demotion", configuration = FeignLogConfig.class, fallback = GoodFeignFallback.class)
    public interface GoodFeign {
        //需要远程调用的方法 (注意url需要将restController的前缀和      后续方法的url都写上)
        @GetMapping("/goods/findById/{id}")
        public Goods findById(@PathVariable Integer id);
        
    }
    
  2. 添加OpenFeign的实现类,以及Configuration对应的日志类

    @Component
    public class GoodFeignFallback implements GoodFeign{
        @Override
        public Goods findById(Integer id) {
            Goods goods = new Goods();
            goods.setTitle("调用方返回异常");
            return goods;
        }
        
    }
    

    日志类

    @Configuration
    public class FeignLogConfig {
        @Bean
        public Logger.Level level(){
            return Logger.Level.FULL;
        }
    }
    

    效果如下

uTools_1665911242726

Sentinel流控配置持久化

​ 在之前,我们的配置都是临时的,只要我们重启了项目,那么就没办法重新生效了,我们往往又要需要重新配置相关资源的流控规则,让他重新生效。为了解决这个问题,我们可以将Sentinel的流控规则持久化到Nacos之中。

步骤

  1. 在想要实现流控配置持久化的项目中pom引入相关项

    <dependency>
                <groupId>com.alibaba.csp</groupId>
                <artifactId>sentinel-datasource-nacos</artifactId>
            </dependency>
    
  2. 然后在本地的yml或者在nacos上对应的配置文件之中添加相关的配置项信息,指明Sentinel配置所在的地点**(需要指定ngd namsespace + group + dataId,指定到准确的地点)**

    (以下为Nacos之中的配置项)

    spring:
      cloud:
        sentinel:
          transport:
            dashboard: localhost:8080    #sentinel
            port: 8719
          datasource:
            ds1:
              nacos:
                server-addr: localhost:8848  #nacos
                dataId: ${spring.application.name}
                groupId: GZ_GROUP
                namespace: c6a23619-dd30-41ad-8435-2ae302982816
                data-type: json
                rule-type: flow
          
    
    #激活Sentinel对Feign的支持
    feign:
      sentinel:
        enabled: true
    

    uTools_1665922749206

  3. 然后在Nacos之中,添加上述yaml之中所指定的流控配置文件。

    [
        {
            "resource": "/test/getFeignController/{id}",
            "limitApp": "default",
            "grade": 1,
            "count": 1,
            "strategy": 0,
            "controlBehavior": 0,
            "clusterMode": false
        },
        {
        	"resource": "url",
            "limitApp": "default",
            "grade": 1,
            "count": 1,
            "strategy": 0,
            "controlBehavior": 0,
            "clusterMode": false
        },
        ··· ···
    ]
    

    uTools_1665922863079

  4. 重启项目,即可查看到相关的配置效果**(需要注意的是,这里的配置虽然可以让我们对项目的具体资源实现流量控制,但是我们在Sentinel的控制台dashboard并不能查看到这些已经持久化了的配置信息!)**

    uTools_1665923066197

配置网关GateWay

(因为后续很少会打开SpringCloud那边的介绍笔记了 ,这里还是需要详细一点介绍片一下网关)

重点

[通过网关,我们就可以通过访问同一个域名调用不同实例下的不同服务,例如通过访问网关的域名,访问到其他IP地址的服务资源]

为什么需要网关GateWay,它是用来干嘛的?

​ 根据冰河在SpingCloud Alibaba实战一文之中介绍GateWay的使用背景如下:

当一个系统使用分布式、微服务架构后,系统会被拆分为一个个小的微服务,每个微服务专注一个小的 业务。那么,客户端如何调用这么多微服务的接口呢?如果不做任何处理,没有服务网关,就只能在客 户端记录下每个微服务的每个接口地址,然后根据实际需要去调用相应的接口。

uTools_1666523659505

这种直接使用客户端记录并管理每个微服务的每个接口的方式,存在着太多的问题。比如,这里我列举几个常见的问题。

  • 由客户端记录并管理所有的接口缺乏安全性。
  • 由客户端直接请求不同的微服务,会增加客户端程序编写的复杂性。
  • 涉及到服务认证与鉴权规则时,需要在每个微服务中实现这些逻辑,增加了代码的冗余性。
  • 客户端调用多个微服务,由于每个微服务可能部署的服务器和域名不同,存在跨域的风险。
  • 当客户端比较多时,每个客户端上都管理和配置所有的接口,维护起来相对比较复杂。

问题: 但是根据该描述我们的问题就来了,记录微服务的每个接口地址?知道调用什么具体的服务?这个部分不是注册中心帮我们完成调用了吗?

但实际上,这个问题是由于没有尝试过多台服务器来实现微服务调用的情况下才会导致认为注册中心能解决这个部分问题的错觉。

​ 实际上,Nacos只能帮助我们能找到服务调用最后一环,即最终处理这个业务的服务,但是我们请求的地址不一定就是我们最终处理的地址,就像是我想要实现 访问我的服务的某个地址,但是实际最终访问的是另外一个实际地址的资源。 这里举一个极端一些的例子。我想要访问localhost:8080/114514,但是实际访问效果是访问 www.baidu.com 如果我想要实现这个效果,那我就可以通过Gateway来实现。

以下是贴吧老哥: nanayask 的描述

uTools_1666525067427

更加值得一提的是

实际上在项目之中微服务网关的位置会在 Nginx 之后,我们的请求是直接到达微服务网关,再由微服务网关的动态路由配置来做负载均衡调用最终决定实际上做哪一个调用处理。注册中心的服务获取是在实际发起业务请求之前做的

![1730272346158](SpringCloud Alibaba/相关图片/1730272346158-17302723613605.jpg)

网关当代技术背景

​ 目前主流的网关处理方式主要有以下几种方式:Nginx + Lua、Kong网关、Zuul网关、Apache Shenyu网关、SpringCloud Gateway。Kong *(基于Nginx和Lua脚本开发,性能较高,但是二次开发很难)*和 Zuul *(Netifix开源的网关,长时间没有更新)*就不介绍了,反正估计也没啥人用。

Nginx + Lua

​ Nginx + Lua 实现网关的基本逻辑来自于Nginx的一些插件,Nginx的插件提供了限流、缓存、黑白名单和灰度发布等一系列功能,另外再加上Nginx的基本功能正向代理和反向代理功能,而基于Lua语言(这个脚本语言曾经在学习Redis的时候提及过)可以编写基本的脚本,因此两者配合可以实现一些基本的网关功能。

Apache Shenyu

​ Dromara社区开发的网关框架,ShenYu 的签名是 soul ,最近正式加入 Apache 的孵化器,因此改名为 ShenYu 。该网关是一个异步的,高性能的,跨语言的,响应的API网关,并在此基础上提供了非常丰富的扩展功能:

  • 支持各种语言(http协议),支持Dubbo, Spring-Cloud, Grpc, Motan, Sofa, Tars等协议。
  • 插件化设计思想,插件热插拔,易扩展。
  • 灵活的流量筛选,能满足各种流量控制。
  • 内置丰富的插件支持,鉴权,限流,熔断,防火墙等等。
  • 流量配置动态化,性能极高。
  • 支持集群部署,支持 A/B Test,蓝绿发布。

SpringCloud Gateway

​ Spring Cloud Gateway是Spring Cloud的一个全新项目,基于Spring 5.0 + Spring Boot 2.0和Project Reactor等技术开发的网关,它旨在为微服务架构提供一种简单有效的统一的API路由管理方式。Spring Cloud Geteway作为Spring Cloud生态系统中的网关,目标是替代Zuul,在Spring Cloud2.0以上版本 中,没有对新版本的Zuul 2.0以上最新高性能版本进行集成,仍然还是使用的Zuul 1.x非Reactor模式的 老版本。而为了提升网关性能,Spring Cloud Gateway是基于WebFlux框架实现的,而WebFlux框架底 层则使用了高性能的Reactor模式通信框架Netty。

​ Spring Cloud Gateway的目标提供统一的路由方式且基于Filter链的方式提供了网关基本的功能,例如: 安全,监控/指标,和限流。

总结一句话:Spring Cloud Gateway使用的Webflux中的reactor-netty响应式编程组件,底层使用 Netty通讯框架

Gateway的基本功能

路由

​ 通过yml的配置即可实现。主要实现的功能有 实现动态代理、不同服务器的一个转发

过滤

通过配置过滤器的方式进行分类,可以认为有两类过滤器。第一种是依靠Gateway内置功能提供的过滤器,统称内置过滤器第二种是根据用户来进行配置的自定义过滤器

  • 内置过滤器 (Gateway内部提供的过滤器)

  • 自定义过滤器 (自己定义的过滤器)

而在过滤器的作用范围上进行分类,我们又可以对过滤器进行分类,根据他的作用范围,我们可以得到以下的两种过滤器类型。第一种作用于某个部分的过滤器又叫做局部过滤器;第二种是作用于全局的又叫做全局过滤器。

  • 局部过滤器 (作用于局部的过滤器)

  • 全局过滤器 (作用于全局的过滤器)

根据过滤器作用的效果来进行分类,我们可以得到以下的两种分类,第一种是在转发之前进行的过滤,叫做pre过滤器;第二种是在响应之前进行的过滤,叫做post过滤器

  • **pre过滤器:**是指在转发之前进行执行,可以进行参数校验、权限校验、流量监控、日志输出、协议转换等工作

  • **post过滤器:**是指在响应之前进行执行,可以做响应内容、响应头的修改、日志的输出、流量的控制等工作

Alibaba配置Gateway步骤

​ Gateway与Nacos、Sentinel、Openfeign等组件不一致,Gateway作为为微服务之中的网关,需要独立作为一个模块而存在。GateWay是SpringCloud Alibaba完整架构之中唯一一个需要独立模块存在,而不是依靠具体业务服务端和消费端而存在的组件。

  1. 新建一个GateWay模块

  2. 在GateWay模块中的pom.xml文件之中添加以下的依赖

    <dependencies>
    <!--        <dependency>-->
    <!--            <groupId>org.springframework.boot</groupId>-->
    <!--            <artifactId>spring-boot-starter-web</artifactId>-->
    <!--        </dependency>-->
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
            </dependency>
    
    
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
            </dependency>
    
            <!-- SpringCloud ailibaba nacos-->
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
            </dependency>
    
            <!-- SpringCloud ailibaba sentinel-->
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-bootstrap</artifactId>
            </dependency>
    
            <dependency>
                <groupId>com.alibaba.csp</groupId>
                <artifactId>sentinel-parameter-flow-control</artifactId>
                <version>1.8.4</version>
            </dependency>
    
            <!-- Gateway配置 -->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-gateway</artifactId>
            </dependency>
        </dependencies>
    
  3. 在application.yml之中添加以下配置信息

    # 例子一
    server:
      port: 7777
    spring:
      application:
        name: server-gateway
      cloud:
        gateway:
          globalcors:
            cors-configurations:
              '[/**]':
                allowedOrigins: "*"
                allowedMethods: "*"
                # allowCredentials: true
                allowedHeaders: "*"
          routes:
            - id: get-yeshoucode
              uri: http://localhost:7001
              order: 1
              predicates:
                - Path=/test/getSetting
    #          filters:
    #            - StripPrefix=1
    
  4. 在启动类之中正常启动即可

  5. 此时,我们就可以通过访问localhost:7777/test/getSetting这一域名,成功访问到localhost:7001/test/getSetting的资源内容

Gateway配置

常见的配置项如下:(因为平时一般都是使用yml文件进行配置的,这里就不再介绍通过注册Bean对象进行配置的方式了)

Gloalcors

  • globalcors: 此节点下的配置是用来解决SpringCloud Gateway存在的跨域的问题的

    • '[/**]'
      • allowedOrigins: "*"
      • allowedMethods: "*"
      • allowCredentials: true
      • allowedHeaders: "*"

Routes

  • routes: 表示一个路由数组(转发规则),可以在此节点下配置多个路由信息

id

  • id: 当前路由的唯一标识

url

  • uri: 表示当前路由,通过规则后会被转发的路径

    • uri: http://localhost:8005 - 静态路径

    • uri: lb://GATEWAY_CONSUMER - 动态路径
      (配置动态路径,我们就需要通过使用Nacos,从Nacos之中找到名字相匹配的微服务路径,根据负载均衡的策略实现分发调用)

      例如:例子之中的GATEWAY_CONSUMER,相当于我们会到Nacos之中寻找GATEWAY_CONSUMER对应的微服务地址,并且提供实现调用。这个名字对应着每一个项目在yml文件之中spring.application.name之中的配置。

      引入Nacos配置GateWay

      如果想要使用动态路径,首先需要引入nacos,并且在yaml/yml之中添加Nacos的相关配置

      步骤

      1. 在GateWay所在的SpringBoot模块之中添加Nacos所需的依赖

        <!-- SpringCloud ailibaba nacos-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <!-- 引用Nacos必须引用OpenFeign (冰河的资料是错的) --> 
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
            <version>3.1.1</version>
        </dependency>
        <!-- 以下是Nacos配置中心配置所需的基本依赖 -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>
        <!-- 配置中心所需要的bootstrap.yml配置所需 -->
        <dependency>
             <groupId>org.springframework.cloud</groupId>
        	 <artifactId>spring-cloud-starter-bootstrap</artifactId>
        </dependency>
        <!-- Sentinel配置中心基本所需的依赖 -->
        <dependency>
            <groupId>com.alibaba.csp</groupId>
            <artifactId>sentinel-parameter-flow-control</artifactId>
            <version>1.8.4</version>
        </dependency>
        
      2. 如果我们只是用了第一个依赖,没有配置配置中心的打算,那么我们只需要在application.yml文件之中,加上相关的配置项即可

        server:
          port: 7777
        spring:
          application:
            name: server-gateway
          cloud:
            nacos:
              discovery:
                server-addr: 127.0.0.1:8848
                username: nacos
                password: nacos
            gateway:
              discovery:
                locator:
                  enabled: true
              globalcors:
                cors-configurations:
                  '[/**]':
                    allowedOrigins: "*"
                    allowedMethods: "*"
        #            allowCredentials: true    # 不注释会报错
                    allowedHeaders: "*"
              routes:
                - id: sentinel-provider-demotion-gateway
                  uri: lb://sentinel-provider-demotion
                  order: 1
                  predicates:
                    - Path=/sentinel-provider-demotion/**
                  filters:
                    - StripPrefix=1
        
      3. 在启动类上添加Nacos发现(Feign)的基本注解

        @SpringBootApplication
        @EnableDiscoveryClient
        public class SpringCloudAlibabaGatewayApplication {
        
            public static void main(String[] args) {
                SpringApplication.run(SpringCloudAlibabaGatewayApplication.class, args);
            }
        
        }
        
      4. 后续我们只需要在浏览器上输入相对应的URL即可 127.0.0.1:7777/sentinel-provider-demotion/test/getSetting

        uTools_1666881793665

      如果我们想要一个更加简单的配置方式的话,也可以实现,我们不再需要在application.yml文件之中添加routes之中某个具体的微服务的相关匹配信息。

      spring:
        application:
      	name: server-gateway
        cloud:
      	nacos:
      	  discovery:
      		server-addr: 127.0.0.1:8848
          gateway:
            globalcors:
      		cors-configurations:
      		  '[/**]':
      			allowedOrigins: "*"
      			allowedMethods: "*"
      			allowCredentials: true
      			allowedHeaders: "*"
      	  discovery:
      		locator:
      		  enabled: true
      

      后续我们通过经由gateway访问具体的服务资源的时候,只需要通过基础的URL + applicationName + 资源相关路径即可

order

  • order:表示当前路由的优先级

predicates

  • predicates: 网关断言,也就是路由转发的条件,也是一个数组,可以配置多个路由转发条件

    • After:请求在参数指定的时间之后才会开始起作用

      #GateWay项目端口号
      server:
        port: 7777
      spring:
        cloud:
          gateway:
            routes:
              - id: get-yeshoucode #路由的ID
                uri:  http://localhost:7001/ #匹配后路由地址
                predicates:
                      - After=2022-04-27T16:35:04.030+08:00[Asia/Shanghai]
                      # - Before=2022-04-27T16:35:04.030+08:00[Asia/Shanghai]
                      # - Between=2022-04-22T16:00:00.020+08:00[Asia/Shanghai],2022-04-22T16:30:00.020+08:00[Asia/Shanghai]
      
    • Before:请求将在参数指定的时间之前才会有作用

      请参考上例的代码

    • Between:请求将在参数在指定的时间内才会有作用

      请参考上例的代码 (第一个时间必须小于第二个时间)

    • Cookie:请求携带指定的Cookie才会匹配

      例如:请求必须携带者内容为name=yellowDuck的才能匹配该路由

      #GateWay项目端口号
      server:
        port: 7777
      spring:
        cloud:
          gateway:
            routes:
              - id: get-yeshoucode #路由的ID
                uri:  http://localhost:7001/ #匹配后路由地址
                predicates:
                  - Cookie=name,yellowDuck
                  # - Header=X-User-Id,\d+
                  # - Host=**.yellowDuck.com
                  # - Method=GET,POST,DELETE
                  # - Path=/duck/**
                  # - Query=duck
                  # - RemoteAddr=127.0.0.1/24
      
    • Header:请求必须携带指定的Header才会匹配

      例如:请求Header必须带有 X-User-Id:001 才可以成功匹配到此路由,其中\d+为校验数字正则表达式

      请参考上例的代码

    • Host:请求必须携带指定的Host才能够匹配

      例如:请求必须携带指定的Host,即Host中要有.yellowDuck.com

      请参考上例的代码

    • Method:请求的方式必须是指定的才能够匹配

      例如:请求的方式必须是GET、POST、DELETE其中一种才会被匹配到

      请参考上例的代码

    • Path:当客户端请求的路径满足Path的规则的时候,进行路由转发操作。

      例如:请求的路径必须是localhost:7777/duck/开始的才会被匹配

      请参考上例的代码

    • Query :请求的必须携带有duck这个参数才能够匹配

      例如:在apipost之中以表单的形式中添加duck参数

      请参考上例的代码

    • RemoteAddr:请求的IP必须在指定的范围之中才能够被匹配到(允许访问某个地址)

      例如:请求的IP必须是127.0.0.1才能被匹配

      请参考上例的代码

    • Weight:根据权重跟配到该路由

      例如:百分之90的请求会请求到localhost:7001,百分之10的请求将会请求到localhost:7002

      #GateWay项目端口号
      server:
        port: 7777
      spring:
        cloud:
          gateway:
            routes:
              - id: get-yeshoucode #路由的ID
                uri:  http://localhost:7001/ #匹配后路由地址
                predicates:
                  - Path=/test/**
                  - Weight=group1,9
              - id: DUCK-CENTER #路由的ID
                uri: http://localhost:7002/ #匹配后路由地址
                predicates:
                  - Path=/test/**
                  - Weight=group2,1
      
    • RemoteAddr=IP - Address ,根据访问的IP地址来判断

      (以下配置示例表示只有与资源同在一个服务器才可能访问,可配合Path和Method对访问的路径和方式进行限制)

      Spring:
        cloud:
          gateway:
            globalcors:
              cors-configurations:
                '[/**]':
                  allowedOrigins: "*"
                  allowedMethods: "*"
                  # allowCredentials: true
                  allowedHeaders: "*"
            routes:
              - id: get-yeshoucide #路由的ID
                uri: http://localhost:7001/
                predicates:
                  #- Path=/test/**
                  - RemoteAddr=127.0.0.1/24  # 限定只有这个网段可以访问
                  #- Method=GET
      
    • 自定义断言:

      步骤

      1. 必须注意的是,配置predicates的时候

        在以下示例之中(请求必须携带参数name=leticiafeng才可以访问)

        Spring:
          cloud:
            gateway:
              globalcors:
                cors-configurations:
                  '[/**]':
                    allowedOrigins: "*"
                    allowedMethods: "*"
                    # allowCredentials: true
                    allowedHeaders: "*"
              routes:
                - id: sentinel-provider-demotion-gateway
                  uri: lb://sentinel-provider-demotion
                  order: 1
                  predicates:
                  # 需要注意的是,这里的Path也就是实际访问的URL之中绝对不能有-等特殊的字符,虽然在一般的Gateway和访问之中可以有,但是自定义断言之中似乎并不能有,否则就会出错
                    - Path=/providerDemotion/** 
                    - Name=leticiafeng
                  filters:
                    - StripPrefix=1
              discovery:
                locator:
                  enabled: true
                  route-id-prefix: gateway-
        
      2. 除此以外,还需要新建两个类才能实现自定义的断言功能

        1. 断言工厂类,需要以RoutePredicateFactory作为后缀、并且实现AbstractRoutePredicateFactory提供读取yaml配置的config类

          @Component
          public class NameRoutePredicateFactory extends AbstractRoutePredicateFactory<NameRoutePredicateConfig> {
              public NameRoutePredicateFactory() {
                  super(NameRoutePredicateConfig.class);
              }
              @Override
              public Predicate<ServerWebExchange> apply(NameRoutePredicateConfig config) {
                  return (serverWebExchange)->{
                      String name =
                              serverWebExchange.getRequest().getQueryParams().getFirst("name");
                      if (StringUtils.isEmpty(name)){
                          name = "";
                      }
                      return name.equals(config.getName());
                  };
              }
              @Override
              public List<String> shortcutFieldOrder() {
                  return Arrays.asList("name");
              }
          }
          
        2. 装载所需配置类的代码如下:

          @Repository
          public class NameRoutePredicateConfig implements Serializable {
              private static final long serialVersionUID = -2865859380407983075L;
              private String name;
              public String getName() {
                  return name;
              }
              public void setName(String name) {
                  this.name = name;
              }
              @Override
              public String toString() {
                  return "NameRoutePredicateConfig{" +
                          "name='" + name + '\'' +
                          '}';
              }
          }
          
      3. 在启动类上添加相关的配置如下:

        @SpringBootApplication
        @EnableDiscoveryClient
        public class SpringCloudAlibabaGatewayApplication {
            public static void main(String[] args) {
                System.setProperty("csp.sentinel.app.type", "1");
                SpringApplication.run(SpringCloudAlibabaGatewayApplication.class, args);
            }
        }
        
      4. 启动该Nacos、Sentinel、微服务提供方的项目、Gateway项目(按照例子中的代码,访问localhost:7777/providerDemotion/test/getSetting?name=leticiafeng) 以及 localhost:7777/providerDemotion/test/getSetting 即可通过对比得出自定义断言配置是否成功的判断。

filters

  • filters:网关过滤器,在过滤器中可以修改请求的参数和header信息,以及相应的结果和header信息,网关过滤器也是一个数组,可以配置多个过滤规则

    (该部分即可配置相关的局部、全局、内部等一系列的过滤器)

    • AddRequestParameter:对请求添加相关的参数

      配置该项之后,我们就可以通过配置该项,为相应匹配的请求添加相关的参数。示例之中添加了参数duck:yellow

    • Hystrix GatewayFilter:添加Hystrix的熔断和降级配置(这里不介绍,因为要直接用Sentinel)

    • RequestRateLimiter GatewayFilter:引入了Redis实现限流

      步骤

      1. 依赖引入

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
        </dependency>
        
      2. yml之中添加配置

        #GateWay项目端口号
        server:
          port: 7777
        spring:
          cloud:
            gateway:
              routes:
                - id: get-yeshoucode #路由的ID
                  uri:  http://localhost:7001/ #匹配后路由地址
                  filters:
                    - name: RequestRateLimiter
                      args:
                        #每秒允许处理的请求数量
                        redis-rate-limiter.replenishRate: 10 
                        #每秒最大处理的请求数量
                        redis-rate-limiter.burstCapacity: 20 
                        redis-rate-limiter.requestedTokens: 1
                  predicates:
                    - Method=GET,POST
        
    • 自定义局部过滤器:我们可以通过自定义局部过滤器的方式来实现灰度发布的一个效果。(给特定的用户给予特定的版本服务)

      具体步骤如下:

      1. 在Gateway的application.yml文件之中,对spring.cloud.gateway.routes节点下添加 - id: user-gateway下面进行如下配置。

        gateway:
              globalcors:
                cors-configurations:
                  '[/**]':
                    allowedOrigins: "*"
                    allowedMethods: "*"
                    # allowCredentials: true
                    allowedHeaders: "*"
              routes:
                - id: sentinel-provider-demotion-gateway
                  uri: lb://sentinel-provider-demotion
                  order: 1
                  predicates:
                    - Path=/providerDemotion/**
                    - Name=leticiafeng
                  filters:
                    - StripPrefix=1
                    - Grwayscale=true
              discovery:
                locator:
                  enabled: true
                  route-id-prefix: gateway-
        
      2. 在网关服务模块shop-gateway中新建filter包,在包下新建GreayscakeGatewayFilterConfig类,用于接收配置中的参数,如下代码所示

        public class NameCheckGatewayFilterConfig implements Serializable {
            private static final long serialVersionUID = 1821713903132837547L;
            private boolean nameCheck;
            public static long getSerialVersionUID() {
                return serialVersionUID;
            }
            public boolean isNameCheck() {
                return nameCheck;
            }
            public void setNameCheck(boolean nameCheck) {
                this.nameCheck = nameCheck;
            }
            @Override
            public String toString() {
                return "NameCheckGatewayFilterConfig{" +
                        "nameCheck=" + nameCheck +
                        '}';
            }
        }
        
      3. 在filter包下添加NameGatewayFilterFactory类,继承AbstractGatewayFilterFactory类,主类如果实现自定义类过滤器,模拟实现灰度发布。代码如下:

        @Component
        public class NameCheckGatewayFilterFactory extends AbstractGatewayFilterFactory<NameCheckGatewayFilterConfig> {
            public NameCheckGatewayFilterFactory() {
                super(NameCheckGatewayFilterConfig.class);
            }
            @Override
            public GatewayFilter apply(NameCheckGatewayFilterConfig config) {
                return (exchange, chain) -> {
                    if (config.isNameCheck()) {
                        System.out.println("开启了灰度发布功能······");
                    } else {
                        System.out.println("关闭了灰度发布功能······");
                    }
                    return chain.filter(exchange);
                };
            }
            @Override
            public List<String> shortcutFieldOrder() {
                return Arrays.asList("name");
            }
        }
        
      4. 启动Nacos、Sentinel、服务提供方和服务消费方,并且在浏览器之中输入相关的网址,即可访问相关的资源,在服务网关的控制台,我们便可以看到相关的灰度发布的相关信息打印。

    • 自定义全局过滤器:自定义全局过滤器则比较简单,只需要实现一个类即可实现全局拦截处理

      具体步骤如下:

      1. 在Gateway项目下新建一个类GlobalGatewayLogFilter,实现GlobalFilter和Ordered即可

        @Component
        public class GlobalGatewayLogFilter implements GlobalFilter, Ordered {
        
            /**
             * 开始访问时间
             */
            private static final String BEGIN_VISIT_TIME = "begin_visit_time";
        
            @Override
            public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain
                    chain) {
        //先记录下访问接口的开始时间
                exchange.getAttributes().put(BEGIN_VISIT_TIME,
                        System.currentTimeMillis());
                return chain.filter(exchange).then(Mono.fromRunnable(()->{
                    Long beginVisitTime = exchange.getAttribute(BEGIN_VISIT_TIME);
                    if (beginVisitTime != null){
                        System.out.println("访问接口主机: " +
                                exchange.getRequest().getURI().getHost());
                        System.out.println("访问接口端口: " +
                                exchange.getRequest().getURI().getPort());
                        System.out.println("访问接口URL: " +
                                exchange.getRequest().getURI().getPath());
                        System.out.println("访问接口URL参数: " +
                                exchange.getRequest().getURI().getRawQuery());
                        System.out.println("访问接口时长: " + (System.currentTimeMillis() -
                                beginVisitTime) + "ms");
                    }
                }));
            }
            @Override
            public int getOrder() {
                return 0;
            }
        
        }
        
      2. 访问相关的服务提供的具体地址,即可测试全局过滤器的执行效果,需要注意的是,自定义全局过滤器的优先级是比局部过滤器的优先级低的

        localhost:7777/providerDemotion/test/getSetting?name=leticiafeng

        uTools_1668348305134

Discovery

  • discovery:可以配置从注册中心动态创建路由的功能,利用微服务名进行路由

    • locator

      • enabled:可以配置从注册中心动态创建路由的功能,并利用微服务名进行路由

      • lower-case-service-id:开启默认serverID小写

      • route-id-prefix:生成流控规则API名称的前缀

引入Sentinel配置

基于route纬度限流

实现基于route纬度限流

步骤

  1. 依赖导入

    <dependencies>
        <!-- web服务器配置 -->
        <!--        <dependency>-->
        <!--            <groupId>org.springfxframework.boot</groupId>-->
        <!--            <artifactId>spring-boot-starter-web</artifactId>-->
        <!--        </dependency>-->
        <!-- SpringCloud ailibaba nacos-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <!-- Gateway配置 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <!--fegin组件-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <!-- Feign Client for loadBalancing -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-loadbalancer</artifactId>
            <version>3.1.1</version>
        </dependency>
        <!-- 配置Sentienl基本配置 -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.csp</groupId>
            <artifactId>sentinel-spring-cloud-gateway-adapter</artifactId>
        </dependency>
        <!--   Nacos配置中心配置 -->
        <!--        <dependency>-->
        <!--            <groupId>com.alibaba.cloud</groupId>-->
        <!--            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>-->
        <!--        </dependency>-->
        <!--        <dependency>-->
        <!--            <groupId>org.springframework.cloud</groupId>-->
        <!--            <artifactId>spring-cloud-starter-bootstrap</artifactId>-->
        <!--        </dependency>-->
    </dependencies>
    
  2. 在服务网关ship-gateway模块之中新建io.binghe.shop.config包,并在包下新建GatewayConfig类,基于Sentinel的Gateway限流是通过其提供的Filter来完成的,使用时只需要注入对应的SentinelGatewayFilter实例以及SentinelGatewayBlockExceptionHandler实例即可。

    GatewayConfig类的源代码如下所示。

    (自定义的返回结果,需要写一个配置类)当然如果不需要自定义返回结果,那么我们也可以在Sentinel之中配置来实现限流的效果,Sentinel的配置版,我们只需要参考正常的Sentinel使用即可。

    /**
     * @auther Leticia FENG
     * @since 2022/10/3016:13
     * @description 网关配置类
     **/
    @Configuration
    public class GatewayConfig {
    
        private final List<ViewResolver> viewResolvers;
    
        private final ServerCodecConfigurer serverCodecConfigurer;
    
        @Value("${spring.cloud.gateway.discovery.locator.route-id-prefix}")
        private String routeIdPrefix;
    
        public GatewayConfig(ObjectProvider<List<ViewResolver>> viewResolversProvider, ServerCodecConfigurer serverCodecConfigurer) {
            this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
            this.serverCodecConfigurer = serverCodecConfigurer;
        }
    
        /**
         * 初始化一个限流的过滤器
         * @return
         */
        @Bean
        @Order(Ordered.HIGHEST_PRECEDENCE)
        public GlobalFilter sentinelGatewayFilter() {
            return new SentinelGatewayFilter();
        }
    
        @PostConstruct
        public void init() {
            this.initGatewayRules();
            this.initBlockHandlers();
        }
    
        /**
         * 配置初始化的限流参数
         */
        private void initGatewayRules() {
            Set<GatewayFlowRule> rules = new HashSet<>();
    
            /**
             * Sentinel整合SpringCloud Gateway使用的API类型为Route ID类型,也就是基于route 纬度时,
             * 由于Sentinel为SpringCloud Gateway网关生成的API名称规则如下:
             * 生成的规则为:${spring.cloud.gateway.discovery.locator.route-id-prefix}
             * 后面直接加上目标微服务的名称,如下所示。
             * ${spring.cloud.gateway.discovery.locator.route-id-prefix}目标微服务的名称
             * 其中,${spring.cloud.gateway.discovery.locator.route-id-prefix}是在yml文件中配置的访问前缀
             * 
             * 为了让通过服务网关访问目标微服务链接后,请求链路中生成的API名称与流控规则中生成的API名称一致,以达到启动项目即可实现访问链接的限流效果,
             * 而无需登录Setinel管理界面手动配置限流规则,可以将resource参数设置为${spring.cloud.gateway.discovery.locator.route-id-prefix}目标微服务的名称
             * 
             * 当然,如果不按照上述配置,也可以在项目启动后,通过服务网关访问目标微服务链接后,在Sentinel管理界面的请求链路中找到对应的API名称所代表的请求链路,
             * 然后手动配置限流规则。
             ** /
             **/ 
            //用户微服务网关
    //        rules.add(this.getGatewayFlowRule("user-gateway"));
            //商品微服务网关
    //        rules.add(this.getGatewayFlowRule("product-gateway"));
            //订单微服务网关
    //        rules.add(this.getGatewayFlowRule("order-gateway"));
            //添加服务网关
            rules.add(this.getGatewayFlowRule(this.getResource("sentinel-provider-demotion")));
            //加载规则
            GatewayRuleManager.loadRules(rules);
        }
    
        private String getResource(String targetServiceName) {
            if (routeIdPrefix == null) {
                routeIdPrefix = "";
            }
            return routeIdPrefix.concat(targetServiceName);
        }
    
        private GatewayFlowRule getGatewayFlowRule(String resource) {
            // 传入资源名称生成GatewayFlowRule
            GatewayFlowRule gatewayFlowRule = new GatewayFlowRule(resource);
            // 限流阈值
            gatewayFlowRule.setCount(1);
            // 统计的时间窗口,单位为second
            gatewayFlowRule.setIntervalSec(1);
            return gatewayFlowRule;
        }
    
        /**
         * 配置限流的异常处理器
         * @return
         */
        @Bean
        @Order(Ordered.HIGHEST_PRECEDENCE)
        public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
            return new SentinelGatewayBlockExceptionHandler(viewResolvers,serverCodecConfigurer);
        }
    
        /**
         * 自定义限流异常返回界面
         */
        private void initBlockHandlers() {
            BlockRequestHandler blockRequestHandler = new BlockRequestHandler() {
                @Override
                public Mono<ServerResponse> handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) {
                    HashMap<Object, Object> map = new HashMap<>();
                    map.put("code",1001);
                    map.put("codeMsg","接口被限流了");
                    return ServerResponse.status(HttpStatus.OK)
                                         .contentType(MediaType.APPLICATION_JSON_UTF8)
                                         .body(BodyInserters.fromObject(map));
                }
            };
            GatewayCallbackManager.setBlockHandler(blockRequestHandler);
        }
    
    }
    
    
  3. 在启动类之中加上相关的配置

    @SpringBootApplication
    @EnableDiscoveryClient
    public class SpringCloudAlibabaGatewayApplication {
    	public static void main(String[] args) {
               System.setProperty("csp.sentinel.app.type", "1");
               SpringApplication.run(SpringCloudAlibabaGatewayApplication.class, args);
           }
    }
    
  4. yaml之中对配置文件进行配置

    server:
      port: 7777
    spring:
      application:
        name: server-gateway
      main:
        allow-bean-definition-overriding: true
      cloud:
        nacos:
          discovery:
            server-addr: localhost:8848
        sentinel:
          transport:
            port: 8719
            dashboard: localhost:8080
          web-context-unify: false
          eager: true
        gateway:
          globalcors:
            cors-configurations:
              '[/**]':
                allowedOrigins: "*"
                allowedMethods: "*"
                # allowCredentials: true
                allowedHeaders: "*"
          discovery:
            locator:
              enabled: true
              route-id-prefix: gateway-
    
  5. 在浏览器之中访问对应网关通过接口的资源路径即可:localhost:7777/sentinel-provider-demotion/test/getSetting

基于自定义API分组纬度限流

基于API分组

​ 当然在实际的开发环境之中,如果我们只做到前面所说的基于route的纬度限流,往往还不足以应对实际的大量基于Gateway转发请求的接口资源限流。这个时候我们就需要基于分组的方式来实现对接口资源的请求限流。

步骤

  1. 需要添加一个配置类(基本与基于routes的限流配置一致,但是还需要添加一个方法[initCustomizedApis() - 用于初始化API管理的信息]),配置类的基本信息如下:

    新增方法代码如下:

    /**
         * 初始化API管理的信息 (以组的形式进行限流的配置)
         */
        private void initCustomizedApis() {
            Set<ApiDefinition> definitions = new HashSet<>();
            // 以组形式进行的拦截
            ApiDefinition api1 = new ApiDefinition("sentinel-provider-demotion").setPredicateItems(new HashSet<ApiPredicateItem>() {{
                // 以/server-user/user/apil 开头的请求
                add(new ApiPathPredicateItem().setPattern("/sentinel-provider-demotion/test/**")
                        .setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX));
            }});
    //        // 以完整路径形式进行的拦截
    //        ApiDefinition api2 = new ApiDefinition("sentinel-provider-demotion-one")
    //                .setPredicateItems(new HashSet<ApiPredicateItem>() {{
    //                    // 以/server-user/user/api2/demo1 完全匹配的url路径
    //                    add(new ApiPathPredicateItem().setPattern("/sentinel-provider-demotion/test/getSetting"));
    //                }});
            definitions.add(api1);
    //        definitions.add(api2);
            GatewayApiDefinitionManager.loadApiDefinitions(definitions);
        }
    

    完整代码如下:

    /**
     * @auther Leticia FENG
     * @since 2022/10/30 16:13
     * @description 网关配置类
     **/
    @Configuration
    public class GatewayConfig {
    
        private final List<ViewResolver> viewResolvers;
    
        private final ServerCodecConfigurer serverCodecConfigurer;
    
        @Value("${spring.cloud.gateway.discovery.locator.route-id-prefix}")
        private String routeIdPrefix;
    
        public GatewayConfig(ObjectProvider<List<ViewResolver>> viewResolversProvider, ServerCodecConfigurer serverCodecConfigurer) {
            this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
            this.serverCodecConfigurer = serverCodecConfigurer;
        }
    
        /**
         * 初始化一个限流的过滤器
         *
         * @return
         */
        @Bean
        @Order(Ordered.HIGHEST_PRECEDENCE)
        public GlobalFilter sentinelGatewayFilter() {
            return new SentinelGatewayFilter();
        }
    
        @PostConstruct
        public void init() {
            // 单个资源限制
    //        this.initGatewayRules();
            this.initBlockHandlers();
            // 分组限制
            this.initCustomizedApis();
        }
    
        /**
         * 配置初始化的限流参数
         */
        private void initGatewayRules() {
            Set<GatewayFlowRule> rules = new HashSet<>();
    
            /**
             * Sentinel整合SpringCloud Gateway使用的API类型为Route ID类型,也就是基于route 纬度时,
             * 由于Sentinel为SpringCloud Gateway网关生成的API名称规则如下:
             * 生成的规则为:${spring.cloud.gateway.discovery.locator.route-id-prefix}
             * 后面直接加上目标微服务的名称,如下所示。
             * ${spring.cloud.gateway.discovery.locator.route-id-prefix}目标微服务的名称
             * 其中,${spring.cloud.gateway.discovery.locator.route-id-prefix}是在yml文件中配置的访问前缀
             *
             * 为了让通过服务网关访问目标微服务链接后,请求链路中生成的API名称与流控规则中生成的API名称一致,以达到启动项目即可实现访问链接的限流效果,
             * 而无需登录Setinel管理界面手动配置限流规则,可以将resource参数设置为${spring.cloud.gateway.discovery.locator.route-id-prefix}目标微服务的名称
             *
             * 当然,如果不按照上述配置,也可以在项目启动后,通过服务网关访问目标微服务链接后,在Sentinel管理界面的请求链路中找到对应的API名称所代表的请求链路,
             * 然后手动配置限流规则。
             ** /
             **/
            //用户微服务网关
    //        rules.add(this.getGatewayFlowRule("user-gateway"));
            //商品微服务网关
    //        rules.add(this.getGatewayFlowRule("product-gateway"));
            //订单微服务网关
    //        rules.add(this.getGatewayFlowRule("order-gateway"));
            //添加服务网关
            rules.add(this.getGatewayFlowRule(this.getResource("sentinel-provider-demotion")));
            //加载规则
            GatewayRuleManager.loadRules(rules);
        }
    
        private String getResource(String targetServiceName) {
            if (routeIdPrefix == null) {
                routeIdPrefix = "";
            }
            return routeIdPrefix.concat(targetServiceName);
        }
    
        private GatewayFlowRule getGatewayFlowRule(String resource) {
            // 传入资源名称生成GatewayFlowRule
            GatewayFlowRule gatewayFlowRule = new GatewayFlowRule(resource);
            // 限流阈值
            gatewayFlowRule.setCount(1);
            // 统计的时间窗口,单位为second
            gatewayFlowRule.setIntervalSec(1);
            return gatewayFlowRule;
        }
    
        /**
         * 配置限流的异常处理器
         *
         * @return
         */
        @Bean
        @Order(Ordered.HIGHEST_PRECEDENCE)
        public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
            return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
        }
    
        /**
         * 自定义限流异常返回界面
         */
        private void initBlockHandlers() {
            BlockRequestHandler blockRequestHandler = new BlockRequestHandler() {
                @Override
                public Mono<ServerResponse> handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) {
                    HashMap<Object, Object> map = new HashMap<>();
                    map.put("code", 1001);
                    map.put("codeMsg", "接口被限流了");
                    return ServerResponse.status(HttpStatus.OK)
                            .contentType(MediaType.APPLICATION_JSON_UTF8)
                            .body(BodyInserters.fromObject(map));
                }
            };
            GatewayCallbackManager.setBlockHandler(blockRequestHandler);
        }
    
        /**
         * 初始化API管理的信息 (以组的形式进行限流的配置)
         */
        private void initCustomizedApis() {
            Set<ApiDefinition> definitions = new HashSet<>();
            // 以组形式进行的拦截
    //        ApiDefinition api1 = new ApiDefinition("sentinel-provider-demotion").setPredicateItems(new HashSet<ApiPredicateItem>() {{
    //            // 以/server-user/user/apil 开头的请求
    //            add(new ApiPathPredicateItem().setPattern("/sentinel-provider-demotion/test/**")
    //                    .setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX));
    //        }});
            // 以完整路径形式进行的拦截
            ApiDefinition api2 = new ApiDefinition("sentinel-provider-demotion-one")
                    .setPredicateItems(new HashSet<ApiPredicateItem>() {{
                        // 以/server-user/user/api2/demo1 完全匹配的url路径
                        add(new ApiPathPredicateItem().setPattern("/sentinel-provider-demotion/test/getSetting"));
                    }});
    //        definitions.add(api1);
            definitions.add(api2);
            GatewayApiDefinitionManager.loadApiDefinitions(definitions);
        }
    }
    

    在上述的代码之中,配置了两个API分组,每一个API分组的规则如下。

    • user_api1分组:匹配以/product-serv/product/api1开头的所有请求

    • user_api2分组:精准匹配/server-user/user/api2/demo1

  2. 在服务网关shop-gateway模块中的io.binghe.shop.config.GatewayConfig配置类中的init()方法之中调用initCustonizedApis()方法,为了避免route纬度的限流对自定义API分组纬度的限流产生影响,这里,同时在init()方法中注释掉调用initGatewayRules()方法,修改后的init()方法的代码如下所示。

    @PostConstruct
    public void init() {
        // 单个资源限制
        // this.initGatewayRules();
        this.initBlockHandlers();
        // 分组限制
        this.initCustomizedApis();
    }
    
  3. 然后因为我们关闭了Route的规则添加,目前我们的限流已经在init方法之中已经关闭了,这导致了我们后续尝试添加的API分组限流还需要在Sentinel之中添加相关的配置才可以使用。

    uTools_1667312794011

  4. 添加完毕后,我们就可以在浏览器多次访问对应的URL测试资源的限流效果。

    uTools_1667313165992

配置链路追踪

背景(为什么需要链路追踪)

​ 在互联网业务快速发展的背景下,企业的业务系统也变得越来越复杂,不少的企业开始向分布式、微服务方向进行发展,将原本的单体应用拆分成了分布式、微服务。这使得当客户端请求系统的接口时,原本在同一个系 统内部的请求逻辑变成了需要在多个微服务之间流转的请求。

​ 在问题是在分布式的微服务场景之下,使用AOP技术来进行链路追踪是不能做到追踪各个微服务的调用情况的,也就无法知道系统中处理一次请求的整体调用链路。

​ 另外,在分布式与微服务场景下,我们需要解决如下问题:

  • 如何快速发现并定位到分布式系统中的问题
  • 如何尽可能的精确的判断故障对系统的影响范围与影响程度
  • 如何尽可能精确的梳理出服务之间的依赖关系,并判断出服务之间的依赖关系是否合理。
  • 如何尽可能精确的分析整个系统调用链路的性能与瓶颈点。
  • 如何尽可能精确的分析系统的存储瓶颈与容量规划。
  • 如何实时观测系统的整体调用链路情况。

​ 上述问题就是分布式链路追踪技术要解决的问题。所谓的分布式链路追踪,就是将对分布式系统的一次 请求转化成一个完整的调用链路。这个完整的调用链路从请求进入分布式系统的入口开始,直到整个请 求返回为止。并在请求调用微服务的过程中,记录相应的调用日志,监控系统调用的性能,并且可以按 照某种方式显示请求调用的情况。

​ 在分布式链路追踪中,可以统计调用每个微服务的耗时,请求会经过哪些微服务的流转,每个微服务的 运行状况等信息。

链路追踪的核心原理

​ 假定三个微服务调用的链路如下图所示:Service 1 调用 Service 2,Service 2 调用 Service 3 和 Service 4。

uTools_1668937516202

​ 那么链路追踪会在每个服务调用的时候加上 Trace ID 和 Span ID。如下图所示:

uTools_1668937530971

​ 相同的颜色代表的是同一个SpanId,说明是链路追踪之中的一个节点。

具体步骤可以简化为以下内容:

  • 第一步:用户端调用 Service 1,生成一个 Request,Trace ID 和 Span ID 为空,那个时候请求还 没有到 Service 1。
  • 第二步:请求到达 Service 1,记录了 Trace ID = X,Span ID 等于 A。
  • 第三步:Service 1 发送请求给 Service 2,Span ID 等于 B,被称作 Client Sent,即用户端发送一 个请求。
  • 第四步:请求到达 Service 2,Span ID 等于 B,Trace ID 不会改变,被称作 Server Received,即 服务端取得请求并准备开始解决它。
  • 第五步:Service 2 开始解决这个请求,解决完之后,Trace ID 不变,Span ID = C。
  • 第六步:Service 2 开始发送这个请求给 Service 3,Trace ID 不变,Span ID = D,被称作 Client Sent,即用户端发送一个请求。
  • 第七步:Service 3 接收到这个请求,Span ID = D,被称作 Server Received。
  • 第八步:Service 3 开始解决这个请求,解决完之后,Span ID = E。
  • 第九步:Service 3 开始发送响应给 Service 2,Span ID = D,被称作 Server Sent,即服务端发送 响应。
  • 第十步:Service 3 收到 Service 2 的响应,Span ID = D,被称作 Client Received,即用户端接收 响应。
  • 第十一步:Service 2 开始返回 响应给 Service 1,Span ID = B,和第三步的 Span ID 相同,被称 作 Client Received,即用户端接收响应。
  • 第十二步:Service 1 解决完响应,Span ID = A,和第二步的 Span ID 相同。
  • 第十三步:Service 1 开始向用户端返回响应,Span ID = A、 Service 3 向 Service 4 发送请求和 Service 3 相似,对应的 Span ID 是 F 和 G。可以参照上面前面 的第六步到第十步。

可以将上述的过程简化为以下的内容:

uTools_1668938147025

​ 简化完毕之后剩下的就是以下的步骤:

  • 第一个节点:Span ID = A,Parent ID = null,Service 1 接收到请求。
  • 第二个节点:Span ID = B,Parent ID= A,Service 1 发送请求到 Service 2 返回响应给Service 1 的过程。
  • 第三个节点:Span ID = C,Parent ID= B,Service 2 的 中间解决过程。
  • 第四个节点:Span ID = D,Parent ID= C,Service 2 发送请求到 Service 3 返回响应给Service 2 的过程。
  • 第五个节点:Span ID = E,Parent ID= D,Service 3 的中间解决过程。
  • 第六个节点:Span ID = F,Parent ID= C,Service 3 发送请求到 Service 4 返回响应给 Service 3 的过程。
  • 第七个节点:Span ID = G,Parent ID= F,Service 4 的中间解决过程。

​ 那么后续我们只需要通过ParentID就可以找到父节点了,便可进行跟踪追溯

目前线路追踪的基本技术有:Cat、ZipKin、Pinpoint、Skywalking、Sleuth。

技术说明
Cat由大众点评开源,基于Java开发的实时应用监控平台,包括实时应用监控,业务监 控 。 集成方案是通过代码埋点的方式来实现监控,比如: 拦截器,过滤器等。 对 代码的侵入性很大,集成成本较高。风险较大。
ZipKin由Twitter公司开源,开放源代码分布式的跟踪系统,用于收集服务的定时数据, 以解决微服务架构中的延迟问题,包括:数据的收集、存储、查找和展现。结合 spring-cloud-sleuth使用较为简单, 集成方便, 但是功能较简单。
PinpointPinpoint是一款开源的基于字节码注入的调用链分析,以及应用监控分析工具。特 点是支持多种插件, UI功能强大,接入端无代码侵入。
SkywalkingSkyWalking是国人开源的基于字节码注入的调用链分析,以及应用监控分析工 具。特点是支持多种插件, UI功能较强,接入端无代码侵入。
SleuthSleuth是SpringCloud中的一个组件,为Spring Cloud实现了分布式跟踪解决方案。

SpringCloudAlibaba配置Sleuth

​ Sleuth是SpringCloud日工的一个分布式链路追踪组件,在设计上大量参考并且借用了Google Dapper的设计

Span简介(记录基础组件)

​ Span在Sleuth之中代表着一组基本的工作单元,在请求与到达各个微服务的时候,Sleuth会通过唯一的标识也就是SpanId来标识开始通过这个微服务,在当前微服务之中执行的具体过程和执行结束,此时,通过SpanId标记的开始时间戳和结束时间戳就能够统计到当前Span的调用时间,也就是说当前微服务的执行时间,另外,也可以通过Span获取到事件的名称,请求的信息等数据。

(相当于记录当前服务部分被某次调用所涉及部分的记录器)

**总结:**远程调用和Span是一对一的关系,是分布式链路追踪中最基本的工作单元,每次发送一个远程调 用服务就会产生一个 Span。Span Id 是一个 64 位的唯一 ID,通过计算 Span 的开始和结束时间,就可 以统计每个服务调用所耗费的时间。

Trace简介(总记录)

​ Trace的粒度比Span的粒度大,Trace主要是由具有一组相同的Trace ID的Span组成的,从请求进入分布式系统入口经过调用各个微服务直到返回的整个过程,都是一个Trace。也就是说,当请求到达分布式系统的入口时,Sleuth会为请求创建一个唯一标识,这个唯一标识就是Trace Id,不管这个请求在分布式 系统中如何流转,也不管这个请求在分布式系统中调用了多少个微服务,这个Trace Id都是不变的,直到整个请求返回。

(相当于一个记录器,一直跟随着该请求调用直到请求结束)

**总结:**一个 Trace 可以对应多个 Span,Trace和Span是一对多的关系。Trace Id是一个64 位的唯一 ID。Trace Id可以将进入分布式系统入口经过调用各个微服务,再到返回的整个过程的请求串联起来, 内部每通过一次微服务时,都会生成一个新的SpanId。Trace串联了整个请求链路,而Span在请求链路 中区分了每个微服务。

Annotation简介(注解)

​ Annotation记录了一段时间内的事件,内部使用的重要注解如下所示。

  • cs (Client Send) 客户端发出请求,标记整个请求的开始时间
  • sr (Server Received) 服务端收到请求开始进行处理,通过sr与cs可以计算网络的延迟时间,例 如:sr- cs = 网络延迟(服务调用的时间)。
  • ss(Server Send)服务端处理完毕准备将结果返回给客户端, 通过ss与sr可以计算服务器上的请 求处理时间,例如:ss - sr = 服务器上的请求处理时间。
  • cr(Client Reveived)客户端收到服务端的响应,请求结束。通过cr与cs可以计算请求的总时间, 例如:cr - cs = 请求的总时间。

**总结:**链路追踪系统内部定义了少量核心注解,用来定义一个请求的开始和结束,通过这些注解,我们 可以计算出请求的每个阶段的时间。需要注解的是,这里说的请求,是在系统内部流转的请求,而不是 从浏览器、APP、H5、小程序等发出的请求。

Sleuth的配置和使用

Sleuth基本使用

​ Sleuth提供了分布式链路追踪能力,如果需要使用Sleuth的链路追踪功能,需要在项目之中继承Sleuth。

步骤如下

  1. 在每个微服务之中,都需要在pom.xml文件之中添加以下的Sleuth的依赖。

    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-sleuth</artifactId>
    </dependency>
    
  2. 将SpringCloud Alibaba微服务中的Gateway项目之中的yaml文件,配置如下(只添加Sentinel部分罢了)

    server:
      port: 7777
    spring:
      application:
        name: server-gateway
      main:
        allow-bean-definition-overriding: true
      cloud:
        nacos:
          discovery:
            server-addr: localhost:8848
        sentinel:
          transport:
            port: 8719
            dashboard: localhost:8080
          web-context-unify: false
          eager: true
    
        gateway:
          globalcors:
            cors-configurations:
              '[/**]':
                allowedOrigins: "*"
                allowedMethods: "*"
                # allowCredentials: true
                allowedHeaders: "*"
          routes:
            - id: sentinel-provider-demotion-gateway
              uri: lb://sentinel-provider-demotion
              order: 1
              predicates:
                - Path=/providerDemotion/**
                - Name=leticiafeng
              filters:
                - StripPrefix=1
                - NameCheck=true
          discovery:
            locator:
              enabled: true
              route-id-prefix: gateway-
    
  3. 接下来只需要依次启动provider - provider2 - consumer - gateway 即可在gateway的路径下访问其他微服务资源。在console控制台中,我们就可以看到相关的追踪信息

    uTools_1669041905643

  4. 该追踪信息为:

    [微服务名称,TraceID,SpanID,是否将结果输出到第三方平台]

Sleuth配置采样数据量

​ Sleuth可以采用如下的方式配置抽样采集数据的占比。(因为Sleuth的抽样采集进行链路追踪的请求效果的记录,这些的具体实现还是会对系统的性能上造成一定的负担,而如果所有的请求都要进行追踪记录势必会导致系统负荷过高,所以这个时候就需要我们对数据进行抽样采集记录,以这种方式来保证数据的正常使用)

spring:
  sleuth:
	sampler:
	  probability: 1.0    # 标识占比为 100%,默认值为 0.1 表示只收集其中百分之十

追踪自定义的线程池

​ Sleuth支持对异步任务的链路追踪,在项目之中使用@Async注解开启一个异步任务之后,Sleuth会为异步任务重新生成一个Span。但是如果使用了自定义的异步任务线程池,则会导致Sleuth无法新建一个Span,而是会重新生成Trace和Span。此时,需要使用Sleuth提供的LazyTraceExecutor类来包装下异步任务线程池,才能在异步任务调用链路中重新创建Span。

步骤
  1. 在服务中开启异步线程池任务,需要使用@EnableAsync。所以,在演示示例前,先在提供服务的启动类上添加注解@EnableAsync。所以,在演示实例之前,先在服务提供方的启动类上添加@EnableAsync注解

    @SpringBootApplication
    @EnableDiscoveryClient
    @EnableAsync
    public class SentinelProviderDemotionApplication {
        public static void main(String[] args) {
            SpringApplication.run(SentinelProviderDemotionApplication.class, args);
        }
    }
    
  2. 后续就是@Async注解使用的基本方式,首先声明一个方法业务层实现的方法,然后基于业务层的方法上添加 @Async 注解,实现方法的并发处理方式

    void asyncMethod();
    

    在Impl包下,声明对应的相关类,对抽象的业务层抽象类方法进行实现

    @Async
    @Override
    public void asyncMethod() {
    	log.info("执行了异步任务...");
    }
    
  3. 在用户微服务的提供方的Controller之中新建 asyncApi() 方法,如下图所示。

    @GetMapping("/async/api")
    public String asyncApi () {
        System.out.println("异步任务开始");
        providerService.asyncMethod();
        System.out.println("异步任务结束");
        return "asyncApi";
    }
    
  4. 分别启动两个提供服务的微服务程序,并且在浏览器之中输入相对应的访问链接。

    访问后,可以看到TraceID未发生改变,但是spanId发生了改变

    uTools_1669539013187

追踪包装自定义线程池

​ 也就是演示对自定义任务线程池的基础上通过使用创建线程池的方式来执行任务,我们所做就是为了验证Sleuth是否为包装后的自定义线程池新创建了Span。

(1) 在服务的提供方中创建 异步线程池相关配置类,并且在类之中注入BeanFactory,并在getAsyncExecutor() 方法之中使用 org.springframework.cloud.sleuth.instrument.async.LazyTraceExecutor()来包装返回异步任务线程池,修改后的 ThreadPoolTaskExecutorConfig 类的代码如下所示。

/**
*  将@Async声明的多线程执行方法的,执行线程池转为自定义生成的线程池。
* @version 1.0.0
* @description Sleuth异步线程池配置
*/
@Configuration
@EnableAutoConfiguration
public class ThreadPoolTaskExecutorConfig extends AsyncConfigurerSupport {
    @Autowired
    private BeanFactory beanFactory;
    
    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(2);
        executor.setMaxPoolSize(5);
		executor.setQueueCapacity(10);
        executor.setThreadNamePrefix("trace-thread-");
        executor.initialize();
        return new LazyTraceExecutor(this.beanFactory, executor);
	}
}

(2) 分别启动用户微服务和网关服务,在浏览器之中输入链接,即可实现对其的一个调用,在控制台之中,我们便可以看到sleuth同样的为其提供了相应的新Span作为此刻的服务调用的追踪记录。

uTools_1669548756015

​ 可以看到,当我们使用自定义声明的线程池来实现的时候,只要通过LazyTraceExecutor的方式来进行线程的提供,是可以实现多线程调用的时候产生新的Span的。

综上所述: Sleuth支持对异步任务的链路追踪,在项目中使用@Async注解开启一个异步任务后, Sleuth会为异步任务重新生成一个Span。但是如果使用了自定义的异步任务线程池,则会导致Sleuth 无法新创建一个Span,而是会重新生成Trace和Span。此时,需要使用Sleuth提供的 LazyTraceExecutor类来包装下异步任务线程池,才能在异步任务调用链路中重新创建Span。

追踪自定义链路过滤器

​ TracingFilter在Sleuth之中可以负责处理请求和响应的组件,可以通过注册自定义的TracingFilter实例来实现一些扩展性的需求。需要注意的是链路过滤器的配置,需要通过动态资源即是通过HTTP又或者是HTTPS的方式才可以成功的被检测出来。(该过滤器的配置是将traceId放入到HtttpRequest之中,在服务端进行获取,并在响应结果之中添加自定义的Header,名称为SLEUTH-HEADER,值为traceId)

步骤

(1) 在提供服务的项目之中新建一个filter包,并且创建MyGenericFilter类,继承GenericFilterBean类,代码如下所示。

@Component
@Order ( Ordered.HIGHEST_PRECEDENCE + 6 )
public class MyGenericFilter extends GenericFilterBean {

    private Pattern skipPattern = Pattern.compile(SleuthWebProperties.DEFAULT_SKIP_PATTERN);

    private final Tracer tracer;

    public MyGenericFilter(Tracer tracer) {
        this.tracer = tracer;
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
                         FilterChain chain) throws IOException, ServletException {
        if (!(request instanceof HttpServletRequest) || !(response instanceof
                HttpServletResponse)){
            throw new ServletException("只支持HTTP访问");
        }
        Span currentSpan = this.tracer.currentSpan();
        if (currentSpan == null) {
            chain.doFilter(request, response);
            return;
        }
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        HttpServletResponse httpServletResponse = ((HttpServletResponse)
                response);
        boolean skipFlag =
                skipPattern.matcher(httpServletRequest.getRequestURI()).matches();
        if (!skipFlag){
            String traceId = currentSpan.context().traceId();
            httpServletRequest.setAttribute("traceId", traceId);
            httpServletResponse.addHeader("SLEUTH-HEADER", traceId);
        }
        chain.doFilter(httpServletRequest, httpServletResponse);
    }
}

(2) 在提供服务的项目的Controller之中新建一个sleuthFilter()方法,在sleuthFilter()方法之中获取并且打印traceId。

@GetMapping(value = "/sleuth/filter/api")
public String sleuthFilter(HttpServletRequest request) {
    Object traceIdObj = request.getAttribute("traceId");
    String traceId = traceIdObj == null ? "" : traceIdObj.toString();
    log.info("获取到的traceId为: " + traceId);
    return "sleuthFilter";
}

(3) 分别按照顺序,启动服务提供方、消费方、网关,再访问对应的URL,即可查看配置的效果

Sleuth整合Zipkin实现链路追踪+可视化展示

​ Sleuth实现链路追踪为我们提供了日志记录了,但问题是,我们仅仅通过日志来处理问题往往也是不太实际的,当我们的系统包含的微服务越来越多的时候,通过日志来进行分析处理,往往就变得很麻烦,反而实际上是一种不太可行的方法,因此,又衍生出了整合Zipkin的方式,来实现链路日志的聚合和可视化展示。并且整合 Zipkin 之后,还可以支持全文检索。

Zipkin核心架构

概述

​ Zipkin是一种分布式链路跟踪系统,能够收集微服务运行过程中的实时调用链路信息,并能够将这些调 用链路信息展示到Web界面上供开发人员分析,开发人员能够从ZipKin中分析出调用链路中的性能瓶 颈,识别出存在问题的应用程序,进而定位问题和解决问题。

uTools_1670123420542

其中,ZipKin核心组件的功能如下所示。

  • Reporter: ZipKin之中上报链路数据的模块,主要配置在具体的微服务应用之中。
  • Transport:ZipKin之中传输数据的模块,此模块的具体可以配置为ElasticSearch、Cassandra或者Mysql,目前ZipKin支持此三种数据持久化的方式
  • Storage:ZipKin之中存储链路数据的模块,此模块的具体可以配置为ElasticSearch、Cassandra或者MYSQL,目前ZipKin支持这三种数据持久化的方式
  • API:ZipKin之中的API组件,主要用来提供外部访问接口。比如给客户端展示跟踪信息,或是开放给外部系统实现监控等。
  • UI:ZipKin之中的UI组件,基于API组件实现的一个上层应用。通过UI组件用户可以方便并且很直观的查询和分析跟踪信息。

​ ZipKin在总体上会分为两个端,第一个是ZipKin服务端,一个则是ZipKin客户端,客户端主要是配置在微服务应用之中的,收集微服务之中的调用链路信息,将数据发送给ZipKin服务端。

使用ZipKin服务端步骤

(1) 下载ZipKin服务端Jar文件,可以直接在浏览器之中输入如下的链接进行下载,Windows版本当然只需要去官网GitHub下载Jar即可,当然也可以选定所需要的版本

https://search.maven.org/remote_content?g=io.zipkin.java&a=zipkinserver&v=LATEST&c=exec 

如果是Linux系统,当然也可以通过命令进行下载启动ZipKin服务端

wget https://search.maven.org/remote_content?g=io.zipkin.java&a=zipkin-server&v=LATEST&c=exec

(2) 在命令行之中输入以下的命令即可启动ZipKin

java -jar zipkin-server-2.12.9-exec.jar

(3) 在ZipKin成功启动之后,只需要直接对ZipKin进行访问即可实现启动的页面进行访问

http://localhost:9411

项目整合ZipKin客户端

(1) 在每个微服务之中都添加ZipKin的相关以来,如下图所示

(需要注意,使用SpringCloudAlibaba2021.0.1.0,SpringCloud2021.0.1)的时候,需要我们同时引用以下部分的依赖才能保证能够正常使用Zipkin和Sleuth

Sentinel-Provider以及Sentinel-Consumer子模块需要以下的配置项

<!-- 链路追踪 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>

<!-- 使用zipkin,就包含了sleuth -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-sleuth-zipkin</artifactId>
</dependency>

Gateway子模块需要以下的配置项 [只引入Sleuth-Zipkin包才行,如果在引入starter-sleuth就会报错] (如果Gateway引入两个依赖的话,会导致后续启动的时候产生报错信息)

<!-- 使用zipkin,就包含了sleuth -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-sleuth-zipkin</artifactId>
</dependency>

Sentinel-Provider以及Sentinel-Consumer完整的Pom.xml文件

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.leticiafeng</groupId>
    <artifactId>sentinel-consumer-denotion</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>sentinel-consumer-denotion</name>
    <description>Sentinel-Consumer-Denotion</description>

    <parent>
        <groupId>com.leticiafeng</groupId>
        <artifactId>LF-SpringCloud-Alibaba</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.cloud</groupId>
                    <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
            <version>3.1.1</version>
        </dependency>

        <!-- SpringCloud ailibaba sentinel-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-bootstrap</artifactId>
        </dependency>

        <dependency>
            <groupId>com.alibaba.csp</groupId>
            <artifactId>sentinel-parameter-flow-control</artifactId>
            <version>1.8.4</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>

        <dependency>
            <groupId>com.alibaba.csp</groupId>
            <artifactId>sentinel-datasource-nacos</artifactId>
        </dependency>
        
        <!-- 链路追踪 需要注意的是Provider、Consumer如果配置了Sleuth追踪自定义线程、追踪过滤链的必须要添加以下两个依赖才可以保证正常使用-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-sleuth</artifactId>
        </dependency>

        <!-- 使用zipkin,就包含了sleuth -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-sleuth-zipkin</artifactId>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

(2) 在网关服务提供服务的微服务项目中的Gateway的微服务的application.yml文件添加以下的配置

spring:
  sleuth:
    sampler:
      probability: 1.0
  zipkin:
    base-url: http://localhost:9411
    discovery-client-enabled: false

配置信息解释:

  • spring.sleuth.sampler.probability:表示Sleuth的采样百分比。
  • spring.zipkin.base-url:ZipKin服务端的地址。
  • spring.zipkin.discovery-client-enabled:配置成false,使Nacos将其当成一个URL,不要按服务名处理。

(3) 通过Gateway访问相关的URL即可让Sleuth产生相关的日志,并且将相关的记录交给Zipkin采样,并且记录相关的调用记录。

访问URL:

localhost:7777/sentinel-consumer-demotion/test/getSetting?name=leticiafeng

调用记录截图

uTools_1670338772042

配置Zipkin数据信息持久化到Mysql

​ 到目前为止,我们便算是成功集成了Zipkin,但是问题是目前我们所配置的Zipkin的相关数据是保存在了内存之中的,这样就导致如果我们重启了Zipkin所有的记录信息都会丢失,而这是我们不想看到的。所以我们需要将Zipkin的相关数据采用持久化的方式保存到数据库之中。

​ 想要实现该效果我们需要使用脚本来将Zipkin的数据持久化到Mysql,而官方为此提供了相关的脚本文件。(其实就是SQL文件帮助我们建立数据库的文件罢了)

配置步骤
  1. 在Github项目上下载相关的脚本文件下来 https://github.com/openzipkin/zipkin/tree/master/zipkin-storage 里面进行下载

  2. 然后,打开navicat,在对应的Mysql服务之中打开查询,创建相关的数据库,并且通过查询创建数据库

    create database if not exists zipkin
    
  3. 最后一步,在查询之中,将sql脚本文件的内容进行执行,即可在数据库之中创建Zipkin数据持久化所需要使用的表格信息。

    uTools_1670423542274

  4. 启动Zipkin时,指定Mysql数据源,如下图所示。

    java -jar zipkin-server-2.12.9-exec.jar --STORAGE_TYPE=mysql --MYSQL_HOST=127.0.0.1 --MYSQL_TCP_PORT=3306 --MYSQL_DB=zipkin --MYSQL_USER=root --MYSQL_PASS=root
    
  5. 启动Zipkin之后,在浏览器之中访问微服务所提供的资源即可发现我们的调用信息已经被存入到数据库之中。

    访问localhost:7777/sentinel-consumer-demotion/test/getSetting

    // 需要注意一点,之前我们在gateway上配置了对于provider的网关拦截策略,这里拦截策略失效的原因也很简单,因为我们访问的实际源是consumer,不再是provider了,因此绕开了gateway的拦截器,直接访问到了provider的实际本体,所以之类不需要?name=leticiafeng一样可以访问到实际的资源。

    zipkin_annotations表信息如下

    uTools_1670507245352

​ zipkin_spans表信息如下

uTools_1670507596287