微服务架构介绍
微服务架构常见问题
- 如何管理这么多小服务——服务治理
- 它们之间如何通讯——服务调用
- 客户端如何访问这些服务——服务网关
- 服务如何进行自排查——服务容错
- 出现问题程序员如何排错——链路追踪
微服务架构常见概念
服务治理
服务治理就是进行服务的自动化管理,其核心是服务的自动注册与发现 。
服务注册:服务实例将自身服务信息注册到注册中心。
服务发现:服务实例通过注册中心,获取到注册到其中的服务实例的信息,通过这些信息去请求它们提供的服务
服务剔除:服务注册中心将出问题的服务自动剔除到可用列表之外,使其不会被调用到。
服务调用
在微服务架构中,通常存在多个服务之间远程调用的需求。木IQ铵主流远程调用技术有基于http的RESTful接口以及基于TCP的RPC协议
比较项 | RESTful | RPC |
---|
通讯协议 | HTTP | 一般使用TCP | 性能 | 略低 | 较高 | 灵活度 | 高 | 低 | 应用 | 微服务架构 | SOA架构 |
服务网关
随着微服务的不断增多,不同的微服务—般会有不同的网络地址,而外部客户端可能需要调用多个服务的接口才能完成一个业务需求,如果让客户端直接与各个微服务通信可能出现:
- 客户端需要调用不同的ur地址,增加难度
- ·在一定的场景下,存在跨域请求的问题
- 每个微服务都需要进行单独的身份认证
针对这些问题,AP网关顺势而生。API网关直面意思是将所有API调用统接入到API网关层,由网关层统接入和输出,是整个微服务的入口 。一个网关的基本功能有:统一接入、安全防护、协议适配、流量管控、长短链接支持、容错能力。
有了网关之后,各个API服务提供团队可以专注于自己的的业务逻辑处理,而API网关更专注于安全、流量、路由等问题。
服务容错
在微服务当中,一个请求经常会涉及到调用几个服务,如果其中某个服务不可用,没有做服务容错的话,极有可能会造成一连串的服务不可用,这就是雪崩效应。我们没法预防雪崩效应的发生,只能尽可能去做好容错。服务容错的三个核心思想是:
链路追踪
随着微服务架构的流行,服务按照不同的维度进行拆分,一次请求往往需要涉及到多个服务。互联网应用构建 在不同的软件模块集上,这些软件模块,有可能是由不同的团队开发、可能使用不同的编程语言来实现、有可能布 在了几千台服务器,横跨多个不同的数据中心。因此,就需要对—次请求涉及的多个服链路进行日志记录、性能 监控,即链路追踪。
微服务架构常见解决方案
Apache ServiceComb
Apache ServiceComb,前身是华为云的微服务引擎 CSE (Cloud Service Engine) 云服务,是全球 首个Apache微服务顶级项目。它提供了一站式的微服务开源解决方案,致力于帮助企业、用户和开发 者将企业应用轻松微服务化上云,并实现对微服务应用的高效运维管理。
SpringCloud
Spring Cloud是一系列框架的集合。它利用Spring Boot的开发便利性巧妙地简化了分布式系统基础设施的开发,如服务发现注册、配置中心、消息总线、负载均衡、断路器、数据监控等,都可以用Spring Boot的开发风格做到一键启动和部署。
Spring Cloud并没有重复制造轮子,它只是将目前各家公司开发的比较成熟、经得起实际考验的服务框架组合起来,通过Spring Boot风格进行再封装屏蔽掉了复杂的配置和实现原理,最终给开发者留出了一套简单易懂、易部署和易维护的分布式系统开发工具包。
Spring Cloud Alibaba
Spring Cloud Alibaba 致力于提供微服务开发的一站式解决方案。此项目包含开发分布式应用微服务的必需组件,方便开发者通过 Spring Cloud 编程模型轻松使用这些组件来开发分布式应用服务。
Spring Cloud Alibaba介绍
相关介绍
Spring Cloud Alibaba致力于提供微服务开发的一站式解决方案。此项目包含开发分布式应用微服务的必需组 件,方便开发者通过 Spring Cloud编程模型轻松使用这些组件来开发分布式应用服务。
依托 Spring Cloud Alibaba,只需要添加一些注解和少量配置,就可以将 Spring Cloud应用接入阿里微服务解决方案,通过阿里中间件来迅速搭建分布式应用系统。
主要功能
- 服务限流降级:默认支持 Web Servlet、WebFlux、 OpenFeign、RestTemplate、Spring Cloud Gateway
Zuul, Dubbo和 RocketMQ限流降级功能的接入,可以在运行时通过控制台实时修改限流降级规则,还支持 查看限流降级 Metrics监控 - 服务注册与发现:适配 Spring Cloud服务注册与发现标准,默认集成了Ribbon的支持。
- 分布式配置管理:支持分布式系统中的外部化配置,配置更改时自动刷新。
- 消息驱动能力:基于 Spring Cloud Stream为微服务应用构建消息驱动能力。
- 分布式事务:使用@GlobalTransactional注解,高效并且对业务零侵入地解决分布式事务问题。。
- 阿里云对象存储:阿里云提供的海量、安全、低成本、高可靠的云存储服务。支持在任何应用、任何时间、任
何地点存储和访问任意类型的数据 - 分布式任务调度:提供秒级、精准、高可靠、高可用的定时(基于Cron表达式)任务调度服务。同时提供分
布式的任务执行模型,如网格任务。网格任务支持海量子任务均匀分配到所有 Worker( schedulerx-client 上执行。 - 阿里云短信服务:覆盖全球的短信服务,友好、高效、智能的互联化通讯能力,帮助全业迅速搭建客户触达通
道
常用组件
- 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: 覆盖全球的短信服务,友好、高效、智能的互联化通讯能力,帮助企业迅速
搭建客户触达通道。
环境搭建
模块设计
springcloud-alibaba——父工程
shop-common——公共模块【实体类、工具类】
shop-user——用户微服务【端口:807X】
shop-product——商品微服务【端口:808×】
shop-order——订单微服务【端口:809x】
创建父工程
修改父工程的pom文件
<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/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.zrl</groupId>
<artifactId>springcloud-alibaba</artifactId>
<packaging>war</packaging>
<version>1.0-SNAPSHOT</version>
<repositories>
<repository>
<id>alimaven</id>
<name>aliyun maven</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
</repository>
</repositories>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.3.RELEASE</version>
</parent>
<properties> <java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF- 8</project.reporting.outputEncoding>
<spring-cloud.version>Greenwich.RELEASE</spring-cloud.version>
<spring-cloud-alibaba.version>2.1.0.RELEASE</spring-cloud-alibaba.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>
</dependencies>
</dependencyManagement>
</project>
版本对应关系
创建公共模块
创建的maven项目继承父工程,添加相关依赖
<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>
创建实体类
@Entity(name = "shop_user")
@Data
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer uid;
private String username;
private String password;
private String telephone;
}
@Entity(name = "shop_product")
@Data
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer pid;
private String pname;
private Double pprice;
private Integer stock;
}
@Entity(name = "shop_order")
@Data
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long oid;
private Integer uid;
private String username;
private Integer pid;
private String pname;
private Double pprice;
private Integer number;
}
创建用户微服务
创建maven项目,添加依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.zrl</groupId>
<artifactId>shop-common</artifactId>
</dependency>
</dependencies>
创建UserApplication.java启动文件
@SpringBootApplication
public class UserApplication {
public static void main(String[] args) {
SpringApplication.run(UserApplication.class);
}
}
在resources文件夹中创建application.yml 配置文件
服务治理
什么是服务治理
服务治理是微服务架构中最核心最基本的模块,用于实现各个微服务的自动化注册与发现。
- 服务注册:在服务治理框架中,都会构建一个注册中心,每个服务单元向注册中心登记自己提供服
务的详细信息,并在注册中心形成一张服务的清单。服务注册中心需要以心跳的方式去监测清单中 的服务是否可用,如果不可用,需要在服务清单中剔除不可用的服务 - 服务发现:服务调用方向服务注册中心咨询服务,并获取所有服务的实例清单,实现对具体服务实
例的访问。
什么是服务注册中心
通过上面的调用图会发现,除了微服务,还有一个组件是服务注册中心,它是微服务架构非常重要的一个组件,在微服务架构里主要起到了协调者的一个作用,一般包含如下几个功能:
-
服务发现:
- 服务注册:保存服务提供者和服务调用者的信息
- 服务订阅:服务调用者订阅服务提供者的信息,注册中心向订阅者推送提供者的信息
-
服务配置:
- 配置订阅:服务提供者和服务调用者订阅微服务相关的配置
- 配置下发:主动将配置推送给服务提供者和服务调用者
-
服务健康检测
- 检测服务提供者的健康情况,如果发现异常,执行服务剔除
常见的注册中心
-
Zookeeper:zookeeper是一个分布式服务框架,是Apache Hadoop 的一个子项目,它主要是用来解决分布式应用中经常遇到的一些数据管理问题,如:统一命名服务、状态同步服务、集群管理、分布式应用配置项的管理等。 -
Eureka:Eureka是Springcloud Netflflix中的重要组件,主要作用就是做服务注册和发现。但是现在已经闭源 -
Consul:Consul是基于GO语言开发的开源工具,主要面向分布式,服务化的系统提供服务注册、服务发现和配置管理的功能。Consul的功能都很实用,其中包括:服务注册/发现、健康检查、Key/Value存储、多数据中心和分布式一致性保证等特性。Consul本身只是一个二进制的可执行文件,所以安装和部署都非常简单,只需要从官网下载后,在执行对应的启动脚本即可。 -
Nacos:Nacos是一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。它是 SpringCloud Alibaba 组件之一,负责服务注册发现和服务配置,可以这样认为nacos=eureka+config。
Nacos实战
什么是Nacos
Nacos就是微服务架构中服务注册中心以及统一配置中心,用来替换原来的(eureka,consul)以及config组件,可以这样认为nacos=eureka+config
https://nacos.io/zh-cn/index.html
环境搭建
下载nacos:
https://github.com/alibaba/nacos/releases
目录介绍:
- bin 启动nacos服务的脚本目录
- conf nacos的配置文件目录
- target nacos的启动依赖存放目录
- data nacos启动成功后保存数据的目录
启动服务:
- linux/unix/mac启动
打开终端进入nacos的bin目录执行如下命令
./startup.sh -m standalone
- windows启动
在cmd中
执行 startup.cmd -m standalone 或者双击startup.cmd运行文件。
访问nacos的web服务管理界面:
- http://localhost:8848/nacos/
- 用户名 和 密码都是nacos
开发注册到Nacos
引入依赖:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
配置注册地址:
server.port=8789 #指定当前服务端口
spring.application.name=nacosclient #指定服务名称
spring.cloud.nacos.server-addr=localhost:8848 #指定nacos服务地址
spring.cloud.nacos.discovery.server-addr=${spring.cloud.nacos.server-addr} #指定注册中心地址
management.endpoints.web.exposure.include=* #暴露所有web端点
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
主类加上注解(新版本可不加):
@EnableDiscoveryClient
@SpringBootApplication
public class ShopProductApplication {
public static void main(String[] args) {
SpringApplication.run(ShopProductApplication.class, args);
}
}
查看服务列表:
使用Nacos进行服务调用
@Autowired
private DiscoveryClient discoveryClient;
@RequestMapping("/order/prod/{pid}")
public Object order(@PathVariable("pid") Integer pid) {
List<ServiceInstance> instances = discoveryClient.getInstances("service-product");
ServiceInstance serviceInstance = instances.get(0);
String host = serviceInstance.getHost();
int port = serviceInstance.getPort();
URI uri = serviceInstance.getUri();
Product product =
restTemplate.getForObject(uri+"/product/" + pid, Product.class);
log.info("查询到{}号商品的信息,内容是:{}", pid, JSON.toJSONString(product));
}
负载均衡
负载均衡的两种形式
- 服务端负载均衡:发生在服务提供者一方,比如常见的nginx负载均衡
- 客户端负载均衡(常用):发生在服务请求的一方,在发送请求之前已经选好了由哪个实例处理请求
自定义实现负载均衡
先配置一下idea,使其启动将同一个微服务启动两次
查看nacos服务列表,发现实例数为2了,酷酷的
使用random随机选择:
List<ServiceInstance> instances = discoveryClient.getInstances("service-product");
int index = new Random().nextInt(instances.size());
ServiceInstance serviceInstance = instances.get(index);
基于Ribbon实现负载均衡
什么是Ribbon
Ribbon是Spring Cloud的一个组件, 它可以让我们使用一个注解就能轻松的搞定负载均衡
使用Ribbon
第一步:在RestTemplate(ShopOrderApplication类中)的生成方法上添加@LoadBalanced 注解
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
第二步:修改服务调用的方法,不用再通过nacos获取实例了,通过服务名就OK
@Autowired
private DiscoveryClient discoveryClient;
@GetMapping("/order/prod/{pid}")
public Order order(@PathVariable("pid") Integer pid) {
Product product =
restTemplate.getForObject("http://service-product/product/" + pid, Product.class);
}
负载均衡策略
Ribbon默认使用轮询(RoundRobinRule)的负载均衡,常用的还有随机、最小并发等策略,支持的负载均衡策略如下:
策略名 | 策略描述 | 实现说明 |
---|
BestAvailableRule | 选择一个最小的并发请求的server | 逐个考察Server,如果Server被tripped了,则忽略,在选择其中ActiveRequestsCount最小的server | AvailabilityFilteringRule | 过滤掉那些因为一直连接失败的被标记为circuit tripped的后端server,并过滤掉那些高并发的的后端server(activeconnections 超过配置的阈值) | 使用一个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的负载均衡策略
在服务调用者上添加yml配置:
service-product:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
基于OpenFeign(粪)实现服务调用
现在存在的问题
- 代码可读性不好:我们的url是拼接的,修改起来不方便,阅读起来麻烦
- 编程风格不统一:我们的service是本地的,还要用RestTemplate调用远程服务
什么是OpenFeign
Feign是Spring Cloud提供的一个声明式的伪Http客户端, 它使得调用远程服务就像调用本地服务一样简单, 只需要创建一个接口并添加一个注解即可
Nacos很好的兼容了Feign, Feign默认集成了 Ribbon, 所以在Nacos下使用Fegin默认就实现了负载均衡的效果
使用OpenFeign
在服务调用者上引入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
在主类添加注解
@EnableFeignClients
@SpringBootApplication
public class ShopOrderApplication {}
编写调用接口
@FeignClient("service-product")
public interface ProductService {
@GetMapping("/product/{pid}")
Product findByPid(@PathVariable("pid") Integer pid);
}
修改controller的调用方式
@Autowired
private ProductService productService;
@GetMapping("/order/prod/{pid}")
public Order order(@PathVariable("pid") Integer pid) {
Product product = productService.findByPid(pid);
}
服务容错
什么是服务雪崩
在分布式系统中,由于网络原因或自身的原因,服务一般无法保证 100% 可用。如果一个服务出现了问题,调用这个服务就会出现线程阻塞的情况,此时若有大量的请求涌入,就会出现多条线程阻塞等待,进而导致服务瘫痪。
由于服务与服务之间的依赖性,故障会传播,会对整个微服务系统造成灾难性的严重后果,这就是服务故障的 “雪崩效应” 。
雪崩发生的原因多种多样,有不合理的容量设计,或者是高并发下某一个方法响应变慢,亦或是某台机器的资源耗尽。我们无法完全杜绝雪崩源头的发生,只有做好足够的容错,保证在一个服务发生问题,不会影响到其它服务的正常运行。也就是"雪落而不雪崩"。
常见的容错方案
常见的容错思路有隔离、超时、限流、熔断、降级这几种
将系统按照一定的原则划分为若干个服务模块,各个模块之间相对独立,无强依赖。当有故障发生时,能将问题和影响隔离在某个模块内部,而不扩散风险,不波及其它模块,不影响整体的系统服务。常见的隔离方式有:线程池隔离和信号量隔离
在上游服务调用下游服务的时候,设置一个最大响应时间,如果超过这个时间,下游未作出反应,就断开请求,释放掉线程。
限流就是限制系统的输入和输出流量已达到保护系统的目的。为了保证系统的稳固运行,一旦达到的需要限制的阈值,就需要限制流量并采取少量措施以完成限制流量的目的。
在互联网系统中,当下游服务因访问压力过大而响应变慢或失败,上游服务为了保护系统整体的可用性,可以暂时切断对下游服务的调用。这种牺牲局部,保全整体的措施就叫做熔断。
服务熔断一般有三种状态:
- 熔断关闭状态(Closed):服务没有故障时,熔断器所处的状态,对调用方的调用不做任何限制
- 熔断开启状态(Open):后续对该服务接口的调用不再经过网络,直接执行本地的fallback方法
- 半熔断状态(Half-Open):尝试恢复服务调用,允许有限的流量调用该服务,并监控调用成功率。如果成功率达到预期,则说明服务已恢复,进入熔断关闭状态;如果成功率仍旧很低,则重新进入熔断关闭状态。
降级其实就是为服务提供一个托底方案,一旦服务无法正常调用,就使用托底方案
常见的容错组件
Hystrix:Hystrix是由Netflflix开源的一个延迟和容错库,用于隔离访问远程系统、服务或者第三方库,防止级联失败,从而提升系统的可用性与容错性。
Resilience4J:Resilicence4J一款非常轻量、简单,并且文档非常清晰、丰富的熔断工具,这也是Hystrix官方推荐的替代产品。不仅如此,Resilicence4j还原生支持Spring Boot 1.x/2.x,而且监控也支持和prometheus等多款主流产品进行整合。
Sentinel:Sentinel 是阿里巴巴开源的一款断路器实现,本身在阿里内部已经被大规模采用,非常稳定。
Sentinel入门
什么是Sentinel
Sentinel (分布式系统的流量防卫兵) 是阿里开源的一套用于服务容错的综合性解决方案。它以流量为切入点, 从流量控制、熔断降级、系统负载保护等多个维度来保护服务的稳定性。
微服务集成Sentinel
放在服务调用者这里
添加依赖:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
添加配置:
spring:
cloud:
sentinel:
transport:
port: 9999
dashboard: localhost:8080
启动Sentinel控制台:
java -Dserver.port=8080 -Dcsp.sentinel.dashboard.server=localhost:8080 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard-1.7.0.jar
通过浏览器访问localhost:8080 进入控制台 ( 默认用户名密码是 sentinel/sentinel )
sentinel是懒加载,只有调用过微服务之后才会出现在sentinel中
Sentinel规则
流控规则
流量控制,其原理是监控应用流量的QPS(每秒查询率) 或并发线程数等指标,当达到指定的阈值时对流量进行控制,以避免被瞬时的流量高峰冲垮,从而保障应用的高可用性。
流控模式规则配置:
直接:接口达到限流条件时,开启限流
关联:被关联的资源达到限制时,该资源自身也限流,类似于让步的操作
链路:当从某个接口过来的资源达到限流条件时,开启限流,针对来源是针对微服务的,这个是更加细粒度的限流
使用链路限流的时候,在需要限流的方法上加上@SentinelResource("message") 注解,value是资源名称。SCA 2.1.1.RELEASE之后的版本,可以通过配置spring.cloud.sentinel.web-context-unify=false即可关闭收敛,否则链路限流不生效
流控效果规则配置:
- 快速失败(默认): 直接失败,抛出异常,不做任何额外的处理,是最简单的效果
- Warm Up:它从开始阈值到最大QPS阈值会有一个缓冲阶段,一开始的阈值是最大QPS阈值的1/3,然后慢慢增长,直到最大阈值,适用于将突然增大的流量转换为缓步增长的场景。
- 排队等待:让请求以均匀的速度通过,单机阈值为每秒通过数量,其余的排队等待; 它还会让设
置一个超时时间,当请求超过超时间时间还未处理,则会被丢弃。
降级规则
-
平均响应时间 :当资源的平均响应时间超过阈值(以 ms 为单位)之后,资源进入准降级状态。如果接下来 1s 内持续进入 5 个请求,它们的 RT都持续超过这个阈值,那么在接下的时间窗口(以 s 为单位)之内,就会对这个方法进行服务降级。 -
异常比例:当资源的每秒异常总数占通过量的比值超过阈值之后,资源进入降级状态,即在接下的时间窗口(以 s 为单位)之内,对这个方法的调用都会自动地返回。异常比率的阈值范围是 [0.0,1.0]。 -
异常数 :当资源近 1 分钟的异常数目超过阈值之后会进行服务降级。注意由于统计时间窗口是分钟级别的,若时间窗口小于 60s,则结束熔断状态后仍可能再进入熔断状态。
热点规则
热点参数流控规则是一种更细粒度的流控规则,它允许将规则具体到参数上。还是对方法添加注解,然后通过参数的索引进行限流,只能设置单秒访问量
在规则的编辑界面有参数例外项,可以对某些参数单独放行
授权规则(不常用)
根据来源进行限流,流控应用需要自己写,步骤如下:
写一个类,定义区分来源,本质作用是通过request域获取到来源标识,然后交给流控应用位置进行匹配
@Component
public class RequestOriginParserDefinition implements RequestOriginParser{
@Override
public String parseOrigin(HttpServletRequest request) {
String serviceName = request.getParameter("serviceName");
return serviceName;
}
}
然后在授权规则中的流控应用写serviceName进行限流
系统规则(不常用)
前面的规则都是基于资源的,这个是应用维度的
- Load(仅对 Linux/Unix-like 机器生效):当系统 load1 超过阈值,且系统当前的并发线程数超过
系统容量时才会触发系统保护。系统容量由系统的 maxQps * minRt 计算得出。设定参考值一般 是 CPU cores * 2.5。 - RT:当单台机器上所有入口流量的平均 RT 达到阈值即触发系统保护,单位是毫秒。
- 线程数:当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护。
- 入口 QPS:当单台机器上所有入口流量的 QPS 达到阈值即触发系统保护。
- CPU使用率:当单台机器上所有入口流量的 CPU使用率达到阈值即触发系统保护。
自定义规则异常返回界面
public class ExceptionHandlerPage implements UrlBlockHandler {
@Override
public void blocked(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, BlockException e) throws IOException {
httpServletResponse.setContentType("application/json;charset=utf-8");
ResponseData data = null;
if (e instanceof FlowException) {
data = new ResponseData(-1, "接口被限流了...");
} else if (e instanceof DegradeException) {
data = new ResponseData(-2, "接口被降级了...");
}
httpServletResponse.getWriter().write(JSON.toJSONString(data));
}
@Data
@AllArgsConstructor
@NoArgsConstructor
class ResponseData {
private int code;
private String message;
}
}
@SentinelResource的使用
在定义了资源点之后,我们可以通过Dashboard来设置限流和降级策略来对资源点进行保护。同时还能 通过@SentinelResource来指定出现异常时的处理策略。
例如定义限流和降级后的处理方法:
@Service
@Slf4j
public class OrderServiceImpl3 {
int i = 0;
@SentinelResource( value = "message",
blockHandler = "blockHandler",
fallback = "fallback"
)
public String message(String name) {
i++;
if (i % 3 == 0) {
throw new RuntimeException();
}return "message";
}
public String blockHandler(String name, BlockException ex) {
log.error("{}", ex);
return "接口被限流或者降级了...";
}
public String fallback(String name, Throwable throwable) {
log.error("{}", throwable);
return "接口发生异常了...";
}
}
@Service
@Slf4j
public class OrderServiceImpl3 {
int i = 0;
@SentinelResource( value = "message",
blockHandlerClass = OrderServiceImpl3BlockHandlerClass.class,
blockHandler = "blockHandler",
fallbackClass = OrderServiceImpl3FallbackClass.class,
fallback = "fallback" )
public String message() {
i++;
if (i % 3 == 0) {
throw new RuntimeException();
}
return "message4";
}
}
@Slf4j
public class OrderServiceImpl3BlockHandlerClass {
public static String blockHandler(BlockException ex) {
log.error("{}", ex);
return "接口被限流或者降级了...";
}
}
@Slf4j public class OrderServiceImpl3FallbackClass {
public static String fallback(Throwable throwable) {
log.error("{}", throwable);
return "接口发生异常了...";
}
}
Sentinel规则持久化
emmm代码有点多,就是写个配置类,再创建个文件夹就好,就不写在笔记里了
fegin整合sentinel实现服务容错
引入sentinel依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
配置fefin对sentinel的支持
feign:
sentinel:
enabled: true
创建容错类
@Component
@Slf4j
public class ProductServiceFallBack implements ProductService {
@Override public Product findByPid(Integer pid) {
Product product = new Product();
product.setPid(-1);
return product;
}
}
为被容器的接口指定容错类
@FeignClient(value = "service-product", fallback = ProductServiceFallBack.class)
public interface ProductService {
@RequestMapping("/product/{pid}")
Product findByPid(@PathVariable Integer pid);
}
如果想在容错类中拿到具体的错误也是可以的,指定fallbackFactory参数就好,注意fallback和fallbackFactory只能使用其中一种方式
@FeignClient( value = "service-product",
fallbackFactory = ProductServiceFallBackFactory.class)
public interface ProductService {
@RequestMapping("/product/{pid}")
Product findByPid(@PathVariable Integer pid);
}
@Component
public class ProductServiceFallBackFactory implements FallbackFactory<ProductService> {
@Override public ProductService create(Throwable throwable) {
return new ProductService() {
@Override
public Product findByPid(Integer pid) {
throwable.printStackTrace();
Product product = new Product();
product.setPid(-1);
return product;
}
};
}
}
服务网关
快速入门
基础版本
创建Spring Boot项目,引入gateway依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
添加配置信息
server:
port: 7000
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
- id: product_route
uri: http://localhost:8081
order: 1
predicates:
- Path=/product-serv/**
filters:
- StripPrefix=1
发起的请求是http://localhost:7000/product-serv/product/findall,如果不写StripPrefix则转发的是http://localhost:8081/product-serv/product/findall,写了就成了http://localhost:8081/product/findall
启动项目去访问网关地址
nacos增强版
引入依赖:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
主类添加注解:
@EnableDiscoveryClient
修改配置文件:
server:
port: 7000
spring:
application:
name: api-gateway
cloud:
nacos:
discovery:
server-addr: localhost:8848
gateway:
discovery:
locator:
enabled: true
routes:
- id: product_route
uri: lb://service-product
order: 1
predicates:
- Path=/product-serv/**
filters:
- StripPrefix=1
启动项目去访问网关地址
nacos简写版
去掉关于路由的配置:
server:
port: 7000
spring:
application:
name: api-gateway
cloud:
nacos:
discovery:
server-addr: localhost:8848
gateway:
discovery:
locator:
enabled: true
访问发现,只要按照网关地址/微服务名/接口 的格式去访问,就可以得到成功响应
Gateway执行流程
- Gateway Client向Gateway Server发送请求
- 请求首先会被HttpWebHandlerAdapter进行提取组装成网关上下文
- 然后网关的上下文会传递到DispatcherHandler,它负责将请求分发给RoutePredicateHandlerMapping
- RoutePredicateHandlerMapping负责路由查找,并根据路由断言判断路由是否可用
- 如果过断言成功,由FilteringWebHandler创建过滤器链并调用6. 请求会一次经过PreFilter–微服务–PostFilter的方法,最终返回响应
断言部分
内置路由断言工厂
-
基于Datetime的断言工厂 AfterRoutePredicateFactory: 接收一个日期参数,判断请求日期是否晚于指定日期 BeforeRoutePredicateFactory: 接收一个日期参数,判断请求日期是否早于指定日期 BetweenRoutePredicateFactory: 接收两个日期参数,判断请求日期是否在指定时间段内
-After=2019-12-31T23:59:59.789+08:00[Asia/Shanghai]
-
基于远程地址(ip段)的断言工厂
-RemoteAddr=192.168.1.1/24
-
基于Cookie的断言工厂
-Cookie=chocolate, 正则表达式
-
基于Header的断言工厂
-Header=X-Request-Id, \d+
-
基于Host的断言工厂
-Host=**.testhost.org
-
基于Method请求方法的断言工厂
-Method=GET
-
基于Path请求路径的断言工厂
-Path=/foo/{segment}
-
基于Query请求参数的断言工厂
-Query=baz, ba.
-
基于路由权重的断言工厂 WeightRoutePredicateFactory:接收一个[组名,权重],然后对于同一个组内的路由按照权重转发
-Weight=group3, 1
内置断言工厂的简单使用
server:
port: 7000
spring:
application:
name: api-gateway
cloud:
nacos:
discovery:
server-addr: localhost:8848
gateway:
discovery:
locator:
enabled: true
routes:
- id: product_route
uri: lb://service-product
order: 1
predicates:
- Path=/product-serv/**
- Before=2019-11-28T00:00:00.000+08:00
- Method=POST
filters:
- StripPrefix=1
自定义路由断言工厂
场景: 假设我们的应用仅仅让age在(min,max)之间的人来访问
在配置文件的predicates中,添加一个Age的断言配置:
- Age=18,60
自定义一个断言工厂, 实现断言方法
public class AgeRoutePredicateFactory extends AbstractRoutePredicateFactory<AgeRoutePredicateFactory.Config> {
public AgeRoutePredicateFactory() {
super(AgeRoutePredicateFactory.Config.class);
}
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("minAge", "maxAge");
}
public Predicate<ServerWebExchange> apply(AgeRoutePredicateFactory.Config config) {
@Override
public boolean test(ServerWebExchange serverWebExchange) {
String ageStr = serverWebExchange.getRequest().getQueryParams().getFirst("age");
if (StringUtils.isNotEmpty(ageStr)) {
int age = Integer.parseInt(ageStr);
if (age < config.getMaxAge() && age > config.getMinAge()) {
return true;
} else {
return false;
}
}
return false;
}
};
}
@Data
@NoArgsConstructor
public static class Config {
private int minAge;
private int maxAge;
}
}
测试:
http://localhost:7000/product-serv/product/1?age=30
过滤器部分
过滤器简介
在Gateway中, Filter的生命周期只有两个:“pre” 和 “post”。
Gateway 的Filter从作用范围可分为两种: GatewayFilter与GlobalFilter。
- GlobalFilter(左半拉):全局过滤器作用于所有路由,无需配置。通过全局过滤器可以实现对权限的统一校验,安全性验证等功能
- GatewayFilter(右半拉):应用到单个路由或者一个分组的路由上
局部过滤器
内置局部过滤器
https://cloud.spring.io/spring-cloud-static/spring-cloud-gateway/2.2.3.RELEASE/reference/html/#gatewayfilter-factories
spring:
cloud:
gateway:
routes:
- id: add_request_parameter_route
uri: https://example.org
filters:
- AddRequestParameter=red, blue
自定义局部过滤器
在配置文件中,添加一个Log的过滤器配置
spring:
cloud:
gateway:
routes:
- id: add_request_parameter_route
uri: https://example.org
filters:
- AddRequestParameter=true, false
自定义一个过滤器工厂,实现方法
public class LogGatewayFilterFactory
extends AbstractGatewayFilterFactory<LogGatewayFilterFactory.Config> {
public LogGatewayFilterFactory() {
super(LogGatewayFilterFactory.Config.class);
}
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("consoleLog", "cacheLog");
}
@Override
public GatewayFilter apply(LogGatewayFilterFactory.Config config) {
return new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
if (config.isCacheLog()) {
System.out.println("cacheLog已经开启了....");
}
if (config.isConsoleLog()) {
System.out.println("consoleLog已经开启了....");
}
return chain.filter(exchange);
}
};
}
@Data
@NoArgsConstructor
public static class Config {
private boolean consoleLog;
private boolean cacheLog;
}
}
还有另外一种方式,实现Gateway接口和Orderd接口,什么注解都不加,然后实现里面的方法,最后通过定义配置类的形式进行配置
全局过滤器
内置全局过滤器
SpringCloud Gateway内部也是通过一系列的内置全局过滤器对整个路由,通常情况下,Spring Cloud的内置局部过滤器已经够我们使用了,我们开发更多的是全局过滤器,统一鉴权啥的
自定义全局过滤器
内置的过滤器已经可以完成大部分的功能,但是对于企业开发的一些业务功能处理,还是需要我们自己编写过滤器来实现的,那么我们一起通过代码的形式自定义一个过滤器,去完成统一的权限校验。
- 当客户端第一次请求服务时,服务端对用户进行信息认证(登录)
- 认证通过,将用户信息进行加密形成token,返回给客户端,作为登录凭证
- 以后每次请求,客户端都携带认证的token
- 服务端对token进行解密,判断是否有效
@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String token = exchange.getRequest().getQueryParams().getFirst("token");
if (!StringUtils.equals(token, "admin")) {
System.out.println("鉴权失败");
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 0;
}
}
跨域问题
什么是跨域问题
跨域:域名不一致就是跨域,主要包括:
跨域问题:浏览器禁止请求的发起者与服务端发生跨域ajax请求,请求被浏览器拦截的问题
解决方案:CORS,这个以前应该学习过,这里不再赘述了。不知道的小伙伴可以查看https://www.ruanyifeng.com/blog/2016/04/cors.html
Gateway解决跨域问题
spring:
cloud:
gateway:
globalcors:
add-to-simple-url-handler-mapping: true
corsConfigurations:
'[/**]':
allowedOrigins:
- "http://localhost:8090"
allowedMethods:
- "GET"
- "POST"
- "DELETE"
- "PUT"
- "OPTIONS"
allowedHeaders: "*"
allowCredentials: true
maxAge: 360000
网关限流
网关是所有请求的公共入口,所以可以在网关进行限流,而且限流的方式也很多,我们本次采用前面学过的Sentinel组件来实现网关的限流。Sentinel支持对SpringCloud Gateway、Zuul等主流网关进行限流。
从1.6.0版本开始,Sentinel提供了SpringCloud Gateway的适配模块,可以提供两种资源维度的限流:
- route维度:即在Spring配置文件中配置的路由条目,资源名为对应的routeId
- 自定义API维度:用户可以利用Sentinel提供的API来自定义一些API分组
路由维度
导入依赖:
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-spring-cloud-gateway-adapter</artifactId>
</dependency>
编写配置类:
基于Sentinel 的Gateway限流是通过其提供的Filter来完成的,使用时只需注入对应的SentinelGatewayFilter实例以及 SentinelGatewayBlockExceptionHandler 实例即可
@Configuration
public class GatewayConfiguration {
private final List<ViewResolver> viewResolvers;
private final ServerCodecConfigurer serverCodecConfigurer;
public GatewayConfiguration(ObjectProvider<List<ViewResolver>> viewResolversProvider,
ServerCodecConfigurer serverCodecConfigurer) {
this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
this.serverCodecConfigurer = serverCodecConfigurer;
}
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public GlobalFilter sentinelGatewayFilter() {
return new SentinelGatewayFilter();
}
@PostConstruct
public void initGatewayRules() {
Set<GatewayFlowRule> rules = new HashSet<>();
rules.add(
new GatewayFlowRule("product_route")
.setCount(1)
.setIntervalSec(1)
);
GatewayRuleManager.loadRules(rules);
}
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
}
@PostConstruct
public void initBlockHandlers() {
BlockRequestHandler blockRequestHandler = new BlockRequestHandler() {
@Override
public Mono<ServerResponse> handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) {
Map map = new HashMap<>();
map.put("code", 0);
map.put("message", "接口被限流了");
return ServerResponse.status(HttpStatus.OK).
contentType(MediaType.APPLICATION_JSON_UTF8).
body(BodyInserters.fromObject(map));
}
};
GatewayCallbackManager.setBlockHandler(blockRequestHandler);
}
}
自定义API维度
@Configuration
public class GatewayConfiguration {
private final List<ViewResolver> viewResolvers;
private final ServerCodecConfigurer serverCodecConfigurer;
public GatewayConfiguration(ObjectProvider<List<ViewResolver>> viewResolversProvider,
ServerCodecConfigurer serverCodecConfigurer) {
this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
this.serverCodecConfigurer = serverCodecConfigurer;
}
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public GlobalFilter sentinelGatewayFilter() {
return new SentinelGatewayFilter();
}
@PostConstruct
public void initGatewayRules() {
Set<GatewayFlowRule> rules = new HashSet<>();
rules.add(new GatewayFlowRule("product_api1").setCount(1).setIntervalSec(1));
rules.add(new GatewayFlowRule("product_api2").setCount(1).setIntervalSec(1));
GatewayRuleManager.loadRules(rules);
}
@PostConstruct
private void initCustomizedApis() {
Set<ApiDefinition> definitions = new HashSet<>();
ApiDefinition api1 = new ApiDefinition("product_api1")
.setPredicateItems(new HashSet<ApiPredicateItem>() {{
add(new ApiPathPredicateItem().setPattern("/product-serv/product/api1/**").
setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX));
}});
ApiDefinition api2 = new ApiDefinition("product_api2")
.setPredicateItems(new HashSet<ApiPredicateItem>() {{
add(new ApiPathPredicateItem().setPattern("/product-serv/product/api2/demo1"));
}});
definitions.add(api1);
definitions.add(api2);
GatewayApiDefinitionManager.loadApiDefinitions(definitions);
}
@PostConstruct
public void initBlockHandlers() {
BlockRequestHandler blockRequestHandler = new BlockRequestHandler() {
@Override
public Mono<ServerResponse> handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) {
Map map = new HashMap<>();
map.put("code", 0);
map.put("message", "接口被限流了");
return ServerResponse.status(HttpStatus.OK).
contentType(MediaType.APPLICATION_JSON_UTF8).
body(BodyInserters.fromObject(map));
}
};
GatewayCallbackManager.setBlockHandler(blockRequestHandler);
}
}
使用局部限流过滤器
RequestRateLimter,内部要使用redis,https://www.bilibili.com/video/BV1Pv41187Er?p=11
整合sentinel
https://www.bilibili.com/video/BV1Pv41187Er?p=13
链路追踪
Sleuth入门
所有微服务都添加依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
调用某个服务的接口,控制台打印:
其中5399d5cb061971bd 是TraceId, 5399d5cb061971bd 是SpanId,依次调用有一个全局的TraceId,将调用链路串起来,false 表示是否将最终结果输出到第三方平台。
查看日志文件并不是一个很好的方法,当微服务越来越多日志文件也会越来越多,通过Zipkin可以将日志聚合,并进行可视化展示和全文检索
Zipkin入门
下载ZipKin服务端的jar包:
https://search.maven.org/remote_content?g=io.zipkin.java&a=zipkin-server&v=LATEST&c=exec
通过命令行,输入下面的命令启动ZipKin Server
java -jar zipkin-server-2.12.9-exec.jar
通过浏览器访问 http://localhost:9411访问
每个微服务导入依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
每个微服务添加配置:
spring:
zipkin:
base-url: http://127.0.0.1:9411/
discoveryClientEnabled: false
sleuth:
sampler:
probability: 1.0
访问微服务,并观察zipkin的ui界面
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
elasticsearch持久化就先启动elasticsearch,然后启动的时候添加一些配置
java -jar zipkin-server-2.12.9-exec.jar --STORAGE_TYPE=elasticsearch --ES- HOST=localhost:9200
服务配置
背景介绍
目前存在的问题:
- 配置文件相对分散。在一个微服务架构下,配置文件会随着微服务的增多变的越来越多,而且分散在各个微服务中,不好统一配置和管理。
- 配置文件无法区分环境。微服务项目可能会有多个环境,例如:测试环境、预发布环境、生产环境。每一个环境所使用的配置理论上都是不同的,一旦需要修改,就需要我们去各个微服务下手动维护,这比较困难。
- 配置文件无法实时更新。我们修改了配置文件之后,必须重新启动微服务才能使配置生效,这对一
个正在运行的项目来说是非常不友好的。
配置中心的思路是:
- 首先把项目中各种配置全部都放到一个集中的地方进行统一管理,并提供一套标准的接口
- 当各个服务需要获取配置的时候,就来配置中心的接口拉取自己的配置
- 当配置中心中的各种参数有更新的时候,也能通知到各个服务实时的过来同步最新的信息,使之动态更新
nacos入门
搭建nacos服务端环境:
启动bin目录下的startup即可
引入依赖:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
添加配置:
spring:
application:
name: service-product
cloud:
nacos:
config:
server-addr: localhost:8848
namespace: public
discovery:
server-addr: localhost:8848
profiles:
active: dev
配置文件优先级:bootstrap.properties > bootstrap.yml > application.properties > application.yml
在nacos添加统一配置:
Data ID格式:服务名+环境+文件格式
配置动态刷新
在统一配置中添加如下内容:
config:
appName: product
方式一:自行获取
@RestController
public class NacosConfigController {
@Autowired
private ConfigurableApplicationContext applicationContext;
@RequestMapping("/test-config1")
public String testConfig1() {
return applicationContext.getEnvironment().getProperty("config.appName");
}
}
方式二:注解形式(推荐)
@RestController
@RefreshScope
public class NacosConfigController {
@Value("${config.appName}")
private String appName;
@RequestMapping("/test-config2")
public String testConfig2() {
return appName;
}
}
配置共享
|