SpringCloudAlibaba从入门到实战
搬运来源:黑马程序员
第一章 微服务介绍
1.1 系统架构演变
随着互联网的发展,网站的规模也在不断地扩大,进而导致系统架构也在不断地变化。
从互联网早期到现在,系统架构大体经历了一下几个过程:单体应用架构 → 垂直应用架构 → 分布式架构 → SOA架构 → 微服务架构,当然还有悄然兴起的Service Mesh(服务网格)。
接下来我们都来了解一下每种架构都是什么样子的,以及各自的优缺点。
1.1.1 单体应用架构
互联网早期,一般网站的应用流量较小,只需一个应用,将所有功能代码都部署在一起就可以,这样可以减少开发、部署和运维的成本。
比如一个电商系统,里面会包含很多用户管理、商品管理、订单管理、物流管理等等很多的模块,我们会把它做成一个web项目,然后部署在一台服务器上。
优点:
- 项目架构简单,小型项目开发成本低
- 项目部署在一个节点上,维护方便
缺点:
- 全部集成在一个工程中,对于大型项目来讲不宜开发和维护
- 项目模块之间紧密耦合,单点容错率低
- 无法针对不同模块进行优化和水平扩展
1.1.2 垂直应用架构
随着访问量的逐渐增大,单一应用只能依靠增加节点来应对,但是这时候就会发现并不是所有模块都有比较大的访问量。
还是以以上的电商为例,用户访问量的增加可能只会影响用户和订单模块,但是对消息模块的影响就比较小,那么我们只希望增加几个订单模块,而不增加消息模块,此时的单体应用就做不到了,垂直应用就应运而生。
所谓的垂直应用架构,就是将原来的一个应用拆分为几个互不相关的应用,以提升效率。比如我们将以上的电商单体架构拆分为:
- 电商系统(用户管理、商品管理、订单管理)
- 后台系统(用户管理、订单管理、客户管理)
- CMS系统(广告管理、营销管理)
这样一拆分之后,一旦用户访问量变大,我们只需要增加电商系统的节点即可,无需添加后台系统和CMS系统的节点。
优点:
- 系统拆分实现了流量分担,解决了并发问题,而且可以针对不同模块进行优化和水平扩展
- 一个系统的问题不会影响其他系统,提高容错率
缺点:
- 系统之间相互独立,无法进行相互调用
- 系统之间相互独立,会有重复的开发任务
1.1.3 分布式架构
当垂直应用越来越多,重复的业务代码就会越来越多。这时候,我们就思考可不可以将重复的代码 抽取出来,做成统一的业务层作为独立的服务,然后由前端控制层调用不同的业务层服务呢?
这就产生了新的分布式系统架构。它将把工程拆分成表现层和服务层两个部分,服务层中包含业务 逻辑。表现层只需要处理和页面的交互,业务逻辑都是调用服务层的服务来实现。
优点:
- 抽取公共的功能为服务,提高代码的复用性
缺点:
- 系统间的耦合度变高,调用关系错综复杂,难以维护
1.1.4 SOA架构
在分布式架构下,当服务越来越多,容量的评估,小服务资源的浪费等问题逐渐显现,此时需增加 一个调度中心对集群进行实时管理。此时,用于资源调度和治理中心(SOA Service Oriented Architecture,面向服务的架构)是关键。
优点:
- 使用注册中心解决了服务间调用关系的自动调节
缺点:
- 服务间会有依赖关系,一旦某个环节出错会影响较大( 服务雪崩 )
- 服务关系复杂,运维、测试部署困难
1.1.5 微服务架构
微服务架构在某种程度上是面向服务的架构SOA继续发展的下一步,它更加强调服务的"彻底拆分"。
优点:
- 服务原子化拆分,独立打包、部署和升级,保证每个微服务清晰的任务划分,利于扩展
- 微服务之间采用Restful等轻量级http协议相互调用
缺点:
- 分布式系统开发的技术成本高(容错、分布式事务等)
1.2 微服务架构的介绍
微服务架构, 简单的说就是将单体应用进一步拆分,拆分成更小的服务,每个服务都是一个可以独 立运行的项目。
1.2.1 微服务架构的常见问题
一旦采用微服务系统架构,就势必会遇到这样几个问题:
- 这么多小服务,如何管理他们?(服务治理 注册中心[服务注册 发现 剔除])
- 这么多小服务,他们之间如何通讯?(restful rpc)
- 这么多小服务,客户端怎么访问他们?(网关)
- 这么多小服务,一旦出现问题了,应该如何自处理?(容错、降级)
- 这么多小服务,一旦出现问题了,应该如何排错? (链路追踪)
对于上面的问题,是任何一个微服务设计者都不能绕过去的,因此大部分的微服务产品都针对每一 个问题提供了相应的组件来解决它们。
1.2.2 微服务架构的常见概念
1.2.2.1 服务治理
服务治理就是进行服务的自动化管理,其核心是服务的自动注册与发现。
**服务注册:**服务实例将自身服务信息注册到注册中心。
**服务发现:**服务实例通过注册中心,获取到注册到其中的服务实例的信息,通过这些信息去请求它们提供的服务。
**服务剔除:**服务注册中心将出问题的服务自动剔除到可用列表之外,使其不会被调用到。
1.2.2.2 服务调用
在微服务架构中,通常存在多个服务之间的远程调用的需求。目前主流的远程调用技术有基于 HTTP的RESTful接口以及基于TCP的RPC协议。
-
REST(Representational State Transfer)
这是一种HTTP调用的格式,更标准,更通用,无论哪种语言都支持http协议
-
RPC(Remote Promote Call)
一种进程间通信方式。允许像调用本地服务一样调用远程服务。RPC框架的主要目标就是让远程服务调用更简单、透明。RPC框架负责屏蔽底层的传输方式、序列化方式和通信细节。开发人员在使用的时候只需要了解谁在什么位置提供了什么样的远程服务接口即可,并不需要关心底层通信细节和调用过程。
区别和联系
比较项 | RESTful | RPC |
---|---|---|
通讯协议 | HTTP | 一般使用TCP |
性能 | 略低 | 较高 |
灵活度 | 高 | 低 |
应用 | 微服务架构 | SOA架构 |
1.2.2.3 服务网关
随着微服务的不断增多,不同的微服务一般会有不同的网络地址,而外部客户端可能需要调用多个 服务的接口才能完成一个业务需求,如果让客户端直接与各个微服务通信可能出现:
- 客户端需要调用不同的url地址,增加难度
- 在一定的场景下,存在跨域请求的问题
- 每个微服务都需要进行单独的身份认证
针对这些问题,API网关顺势而生。
API网关直面意思是将所有API调用统一接入到API网关层,由网关层统一接入和输出。一个网关的 基本功能有:统一接入、安全防护、协议适配、流量管控、长短链接支持、容错能力。有了网关之后, 各个API服务提供团队可以专注于自己的的业务逻辑处理,而API网关更专注于安全、流量、路由等问 题。
1.2.2.4 服务容错
在微服务当中,一个请求经常会涉及到调用几个服务,如果其中某个服务不可用,没有做服务容错 的话,极有可能会造成一连串的服务不可用,这就是雪崩效应。
我们没法预防雪崩效应的发生,只能尽可能去做好容错。服务容错的三个核心思想是:
- 不被外界环境影响
- 不被上游请求压垮
- 不被下游响应拖垮
1.2.2.4 链路追踪
随着微服务架构的流行,服务按照不同的维度进行拆分,一次请求往往需要涉及到多个服务。互联 网应用构建在不同的软件模块集上,这些软件模块,有可能是由不同的团队开发、可能使用不同的编程 语言来实现、有可能布在了几千台服务器,横跨多个不同的数据中心。因此,就需要对一次请求涉及的 多个服务链路进行日志记录,性能监控即链路追踪
1.2.3 微服务架构的常见解决方案
1.2.3.1 ServiceComb
Apache ServiceComb,前身是华为云的微服务引擎 CSE (Cloud Service Engine) 云服务,是全球 首个Apache微服务顶级项目。它提供了一站式的微服务开源解决方案,致力于帮助企业、用户和开发 者将企业应用轻松微服务化上云,并实现对微服务应用的高效运维管理。
1.2.3.2 SpringCloud
Spring Cloud是一系列框架的集合。它利用Spring Boot的开发便利性巧妙地简化了分布式系统基 础设施的开发,如服务发现注册、配置中心、消息总线、负载均衡、断路器、数据监控等,都可以用 Spring Boot的开发风格做到一键启动和部署。
Spring Cloud并没有重复制造轮子,它只是将目前各家公司开发的比较成熟、经得起实际考验的服 务框架组合起来,通过Spring Boot风格进行再封装屏蔽掉了复杂的配置和实现原理,最终给开发者留 出了一套简单易懂、易部署和易维护的分布式系统开发工具包。
1.2.3.3 SpringCloud Alibaba
Spring Cloud Alibaba 致力于提供微服务开发的一站式解决方案。此项目包含开发分布式应用微服 务的必需组件,方便开发者通过 Spring Cloud 编程模型轻松使用这些组件来开发分布式应用服务。
1.3 SpringCloud Alibaba介绍
Spring Cloud Alibaba 致力于提供微服务开发的一站式解决方案。此项目包含开发分布式应用微服 务的必需组件,方便开发者通过 Spring Cloud 编程模型轻松使用这些组件来开发分布式应用服务。
依托 Spring Cloud Alibaba,您只需要添加一些注解和少量配置,就可以将 Spring Cloud 应用接 入阿里微服务解决方案,通过阿里中间件来迅速搭建分布式应用系统。
1.3.1 主要功能
- 服务限流降级: 默认支持 WebServlet、WebFlux, OpenFeign、RestTemplate、Spring Cloud Gateway, Zuul, Dubbo 和 RocketMQ 限流降级功能的接入,可以在运行时通过控制台实时修 改限流降级规则,还支持查看限流降级 Metrics 监控。
- **服务注册与发现:**适配 Spring Cloud 服务注册与发现标准,默认集成了 Ribbon 的支持。
- **分布式配置管理:**支持分布式系统中的外部化配置,配置更改时自动刷新。
- **消息驱动能力:**基于 Spring Cloud Stream 为微服务应用构建消息驱动能力。
- **分布式事务:**使用 @GlobalTransactional 注解, 高效并且对业务零侵入地解决分布式事务问题。
- **阿里云对象存储:**阿里云提供的海量、安全、低成本、高可靠的云存储服务。支持在任何应用、任 何时间、任何地点存储和访问任意类型的数据。
- **分布式任务调度:**提供秒级、精准、高可靠、高可用的定时(基于 Cron 表达式)任务调度服务。 同时提供分布式的任务执行模型,如网格任务。网格任务支持海量子任务均匀分配到所有 Worker(schedulerx-client)上执行。
- **阿里云短信服务:**覆盖全球的短信服务,友好、高效、智能的互联化通讯能力,帮助企业迅速搭建 客户触达通道。
1.3.2 组件
- **Sentinel:**把流量作为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳 定性。
- **Nacos:**一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。
- **RocketMQ:**一款开源的分布式消息系统,基于高可用分布式集群技术,提供低延时的、高可靠 的消息发布与订阅服务。
- **Dubbo:**Apache Dubbo™ 是一款高性能 Java RPC 框架。
- **Seata:**阿里巴巴开源产品,一个易于使用的高性能微服务分布式事务解决方案。
- **Alibaba Cloud ACM:**一款在分布式架构环境中对应用配置进行集中管理和推送的应用配置中心 产品。
- Alibaba Cloud OSS: 阿里云对象存储服务(Object Storage Service,简称 OSS),是阿里云提 供的海量、安全、低成本、高可靠的云存储服务。您可以在任何应用、任何时间、任何地点存储和 访问任意类型的数据。
- Alibaba Cloud SchedulerX: 阿里中间件团队开发的一款分布式任务调度产品,提供秒级、精 准、高可靠、高可用的定时(基于 Cron 表达式)任务调度服务。
- Alibaba Cloud SMS: 覆盖全球的短信服务,友好、高效、智能的互联化通讯能力,帮助企业迅速搭建客户触达通道。
第二章 微服务环境搭建
我们本次是使用的电商项目中的商品、订单、用户为案例进行讲解。以下项目如果启动报错先将nacos部分去掉即可
2.1 案例准备
2.1.1 技术选型
- maven:3.3.9
- 数据库:MySQL 5.7
- 持久层: SpingData Jpa
- 其他: SpringCloud Alibaba 技术栈
2.1.2 模块设计
-
springcloud-alibaba 父工程
-
shop-common 公共模块【实体类】
-
shop-user 用户微服务 【端口: 807x】
-
shop-product 商品微服务 【端口: 808x】
-
shop-order 订单微服务 【端口: 809x】
2.2 创建父工程
创建一个maven工程,然后在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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<packaging>pom</packaging>
<modules>
<module>shop-common</module>
<module>shop-user</module>
<module>shop-product</module>
<module>shop-order</module>
<module>api-gateway</module>
</modules>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.2.RELEASE</version>
</parent>
<groupId>cn.apotato</groupId>
<artifactId>springcloud-alibaba</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF8</project.reporting.outputEncoding>
<spring-cloud.version>Hoxton.SR9</spring-cloud.version>
<spring-cloud-alibaba.version>2.2.6.RELEASE</spring-cloud-alibaba.version>
<hutool.version>5.7.22</hutool.version>
<inclusion.version>1.0.1-RELEASE</inclusion.version>
</properties>
<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>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
</dependency>
<!-- 统一返回处理工具-->
<dependency>
<groupId>cn.apotato</groupId>
<artifactId>inclusion</artifactId>
<version>${inclusion.version}</version>
</dependency>
<!-- swagger 3-->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<!--在引用时请在maven中央仓库搜索3.X最新版本号-->
<version>3.0.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.github.xiaoymin/knife4j-spring-ui -->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-ui</artifactId>
<version>3.0.3</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.22</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.79</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.27</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>
2.3 创建基础模块
- 创建 shop-common 模块,在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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>springcloud-alibaba</artifactId>
<groupId>cn.apotato</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>shop-common</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>
</project>
- 创建实体类
@Entity(name = "shop_order")
@Data
public class Order implements Serializable {
/**
* 订单id
*/
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long oid;
/**
* 用户id
*/
private Integer uid;
/**
* 用户名
*/
private String username;
/**
* 商品id
*/
private Integer pid;
/**
* 商品名称
*/
private String productName;
/**
* 商品单价
*/
private Double productPrice;
/**
* 购买数量
*/
private Integer number;
}
/**
* 商品
*/
@Entity(name = "shop_product")
@Data
public class Product implements Serializable {
/**
* 主键
*/
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer pid;
/**
* 商品名称
*/
private String productName;
/**
* 商品价格
*/
private Double productPrice;
/**
* 库存
*/
private Integer stock;
}
/**
* 用户
*/
@Entity(name = "shop_user")
@Data
public class User implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer uid;
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 手机号
*/
private String telephone;
}
2.4 创建用户微服务
步骤:
- 创建模块 导入依赖
- 创建SpringBoot主类
- 加入配置文件
- 创建必要的接口和实现类(controller service dao)
新建一个 shop-user 模块,然后进行下面操作
- 创建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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>springcloud-alibaba</artifactId>
<groupId>cn.apotato</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>shop-user</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!--nacos客户端
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>-->
<!-- 实体类-->
<dependency>
<groupId>cn.apotato</groupId>
<artifactId>shop-common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<!-- 统一返回处理工具-->
<dependency>
<groupId>cn.apotato</groupId>
<artifactId>inclusion</artifactId>
</dependency>
<!-- swagger 3-->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/com.github.xiaoymin/knife4j-spring-ui -->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-ui</artifactId>
</dependency>
</dependencies>
</project>
- 编写主类
@EnableInclusion
@SpringBootApplication
public class UserApplication {
public static void main(String[] args) {
SpringApplication.run(UserApplication.class, args);
}
}
- 创建配置文件
server:
port: 8071
spring:
application:
name: service-user
mvc:
pathmatch:
matching-strategy: ant_path_matcher
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://${DB_SERVER:xxxx}:${DB_PORT:3306}/shop?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
username: ${DB_USER_NAME:root}
password: ${DB_PASSWORD:root}
jpa:
properties:
hibernate:
hbm2ddl:
auto: update
dialect: org.hibernate.dialect.MySQL5InnoDBDialect
show-sql: true
jackson:
serialization:
fail-on-empty-beans: false
springfox:
documentation:
enabled: true #是否启用swagger,默认:true
swagger:
title: service-user API文档 #标题
description: #描述
version: v1.0 #版本
contact:
name: 一颗小土豆 #维护人
email: 2426712259@qq.com
# base-package:swagger扫描的基础包,默认:全扫描
# base-path:需要处理的基础URL规则,默认:/**
# exclude-path:需要排除的URL规则,默认:空
# host:文档的host信息,默认:空
# globalOperationParameters[0]:
# name:参数名
# description:描述信息
# modelRef:指定参数类型
# parameterType:指定参数存放位置,可选header,query,path,body.form
# required:指定参数是否必传,true,false
2.5 创建商品微服务
- 创建一个名为 shop_product 的模块,并添加springboot依赖
<?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">
<parent>
<artifactId>springcloud-alibaba</artifactId>
<groupId>cn.apotato</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>shop-product</artifactId>
<dependencies>
<!-- spring-web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--nacos客户端
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>-->
<!-- 实体类-->
<dependency>
<groupId>cn.apotato</groupId>
<artifactId>shop-common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<!-- 统一返回处理工具-->
<dependency>
<groupId>cn.apotato</groupId>
<artifactId>inclusion</artifactId>
</dependency>
<!-- Flyway 对数据库进行版本控制-->
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>
<!-- swagger 3-->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/com.github.xiaoymin/knife4j-spring-ui -->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-ui</artifactId>
</dependency>
</dependencies>
</project>
- 创建工程的主类
@EnableOpenApi
@EnableInclusion
@SpringBootApplication
public class ProductApplication {
public static void main(String[] args) {
SpringApplication.run(ProductApplication.class, args);
}
}
- 创建配置文件application.yml
server:
port: 8081
spring:
application:
name: service-product
mvc:
pathmatch:
matching-strategy: ant_path_matcher
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://${DB_SERVER:xxxx}:${DB_PORT:3306}/shop?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
username: ${DB_USER_NAME:root}
password: ${DB_PASSWORD:root}
jpa:
properties:
hibernate:
hbm2ddl:
auto: update
dialect: org.hibernate.dialect.MySQL5InnoDBDialect
show-sql: true
jackson:
serialization:
fail-on-empty-beans: false
# 数据库版本控制
flyway:
# 是否启用flyway
enabled: true
# 执行时标记的tag 默认为<<Flyway Baseline>>
baseline-description: <<Flyway Baseline>>
# 检测迁移脚本的路径是否存在,如不存在,则抛出异常
check-location: true
# 脚本位置
locations: classpath:db/migration
# 在迁移时,是否校验脚本,假设V1.0__初始.sql已经迁移过了,在下次启动时会校验该脚本是否有变更过,则抛出异常
validate-on-migrate: false
#若启动环境中的的数据库中有数据,但是没有flyway的历史表时,
#false--启动会报错,true--继续生成flyway历史表完成脚本
baseline-on-migrate: true
# 指定 baseline 的版本号,默认值为 1, 低于该版本号的 SQL 文件, migrate 时会被忽略
baseline-version: 20220404
springfox:
documentation:
enabled: true #是否启用swagger,默认:true
swagger:
title: service-product API文档 #标题
description: #描述
version: v1.0 #版本
contact:
name: 一颗小土豆 #维护人
email: 2426712259@qq.com
# base-package:swagger扫描的基础包,默认:全扫描
# base-path:需要处理的基础URL规则,默认:/**
# exclude-path:需要排除的URL规则,默认:空
# host:文档的host信息,默认:空
# globalOperationParameters[0]:
# name:参数名
# description:描述信息
# modelRef:指定参数类型
# parameterType:指定参数存放位置,可选header,query,path,body.form
# required:指定参数是否必传,true,false
- 创建ProductDao接口
public interface ProductDao extends JpaRepository<Product, Integer> {
}
- 创建ProductService接口和实现类
@AllArgsConstructor
@Service
public class ProductServiceImpl implements ProductService {
private final ProductDao productDao;
@Override
public Product findByPid(Integer pid) {
return productDao.getOne(pid);
}
}
- 创建Controller
@Slf4j
@RequestMapping("product")
@RestController
public class ProductController {
@Resource
private ProductService productService;
@Inclusion
@GetMapping("/{pid}")
public Product product(@PathVariable("pid") Integer pid) {
Product product = productService.findByPid(pid);
log.info("查询到商品:" + JSON.toJSONString(product));
return product;
}
}
- 通过swagger访问服务
http://localhost:8081/doc.html
2.6 创建订单微服务
- 创建一个名为 shop-order 的模块,并添加springboot依赖
<?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">
<parent>
<artifactId>springcloud-alibaba</artifactId>
<groupId>cn.apotato</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>shop-order</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--nacos客户端
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>-->
<!-- 实体类-->
<dependency>
<groupId>cn.apotato</groupId>
<artifactId>shop-common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<!-- 统一返回处理工具-->
<dependency>
<groupId>cn.apotato</groupId>
<artifactId>inclusion</artifactId>
</dependency>
<!-- swagger 3-->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/com.github.xiaoymin/knife4j-spring-ui -->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-ui</artifactId>
</dependency>
</dependencies>
</project>
- 创建工程的主类
@EnableOpenApi
@EnableInclusion
@SpringBootApplication
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}
- 创建配置文件application.yml
server:
port: 8091
spring:
application:
name: service-order
mvc:
pathmatch:
matching-strategy: ant_path_matcher
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://${DB_SERVER:xxx}:${DB_PORT:3306}/shop?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
username: ${DB_USER_NAME:root}
password: ${DB_PASSWORD:root}
jpa:
properties:
hibernate:
hbm2ddl:
auto: update
dialect: org.hibernate.dialect.MySQL5InnoDBDialect
show-sql: true
jackson:
serialization:
fail-on-empty-beans: false
springfox:
documentation:
enabled: true #是否启用swagger,默认:true
swagger:
title: service-order API文档 #标题
description: #描述
version: v1.0 #版本
contact:
name: 一颗小土豆 #维护人
email: 2426712259@qq.com
- 创建OrderDao接口
public interface OrderDao extends JpaRepository<Order, Integer> {
}
- 创建OrderService接口和实现类
@AllArgsConstructor
@Service
public class OrderServiceImpl implements OrderService {
private final OrderDao orderDao;
@Override
public Order save(Order order) {
return orderDao.saveAndFlush(order);
}
}
- 创建RestTemplate
@Configuration
public class SpringBeanConfig {
/**
* Ribbon实现负载均衡,默认轮询
* @return RestTemplate
*/
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
- 创建Controller
@Slf4j
@AllArgsConstructor
@RequestMapping("order")
@RestController
public class OrderController {
private final OrderService orderService;
private final RestTemplate restTemplate;
/**
* 准备买1件商品
* 商品购买
* @param pid
* @return
*/
@Inclusion
@GetMapping("/prod/{pid}")
public Order order(@PathVariable("pid") Integer pid) {
log.info(">>客户下单,这时候要调用商品微服务查询商品信息");
String url = "localhost:8081";
log.info(">>微服务地址为:" + url);
//通过restTemplate调用商品微服务
Product product = restTemplate.getForObject(
"http://" + url + "/product/" + pid, Product.class);
log.info(">>商品信息,查询结果:" + JSON.toJSONString(product));
Order order = new Order();
order.setUid(1);
order.setUsername("测试用户");
order.setPid(product.getPid());
order.setProductName(product.getProductName());
order.setProductPrice(product.getProductPrice());
order.setNumber(1);
return orderService.save(order);
}
}
第三章 Nacos Discovery 服务治理
3.1 服务治理介绍
3.1.1 先来思考一个问题
通过上一章的操作,我们已经可以实现微服务之间的调用。但是我们把服务提供者的网络地址 (ip,端口)等硬编码到了代码中,这种做法存在许多问题:
- 一旦服务提供者地址变化,就需要手工修改代码
- 一旦是多个服务提供者,无法实现负载均衡功能
- 一旦服务变得越来越多,人工维护调用关系困难
那么应该怎么解决呢, 这时候就需要通过注册中心动态的实现服务治理
3.1.2 什么是服务治理
服务治理是微服务架构中最核心最基本的模块。用于实现各个微服务的自动化注册与发现。
-
服务注册:在服务治理框架中,都会构建一个注册中心,每个服务单元向注册中心登记自己提供服 务的详细信息。并在注册中心形成一张服务的清单,服务注册中心需要以心跳的方式去监测清单中 的服务是否可用,如果不可用,需要在服务清单中剔除不可用的服务。
-
服务发现:服务调用方向服务注册中心咨询服务,并获取所有服务的实例清单,实现对具体服务实 例的访问。
通过上面的调用图会发现,除了微服务,还有一个组件是服务注册中心,它是微服务架构非常重要 的一个组件,在微服务架构里主要起到了协调者的一个作用。注册中心一般包含如下几个功能:
- 服务发现:
- 服务注册:保存服务提供者和服务调用者的信息
- 服务订阅:服务调用者订阅服务提供者的信息,注册中心向订阅者推送提供者的信息
- 服务配置:
- 配置订阅:服务提供者和服务调用者订阅微服务相关的配置
- 配置下发:主动将配置推送给服务提供者和服务调用者
- 服务健康检测
- 检测服务提供者的健康情况,如果发现异常,执行服务剔除
- 服务发现:
3.1.3 常见的注册中心
-
zookeeper
zookeeper是一个分布式服务框架,是Apache Hadoop 的一个子项目,它主要是用来解决分布式 应用中经常遇到的一些数据管理问题,如:统一命名服务、状态同步服务、集群管理、分布式应用 配置项的管理等。
-
Eureka
Eureka是Springcloud Netflix中的重要组件,主要作用就是做服务注册和发现。但是现在已经闭 源
-
Consul
Consul是基于GO语言开发的开源工具,主要面向分布式,服务化的系统提供服务注册、服务发现 和配置管理的功能。Consul的功能都很实用,其中包括:服务注册/发现、健康检查、Key/Value 存储、多数据中心和分布式一致性保证等特性。Consul本身只是一个二进制的可执行文件,所以 安装和部署都非常简单,只需要从官网下载后,在执行对应的启动脚本即可
-
Nacos
Nacos是一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。它是 Spring Cloud Alibaba 组件之一,负责服务注册发现和服务配置,可以这样认为nacos=eureka+config。
3.2 Nacos简介
Nacos 致力于帮助您发现、配置和管理微服务。Nacos 提供了一组简单易用的特性集,帮助您快速 实现动态服务发现、服务配置、服务元数据及流量管理。
从上面的介绍就可以看出,nacos的作用就是一个注册中心,用来管理注册上来的各个微服务。
3.3 Nacos入门实战
接下来,我们就在现有的环境中加入nacos,并将我们的两个微服务注册上去。
3.3.1 搭建Nacos环境
第1步:安装nacos
下载地址: https://github.com/alibaba/nacos/releases
下载zip格式的安装包,然后进行解压缩操作
第2步: 启动nacos
#切换目录
cd nacos/bin
#命令启动
startup.cmd -m standalone
第3步: 访问nacos
打开浏览器输入http://localhost:8848/nacos,即可访问服务, 默认密码是nacos/nacos
3.3.2 将商品服务注册到nacos
接下来开始修改 shop-product 模块的代码, 将其注册到nacos服务上
- 在pom.xml中添加nacos的依赖
<!--nacos客户端-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
- 在主类上添加@EnableDiscoveryClient注解
@EnableDiscoveryClient //开启NacosClient
@EnableInclusion
@SpringBootApplication
public class ProductApplication {
public static void main(String[] args) {
SpringApplication.run(ProductApplication.class, args);
}
}
- 在application.yml中添加nacos服务的地址
spring:
cloud:
nacos:
discovery:
server-addr: localhost:8848
- 修改OrderController, 实现微服务调用
private final DiscoveryClient discoveryClient;
/**
* 准备买1件商品
* 商品购买
* @param pid
* @return
*/
@Inclusion
@GetMapping("/prod/{pid}")
public Order order(@PathVariable("pid") Integer pid) {
log.info(">>客户下单,这时候要调用商品微服务查询商品信息");
//从nacos中获取服务地址
List<ServiceInstance> instances = discoveryClient.getInstances("service-product");
// 随机获取服务实例下标
int size = new Random().nextInt(instances.size());
ServiceInstance serviceInstance = instances.get(size);
String url = serviceInstance.getHost() +":"+ serviceInstance.getPort();
log.info(">>从nacos中获取到的微服务地址为:" + url);
//通过restTemplate调用商品微服务
Message message = restTemplate.getForObject(
"http://" + url + "/product/" + pid, Message.class);
Product product = message.getDataToBean(Product.class);
log.info(">>商品信息,查询结果:" + JSON.toJSONString(product));
Order order = new Order();
order.setUid(1);
order.setUsername("测试用户");
order.setPid(product.getPid());
order.setProductName(product.getProductName());
order.setProductPrice(product.getProductPrice());
order.setNumber(1);
return orderService.save(order);
}
DiscoveryClient是专门负责服务注册和发现的,我们可以通过它获取到注册到注册中心的所有服务
- 启动服务,观察nacos的控制面板中是否有注册上来的订单微服务,然后通过访问消费者服务验证调 用是否成功
3.4 实现服务调用的负载均衡
3.4.1 什么是负载均衡
通俗的讲, 负载均衡就是将负载(工作任务,访问请求)进行分摊到多个操作单元(服务器,组件)上进行执行。
根据负载均衡发生位置的不同,一般分为服务端负载均衡和客户端负载均衡。
服务端负载均衡指的是发生在服务提供者一方,比如常见的nginx负载均衡
而客户端负载均衡指的是发生在服务请求的一方,也就是在发送请求之前已经选好了由哪个实例处理请求。
我们在微服务调用关系中一般会选择客户端负载均衡,也就是在服务调用的一方来决定服务由哪个提供 者执行。
3.4.2 自定义实现负载均衡
-
通过idea再启动一个 shop-product 微服务,设置其端口为8082
-
通过nacos查看微服务的启动情况
-
修改 shop-order 的代码,实现负载均衡
/**
* 准备买1件商品
* 商品购买
* @param pid
* @return
*/
@Inclusion
@GetMapping("/prod/{pid}")
public Order order(@PathVariable("pid") Integer pid) {
log.info(">>客户下单,这时候要调用商品微服务查询商品信息");
//从nacos中获取服务地址
List<ServiceInstance> instances = discoveryClient.getInstances("service-product");
// 随机获取服务实例下标
int size = new Random().nextInt(instances.size());
ServiceInstance serviceInstance = instances.get(size);
String url = serviceInstance.getHost() +":"+ serviceInstance.getPort();
log.info(">>从nacos中获取到的微服务地址为:" + url);
//通过restTemplate调用商品微服务
Message message = restTemplate.getForObject(
"http://" + url + "/product/" + pid, Message.class);
Product product = message.getDataToBean(Product.class);
log.info(">>商品信息,查询结果:" + JSON.toJSONString(product));
Order order = new Order();
order.setUid(1);
order.setUsername("测试用户");
order.setPid(product.getPid());
order.setProductName(product.getProductName());
order.setProductPrice(product.getProductPrice());
order.setNumber(1);
return orderService.save(order);
}
- 启动两个服务提供者和一个服务消费者,多访问几次消费者测试效果
3.4.3 基于Ribbon实现负载均衡
Ribbon是Spring Cloud的一个组件, 它可以让我们使用一个注解就能轻松的搞定负载均衡
3.4.3.1 Ribbon支持的负载均衡策略
Ribbon内置了多种负载均衡策略,内部负载均衡的顶级接口为 com.netflix.loadbalancer.IRule , 具体的负载策略如下图所示:
策略名 | 策略描述 | 实现说明 |
---|---|---|
BestAvailableRule | 选择一个最小的并发 请求的server | 逐个考察Server,如果Server被 tripped了,则忽略,在选择其中 ActiveRequestsCount最小的server |
AvailabilityFilteringRule | 过滤掉那些因为一直 连接失败的被标记为 circuit tripped的后 端server,并过滤掉 那些高并发的的后端 server(active connections 超过配 置的阈值) | 使用一个AvailabilityPredicate来包含 过滤server的逻辑,其实就就是检查 status里记录的各个server的运行状 态 |
WeightedResponseTimeRule | 根据相应时间分配一 个weight,相应时 间越长,weight越 小,被选中的可能性 越低。 | 一个后台线程定期的从status里面读 取评价响应时间,为每个server计算 一个weight。Weight的计算也比较简 单responsetime 减去每个server自己 平均的responsetime是server的权 重。当刚开始运行,没有形成statas 时,使用roubine策略选择server。 |
RetryRule | 对选定的负载均衡策 略机上重试机制。 | 在一个配置时间段内当选择server不 成功,则一直尝试使用subRule的方 式选择一个可用的server |
RoundRobinRule | 轮询方式轮询选择 server | 轮询index,选择index对应位置的 server |
RandomRule | 随机选择一个server | 在index上随机,选择index对应位置 的server |
ZoneAvoidanceRule | 复合判断server所在 区域的性能和server 的可用性选择server | 使用ZoneAvoidancePredicate和 AvailabilityPredicate来判断是否选择 某个server,前一个判断判定一个 zone的运行性能是否可用,剔除不可 用的zone(的所有server), AvailabilityPredicate用于过滤掉连接 数过多的Server。 |
我们可以通过修改配置来调整Ribbon的负载均衡策略,具体代码如下
service-product: # 调用的提供者的名称
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
3.4.3.2 项目改造
- 在RestTemplate 的生成方法上添加
@LoadBalanced
注解
@Configuration
public class SpringBeanConfig {
/**
* Ribbon实现负载均衡,默认轮询
* @return RestTemplate
*/
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
- 修改服务调用的方法
/**
* 准备买1件商品
* 通过nacos+@LoadBalanced来实现,使用服务名称进行负载的服务调用
* @param pid
* @return
*/
@Inclusion
@GetMapping("/prod/{pid}")
public Order order2(@PathVariable("pid") Integer pid) {
log.info(">>客户下单,这时候要调用商品微服务查询商品信息");
//直接使用微服务名字, 从nacos中获取服务地址
String url = "service-product";
log.info(">>从nacos中获取到的微服务地址为:" + url);
//通过restTemplate调用商品微服务
Message message = restTemplate.getForObject(
"http://" + url + "/product/" + pid, Message.class);
Assert.notNull(message, "微服务调用失败,message=null");
Product product = message.getDataToBean(Product.class);
log.info(">>商品信息,查询结果:" + JSON.toJSONString(product));
Order order = new Order();
order.setUid(1);
order.setUsername("测试用户");
order.setPid(product.getPid());
order.setProductName(product.getProductName());
order.setProductPrice(product.getProductPrice());
order.setNumber(1);
return orderService.save(order);
}
3.5 基于Feign实现服务调用
3.5.1 什么是Feign
Feign是Spring Cloud提供的一个声明式的伪Http客户端, 它使得调用远程服务就像调用本地服务 一样简单, 只需要创建一个接口并添加一个注解即可。
Nacos很好的兼容了Feign, Feign默认集成了 Ribbon, 所以在Nacos下使用Fegin默认就实现了负 载均衡的效果。
3.5.2 Feign的使用
- 加入Fegin的依赖
<!--fegin组件-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
- 在主类上添加Fegin的注解
@EnableFeignClients //开启openfeign
@EnableDiscoveryClient //开启nacos
@EnableOpenApi
@EnableInclusion
@SpringBootApplication
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}
- 创建一个client, 并使用Fegin实现微服务调用
@FeignClient(value = "service-product")
public interface ProductClient {
@GetMapping("/product/{pid}")
Message findById(@PathVariable("pid") Integer pid);
}
- 修改controller代码,并启动验证
@Slf4j
@AllArgsConstructor
@RequestMapping("order")
@RestController
public class OrderController {
private final OrderService orderService;
private final ProductClient productClient;
/**
* 准备买1件商品
* 通过openfeign实现像调用本地方法一样,来调用服务
* @param pid
* @return Order
*/
@Inclusion
@GetMapping("/prod/{pid}")
public Order order3(@PathVariable("pid") Integer pid) {
log.info(">>客户下单,这时候要调用商品微服务查询商品信息");
Message message = productClient.findById(pid);
Product product = message.getDataToBean(Product.class);
log.info(">>商品信息,查询结果:" + JSON.toJSONString(product));
Order order = new Order();
order.setUid(1);
order.setUsername("测试用户");
order.setPid(product.getPid());
order.setProductName(product.getProductName());
order.setProductPrice(product.getProductPrice());
order.setNumber(1);
return orderService.save(order);
}
}
第四章 Sentinel 服务容错
4.1 高并发带来的问题
在微服务架构中,我们将业务拆分成一个个的服务,服务与服务之间可以相互调用,但是由于网络 原因或者自身的原因,服务并不能保证服务的100%可用,如果单个服务出现问题,调用这个服务就会 出现网络延迟,此时若有大量的网络涌入,会形成任务堆积,最终导致服务瘫痪。
接下来,我们来模拟一个高并发的场景
- 编写java代码
@Slf4j
@AllArgsConstructor
@RequestMapping("order2")
@RestController
public class OrderController2 {
private final OrderService orderService;
private final ProductClient productClient;
/**
* 准备买1件商品
* 商品购买
* @param pid
* @return
*/
@Inclusion
@GetMapping("/prod/{pid}")
public Order order(@PathVariable("pid") Integer pid) {
log.info(">>客户下单,这时候要调用商品微服务查询商品信息");
Message message = productClient.findById(pid);
Product product = message.getDataToBean(Product.class);
log.info(">>商品信息,查询结果:" + JSON.toJSONString(product));
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Order order = new Order();
order.setUid(1);
order.setUsername("测试用户");
order.setPid(product.getPid());
order.setProductName(product.getProductName());
order.setProductPrice(product.getProductPrice());
order.setNumber(1);
return orderService.save(order);
}
@GetMapping("test")
public String test(){
return "接口测试";
}
}
- 修改配置文件中tomcat的并发数
server:
port: 8091
tomcat:
max-threads: 10 #tomcat的最大并发值修改为10,默认是200
- 接下来使用压测工具,对请求进行压力测试
下载地址: https://jmeter.apache.org/
-
第一步:修改配置,并启动软件
进入bin目录,修改jmeter.properties文件中的语言支持为language=zh_CN,然后点击jmeter.bat 启动软件。
-
第二步:添加线程组
-
第三步:配置线程并发数
-
第四步:添加Http取样
- 访问test方法观察效果
结论: 此时会发现, 由于order方法囤积了大量请求, 导致test方法的访问出现了问题,这就是服务雪崩的雏形。
4.2 服务雪崩效应
在分布式系统中,由于网络原因或自身的原因,服务一般无法保证 100% 可用。如果一个服务出现了问题,调用这个服务就会出现线程阻塞的情况,此时若有大量的请求涌入,就会出现多条线程阻塞等 待,进而导致服务瘫痪。
由于服务与服务之间的依赖性,故障会传播,会对整个微服务系统造成灾难性的严重后果,这就是服务故障的 “雪崩效应” 。
雪崩发生的原因多种多样,有不合理的容量设计,或者是高并发下某一个方法响应变慢,亦或是某 台机器的资源耗尽。我们无法完全杜绝雪崩源头的发生,只有做好足够的容错,保证在一个服务发生问 题,不会影响到其它服务的正常运行。也就是"雪落而不雪崩"。
4.3 常见容错方案
要防止雪崩的扩散,我们就要做好服务的容错,容错说白了就是保护自己不被猪队友拖垮的一些措 施, 下面介绍常见的服务容错思路和组件。
常见的容错思路
常见的容错思路有隔离、超时、限流、熔断、降级这几种,下面分别介绍一下。
-
隔离
它是指将系统按照一定的原则划分为若干个服务模块,各个模块之间相对独立,无强依赖。当有故障发生时,能将问题和影响隔离在某个模块内部,而不扩散风险,不波及其它模块,不影响整体的系统服务。常见的隔离方式有:线程池隔离和信号量隔离.
- 线程池隔离:为每一个服务接口单独开辟一个线程池,保持与其他服务接口线程的隔离,提高该服务接口的独立性和高可用。
- 信号量隔离:可以把信号量理解成一个计数器 , 对这个计数器规定一个计数上限, 代表一个接口被访问的最大量。假定设置 付款接口的信号量最大值为10,(这个接口最多占用线程池中10个线程) 初始值为0. 每调用一次接口信号量加一 , 接口处理完后信号量减一. 当信号量值达到最大时 , (10时) , 对后续的调用请求拒接处理.
-
超时
在上游服务调用下游服务的时候,设置一个最大响应时间,如果超过这个时间,下游未作出反应, 就断开请求,释放掉线程。
-
限流
限流就是限制系统的输入和输出流量已达到保护系统的目的。为了保证系统的稳固运行,一旦达到 的需要限制的阈值,就需要限制流量并采取少量措施以完成限制流量的目的。
-
熔断
在互联网系统中,当下游服务因访问压力过大而响应变慢或失败,上游服务为了保护系统整 体的可用性,可以暂时切断对下游服务的调用。这种牺牲局部,保全整体的措施就叫做熔断。
服务熔断一般有三种状态:
- 熔断关闭状态(Closed) 服务没有故障时,熔断器所处的状态,对调用方的调用不做任何限制
- 熔断开启状态(Open) 后续对该服务接口的调用不再经过网络,直接执行本地的fallback方法
- 半熔断状态(Half-Open) 尝试恢复服务调用,允许有限的流量调用该服务,并监控调用成功率。如果成功率达到预 期,则说明服务已恢复,进入熔断关闭状态;如果成功率仍旧很低,则重新进入熔断关闭状 态。
-
降级
降级其实就是为服务提供一个托底方案,一旦服务无法正常调用,就使用托底方案。
常见的容错组件
-
Hystrix
Hystrix是由Netflix开源的一个延迟和容错库,用于隔离访问远程系统、服务或者第三方库,防止 级联失败,从而提升系统的可用性与容错性。
-
Resilience4J
Resilicence4J一款非常轻量、简单,并且文档非常清晰、丰富的熔断工具,这也是Hystrix官方推 荐的替代产品。不仅如此,Resilicence4j还原生支持Spring Boot 1.x/2.x,而且监控也支持和 prometheus等多款主流产品进行整合。
-
Sentinel
Sentinel 是阿里巴巴开源的一款断路器实现,本身在阿里内部已经被大规模采用,非常稳定。
下面是三个组件在各方面的对比:
对比功能 Sentinel Hystrix resilience4j 隔离策略 信号量隔离(并发线程数限流) 线程池隔离/信号量隔离 信号量隔离 熔断降级策略 基于响应时间、异常比率、异常数 基于异常比率 基于异常比率、响应时间 实时统计实现 滑动窗口(LeapArray) 滑动窗口(基 于 RxJava) Ring Bit Buffer 动态规则配置 支持多种数据源 支持多种数据 源 有限支持 扩展性 多个扩展点 插件的形式 接口的形式 基于注解的支持 支持 支持 支持 限流 基于 QPS,支持基于调用关系的限流 有限的支持 Rate Limiter 流量整形 支持预热模式、匀速器模式、预热排队 模式 不支持 简单的 Rate Limiter 模式 系统自适应保护 支持 不支持 不支持 控制台 提供开箱即用的控制台,可配置规则、 查看秒级监控、机器发现等 简单的监控查看 不提供控制台,可对 接其它监控系统 -
4.4 Sentinel入门
4.4.1 什么是Sentinel
Sentinel (分布式系统的流量防卫兵) 是阿里开源的一套用于服务容错的综合性解决方案。它以流量 为切入点, 从流量控制、熔断降级、系统负载保护等多个维度来保护服务的稳定性。
Sentinel 具有以下特征:
- **丰富的应用场景:**Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景, 例如秒杀(即 突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。
- **完备的实时监控:**Sentinel 提供了实时的监控功能。通过控制台可以看到接入应用的单台机器秒 级数据, 甚至 500 台以下规模的集群的汇总运行情况。
- **广泛的开源生态:**Sentinel 提供开箱即用的与其它开源框架/库的整合模块, 例如与 Spring Cloud、Dubbo、gRPC 的整合。只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。
- **完善的 SPI 扩展点:**Sentinel 提供简单易用、完善的 SPI 扩展接口。您可以通过实现扩展接口来快 速地定制逻辑。例如定制规则管理、适配动态数据源等。
Sentinel 分为两个部分:
- 核心库(Java 客户端)不依赖任何框架/库,能够运行于所有 Java 运行时环境,同时对 Dubbo / Spring Cloud 等框架也有较好的支持。
- 控制台(Dashboard)基于 Spring Boot 开发,打包后可以直接运行,不需要额外的 Tomcat 等 应用容器。
4.4.2 微服务集成Sentinel
为微服务集成Sentinel非常简单, 只需要加入Sentinel的依赖即可
- 在pom.xml中加入下面依赖
<!-- sentinel 流量卫士-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
- 编写一个Controller测试使用
@RequestMapping("order3")
@RestController
public class OrderController3 {
@RequestMapping("/message1")
public String message1() {
return "message1";
}
@RequestMapping("/message2")
public String message2() {
return "message2";
}
}
4.4.3 安装Sentinel控制台
Sentinel 提供一个轻量级的控制台, 它提供机器发现、单机资源实时监控以及规则管理等功能。
- 下载jar包,解压到文件夹 https://github.com/alibaba/Sentinel/releases
- 启动控制台
# 直接使用jar命令启动项目(控制台本身是一个SpringBoot项目)
java -Dserver.port=8080 -Dcsp.sentinel.dashboard.server=localhost:8080 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard-1.7.0.jar
- 修改 shop-order ,在里面加入有关控制台的配置
spring:
cloud:
sentinel:
transport:
port: 9999 #跟控制台交流的端口,随意指定一个未使用的端口即可
dashboard: localhost:8080 # 指定控制台服务的地址
- 通过浏览器访问localhost:8080 进入控制台 ( 默认用户名密码是 sentinel/sentinel )
补充:了解控制台的使用原理
Sentinel的控制台其实就是一个SpringBoot编写的程序。我们需要将我们的微服务程序注册到控制台上, 即在微服务中指定控制台的地址, 并且还要开启一个跟控制台传递数据的端口, 控制台也可以通过此端口 调用微服务中的监控程序获取微服务的各种信息。
4.4.4 实现一个接口的限流
- 通过控制台为/order4/message3添加一个流控规则
实现QPS如果超过2进行限流。
快速访问接口,发现已经实现了限流。
4.5 Sentinel的概念和功能
4.5.1 基本概念
-
资源
资源就是Sentinel要保护的东西
资源是 Sentinel 的关键概念。它可以是 Java 应用程序中的任何内容,可以是一个服务,也可以是 一个方法,甚至可以是一段代码。
我们入门案例中的message3方法就可以认为是一个资源
-
规则
规则就是用来定义如何进行保护资源的
作用在资源之上, 定义以什么样的方式保护资源,主要包括流量控制规则、熔断降级规则以及系统 保护规则。
我们入门案例中就是为message1资源设置了一种流控规则, 限制了进入message1的流量
4.5.2 重要功能
Sentinel的主要功能就是容错,主要体现为下面这三个:
-
流量控制
流量控制在网络传输中是一个常用的概念,它用于调整网络包的数据。任意时间到来的请求往往是 随机不可控的,而系统的处理能力是有限的。我们需要根据系统的处理能力对流量进行控制。 Sentinel 作为一个调配器,可以根据需要把随机的请求调整成合适的形状。
-
熔断降级
当检测到调用链路中某个资源出现不稳定的表现,例如请求响应时间长或异常比例升高的时候,则 对这个资源的调用进行限制,让请求快速失败,避免影响到其它的资源而导致级联故障。
Sentinel 对这个问题采取了两种手段:
-
通过并发线程数进行限制
Sentinel 通过限制资源并发线程的数量,来减少不稳定资源对其它资源的影响。当某个资源 出现不稳定的情况下,例如响应时间变长,对资源的直接影响就是会造成线程数的逐步堆 积。当线程数在特定资源上堆积到一定的数量之后,对该资源的新请求就会被拒绝。堆积的 线程完成任务后才开始继续接收请求。
-
通过响应时间对资源进行降级
除了对并发线程数进行控制以外,Sentinel 还可以通过响应时间来快速降级不稳定的资源。 当依赖的资源出现响应时间过长后,所有对该资源的访问都会被直接拒绝,直到过了指定的 时间窗口之后才重新恢复。
-
-
系统负载保护
Sentinel 同时提供系统维度的自适应保护能力。当系统负载较高的时候,如果还持续让 请求进入可能会导致系统崩溃,无法响应。在集群环境下,会把本应这台机器承载的流量转发到其 它的机器上去。如果这个时候其它的机器也处在一个边缘状态的时候,Sentinel 提供了对应的保 护机制,让系统的入口流量和系统的负载达到一个平衡,保证系统在能力范围之内处理最多的请求。
Sentinel 和 Hystrix 的区别
两者的原则是一致的, 都是当一个资源出现问题时, 让其快速失败, 不要波及到其它服务
但是在限制的手段上, 确采取了完全不一样的方法:
- Hystrix 采用的是线程池隔离的方式, 优点是做到了资源之间的隔离, 缺点是增加了线程切换的成本。
- Sentinel 采用的是通过并发线程的数量和响应时间来对资源做限制。
总之一句话: 我们需要做的事情,就是在Sentinel的资源上配置各种各样的规则,来实现各种容错的功能。
4.6 Sentinel规则
4.6.1 流控规则
流量控制,其原理是监控应用流量的QPS(每秒查询率) 或并发线程数等指标,当达到指定的阈值时 对流量进行控制,以避免被瞬时的流量高峰冲垮,从而保障应用的高可用性。
点击簇点链路,我们就可以看到访问过的接口地址,然后点击对应的流控按钮,进入流控规则配 置页面。新增流控规则界面如下:
-
**资源名:**唯一名称,默认是请求路径,可自定义
-
**针对来源:**指定对哪个微服务进行限流,默认指default,意思是不区分来源,全部限制
-
阈值类型/单机阈值:
- QPS(每秒请求数量): 当调用该接口的QPS达到阈值的时候,进行限流
- 线程数:当调用该接口的线程数达到阈值的时候,进行限流
-
**是否集群:**暂不需要集群
4.6.1.1 简单配置
接下来我们以QPS为例来研究限流规则的配置
我们先做一个简单配置,设置阈值类型为QPS,单机阈值为2。即每秒请求量大于2的时候开始限流。
接下来,在流控规则页面就可以看到这个配置。
然后快速访问 /order4/message3 接口,观察效果。此时发现,当QPS > 3的时候,服务就不能正常响 应,而是返回 Blocked by Sentinel (flow limiting)结果。
4.6.1.2 配置流控模式
点击上面设置流控规则的编辑按钮,然后在编辑页面点击高级选项,会看到有流控模式一栏。
sentinel共有三种流控模式,分别是:
- 直接(默认):接口达到限流条件时,开启限流
- 关联:当关联的资源达到限流条件时,开启限流 [适合做应用让步]
- 链路:当从某个接口过来的资源达到限流条件时,开启限流
下面呢分别演示三种模式:
直接流控模式
直接流控模式是最简单的模式,当指定的接口达到限流条件时开启限流。上面案例使用的就是直接流控模式。
关联流控模式
关联流控模式指的是,当指定接口关联的接口达到限流条件时,开启对指定接口开启限流。
案例:
第1步:配置限流规则, 将流控模式设置为关联,关联资源设置为的 /order4/message2
第2步:通过postman软件向/order4/message2连续发送请求,注意QPS一定要大于2
第3步:访问/order4/message3,发现已经限流
链路流控模式
链路流控模式指的是,当从某个接口过来的资源达到限流条件时,开启限流。它的功能有点类似于针对 来源配置项,区别在于:针对来源是针对上级微服务,而链路流控是针对上级接口,也就是说它的粒度更细。
第1步: 编写一个service,在里面添加一个方法getMessage
@AllArgsConstructor
@Service
public class OrderService2{
@SentinelResource("message")
public String getMessage(){
return "message";
}
}
第2步:编写OrderController3,在Controller中声明两个方法,分别调用service中的方法
@Slf4j
@RequestMapping("order3")
@RestController
public class OrderController3 {
@Resource
private OrderService2 orderService2;
@GetMapping("/message1")
public String message1() {
orderService2.getMessage();
return "message1";
}
@GetMapping("/message2")
public String message2() {
orderService2.getMessage();
return "message2";
}
}
第3步:Sentinel默认会将Controller方法做context整合,导致链路模式的流控失效,需要修改application.yml,添加配置:
spring:
cloud:
sentinel:
transport:
dashboard: localhost:8080 # sentinel控制台地址
web-context-unify: false # 关闭context整合
注意版本我这里的 Sentinel版本时2.2.3
禁止收敛URL的入口 context
从1.6.3 版本开始,Sentinel Web filter默认收敛所有URL的入口context,因此链路限流不生效。
1.7.0 版本开始(对应SCA的2.1.1.RELEASE),官方在CommonFilter 引入了 WEB_CONTEXT_UNIFY 参数,用于控制是否收敛context。将其配置为 false 即可根据不同的 URL 进行链路限流。 SCA 2.1.1.RELEASE之后的版本,可以通过配置spring.cloud.sentinel.web-context-unify=false即可关闭收敛
第4步:配置message流控
第5步: 分别通过 /order3/message1 和 /order3/message2 访问, 发现1没问题, 2的被限流了
4.6.1.3 配置流控效果
- 快速失败(默认): 直接失败,抛出异常,不做任何额外的处理,是最简单的效果
- **Warm Up:**它从开始阈值到最大QPS阈值会有一个缓冲阶段,一开始的阈值是最大QPS阈值的 1/3,然后慢慢增长,直到最大阈值,适用于将突然增大的流量转换为缓步增长的场景。
- **排队等待:**让请求以均匀的速度通过,单机阈值为每秒通过数量,其余的排队等待; 它还会让设 置一个超时时间,当请求超过超时间时间还未处理,则会被丢弃。
4.6.2 熔断规则
熔断规则就是设置当满足什么条件的时候,对服务进熔断。Sentinel提供了三个衡量条件:
4.6.2.1 慢调用比例
慢调用比例 (SLOW_REQUEST_RATIO
):选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大的响应时间),请求的响应时间大于该值则统计为慢调用。
当单位统计时长(statIntervalMs
)内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断。
案例:规则开启后,在统计时长1秒内,当请求数目大于10,并且慢调用的比例大于30%的时候,则在接下来10秒的熔断时长内,请求都会快速失败。经过10秒后熔断器会进入探测恢复状态,若接下来的一个请求响应时间小于设置的1000 ms则结束熔断,若大于1000 ms则会再次被熔断。
第1步:修改/order4/message1接口,使其满足以上的条件。通过一个 0-9随机数求余2来满足 50%概率的满调用。
@GetMapping("/message1")
public String message1() {
int i = new Random().nextInt(10);
if (i % 2 == 1){
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return "message1";
}
第2步:我们在Sentinel DashBoard界面点击熔断规则,然后按照下图配置新增慢调用比例的熔断规则。
如下所示
第3步:通过postman工具调试,模拟每秒钟最小10的请求
第4步:然后通过浏览器访问发现实现熔断
4.6.2.2 异常比例
异常比例 (DEGRADE_GRADE_EXCEPTION_RATIO):当资源的每秒请求量 >= N(可配置),
并且每秒异常总数占通过量的比值超过阈值(DegradeRule 中的 count)之后,
资源进入降级状态,即在接下的时间窗口(DegradeRule 中的 timeWindow,以 s 为单位)之内,
对这个方法的调用都会自动地返回。
异常比率的阈值范围是 [0.0, 1.0],代表 0% - 100%。
案例:修改/order4/message2接口,使其满足以下的条件。异常比例大于20%
@Slf4j
@RequestMapping("order4")
@RestController
public class OrderController4 {
@GetMapping("/message2")
public String message2() {
int i = new Random().nextInt(4);
if (i % 3 == 1){
throw new RuntimeException("出现异常");
}
return "message2";
}
}
设置nacos的异常比例
快速多次访问 /order4/message2 接口发现
4.6.2.2 异常数
异常数 (DEGRADE_GRADE_EXCEPTION_COUNT):当资源近 1 分钟的异常数目超过阈值之后会进行熔断。
注意由于统计时间窗口是分钟级别的,若 timeWindow 小于 60s,
则结束熔断状态后仍可能再进入熔断状态。
案例:相比于前面两个异常数就更简单了,我们按下图所示修改熔断规则。
如图所示:
还是使用测试异常比例的demo进行演示这个效果,我们在浏览器狂刷请求
4.6.3 热点规则
热点参数流控规则是一种更细粒度的流控规则, 它允许将规则具体到参数上。
热点规则简单使用
第1步: 编写代码
@RequestMapping("order5")
@RestController
public class OrderController5 {
@SentinelResource("order5:message1")
@GetMapping("/message1")
public String message1(String name, Integer age) {
return name+age;
}
}
第2步: 配置热点规则
4.6.4 授权规则
很多时候,我们需要根据调用来源来判断该次请求是否允许放行,这时候可以使用 Sentinel 的来源 访问控制的功能。来源访问控制根据资源的请求来源(origin)限制资源是否通过:
-
若配置白名单,则只有请求来源位于白名单内时才可通过;
-
若配置黑名单,则请求来源位于黑名单时不通过,其余的请求通过。
上面的资源名和授权类型不难理解,但是流控应用怎么填写呢?
其实这个位置要填写的是来源标识,Sentinel提供了 RequestOriginParser 接口来处理来源。 只要Sentinel保护的接口资源被访问,Sentinel就会调用 RequestOriginParser 的实现类去解析 访问来源。
第1步: 自定义来源处理规则
@Component
public class RequestOriginParserDefinition implements RequestOriginParser {
public static final String ORIGIN_KEY = "origin";
/**
* Parse the origin from given HTTP request.
* 解析http中的请求来源,可以在请求头中获取、可以在参数上获取,也可以获取cookie中的参数
* @param request HTTP request
* @return parsed origin
*/
@Override
public String parseOrigin(HttpServletRequest request) {
String origin = null;
// 通过ORIGIN_KEY获取请求来源,通过这个来源与Sentienl流控应用中的值进行比较
if (StringUtils.isEmpty((origin = getParamOrigin(ORIGIN_KEY, request)))){
if (StringUtils.isEmpty(origin = getCookieOrigin(ORIGIN_KEY, request))){
origin = getHeadParamOrigin(ORIGIN_KEY, request);
}
}
return origin;
}
/**
* 从请求参数中获取值
* @param key key
* @param request HttpServletRequest
* @return value
*/
public String getParamOrigin(String key, HttpServletRequest request){
return request.getParameter(key);
}
/**
* 从请求Cookie中获取值
* @param key key
* @param request HttpServletRequest
* @return value
*/
public String getCookieOrigin(String key, HttpServletRequest request){
Cookie[] cookies = request.getCookies();
for (Cookie cookie : cookies) {
if (key.equals(cookie.getName())) {
return cookie.getValue();
}
}
return null;
}
/**
* 从请求Head中中获取值
* @param key key
* @param request HttpServletRequest
* @return value
*/
public String getHeadParamOrigin(String key, HttpServletRequest request){
return request.getHeader(key);
}
}
第2步: 授权规则配置
这个配置的意思是只有origin=pc不能访问(黑名单),其他的都能访问
访问/order3/message2,通过参数携带规则,访问出现异常
/order3/message2?origin=pc
访问/order3/message2不携带规则参数或者参数不为 “pc”,接口正常访问
4.6.5 系统规则
系统保护规则是从应用级别的入口流量进行控制,从单台机器的总体 Load、RT、入口 QPS 、CPU 使用率和线程数五个维度监控应用数据,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。 系统保护规则是应用整体维度的,而不是资源维度的,并且仅对入口流量 (进入应用的流量) 生效。
- Load(仅对 Linux/Unix-like 机器生效):当系统 load1 超过阈值,且系统当前的并发线程数超过 系统容量时才会触发系统保护。系统容量由系统的 maxQps * minRt 计算得出。设定参考值一般 是 CPU cores * 2.5。
- RT:当单台机器上所有入口流量的平均 RT 达到阈值即触发系统保护,单位是毫秒。
- 线程数:当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护。
- 入口 QPS:当单台机器上所有入口流量的 QPS 达到阈值即触发系统保护。
- CPU使用率:当单台机器上所有入口流量的 CPU使用率达到阈值即触发系统保护。
4.7 自定义异常返回
在上面案例中发现有的异常直接就是
而有的异常非常的友好,返回了json格式,并且将熔断,限流等异常区分返回,呢是因为我配置了自定义的异常返回如下:
/**
* sentinel 全局异常处理
* @program: springcloud-alibaba
* @author: xphu
* @create: 2022-04-13 15:48
*/
@Component
public class ExceptionHandlerPage implements BlockExceptionHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, BlockException e) throws Exception {
//BlockException 异常接口,包含Sentinel的五个异常
// FlowException 限流异常
// DegradeException 降级异常
// ParamFlowException 参数限流异常
// AuthorityException 授权异常
// SystemBlockException 系统负载异常
String message = "";
Integer code = 500;
if (e instanceof FlowException){
message = "限流异常!";
code = 503;
}else if (e instanceof DegradeException){
message = "降级异常!";
code = 504;
}else if (e instanceof ParamFlowException){
message = "参数限流异常!";
code = 505;
}else if (e instanceof AuthorityException){
message = "授权异常!";
code = 506;
}else if (e instanceof SystemBlockException){
message = "系统负载异常!";
code = 507;
}else {
message = e.getMessage();
code = 500;
}
httpServletResponse.setContentType("application/json;charset=utf-8");
httpServletResponse.getWriter().write(JSONUtil.toJsonStr(ResponseMessage.builder().fail(code, message).build()));
}
}
4.8 @SentinelResource的使用
在定义了资源点之后,我们可以通过Dashboard来设置限流和降级策略来对资源点进行保护。同时还能 通过@SentinelResource来指定出现异常时的处理策略。
@SentinelResource 用于定义资源,并提供可选的异常处理和 fallback 配置项。其主要参数如下:
属性 | 作用 |
---|---|
value | 资源名称 |
entryType | entry类型,标记流量的方向,取值IN/OUT,默认是OUT |
blockHandler | 处理BlockException的函数名称,函数要求: 1. 必须是 public 2.返回类型 参数与原方法一致 3. 默认需和原方法在同一个类中。若希望使用其他类的函数,可配置 blockHandlerClass ,并指定blockHandlerClass里面的方法。 |
blockHandlerClass | 存放blockHandler的类,对应的处理函数必须static修饰 |
fallback | 用于在抛出异常的时候提供fallback处理逻辑。fallback函数可以针对所 有类型的异常(除了 exceptionsToIgnore 里面排除掉的异常类型)进 行处理。函数要求:1. 返回类型与原方法一致 2. 参数类型需要和原方法相匹配 3. 默认需和原方法在同一个类中。若希望使用其他类的函数,可配置 fallbackClass ,并指定fallbackClass里面的方法。 |
fallbackClass | 存放fallback的类。对应的处理函数必须static修饰。 |
defaultFallback | 用于通用的 fallback 逻辑。默认fallback函数可以针对所有类型的异常进 行处理。若同时配置了 fallback 和 defaultFallback,以fallback为准。函 数要求:1. 返回类型与原方法一致 2. 方法参数列表为空,或者有一个 Throwable 类型的参数。 3. 默认需要和原方法在同一个类中。若希望使用其他类的函数,可配置 fallbackClass ,并指定 fallbackClass 里面的方法。 |
exceptionsToIgnore | 指定排除掉哪些异常。排除的异常不会计入异常统计,也不会进入 fallback逻辑,而是原样抛出。 |
exceptionsToTrace | 需要trace的异常 |
4.8.1 定义限流和降级后的处理方法
4.8.1.1 直接将限流和降级方法定义在方法中
@Slf4j
@Service
public class OrderService2{
Integer i = 0;
/**
* Sentinel实现降级处理
* @SentinelResource
* value: 资源名称
* blockHandlerClass & blockHandler: 指定发生BlockException时进入的方法
* fallback & fallbackClass: 指定发生Throwable时进入的方法
*/
@SentinelResource(
value = "message",
blockHandler="blockHandler",//指定发生BlockException时进入的方法
fallback="fallback" //指定发生Throwable时进入的方法
)
public String getMessage(){
i++;
if (i % 3 == 0) {
throw new RuntimeException();
}
return "message";
}
/**
* BlockException时进入的方法
*/
public String blockHandler(BlockException e){
log.error("{}", e.getMessage());
return "接口被限流或者降级了...";
}
/**
* Throwable时进入的方法
*/
public String fallback(Throwable e){
log.error("{}", e.getMessage());
return "接口发生异常了....";
}
}
4.8.1.2 将限流和降级方法外置到单独的类中
@Slf4j
@Service
public class OrderService2{
Integer i = 0;
/**
* Sentinel实现降级处理
* @SentinelResource
* value: 资源名称
* blockHandlerClass & blockHandler: 指定发生BlockException时进入的方法
* fallback & fallbackClass: 指定发生Throwable时进入的方法
*/
@SentinelResource(
value = "message",
blockHandlerClass = OrderService2BlockHandler.class,
blockHandler="blockHandler",//指定发生BlockException时进入的方法
fallback="fallback", //指定发生Throwable时进入的方法
fallbackClass = OrderService2FallBack.class
)
public String getMessage(){
i++;
if (i % 3 == 0) {
throw new RuntimeException();
}
return "message";
}
}
OrderService2BlockHandler.java
@Slf4j
public class OrderService2BlockHandler {
/**
* 方法必须是静态方法
* BlockException时进入的方法
*/
public static String blockHandler(BlockException e){
log.error("{}", e.getMessage());
return "接口被限流或者降级了...";
}
}
OrderService2FallBack.java
@Slf4j
public class OrderService2FallBack {
/**
* 方法必须是静态方法
* Throwable时进入的方法
*/
public static String fallback(Throwable e){
log.error("{}", e.getMessage());
return "接口发生异常了....";
}
}
4.9 Sentinel规则持久化
通过前面的讲解,我们已经知道,可以通过Dashboard来为每个Sentinel客户端设置各种各样的规 则,但是这里有一个问题,就是这些规则默认是存放在内存中,极不稳定,微服务重启之后配置就消失了,所以需要将其持久化。
本地文件数据源会定时轮询文件的变更,读取规则。这样我们既可以在应用本地直接修改文件来更 新规则,也可以通过 Sentinel 控制台推送规则。以本地文件数据源为例,推送过程如下图所示:
首先 Sentinel 控制台通过 API 将规则推送至客户端并更新到内存中,接着注册的写数据源会将新的 规则保存到本地的文件中。
具体实现
我们需要在微服务端做一下配置即可开启数据持久化
- 编写代码配置
package cn.apotato.config;
import com.alibaba.csp.sentinel.command.handler.ModifyParamFlowRulesCommandHandler;
import com.alibaba.csp.sentinel.datasource.*;
import com.alibaba.csp.sentinel.init.InitFunc;
import com.alibaba.csp.sentinel.slots.block.authority.AuthorityRule;
import com.alibaba.csp.sentinel.slots.block.authority.AuthorityRuleManager;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRuleManager;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowRuleManager;
import com.alibaba.csp.sentinel.slots.system.SystemRule;
import com.alibaba.csp.sentinel.slots.system.SystemRuleManager;
import com.alibaba.csp.sentinel.transport.util.WritableDataSourceRegistry;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import org.springframework.beans.factory.annotation.Value;
import java.io.File;
import java.io.IOException;
import java.util.List;
/**
* Sentinel规则持久化
*
* @program: springcloud-alibaba
* @author: xphu
* @create: 2022-04-13 22:32
*/
public class SentinelFilePersistence implements InitFunc {
@Value("spring.application.name")
private String applicationName;
@Override
public void init() throws Exception {
String ruleDir = System.getProperty("user.home") + "/sentinelrules/" + applicationName;
String flowRulePath = ruleDir + "/flow-rule.json";
String degradeRulePath = ruleDir + "/degrade-rule.json";
String systemRulePath = ruleDir + "/system-rule.json";
String authorityRulePath = ruleDir + "/authority-rule.json";
String paramFlowRulePath = ruleDir + "/param-flow-rule.json";
this.mkdirIfNotExits(ruleDir);
this.createFileIfNotExits(flowRulePath);
this.createFileIfNotExits(degradeRulePath);
this.createFileIfNotExits(systemRulePath);
this.createFileIfNotExits(authorityRulePath);
this.createFileIfNotExits(paramFlowRulePath);
// 流控规则
ReadableDataSource<String, List<FlowRule>> flowRuleRDS = new
FileRefreshableDataSource<>(
flowRulePath,
flowRuleListParser
);
FlowRuleManager.register2Property(flowRuleRDS.getProperty());
WritableDataSource<List<FlowRule>> flowRuleWDS = new
FileWritableDataSource<>(
flowRulePath,
this::encodeJson
);
WritableDataSourceRegistry.registerFlowDataSource(flowRuleWDS);
// 降级规则
ReadableDataSource<String, List<DegradeRule>> degradeRuleRDS = new
FileRefreshableDataSource<>(
degradeRulePath,
degradeRuleListParser
);
DegradeRuleManager.register2Property(degradeRuleRDS.getProperty());
WritableDataSource<List<DegradeRule>> degradeRuleWDS = new
FileWritableDataSource<>(
degradeRulePath,
this::encodeJson
);
WritableDataSourceRegistry.registerDegradeDataSource(degradeRuleWDS);
// 系统规则
ReadableDataSource<String, List<SystemRule>> systemRuleRDS = new
FileRefreshableDataSource<>(
systemRulePath,
systemRuleListParser
);
SystemRuleManager.register2Property(systemRuleRDS.getProperty());
WritableDataSource<List<SystemRule>> systemRuleWDS = new
FileWritableDataSource<>(
systemRulePath,
this::encodeJson);
WritableDataSourceRegistry.registerSystemDataSource(systemRuleWDS);
// 授权规则
ReadableDataSource<String, List<AuthorityRule>> authorityRuleRDS = new
FileRefreshableDataSource<>(
authorityRulePath,
authorityRuleListParser
);
AuthorityRuleManager.register2Property(authorityRuleRDS.getProperty());
WritableDataSource<List<AuthorityRule>> authorityRuleWDS = new
FileWritableDataSource<>(
authorityRulePath,
this::encodeJson
);
WritableDataSourceRegistry.registerAuthorityDataSource(authorityRuleWDS);
// 热点参数规则
ReadableDataSource<String, List<ParamFlowRule>> paramFlowRuleRDS = new
FileRefreshableDataSource<>(
paramFlowRulePath,
paramFlowRuleListParser
);
ParamFlowRuleManager.register2Property(paramFlowRuleRDS.getProperty());
WritableDataSource<List<ParamFlowRule>> paramFlowRuleWDS = new
FileWritableDataSource<>(
paramFlowRulePath,
this::encodeJson
);
ModifyParamFlowRulesCommandHandler.setWritableDataSource(paramFlowRuleWDS);
}
private Converter<String, List<FlowRule>> flowRuleListParser = source ->
JSON.parseObject(
source,
new TypeReference<List<FlowRule>>() {
}
);
private Converter<String, List<DegradeRule>> degradeRuleListParser = source
-> JSON.parseObject(
source,
new TypeReference<List<DegradeRule>>() {
}
);
private Converter<String, List<SystemRule>> systemRuleListParser = source ->
JSON.parseObject(
source, new TypeReference<List<SystemRule>>() {
}
);
private Converter<String, List<AuthorityRule>> authorityRuleListParser =
source -> JSON.parseObject(
source,
new TypeReference<List<AuthorityRule>>() {
}
);
private Converter<String, List<ParamFlowRule>> paramFlowRuleListParser =
source -> JSON.parseObject(
source,
new TypeReference<List<ParamFlowRule>>() {
}
);
private void mkdirIfNotExits(String filePath) throws IOException {
File file = new File(filePath);
if (!file.exists()) {
file.mkdirs();
}
}
private void createFileIfNotExits(String filePath) throws IOException {
File file = new File(filePath);
if (!file.exists()) {
file.createNewFile();
}
}
private <T> String encodeJson(T t) {
return JSON.toJSONString(t);
}
}
-
添加配置
在resources下创建配置目录 META-INF/services ,然后添加文件
com.alibaba.csp.sentinel.init.InitFunc
在文件中添加配置类的全路径
cn.apotato.config.SentinelFilePersistence
4.10 Feign整合Sentinel
Feign用过spring cloud的小火鸡都应该用过,之前的demo中我们也使用了。Feign通过hystrix的也能实现降级,我们来实现一下
-
引入sentinel的依赖
<!--sentinel客户端--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> </dependency>
-
在配置文件中开启Feign对Sentinel的支持
feign: sentinel: enabled: true
-
创建容错类
/**
* 容错类要求必须实现被容错的接口,并为每个方法实现容错方案
*
* @program: springcloud-alibaba
* @description
* @author: xphu
* @create: 2022-04-14 09:48
*/
@Slf4j
@Component
public class ProductClientFallback implements ProductClient {
@Override
public Message findById(Integer pid) {
log.error("ProductClient.findById() 进入降级");
return ResponseData.fail(500, "服务调用异常").build();
}
}
-
为被容器的接口指定容错类
@FeignClient(value = "service-product",fallback = ProductClientFallback.class) public interface ProductClient { @GetMapping("/product/{pid}") Message findById(@PathVariable("pid") Integer pid); }
-
修改controller
@Inclusion @GetMapping("/prod/{pid}") public Order order(@PathVariable("pid") Integer pid) { log.info(">>客户下单,这时候要调用商品微服务查询商品信息"); Message message = productClient.findById(pid); if (message.getCode() == 500){ Order order = new Order(); order.setUid(-1); order.setUsername("失败"); order.setProductName("失败专用商品"); order.setProductPrice(0D); order.setNumber(1); return order; } Product product = message.getDataToBean(Product.class); log.info(">>商品信息,查询结果:" + JSON.toJSONString(product)); Order order = new Order(); order.setUid(1); order.setUsername("测试用户"); order.setPid(product.getPid()); order.setProductName(product.getProductName()); order.setProductPrice(product.getProductPrice()); order.setNumber(1); return orderService.save(order); }
-
停止所有 shop-product 服务,重启 shop-order 服务,访问请求,观察容错效果
-
扩展: 如果想在容错类中拿到具体的错误,可以使用下面的方式
@FeignClient(value = "service-product", // fallback & fallbackFactory 只能选其一 // fallback = ProductClientFallback.class, fallbackFactory = ProductServiceFallBackFactory.class ) public interface ProductClient { @GetMapping("/product/{pid}") Message findById(@PathVariable("pid") Integer pid); }
@Slf4j @Component public class ProductServiceFallBackFactory implements FallbackFactory<ProductClient> { @Override public ProductClient create(Throwable throwable) { return new ProductClient() { @Override public Message findById(Integer pid) { log.error("ProductClient.findById() 进入降级, e={}", throwable); return ResponseData.fail(500, "服务调用异常").build(); } }; } }
注意: fallback和fallbackFactory只能使用其中一种方式
第五章 Gateway 服务网关
5.1 网关简介
大家都都知道在微服务架构中,一个系统会被拆分为很多个微服务。那么作为客户端要如何去调用 这么多的微服务呢?如果没有网关的存在,我们只能在客户端记录每个微服务的地址,然后分别去调用。
这样的架构,会存在着诸多的问题:
- 客户端多次请求不同的微服务,增加客户端代码或配置编写的复杂性
- 认证复杂,每个服务都需要独立认证。
- 存在跨域请求,在一定场景下处理相对复杂。
上面的这些问题可以借助API网关来解决。
所谓的API网关,就是指系统的统一入口,它封装了应用程序的内部结构,为客户端提供统一服 务,一些与业务本身功能无关的公共逻辑可以在这里实现,诸如认证、鉴权、监控、路由转发等等。
添加上API网关之后,系统的架构图变成了如下所示:
在业界比较流行的网关,有下面这些:
-
Ngnix+lua
使用nginx的反向代理和负载均衡可实现对api服务器的负载均衡及高可用
lua是一种脚本语言,可以来编写一些简单的逻辑, nginx支持lua脚本
-
Kong
基于Nginx+Lua开发,性能高,稳定,有多个可用的插件(限流、鉴权等等)可以开箱即用。
问题: 只支持Http协议;二次开发,自由扩展困难;提供管理API,缺乏更易用的管控、配置方式。
-
Zuul
Netflix开源的网关,功能丰富,使用JAVA开发,易于二次开发 问题:缺乏管控,无法动态配 置;依赖组件较多;处理Http请求依赖的是Web容器,性能不如Nginx
-
Spring Cloud Gateway
Spring公司为了替换Zuul而开发的网关服务,将在下面具体介绍。
5.2 Gateway简介
Spring Cloud Gateway是Spring公司基于Spring 5.0,Spring Boot 2.0 和 Project Reactor 等技术 开发的网关,它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式。它的目标是替代 Netflix Zuul,其不仅提供统一的路由方式,并且基于 Filter 链的方式提供了网关基本的功能,例如:安全,监控和限流。
优点:
- 功能强劲:是第一代网关 Zuul的1.6倍
- 功能强大:内置了很多实用的功能,例如转发、监控、限流
- 设计优雅,容易扩展
缺点:
- 内部实现依赖Netty与WebFlux,不是传统的Servlet编程模式,学习成本高
- 不能内置在Tomcat,Jetty等Servlet容器里面,只能打包成jar包执行
- 需要SpringBoot 2.0及以上的版本才支持
5.3 Gateway快速入门
要求:通过浏览器访问api网关,然后通过网关请求转发到商品微服务
5.3.1 基础版本
-
创建一个 api-gateway 的模块,导入相关依赖
<?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"> <parent> <artifactId>springcloud-alibaba</artifactId> <groupId>cn.apotato</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>api-gateway</artifactId> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> </dependencies> </project>
-
创建主类
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
-
添加配置文件
server: port: 7000 spring: application: name: api-gateway cloud: gateway: routes: # 路由数组[路由 就是指定当请求满足什么条件的时候转到哪个微服务] - id: service-product uri: http://localhost:8081 # 请求要转发到的地址 order: 1 # 路由优先级,数字越小级别越大 predicates: # 断言(就是路由转发要满足的条件) - Path=/product-serv/** #当请求路径满足Path指定的规则时,才进行路由转发 filters: # 过滤器,请求在传递过程中可以通过过滤器对其进行一定的修改 - StripPrefix=1 #转发之前去掉1层路径
-
启动项目, 并通过网关去访问微服务
5.3.2 增强版
现在在配置文件中写死了转发路径的地址, 前面我们已经分析过地址写死带来的问题, 接下来我们从 注册中心获取此地址。
-
加入nacos依赖
<!--nacos客户端--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency>
-
在主类上添加注解
@EnableDiscoveryClient
@EnableDiscoveryClient @SpringBootApplication public class GatewayApplication { public static void main(String[] args) { SpringApplication.run(GatewayApplication.class, args); } }
5.3.3 简写版
去掉路由的配置使用默认的路由配置
server:
port: 7000
spring:
application:
name: api-gateway
cloud:
nacos:
discovery:
server-addr: localhost:8848 #apotato.cn:8448
gateway:
discovery:
locator:
enabled: true # 让gateway可以发现nacos中的微服务
启动项目,并通过网关去访问微服务
5.4 Gateway核心架构
5.4.1 基本概念
路由(Route) 是 gateway 中最基本的组件之一,表示一个具体的路由信息载体。主要定义了下面的几个信息:
- id:路由标识符,区别于其他 Rout
- uri:路由指向的目的地 uri,即客户端请求最终被转发到的微服务。
- order:用于多个 Route 之间的排序,数值越小排序越靠前,匹配优先级越高。
- predicate:断言的作用是进行条件判断,只有断言都返回真,才会真正的执行路由。
- filter:过滤器用于修改请求和响应信息。
5.4.2 执行流程
执行流程大体如下:
- Gateway Client向Gateway Server发送请求
- 请求首先会被HttpWebHandlerAdapter进行提取组装成网关上下文
- 然后网关的上下文会传递到DispatcherHandler,它负责将请求分发给 RoutePredicateHandlerMapping
- RoutePredicateHandlerMapping负责路由查找,并根据路由断言判断路由是否可用
- 如果过断言成功,由FilteringWebHandler创建过滤器链并调用
- 请求会一次经过PreFilter–微服务–PostFilter的方法,最终返回响应
5.5 断言
Predicate(断言, 谓词) 用于进行条件判断,只有断言都返回真,才会真正的执行路由。
断言就是说: 在 什么条件下 才能进行路由转发
5.5.1 内置路由断言工厂
SpringCloud Gateway包括许多内置的断言工厂,所有这些断言都与HTTP请求的不同属性匹配。具体 如下:
-
基于Datetime类型的断言工厂
此类型的断言根据时间做判断,主要有三个:
- AfterRoutePredicateFactory:接收一个日期参数,判断请求日期是否晚于指定日期
- BeforeRoutePredicateFactory:接收一个日期参数,判断请求日期是否早于指定日期
- BetweenRoutePredicateFactory:接收两个日期参数,判断请求日期是否在指定时间段内
-After=2019-12-31T23:59:59.789+08:00[Asia/Shanghai]
-
基于远程地址的断言工厂 RemoteAddrRoutePredicateFactory:接收一个IP地址段,判断请求主 机地址是否在地址段中
-RemoteAddr=192.168.1.1/24
-
基于Cookie的断言工厂
CookieRoutePredicateFactory:接收两个参数,cookie 名字和一个正则表达式。 判断请求 cookie是否具有给定名称且值与正则表达式匹配。
-Cookie=chocolate, ch.
-
基于Header的断言工厂
HeaderRoutePredicateFactory:接收两个参数,标题名称和正则表达式。 判断请求Header是否 具有给定名称且值与正则表达式匹配。
-Header=X-Request-Id, \d+
-
基于Host的断言工厂
HostRoutePredicateFactory:接收一个参数,主机名模式。判断请求的Host是否满足匹配规则。
-Host=**.testhost.org
-
基于Method请求方法的断言工厂
MethodRoutePredicateFactory:接收一个参数,判断请求类型是否跟指定的类型匹配。
-Method=GET
-
基于Path请求路径的断言工厂
PathRoutePredicateFactory:接收一个参数,判断请求的URI部分是否满足路径规则。
-Path=/foo/{segment}
-
基于Query请求参数的断言工厂
QueryRoutePredicateFactory :接收两个参数,请求param和正则表达式, 判断请求参数是否具有给定名称且值与正则表达式匹配。
-Query=baz, ba.
-
基于路由权重的断言工厂
WeightRoutePredicateFactory:接收一个[组名,权重], 然后对于同一个组内的路由按照权重转发
routes:
-id: weight_route1 uri: host1 predicates: -Path=/product/**
-Weight=group3, 1 -id: weight_route2 uri: host2 predicates: -Path=/product/** -Weight= group3, 9
内置路由断言工厂的使用
spring:
application:
name: api-gateway
cloud:
nacos:
discovery:
server-addr: localhost:8848 #apotato.cn:8448
gateway:
discovery:
locator:
enabled: true # 让gateway可以发现nacos中的微服务
routes: # 路由数组[路由 就是指定当请求满足什么条件的时候转到哪个微服务]
- id: service-product
uri: lb://service-product # 请求要转发到的地址,# lb指的是从nacos中按照名称获取微服务,并遵循负载均衡策略
order: 1 # 路由优先级,数字越小级别越大
predicates: # 断言(就是路由转发要满足的条件)
- Path=/product-serv/** #当请求路径满足Path指定的规则时,才进行路由转发
- After=2021-12-31T23:59:59.789+08:00[Asia/Shanghai] #接收一个日期参数,判断请求日期是否晚于指定日期
filters: # 过滤器,请求在传递过程中可以通过过滤器对其进行一定的修改
- StripPrefix=1 #转发之前去掉1层路径
5.5.2 自定义路由断言工厂
我们来设定一个场景: 假设我们的应用仅仅让pid在(min,max)之间的的商品访问。
-
在配置文件中,添加一个ProductId的断言配置
spring: cloud: gateway: routes: # 路由数组[路由 就是指定当请求满足什么条件的时候转到哪个微服务] - id: service-product uri: lb://service-product # 请求要转发到的地址,# lb指的是从nacos中按照名称获取微服务,并遵循负载均衡策略 order: 1 # 路由优先级,数字越小级别越大 predicates: # 断言(就是路由转发要满足的条件) - Path=/product-serv1/** #当请求路径满足Path指定的规则时,才进行路由转发 - ProductId=3,6 #接受一个pid区间 filters: # 过滤器,请求在传递过程中可以通过过滤器对其进行一定的修改 - StripPrefix=1 #转发之前去掉1层路径
-
自定义一个断言工厂, 实现断言方法
-
yml配置文件中以 predicates:xxx=value 的方式进行使用
-
创建一个类 必须以 xxxRoutePredicateFactory的命名方式
-
xxxRoutePredicateFactory格式以下面demo为例
package cn.apotato.gateway.predicates; import lombok.Data; import org.apache.commons.lang.StringUtils; import org.springframework.cloud.gateway.handler.predicate.AbstractRoutePredicateFactory; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import java.util.Arrays; import java.util.List; import java.util.function.Predicate; /** * 自定义路由断言 * 商品id区间限制 路由断言 * @program: springcloud-alibaba * @description * @author: xphu * @create: 2022-04-19 18:16 */ @Component public class ProductIdRoutePredicateFactory extends AbstractRoutePredicateFactory<ProductIdRoutePredicateFactory.Config> { public ProductIdRoutePredicateFactory() { super(Config.class); } /** * 用于从配置文件中获取参数值赋值到配置类中的属性上 * @return List */ @Override public List<String> shortcutFieldOrder() { return Arrays.asList("minPid", "maxPid"); } @Override public Predicate<ServerWebExchange> apply(Config config) { return new Predicate<ServerWebExchange>() { @Override public boolean test(ServerWebExchange serverWebExchange) { // 从serverWebExchange获取传入的参数 String pidStr = serverWebExchange.getRequest() .getQueryParams().getFirst("pid"); if(StringUtils.isNotEmpty(pidStr)){ int pid = Integer.parseInt(pidStr); return pid >= config.getMinPid() && pid <= config.getMaxPid(); } return false; } }; } @Data public static class Config { private int minPid; private int maxPid; } }
-
-
重新启动gateway进行测试
-
pid<3 时
-
3<= pid <= 6 时
-
5.6.2 全局过滤器
全局过滤器作用于所有路由, 无需配置。通过全局过滤器可以实现对权限的统一校验,安全性验证等功 能。
5.6.2.1 内置全局过滤器
SpringCloud Gateway内部也是通过一系列的内置全局过滤器对整个路由转发进行处理如下:
5.6.2.2 自定义全局过滤器
内置的过滤器已经可以完成大部分的功能,但是对于企业开发的一些业务功能处理,还是需要我们自己编写过滤器来实现的,那么我们一起通过代码的形式自定义一个过滤器,去完成统一的权限校验。
开发中的鉴权逻辑:
- 当客户端第一次请求服务时,服务端对用户进行信息认证(登录)
- 认证通过,将用户信息进行加密形成token,返回给客户端,作为登录凭证
- 以后每次请求,客户端都携带认证的token
- 服务端对token进行解密,判断是否有效。
如上图,对于验证用户是否已经登录鉴权的过程可以在网关统一检验。
检验的标准就是请求中是否携带token凭证以及token的正确性。
下面的我们自定义一个GlobalFilter,去校验所有请求的请求参数中是否包含“token”,如何不包含请求 参数“token”则不转发路由,否则执行正常的逻辑。
package cn.apotato.gateway.filter;
import cn.apotato.gateway.exception.AuthorityException;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpCookie;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.RequestPath;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.List;
import java.util.regex.Pattern;
/**
* 自定义全局拦截器,鉴权操作在此处处理
* @program: springcloud-alibaba
* @description
* @author: xphu
* @create: 2022-04-20 15:49
*/
@Slf4j
@Data
@ConfigurationProperties("spring.cloud.gateway")
@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {
private static final String TOKEN_KEY = "token";
private List<String> whitelist;
private List<String> blacklist;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String path = exchange.getRequest().getPath().toString();
// 黑白名单也可以在过滤器链中处理
// 黑名单处理
if (blacklist != null && blacklist.size() > 0){
for (String bl : blacklist) {
if(Pattern.matches(bl, path)){
log.info("访问接口: [{}],权限不足,黑名单中的接口不能够访问!", path);
// 直接抛出异常,配合全局异常处理器,返回统一的异常格式
throw new AuthorityException("访问接口: ["+path+"],权限不足,黑名单中的接口不能够访问!");
// exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
// return exchange.getResponse().setComplete();
}
}
}
// 白名单处理
if(whitelist != null && whitelist.size() > 0){
for (String wl : whitelist) {
if(Pattern.matches(wl, path)){
log.info("访问接口: [{}],白名单中的接口跳过鉴权!", path);
return chain.filter(exchange);
}
}
}
// 获取token
String token = getToken(exchange, TOKEN_KEY);
if (StringUtils.isEmpty(token)){
log.info("访问接口: [{}], 权限不足!", path);
throw new AuthorityException("访问接口: ["+path+"], 权限不足!");
// exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
// return exchange.getResponse().setComplete();
}
// todo 鉴权处理
// ...
// 方案1: 通过redis获取权限路径
// 方案2: 直接访问鉴权服务
return chain.filter(exchange);
}
/**
* 从参数/head/cookie中获取token
* @param exchange ServerWebExchange
* @param key 键
* @return token
*/
private String getToken(ServerWebExchange exchange, String key){
String token = null;
ServerHttpRequest request = exchange.getRequest();
token = request.getQueryParams().getFirst(key);
if(StringUtils.isEmpty(token)){
token = request.getHeaders().getFirst(key);
}
if(StringUtils.isEmpty(token)){
HttpCookie httpCookie = request.getCookies().getFirst(key);
if (httpCookie != null){
token = httpCookie.getValue();
}
}
return token;
}
@Override
public int getOrder() {
return 0;
}
}
在鉴权基础上还拓展了,URL黑名单,配置如下
spring:
cloud:
gateway:
# 黑白名单
# value为一个正则表达式:例如 /api/** 之后的都可以 /api.*
whitelist:
- /product-serv2/.*
blacklist:
- /product-serv1/.*
黑名单设置了 /product-serv1/* 的请求都不让访问,白名单设置了/product-serv2/* 的请求全部放行不需要进行鉴权
我们测试一下
访问:http://localhost:7000/product-serv1/product/1?pid=4
访问:http://localhost:7000/product-serv2/product/1
第六章 Sleuth 链路追踪
6.1 链路追踪介绍
在大型系统的微服务化构建中,一个系统被拆分成了许多模块。这些模块负责不同的功能,组合成 系统,最终可以提供丰富的功能。在这种架构中,一次请求往往需要涉及到多个服务。互联网应用构建 在不同的软件模块集上,这些软件模块,有可能是由不同的团队开发、可能使用不同的编程语言来实 现、有可能布在了几千台服务器,横跨多个不同的数据中心,也就意味着这种架构形式也会存在一些问 题:
- 如何快速发现问题?
- 如何判断故障影响范围?
- 如何梳理服务依赖以及依赖的合理性?
- 如何分析链路性能问题以及实时容量规划?
分布式链路追踪(Distributed Tracing),就是将一次分布式请求还原成调用链路,进行日志记 录,性能监控并将一次分布式请求的调用情况集中展示。比如各个服务节点上的耗时、请求具体到达哪 台机器上、每个服务节点的请求状态等等
常见的链路追踪技术有下面这些:
-
cat 由大众点评开源,基于Java开发的实时应用监控平台,包括实时应用监控,业务监控 。 集成 方案是通过代码埋点的方式来实现监控,比如: 拦截器,过滤器等。 对代码的侵入性很大,集成 成本较高。风险较大。
-
zipkin 由Twitter公司开源,开放源代码分布式的跟踪系统,用于收集服务的定时数据,以解决微 服务架构中的延迟问题,包括:数据的收集、存储、查找和展现。该产品结合spring-cloud-sleuth 使用较为简单, 集成很方便, 但是功能较简单。
-
pinpoint Pinpoint是韩国人开源的基于字节码注入的调用链分析,以及应用监控分析工具。特点 是支持多种插件,UI功能强大,接入端无代码侵入
-
skywalking SkyWalking是本土开源的基于字节码注入的调用链分析,以及应用监控分析工具。特点是支持多 种插件,UI功能较强,接入端无代码侵入。目前已加入Apache孵化器
-
Sleuth SpringCloud 提供的分布式系统中链路追踪解决方案
注意:SpringCloud alibaba技术栈中并没有提供自己的链路追踪技术的,我们可以采用Sleuth + Zinkin来做链路追踪解决方案
6.2 Sleuth入门
6.2.1 Sleuth介绍
SpringCloud Sleuth主要功能就是在分布式系统中提供追踪解决方案。它大量借用了Google Dapper的设计, 先来了解一下Sleuth中的术语和相关概念。
-
Trace
由一组Trace Id相同的Span串联形成一个树状结构。为了实现请求跟踪,当请求到达分布式系统的 入口端点时,只需要服务跟踪框架为该请求创建一个唯一的标识(即TraceId),同时在分布式系 统内部流转的时候,框架始终保持传递该唯一值,直到整个请求的返回。那么我们就可以使用该唯 一标识将所有的请求串联起来,形成一条完整的请求链路。
-
Span
Span 代表了一组基本的工作单元。为了统计各处理单元的延迟,当请求到达各个服务组件的时 候,也通过一个唯一标识(SpanId)来标记它的开始、具体过程和结束。通过SpanId的开始和结 束时间戳,就能统计该span的调用时间,除此之外,我们还可以获取如事件的名称。请求信息等 元数据。
-
Annotation
用它记录一段时间内的事件,内部使用的重要注释:
-
cs(Client Send)客户端发出请求,开始一个请求的生命
-
sr(Server Received)服务端接受到请求开始进行处理, sr-cs = 网络延迟(服务调用的时间)
-
ss(Server Send)服务端处理完毕准备发送到客户端,ss - sr = 服务器上的请求处理时间
-
cr(Client Reveived)客户端接受到服务端的响应,请求结束。 cr - sr = 请求的总时间
-
6.2.2 Sleuth入门
微服务名称, traceId, spanid,是否将链路的追踪结果输出到第三方平台
[api-gateway,3977125f73391553,3977125f73391553,false]
[service-order,3977125f73391553,57547b5bf71f8242,false]
[service-product,3977125f73391553,449f5b3f3ef8d5c5,false]
接下来通过之前的项目案例整合Sleuth,完成入门案例的编写。
修改父工程引入Sleuth依赖
<!--链路追踪 Sleuth-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
启动微服务,调用之后,我们可以在控制台观察到sleuth的日志输出
其中 8a65dfacbb461269 是TraceId, 34740ca78174dd00 是SpanId,依次调用有一个全局的 TraceId,将调用链路串起来。仔细分析每个微服务的日志,不难看出请求的具体过程。
查看日志文件并不是一个很好的方法,当微服务越来越多日志文件也会越来越多,通过Zipkin可以 将日志聚合,并进行可视化展示和全文检索。
6.3 Zipkin的集成
6.3.1 ZipKin介绍
Zipkin 是 Twitter 的一个开源项目,它基于Google Dapper实现,它致力于收集服务的定时数据, 以解决微服务架构中的延迟问题,包括数据的收集、存储、查找和展现。
我们可以使用它来收集各个服务器上请求链路的跟踪数据,并通过它提供的REST API接口来辅助我 们查询跟踪数据以实现对分布式系统的监控程序,从而及时地发现系统中出现的延迟升高问题并找出系 统性能瓶颈的根源。
除了面向开发的 API 接口之外,它也提供了方便的UI组件来帮助我们直观的搜索跟踪信息和分析请 求链路明细,比如:可以查询某段时间内各用户请求的处理时间等。
Zipkin 提供了可插拔数据存储方式:In-Memory、MySql、Cassandra 以及 Elasticsearch。
上图展示了 Zipkin 的基础架构,它主要由 4 个核心组件构成:
- Collector:收集器组件,它主要用于处理从外部系统发送过来的跟踪信息,将这些信息转换为 Zipkin内部处理的 Span 格式,以支持后续的存储、分析、展示等功能。
- Storage:存储组件,它主要对处理收集器接收到的跟踪信息,默认会将这些信息存储在内存中, 我们也可以修改此存储策略,通过使用其他存储组件将跟踪信息存储到数据库中。
- RESTful API:API 组件,它主要用来提供外部访问接口。比如给客户端展示跟踪信息,或是外接系统访问以实现监控等。
- Web UI:UI 组件, 基于API组件实现的上层应用。通过UI组件用户可以方便而有直观地查询和分析跟踪信息。
Zipkin分为两端,一个是 Zipkin服务端,一个是 Zipkin客户端,客户端也就是微服务的应用。 客户端会 配置服务端的 URL 地址,一旦发生服务间的调用的时候,会被配置在微服务里面的 Sleuth 的监听器监 听,并生成相应的 Trace 和 Span 信息发送给服务端。
6.3.2 ZipKin服务端安装
第1步: 下载ZipKin的jar包
https://repo1.maven.org/maven2/io/zipkin/java/zipkin-server/2.12.9/zipkin-server-2.12.9-exec.jar
访问上面的网址,即可得到一个jar包,这就是ZipKin服务端的jar包
第2步: 通过命令行,输入下面的命令启动ZipKin Server
java -jar zipkin-server-2.12.9-exec.jar
第3步:通过浏览器访问 http://localhost:9411访问
6.3.3 Zipkin客户端集成
ZipKin客户端和Sleuth的集成非常简单,只需要在微服务中添加其依赖和配置即可。
第1步:在每个微服务上添加依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
第2步:添加配置
spring:
zipkin:
base-url: http://127.0.0.1:9411/ #zipkin server的请求地址
discoveryClientEnabled: false #让nacos把它当成一个URL,而不要当做服务名
sleuth:
sampler:
probability: 1.0 #采样的百分比
第3步: 访问微服务
http://localhost:7000/api/order2/prod/1?token=1
第4步: 访问zipkin的UI界面,观察效果
点击查看详情,每个服务的用时和参数返回值都能查询到