分布式概念、微服务架构
分布式
分布式系统是若干独立计算机的集合,这些计算机对于用户来说就像单个相关系统。
分布式系统是建立在网络之上的软件系统。
分布式和集群的关系
集群指的是将几台服务器几种在一起,实现同一个业务。
分布式中的每一个节点,都可以做集群。而集群不一定就是分布式。
软件架构演变
单一应用架构->垂直应用架构->分布式服务架构->流动计算架构
RPC是什么
RPC是指远程过程调用,是一种进程间通信方式,他是一种技术的思想,而不是规范。
它允许程序调用另一个地址空间(通常是共享网络的另一台机器上)的过程或者韩式,而不用程序员显示编码这个远程调用的细节。
即程序员无论是调用本地的还是远程的函数,本质上编写的调用代码基本相同。
服务之间的交互可以用两种方式
- RPC
- Netty(Socket)+自定义序列化
- RestAPI(严格来说,SpringCloud是使用Rest方式进行服务之间的交互的,不属于RPC)
- HTTP+JSON
分布式思想
高并发
-
通过设计保证系统可以并行处理很多请求,应对大量流量和请求
-
Tomcat最多支持并发数?
Tomcat默认配置的最大请求数是150,也就是说同时支持150个并发,当然,我们也可以把他改大。
当某个应用拥有250个以上并发的时候,应该考虑服务器的集群。
具体能承载多少并发,需要看硬件的配置,CPU越多性能越高,分配给JVM的内存越多性能也就越高,但也会加重GC的负担。
-
操作系统对于进程中的线程数有一定的限制:
windows每个进程中的线程数不允许超过2000
Linux每个进程中的线程数不允许超过1000
另外,在Java中每开启一个线程需要耗用1MB的JVM内存空间作为线程栈使用。
Tomcat默认的HTTP实现是采用阻塞式的Socket通信,每个请求都需要创建一个线程处理,这种模式下的并发量受到线程数的限制,但对于Tomcat来说几乎就没有BUG存在了。
Tomcat还可以配置NIO方式的Socket通信,在性能上高于阻塞式的,每个请求也不需要创建一个线程进行处理,并发能力比前者高,但没有阻塞式的成熟。
这个并发能力还和应用的逻辑密切相关,如果逻辑很复杂需要大量计算,那么并发能力势必会下降。如果每个请求都含有很多的数据库操作,那么对于数据库的性能也是非常高的。
对于单台数据库服务器来说,允许客户端的连接数量是有限制的。
并发能力问题设计真个系统架构和业务逻辑。
系统环境不同,Tomcat版本不同,以及修改的设定参数不同。并发量的差异还是很大的。
-
-
高并发衡量指标
- 响应时间(RT)
- 请求做出相应的事件,即一个http请求返回所用的时间
- 吞吐量
- 系统在单位时间内处理请求的数量
- QPS、TPS
- 每秒查询(请求)数、每秒事务数
- 专业测试工具:Load Runner
- Apache ab
- Apache JMeter
- 并发用户数
- 承载的正常使用系统功能的用户的数量
- 响应时间(RT)
高可用
服务集群部署
数据库主从+双机热备
-
主-备方式
主备方式指的是一台服务器处于某种业务的激活状态(即Active状态),另一台服务器处于该业务的备用状态(即Standby状态)。
-
双主机方式
双主机方式指的是两种不同业务分别在两台服务器上互为主备状态。
服务雪崩
服务之间复杂调用,一个服务不可用,导致整个系统受影响不可用。
熔断
某个服务频繁超时,直接将其短路,快速返回mock(虚拟)值。
限流
限制某个服务每秒的调用本服务的频率。(防爬虫,洪水攻击)
弹性云
Elastic Compute Service(ECS)弹性计算服务
动态扩容,压榨服务器闲时能力
例如:双11,618,高峰时多配置些服务器,平时减少多余的服务器配置(用于其他服务应用),避免资源浪费。
微服务架构
本篇springcloud版本选择
- cloud Hoxton.SR9
- boot 2.3.6.RELEASE
- cloud Alibaba 2.2.6.RELEASE
- java JAVA8
- maven 3.5及以上
- mysql 5.7及以上
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.dyy.springcloud</groupId>
<artifactId>cloud2021</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<!-- 统一管理jar包版本 -->
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<junit.version>4.12</junit.version>
<log4j.version>1.2.17</log4j.version>
<lombok.version>1.16.18</lombok.version>
<mysql.version>5.1.47</mysql.version>
<druid.version>1.1.16</druid.version>
<mybatis.spring.boot.version>1.3.0</mybatis.spring.boot.version>
</properties>
<!-- 子模块继承之后,提供作用:锁定版本+子modlue不用写groupId和version -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.3.6.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR9</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.2.6.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>${druid.version}</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis.spring.boot.version}</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>${log4j.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<optional>true</optional>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<fork>true</fork>
<addResources>true</addResources>
</configuration>
</plugin>
</plugins>
</build>
</project>
SpringCloud和SpringBoot之间的依赖关系
版本对应查看方法: start.spring.io/actuator/in…
yml文件示范
server:
port: 8001
spring:
application:
name: cloud-payment-service
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/cloud2021?useUnicode=true&characterEncoding=utf-8&useSSL=false
username: root
password: root
mybatis:
mapperLocations: classpath:/mapper/*.xml
type-aliases-package: com.dyy.springcloud.entities
统一发送消息格式的类,需要可序列化,此处使用了lombok注解,可在idea插件中下载(下载后重启才能生效)。
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CommonResult <T> implements Serializable{
private Integer code;
private String message;
private T data;
public CommonResult(Integer code,String message){
this(code,message,null);//如果这行报错,请安装lombok插件
}
}
dao层
import com.dyy.springcloud.entities.Payment;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
@Component //代替@Repository声明bean
@Mapper //mybatis提供的,等价:@MapperScan("com.dyy.springcloud.dao")
//@Repository //spring提供的。在此,只是为了声明bean对象
public interface PaymentDao {
public int create(Payment payment);
public Payment getPaymentById(@Param("id") Long id);
}
service层(实现类)
import com.dyy.springcloud.dao.PaymentDao;
import com.dyy.springcloud.entities.Payment;
import com.dyy.springcloud.service.PaymentService;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Service
public class PaymentServiceImpl implements PaymentService {
@Resource
//@Autowired
private PaymentDao paymentDao;
public int create(Payment payment){
return paymentDao.create(payment);
}
public Payment getPaymentById( Long id){
return paymentDao.getPaymentById(id);
}
}
controller调用service层方法
import com.dyy.springcloud.entities.CommonResult;
import com.dyy.springcloud.entities.Payment;
import com.dyy.springcloud.service.PaymentService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
@Slf4j
public class PaymentController {
@Resource
private PaymentService paymentService;
@PostMapping(value = "/payment/create")
public CommonResult<Payment> create(Payment payment){ //埋雷
int result = paymentService.create(payment);
log.info("*****插入结果:"+result);
if (result>0){ //成功
return new CommonResult(200,"插入数据库成功",result);
}else {
return new CommonResult(444,"插入数据库失败",null);
}
}
@GetMapping(value = "/payment/get/{id}")
public CommonResult<Payment> getPaymentById(@PathVariable("id") Long id){
Payment payment = paymentService.getPaymentById(id);
log.info("*****查询结果:"+payment);
if (payment!=null){ //说明有数据,能查询成功
return new CommonResult(200,"查询成功",payment);
}else {
return new CommonResult(444,"没有对应记录,查询ID:"+id,null);
}
}
}
@Resource注解和@Autowired作用相同,自动装配,前提是有这个bean才能装配成功。
启动主启动类运行项目。
controller的映射可以通过软件postman进行测试。
热部署
依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
pom的插件
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<fork>true</fork>
<addResources>true</addResources>
</configuration>
</plugin>
</plugins>
</build>
修改setting
Ctrl+Shift+Alt+/选择Registry…
弹出窗口:
compiler.automake.allow.when.app.running -> 自动编译
compile.document.save.trigger.delay -> 自动更新文件;它主要是针对静态文件如JS CSS的更新,将延迟时间减少后,直接按F5刷新页面就能看到效果!
重启idea生效。
注册中心:Eureka
已经停止更新,当微服务数量达到几百时会报错,现在更推荐使用Nacos。
服务治理
SpringCloud封装了Netflix公司开发的Eureka模块来实现服务治理。
在传统的RPC远程调用框架中,管理每个服务和服务之间依赖关系比较复杂、所以需要进行服务治理,管理服务与服务之间依赖关联,以实现服务调用,负载均衡、容错等,实现服务发现与注册。
服务注册
Eureka采用了CS的设计架构,Eureka Server作为服务注册功能的服务器,他是服务注册中心。
而系统中的其他微服务,使用Eureka的客户端连接到Eureka Server并维持心跳连接。这样系统的维护人员可以通过Eureka Server来监控系统中各个微服务是否正常运行。
RPC远程嗲用框架核心设计思想:在于注册中心,因为注册中心管理每个服务和服务之间的一个依赖关系(服务治理概念)。
在任何RPC远程框架中,都会有一个注册中心(存放服务地址相关信息(接口地址))。

Eureka两组件
Eureka Server提供服务注册服务
各个微服务节点通过配置启动后,会在Eureka Server中进行注册,这样Eureka Server中的服务注册表中将会存储所有可用服务节点的信息,服务节点的信息可以在界面中直观看到。
Eureka Client通过中心进行访问
是一个Java客户端,用于简化Eureka Server的交互,客户端同时也具备一个内置的、使用轮询(round-robin)负载算法的负载均衡器。在应用启动后,将会在Eureka Server发送心跳(默认周期30秒)。如果Eureka Server在多个心跳周期内没有收到某个节点的心跳,Eureka Server将会从服务注册表中把这个服务节点移出(默认90秒)
Eureka示例
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
server:
port: 7001
eureka:
instance:
hostname: localhost
client:
register-with-eureka: false
fetchRegistry: false
service-url:
defaultZone: http://localhost:7001/eureka
Eureka主启动类注解
@EnableEurekaServer
通过主启动类启动后访问 http://localhost:7001/
Eureka服务提供者和消费者示例
提供者消费者依赖配置均相同,都要注册到Eureka注册中心。
依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
yml,端口配置略(不能冲突即可)。
eureka:
client:
register-with-eureka: true
fetchRegistry: true
service-url:
defaultZone: http://localhost:7001/eureka
主启动类注解都是@EnableEurekaClient
启动顺序:
先启动EurekaServer,7001服务,再启动提供者和消费者。
可以打开http://localhost:7001/eureka看是否注册成功。
再访问控制器,看远程调用是否成功。
负载均衡:Ribbon
Ribbon的主要功能是提供客户端的软件负载均衡算法和服务调用。
Ribbon提供连接超时、重试等配置项。简单来说,就是在配置文件中列出Load Balancer(LB,负载均衡)后面的所有机器,Ribbon会自动的帮助你基于某种规则(如简单轮询,随机连接等)去连接这些机器。
作用
负载均衡(LB)
- 将用户的请求平均分配到多个服务器上,从而达到系统的高可用。
- 常见的负载均衡软件Nginx,LVS,硬件F5等。
- Ribbon的本地负载均衡客户端 和 Nginx服务端负载的区别:
- Nginx是服务端负载均衡,客户端所有请求都会交给Nginx,然后,由Nginx实现转发请求。负载均衡是服务器端完成的。
- Ribbon本地负载均衡,在调用微服务接口时候,会在注册中心上获取注册信息服务列表之后缓存到JVM本地,从而在本地实现RPC远程服务调用。
- 集中式负载均衡。就是在服务的消费方和提供方之间使用独立的负载均衡设施(可以是硬件,如F5,也可以是软件,如Nginx),由该设施负责把访问请求通过某种策略转发至服务的提供方。
- 进程内负载均衡。将负载均衡逻辑集成到消费方,消费方从服务注册中心获知有哪些地址可用,然后自己再从这些地址中选择出一个合适的服务器。Ribbon就属于进程内负载均衡,它只是一个类库,集成于消费方进程,消费方通过它来获取到服务提供方的地址。
总之:
Ribbon = 负载均衡 + RestTemplate调用
工作原理
第一步,先选择EurekaServer,它优先选择在同一个区域内负载比较少的server。
第二部,再根据用户指定的策略,再从server渠道的服务注册列表中选择一个地址,其中Ribbon提供了多种策略。比如:轮询、随机和根据响应时间加权。
总结:
Ribbon其实就是一个软负载均衡的客户端组件的客户端组件,它可以和其他所需请求的客户端结合使用,和Eureka结合只是其中的一个实例。
使用
依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
这个不需要手动引用,Eureka客户端自带Ribbon。
Ribbon核心组件IRule
com.netflix.loadbalancer.IRule
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package com.netflix.loadbalancer;
public interface IRule {
Server choose(Object var1);
void setLoadBalancer(ILoadBalancer var1);
ILoadBalancer getLoadBalancer();
}
IRule:根据特定算法从服务列表中选取一个要访问的服务
- com.netflix.loadbalancer.RoundRobinRule 轮询,默认策略
- com.netflix.loadbalancer.RandomRule 随机
- com.netflix.loadbalancer.RetryRule 先按照RoundRobinRule的策略获取服务,如果获取服务失败则在指定时间内会进行重试,获取可用的服务
- WeightedResponseTimeRule 对RoundRobinRule的扩展,响应速度越快的实例选择权重越大,越容易被选择
- BestAvailableRule 会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,然后选择一个并发量最小的服务
- AvailabilityFilteringRule 先过滤故障实例,再选择并发较小的实例
- ZoneAvoidanceRule 默认规则,复合判断server所在区域的性能和server的可用性选择服务器。
如何修改策略
官方文档明确给出警告:
自定义策略类
@Configuration
public class MySelfRule {
@Bean
public IRule myRule(){
return new RandomRule();//定义为随机
}
}
主启动类
@EnableEurekaClient
@SpringBootApplication
@RibbonClient(name = "CLOUD-PAYMENT-SERVICE",configuration = MySelfRule.class)
public class OrderMain80 {
public static void main(String[] args) {
SpringApplication.run(OrderMain80.class,args);
}
}
使用注意!!!
这个自定义配置类不能放在@ComponentScan所扫描的当前包以及子包下,否则我们自定义的这个被之类就会被所有的Ribbon客户端所共享,达不到特殊化定制的目的了。
Ribbon负载均衡算法
原理
负载均衡算法:
rest接口第几次请求数 % 服务器集群总数量 = 实际调用服务器位置下标,每次服务重启动后rest接口计数从1开始
List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMANT-SERVICE");
比如:
List[0] instances = 127.0.0.1:8002
List[1] instances = 127.0.0.1:8001
8001和8002组合成为集群,他们共计2台机器,集群总数为2,按照轮询算法原理:
当总请求数为1:1%2=1对应下标位置为1,那么获得服务地址为127.0.0.1:8001
当总请求数为2:2%2=0对应下标位置为0,那么获得服务地址为127.0.0.1:8002
当总请求数为3:3%2=1对应下标位置为1,那么获得服务地址为127.0.0.1:8001
以此类推...
服务接口调用:OpenFeign
是什么
是一个声明式的web服务客户端,让编写web服务客户端变得非常容易,只需要创建一个接口并在接口上添加注解即可。
SpringCloud对Feign进行了封装使其支持了SpringMVC标准注解和HttpMessageConverters。Feign可以和Eureka和Ribbon组合使用以支持负载均衡。
官方文档 docs.spring.io/spring-clou…
作用
目的是使用编写Java Http客户端变得更容易。
前面在使用Ribbon+RestTemplate时,利用RestTemplate对Http请求的封装处理,形成了一套模板化的调用方法。
但是在实际开发中,由于对服务依赖的调用可能不止一处,往往一个接口会被多出调用,所以通常都会针对每个微服务自行封装一些客户端类包装这些依赖服务端调用。所以,Feign在此基础上做了进一步封装,由它来帮我们定义和实现依赖服务接口的定义。
在Feign的实现下,我们只需要创建一个接口并使用注解的方式来配置它(以前是在DAO接口上面标注Mapper注解,现在是一个微服务接口上面标注一个Feign注解即可),就可以完成对服务提供方的接口绑定,简化了使用SpringCloud Ribbon时,自动封装服务调用客户端的开发量。
-
Feign继承了Ribbon,利用Ribbon维护了Payment的服务列表信息,并且通过轮询实现了客户端的负载均衡。和Ribbon不同的是,通过Feign只需要定义服务绑定接口并且以声明式的方法,优雅而简单的实现了服务调用。
-
Feign和Open Feign两者的区别
Feign OpenFeign Feign是Spring Cloud组件中的一个轻量级RESTful的HTTP服务客户端,Feign内置了Ribbon,用来做客户端负载均衡,去调用服务注册中心服务,Feign的使用方式是:使用Feign的注解定义接口,调用这个接口,就可以调用服务注册中心的服务。 OpenFeign是Spring Cloud在Feign的基础上支持了SpringMVC的注解,比如@RequestMapping等。OpenFeign的@FeignClient可以解析SpringMVC的@Request Mapping注解下的接口,并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务 <dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-feign</artifactId></denpendency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId></denpendency>
接口、注解
微服务调用接口,@FeignClient
注意OpenFeign也自带Ribbon。
例子
server:
port: 80
spring:
application:
name: cloud-consumer-feign-order80
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://localhost:7001/eureka
主启动类
package com.dyy.springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
public class OrderFeignMain80 {
public static void main(String[] args) {
SpringApplication.run(OrderFeignMain80.class,args);
}
}
业务类
import com.dyy.springcloud.entities.CommonResult;
import com.dyy.springcloud.entities.Payment;
import feign.Param;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@Component
@FeignClient(value = "CLOUD-PAYMENT-SERVICE")
public interface PaymentFeignService {
@GetMapping(value = "/payment/get/{id}")
public CommonResult getPaymentById(@PathVariable("id") Long id);
}
控制器
import com.dyy.springcloud.entities.CommonResult;
import com.dyy.springcloud.entities.Payment;
import com.dyy.springcloud.service.PaymentFeignService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
public class OrderFeignController {
@Resource
private PaymentFeignService paymentFeignService; //调用远程的微服接口
@GetMapping(value = "/consumer/payment/get/{id}")
public CommonResult<Payment> getPaymentById(@PathVariable("id") Long id){
return paymentFeignService.getPaymentById(id);
}
}
工作过程
超时控制
超时演示
-
服务提供方故意写暂停程序
@GetMapping(value = "/payment/feign/timeout") public String paymentFeignTimeout(){ try { TimeUnit.SECONDS.sleep(3); }catch (Exception e) {e.printStackTrace();} //单位秒 return serverPort; } -
服务消费方80在service接口调用超时方法
@GetMapping(value = "/payment/feign/timeout") public String paymentFeignTimeout(); -
服务消费方80添加映射
@GetMapping(value = "/consumer/payment/feign/timeout") public String paymentFeignTimeout(){ return paymentFeignService.paymentFeignTimeout(); } -
访问http://localhost/consumer/payment/feign/timeout(默认访问端口80),发现出错
这是因为OpenFeign默认等待一秒,而提供方的方法要处理要超过1秒,就会导致超时,OpenFeign直接报错。
为了避免这样的情况,有时我们需要设置Feign客户端的超时控制,也就是Ribbon的超时时间,因为Feign集成了Ribbon进行负载均衡。
YML开启OpenFeign客户端超时控制
使用Feign调用接口分两层,ribbon 的调用和hystrix的调用,所以ribbon的超时时间和Hystrix的超时时间结合就是Feign的超时时间。
#设置Feign客户端超时时间(openfeign默认支持ribbon)
ribbon:
ReadTimeout: 3000
ConnectTimeout: 3000
MaxAutoRetries: 1 #同一台实例最大重试次数,不包括首次调用
MaxAutoRetriesNextServer: 1 #重试负载均衡其他的实例最大重试次数,不包括首次调用
OkToRetryOnAllOperations: false #非Get请求是否重试
#hystrix的超时时间
hystrix:
command:
default:
execution:
timeout:
enabled: true
isolation:
thread:
timeoutInMilliseconds: 9000
一般情况下,都是ribbon的超时时间小于hystrix的超时时间(因为涉及到ribbon的重试机制)
这是因为ribbon的重试机制和Feign的重试机制有冲突,所以在源码中是默认关闭Feign的重试机制的。
要开启Feign的重试机制如下:
@Bean
Retryer feignRetryer() {
return new Retryer.Default();
}
根据上面的参数计算重试的次数:
MaxAutoRetries+MaxAutoRetriesNextServer+(MaxAutoRetries *MaxAutoRetriesNextServer) 即重试3次 则一共产生4次调用
如果在重试期间,时间超过了Hystrix的超时时间,便会立即执行熔断,fallback。所以要根据上面配置的参数计算Hystrix的超时时间,使得在重试期间不能达到Hystrix的超时时间,否则重试机制就没有意义了。
*Hystrix超时时间的计算:(1 + MaxAutoRetries + MaxAutoRetriesNextServer) * ReadTimeout 即按照以上的配置 hystrix的超时时间应该配置为 (1+1+1)3=9秒
当ribbon超时后且hystrix没有超时,就会采取重试机制。当OkToRetryOnAllOperations设置为false时,只会对get请求进行重试。如果设置为true,便会对所有的请求进行重试,如果是put或者post等写操作,如果服务器接口没有做幂等性,会产生不好的结果,所以OkToRetryOnAllOperations慎用。
如果不配置ribbon的重试次数,默认重试一次。
注意!!!
默认情况下,GET方式请求无论是连接异常还是读取异常,都会进行重试。
非GET方式请求,只有连接异常时,才会进行重试。
日志打印功能
Feign提供了日志打印功能,我们可以通过配置来调整日志级别,从而了解Feign中Http请求的细节。通俗讲就是对Feign接口的调用情况进行监控和输出。
日志级别
NONE:默认的,不显示任何日志
BASIC:仅记录请求方法、URL、响应状态码一级执行时间。
HEADERS:除了BASIC中定义的信息之外,还有请求和响应的头信息
FULL:除了HEADERS中定义的信息之外,好友请求和响应的正文以及元数据。
配置日志bean
import feign.Logger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FeignConfig {
@Bean
public Logger.Level feignLoggerLevel(){
return Logger.Level.FULL;
}
}
yml文件里需要开启日志的Feign客户端
logging:
level:
com.dyy.springcloud.service.PaymentFeignService: debug
熔断、降级、监控:Hystrix
服务雪崩:
多个微服务之间调用的时候,比如微服务A调用微服务B和微服务C,微服务B和微服务C又调用其他的微服务,这就是“扇出”
如果删除的链路上某个微服务的调用响应时间过长或者不可用,对微服务A的调用就会占用越来越多的系统资源,从而引起系统崩溃,就是所谓的“雪崩效应”
对于高流量的应用来说。单一的后端依赖可能会导致所有的服务器上的所有资源都在几秒钟内饱和。比失败更糟糕的是,这些应用程序还可能导致服务之间的延迟增加,备份列表,线程和其他系统资源紧张, 导致整个系统发生更多的级联故障。这些都表示需要对故障和延迟进行隔离和管理,以便于单个依赖关系的失败不会导致取消整个应用程序或者系统。
所以,通常当你发现一个模块下的某个实例失败后,这个模块依然还会接收流量,然后这个有问题的模块还调用了其他的模块,这样就会发生级联故障,或者叫做雪崩。
简介
Hystrix是一个用于处理分布式系统的延迟和容错的开源库,在分布式系统里,许多依赖会不可避免的会调用失败,比如超时、异常等。
Hystrix 能够保证在一个依赖出问题的情况下,不会导致整体服务失效,避免级联故障,以提高分布式系统的弹性。
“断路器”本身是一种开关装置,当某个服务单元发生故障之后,通过断路器的故障监控,向调用方返回一个符合预期的、可处理的备选响应(fallback),而不是长时间的等待或者抛出调用方法无法处理的异常,这样就保证了服务调用方的线程不会被长时间、不必要的占用,从而避免了故障在分布式系统中的的蔓延,乃至雪崩。
现在Hystrix已经停止更新维护了。
功能
- 服务降级
- 服务熔断
- 接近实时的监控
- 。。。
服务降级Fallback
不让客户端等待,并且立刻返回一个友好的提示。
什么情况触发服务降级
- 程序运行异常
- 超时自动降级
- 服务熔断触发服务降级
- 线程池/信号量打满也会导致服务降级
- 人工降级
服务熔断Breaker
同保险丝的效果。
达到最大量服务访问后,直接拒绝掉访问,拉闸限电,然后调用服务降级的方法并返回友好提示。
服务降级-->进而熔断-->恢复调用链路
服务限流FlowLimit
针对秒杀高并发等一些操作,禁止访问量的激增拥挤,一起排队,限制每秒处理的请求数量,有序进行。
使用
依赖
<dependencies>
<!--新增hystrix-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>com.dyy.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
yml
server:
port: 8001
spring:
application:
name: cloud-hystrix-payment-service
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url
defaultZone: http://localhost:7001/eureka/
主启动类
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
@SpringBootApplication
@EnableEurekaClient
public class PaymentHystrixMain8001 {
public static void main(String[] args) {
SpringApplication.run(PaymentHystrixMain8001.class,args);
}
}
业务类
public interface PaymentService {
public String paymentInfo_OK(Integer id);
public String payment_Timeout(Integer id);
}
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Service
public class PaymentServiceImpl implements PaymentService {
//成功
public String paymentInfo_OK(Integer id){
return "线程池:"+Thread.currentThread().getName()+" paymentInfo_OK,id: "+id+"\t"+"哈哈哈" ;
}
//失败
public String payment_Timeout(Integer id){
int timeNumber = 3;
try { TimeUnit.SECONDS.sleep(timeNumber); }catch (Exception e) {e.printStackTrace();}
return "线程池:"+Thread.currentThread().getName()+" paymentInfo_TimeOut,id: "+id+"\t"+"呜呜呜"+" 耗时(秒)"+timeNumber;
}
}
视图
import com.dyy.springcloud.service.PaymentService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
@Slf4j
public class PaymentController {
@Resource
private PaymentService paymentService;
@Value("${server.port}")
private String serverPort;
@GetMapping("/payment/hystrix/ok/{id}")
public String paymentInfo_OK(@PathVariable("id") Integer id){
String result = paymentService.paymentInfo_OK(id);
log.info("*******result:"+result);
return result;
}
@GetMapping("/payment/hystrix/timeout/{id}")
public String paymentInfo_TimeOut(@PathVariable("id") Integer id){
String result = paymentService.payment_TimeOut(id);
log.info("*******result:"+result);
return result;
}
}
启动服务,访问 http://localhost:8001/payment/hystrix/ok/31
每次调用耗费3秒钟: http://localhost:8001/payment/hystrix/timeout/31
过程:正确-->错误-->降级熔断-->恢复
高并发测试
上述服务在非高并发场景下,还能勉强满足,那么我们使用Jmeter进行压力测试。
Jmeter下载地址:archive.apache.org/dist/jmeter…
开启Jmeter,开启20000个并发请求访问paymentInfo_TimeOut服务。
在压测过程中访问下微服务:
http://localhost:8001/payment/hystrix/ok/31
http://localhost:8001/payment/hystrix/timeout/31
发现页面一直加载。
这是因为tomcat的默认工作线程数被打满了,没有多余的线程来分解压力和处理请求。
分析
这还只是在服务提供者8001方进行的测试,如果这时消费者80服务也来访问,那么80只能一直等待,最终80不满意,8001直接被拖死。
那么现在来模拟80访问8001的情形来验证。
80消费者
<modelVersion>4.0.0</modelVersion>
<artifactId>cloud-consumer-feign-hystrix-order80</artifactId>
<dependencies>
<!--新增hystrix-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>com.dyy.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
yml
server:
port: 80
spring:
application:
name: cloud-provider-hystrix-payment-service
eureka:
client:
register-with-eureka: true #表识不向注册中心注册自己
fetch-registry: true #表示自己就是注册中心,职责是维护服务实例,并不需要去检索服务
service-url:
defaultZone: http://localhost:7001/eureka/
主启动
mport org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
public class OrderHystrixMain80 {
public static void main(String[] args) {
SpringApplication.run(OrderHystrixMain80.class,args);
}
}
业务类
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@FeignClient("CLOUD-HYSTRIX-PAYMENT-SERVICE")
public interface PaymentHystrixService {
@GetMapping("/payment/hystrix/ok/{id}")
public String paymentInfo_OK(@PathVariable("id") Integer id);
@GetMapping("/payment/hystrix/timeout/{id}")
public String payment_Timeout(@PathVariable("id") Integer id);
}
视图
import com.dyy.springcloud.service.PaymentHystrixService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
@Slf4j
public class OrderHystrixController {
@Resource
private PaymentHystrixService paymentHystrixService;
@GetMapping("/consumer/payment/hystrix/ok/{id}")
public String paymentInfo_OK(@PathVariable("id") Integer id){
String result = paymentHystrixService.paymentInfo_OK(id);
log.info("*******result:"+result);
return result;
}
@GetMapping("/consumer/payment/hystrix/timeout/{id}")
public String paymentInfo_TimeOut(@PathVariable("id") Integer id){
String result = paymentHystrixService.payment_Timeout(id);
log.info("*******result:"+result);
return result;
}
}
启动服务,访问http://localhost/consumer/payment/hystrix/ok/32
同样开启20000个并发压8001
我们去使用80微服务访问正常的ok方法,此时页面要么一直转圈,要么80就报超时的错误了。
分析原因
8001同一层次的其他接口服务被困死,因为tomcat线程里面的工作线程都已经被挤占完毕了。
80此时调用8001,客户端访问响应缓慢。
如何解决,有什么要求?
问题:超时导致服务器变慢
那么我们超时不再等待
问题:宕机或者服务运行出错。
那么我们提供出错之后的兜底方案。
解决:
- 对方服务超时,调用者不能一直卡死等待,必须有服务降级
- 对方服务宕机了,调用者不能一直卡死等待,必须有服务降级
- 对方服务OK,调用者自己出现故障或者有自我要求(比如自己的等待时间小于服务提供者),自己处理降级。
配置服务降级
-
降低配置:@HystrixCommand
-
8001先从自身找问题,设置自身调用超时时间的峰值,峰值内可以正常运行,超过了需要有兜底的方法处理,作为服务降级fallback
-
8001 fallback:业务类启用阶级处理:使用@HystrixCommand注解来工作
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand; import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty; @Service public class PaymentServiceImpl implements PaymentService { @Override public String paymentInfo_OK(Integer id) { return "线程池:"+Thread.currentThread().getName()+" paymentInfo_OK,id="+id +" \t O(∩_∩)O哈哈~"; } //超时降级演示 @HystrixCommand(fallbackMethod = "payment_TimeoutHandler",commandProperties = { @HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value="5000") //5秒钟以内就是正常的业务逻辑 }) @Override public String payment_Timeout(Integer id) { //int timeNumber = 3; //小于等于3秒算是正常情况 int timeNumber = 15; //模拟非正常情况 //int i = 1/0 ; //模拟非正常情况 try { TimeUnit.SECONDS.sleep(timeNumber); } catch (InterruptedException e) { e.printStackTrace(); } return "线程池:"+Thread.currentThread().getName()+" payment_Timeout,id="+id+" \t o(╥﹏╥)o 耗时:"+timeNumber; } //兜底方法,上面方法出问题,我来处理,返回一个出错信息 public String payment_TimeoutHandler(Integer id) { return "线程池:"+Thread.currentThread().getName()+" payment_TimeoutHandler,系统繁忙,请稍后再试\t o(╥﹏╥)o "; } }一旦调用服务方法失败并抛出了错误信息后,会自动调用@HystrixCommand标注号的fallbackMethod调用类中的指定方法
主启动类激活:@EnableCircuitBreak
测试超时和算数异常,都会走兜底方法——服务降级。此时压测再访问http://localhost:8001/payment/hystrix/timeout/1v就会走payment_TimeoutHandler这个方法。
-
80 fallback
服务降级可以在服务提供者,也可以在服务消费者,更多是在服务消费者。(注:如果配置了热部署,是可以更新代码的,但是对于@HystrixCommand内的属性修改建议是重启微服务)
feign: hystrix: enabled: true #如果处理自身的容错就开启。开启方式与生产端不一样。主启动:@EnableHystrix
业务类:
//超时降级演示 @HystrixCommand(fallbackMethod = "payment_TimeoutHandler",commandProperties = { @HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value="1500")//超过1.5秒就降级自己 }) @GetMapping("/consumer/payment/hystrix/timeout/{id}") public String paymentInfo_TimeOut(@PathVariable("id") Integer id){ //int age= 1/0; String result = paymentHystrixService.payment_Timeout(id); log.info("*******result:"+result); return result; } //兜底方法,上面方法出问题,我来处理,返回一个出错信息 public String payment_TimeoutHandler(Integer id) { return "我是消费者80,对方支付系统繁忙请10秒后再试。或自己运行出错,请检查自己。"; }对80timeout方法进行压测,我们访问80微服务http://localhost/consumer/payment/hystrix/timeout/1,调用的就是80的payment_TimeoutHandler方法。
现在出现了新的问题:
每个业务方法都对应一个兜底方法,代码膨胀,代码耦合。
那么我们需要想办法把统一通用处理和自定义处理分开。
解决问题
-
每个方法配置一个
feign接口系列:@DefaultProperties(defaultFallback="方法名"),声明位置,类上
说明:
1:1每个方法配置一个服务降级方法,技术上可以,但是不符合实际。
1:N 除了个别重要核心业务需要有专属降级方法,其他的普通的业务可以通过@DefaultProperties(defaultFallback="")统一跳转到统一处理结果页面。
通用的和独享的各自分开,避免了代码的膨胀,合理减少了代码量。
示例:
@RestController @Slf4j @DefaultProperties(defaultFallback = "payment_Global_FallbackMethod") //全局的 public class OrderHystrixController { @Resource private PaymentHystrixService paymentHystrixService; @GetMapping("/consumer/payment/hystrix/ok/{id}") public String paymentInfo_OK(@PathVariable("id") Integer id){ String result = paymentHystrixService.paymentInfo_OK(id); return result; } @HystrixCommand public String paymentInfo_TimeOut(@PathVariable("id") Integer id){ int age = 10/0; String result = paymentHystrixService.paymentInfo_TimeOut(id); return result; } //兜底方法 public String paymentTimeOutFallbackMethod(@PathVariable("id") Integer id){ return "我是消费者80,对付支付系统繁忙请10秒钟后再试或者自己运行出错请检查自己,(┬_┬)"; } //下面是全局fallback方法 public String payment_Global_FallbackMethod(){ return "Global异常处理信息,请稍后再试,(┬_┬)"; } } -
和业务逻辑混在一起
服务降级,客户端去调用服务端,碰上服务端宕机或者关闭。
本次服务降级处理是在客户端80实现完成的,和服务端8001没有关系,只需为Feign客户端定义的接口添加一个服务降级处理的实现类即可实现解耦。
可能面对的异常:运行,超时,宕机
重新新建一个类(PaymentFallbackService)实现PaymentHystrixService接口,统一为接口里的方法进行异常处理。
@Component public class PaymentFallbackService implements PaymentHystrixService { @Override public String paymentInfo_OK(Integer id) { return "-----PaymentFallbackService fall back-paymentInfo_OK , (┬_┬)"; } @Override public String paymentInfo_TimeOut(Integer id) { return "-----PaymentFallbackService fall back-paymentInfo_TimeOut , (┬_┬)"; } }yml
feign: hystrix: enabled: true #如果处理自身的容错就开启。开启方式与生产端不一样。PaymentFeignClientService接口
import org.springframework.cloud.openfeign.FeignClient; import org.springframework.stereotype.Component; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @Component @FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-PAYMENT",fallback = PaymentFallbackService.class) public interface PaymentHystrixService { @GetMapping("/payment/hystrix/ok/{id}") public String paymentInfo_OK(@PathVariable("id") Integer id); @GetMapping("/payment/hystrix/timeout/{id}") public String paymentInfo_TimeOut(@PathVariable("id") Integer id); }那么正常访问消费端,然后故意关闭提供方8001,即使客户端在服务不可用时也会获得提示信息,而不会挂起耗死服务器。
配置服务熔断
概述:熔断机制时应对雪崩效应的一种微服务链路保护机制,当扇出链路的某个微服务出错不可用或者响应时间太长时,会进行服务的降级,进而熔断该节点服务的调用,快速返回错误的响应信息。
当检测到该节点微服务调用响应正常后,回复调用链路。
在SpringCloud框架里,熔断机制通过Hystrix实现。Hystrix会监控微服务间调用的状态,当失败的调用到一定阈值,缺省是10秒内20次调用并有50%的失败情况,就会启动熔断机制。熔断机制的注解是@HystrixCommand
如何操作?
服务实现类
//服务熔断
@HystrixCommand(fallbackMethod = "paymentCircuitBreaker_fallback",commandProperties = {
@HystrixProperty(name = "circuitBreaker.enabled",value = "true"), //是否开启断路器
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold",value = "10"), //当在配置时间窗口内达到此数量,打开断路,默认20个
@HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds",value = "10000"), //断路多久以后开始尝试是否恢复,默认5s
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage",value = "60"), //出错百分比阈值,当达到此阈值后,开始短路。默认50%
})
public String paymentCircuitBreaker(Integer id){
if (id < 0){
throw new RuntimeException("*****id 不能负数");
}
String serialNumber = IdUtil.simpleUUID();//hutool.cn工具包
return Thread.currentThread().getName()+"\t"+"调用成功,流水号:"+serialNumber;
}
public String paymentCircuitBreaker_fallback(@PathVariable("id") Integer id){
return "id 不能负数,请稍候再试,(┬_┬)/~~ id: " +id;
}
PaymentController
//===服务熔断
@GetMapping("/payment/circuit/{id}")
public String paymentCircuitBreaker(@PathVariable("id") Integer id){
String result = paymentService.paymentCircuitBreaker(id);
log.info("*******result:"+result);
return result;
}
自测cloud-provider-hystrix-payment8001
正确: http://localhost:8001/payment/circuit/31
错误: http://localhost:8001/payment/circuit/-31
一次正确一次错误这样尝试。
可以试一下多次错误(狂点),然后访问正确的,发现刚开始不满足条件,就算是正确访问地址也是失败的,需要慢慢的恢复链路。
熔断类型
- 熔断打开:请求不再进行调用当前服务,内部设置时钟一般为MTTR(平均故障处理时间),当打开时常达到所设时钟则进入熔断状态。
- 熔断关闭:熔断关闭不会对服务进行熔断。
- 熔断半开:部分请求根据规则调用当前服务,如果请求成功并且符合规则则认为当前服务恢复正常,然后关闭熔断。
断路器在什么情况下开始起作用?
//服务熔断
@HystrixCommand(fallbackMethod = "paymentCircuitBreaker_fallback",commandProperties = {
@HystrixProperty(name = "circuitBreaker.enabled",value = "true"), //是否开启断路器
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold",value = "20"), //当快照时间窗(默认10秒)内达到此数量才有资格打开断路,默认20个
@HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds",value = "50000"), //断路多久以后开始尝试是否恢复,默认5s
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage",value = "50"), //出错百分比阈值,当达到此阈值后,开始短路。默认50%
})
涉及断路器的三个重要参数:快照时间窗、请求总数阈值、错误百分比阈值
配置属性可以参考:github.com/Netflix/Hys…
- 快照时间窗:断路器确定是否打开需要统计一些请求和错误数据,而统计的时间范围就是快照时间窗,默认为最近10秒。
- 请求总数阈值:在快照时间窗内,必须满足请求总数阈值才有资格熔断。默认20,意味着在10秒内,如果该hystrix命令的调用次数不足20次,即使所有的请求都超时,或者其他原因失败,断路器都不会打开。
- 错误百分比阈值:当请求总数在快照时间窗内超过了阈值,比如发生了30次调用,如果在这30次调用,有15次发生了超时异常,也就是超过50%的错误百分比,在默认设定50%阈值情况下,这个时候就会把断路器打开。
断路器开启或者关闭的条件
- 当满足一定阈值的时候(默认10s内超过20个请求次数)
- 当失败率达到一定的时候(默认10s内超过50%请求失败)
到达以上阈值,断路器将会开启
当开启的时候,所有请求都不会进行转发
一段时间之后,(默认是5s),这个时候断路器是半开状态,会让其中一个请求进行转发,如果成功,断路器会关闭,如果失败,继续开启。
断路器打开之后发生什么?
再有请求调用的时候,将不会调用主逻辑,而是直接调用降级fallback。通过断路器,实现了自动的发现错误并且将降级逻辑切换为主逻辑,减少响应延迟的效果。
如何恢复主逻辑?
当断路器打开,对主逻辑进行熔断之后,hystrix会启动一个休眠时间窗,在这个时间窗内,降级逻辑临时成为主逻辑,当休眠时间窗到期,断路器将进入半开状态,释放一次请求到原来的主逻辑上,如果此次请求正常返回,那么断路器将关闭,主逻辑恢复;如果这次请求依然出错,断路器继续进入打开状态,休眠时间窗重新计时。
服务监控HystrixDashboard
除了隔离依赖服务的调用之外,Hystrix还提供了准实时的调用监控,Hystrix会持续的记录所有通过Hystrix发起的请求执行信息,并以统计报表和图形的形式展示给用户,包括每秒执行了请求多少成功,多少失败等。
Netflix通过hystrix-metrics-event-stram项目实现了对以上只是的监控。Spring Cloud也提供了Hystrix Dashboard的整合,对监控内容转化成可视化界面。
仪表盘9001
<modelVersion>4.0.0</modelVersion>
<artifactId>cloud-consumer-hystrix-dashboard9001</artifactId>
<dependencies>
<!--新增hystrix dashboard-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
yml
server:
port: 9001
hystrix:
dashboard:
proxy-stream-allow-list: "*"
新注解:@EnableHystrixDashboard
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.hystrix.dashboard.EnableHystrixDashboard;
@SpringBootApplication
@EnableHystrixDashboard
public class HystrixDashboardMain9001 {
public static void main(String[] args) {
SpringApplication.run(HystrixDashboardMain9001.class,args);
}
}
所有的Provider微服务提供类(8001、8002、8003)都需要监控依赖配置。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
启动cloud-consumer-hystrix-dashboard9001该微服务后续将监控微服务8001,启动后访问http://localhost:9001/hystrix
断路器演示
-
修改cloud-provider-hystrix-payment8001(注意:新版本Hystrix需要在主启动类MainAppHystrix8001中指定监控路径)
/** *此配置是为了服务监控而配置,与服务容错本身无关,springcloud升级后的坑 *ServletRegistrationBean因为springboot的默认路径不是"/hystrix.stream", *只要在自己的项目里配置上下面的servlet就可以了 */ @Bean public ServletRegistrationBean getServlet() { HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet(); ServletRegistrationBean registrationBean = new ServletRegistrationBean(streamServlet); registrationBean.setLoadOnStartup(1); registrationBean.addUrlMappings("/hystrix.stream"); registrationBean.setName("HystrixMetricsStreamServlet"); return registrationBean; } -
监控测试
网关:Gateway
官网:docs.spring.io/spring-clou…
Cloud在1.x版本中都是采用Zuul网关github.com/Netflix/zuu…
但是在2.x的版本中,zuul的升级一直跳票,SpringCloud最后自己研发了一个网关代替zuul,那就是SpringCloud Gateway。
也就是说:Gateway是原zuul1.x版的代替。
Gateway具有强大的过滤功能,比如:熔断,限流,重试等等。
Springcloud Gateway是SpringCloud的一个全新项目,是基于Spring5.X+SpringBoot2.X和Project Reactor等技术开发的网关,它旨在为微服务提供一种简单有效的统一的API路由方式。
为了提升网关的性能,SpringCloud Gateway是基于WebFlux框架来实现的,而WebFlux框架底层则使用了高性能的Reactor模式通讯框架Netty。(Netty通过Http协议进行通讯,socket使用二进制进行传输)
SpringCloud Gateway的目标是提供统一的路由方式并且基于Filter链的方式提供了网关的基本的功能:比如:安全、监控/指标、和限流
功能
- 反向代理
- 鉴权
- 流量控制
- 熔断
- 日志监控
- ...
微服务架构中网关在哪里
概念
路由(Route)
路由是构建网关的基本模块,它由ID,目标URI,一系列的断言和过滤器组成,如果断言为true则匹配该路由。
断言(Predicate)
参考的是java8的java.util.function.Predicate,开发人员可以匹配Http请求中的所有内容(例如请求头或者请求参数),如果请求和断言相匹配则进行路由。
过滤(Filter)
指的是Spring框架中GatewayFilter的实例,使用过滤器,可以在请求被路由前或者之后对请求进行修改。
结构
Web请求,通过一些匹配条件,定位到真正的服务节点。并且在这个转发过程的前后,进行一些精细化控制。
Predicate就是我们的匹配条件:而Filter,就是可以理解为一个无所不能的拦截器。有了这两个元素,在加上目标uri,就可以实现一个具体的路由了。
工作流程
官方文档:docs.spring.io/spring-clou…
客户端向Spring Cloud Gateway发出请求。然后再Gateway Handler Mapping中找到与请求匹配的路由,然后把他发送到Gateway Web Handler
Handler再通过指定的过滤器来将请求发送给我们实际的服务执行业务逻辑,然后返回。
过滤器可能会在发送代理请求之前(“pre”)或者之后(“post”)执行业务逻辑。
Filter在“pre”类型的过滤器可以做 参数校验、权限校验、流量监控、日志输出、协议转换等,在“post”类型的过滤器中可以做 响应内容、响应头的修改、日志的输出、流量控制等有着非常重要的作用。
核心逻辑
路由转发 + 执行过滤器链
如何配置
pom
<dependencies>
<!--新增gateway,不需要引入web和actuator模块-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>com.dyy.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
yml
server:
port: 9527
spring:
application:
name: cloud-gateway
eureka:
instance:
hostname: cloud-gateway-service
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://localhost:7001/eureka
不需要写业务类,给一个主启动类启动即可,这里也需要注册到Eureka注册中心。
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
@SpringBootApplication
@EnableEurekaClient
public class GateWayMain9527 {
public static void main(String[] args) {
SpringApplication.run( GateWayMain9527.class,args);
}
}
9527网关是怎么做的路由映射呢?
假设我们目前不想暴露8001端口,希望在8001外套一层9527,那么我们可以在yml中新增网关配置
server:
port: 9527
spring:
application:
name: cloud-gateway
cloud:
gateway:
routes:
- id: payment_routh #路由的ID,没有固定规则但要求唯一,建议配合服务名
uri: http://localhost:8001 #匹配后提供服务的路由地址
predicates:
- Path=/payment/get/** #断言,路径相匹配的进行路由
- id: payment_routh2
uri: http://localhost:8001
predicates:
- Path=/payment/lb/** #断言,路径相匹配的进行路由
eureka:
instance:
hostname: cloud-gateway-service
client:
service-url:
register-with-eureka: true
fetch-registry: true
defaultZone: http://localhost:7001/eureka
添加网关前: http://localhost:8001/payment/get/31
添加网关后: http://localhost:9527/payment/get/31
通过微服务名实现动态路由
默认情况下Gateway会根据注册中心的服务列表,以注册中心上微服务名为路径创建动态路由进行转发,从而实现动态路由的功能。
-
创建三个项目:一个eureka7001+两个服务提供者8001、8002
-
server: port: 9527 spring: application: name: cloud-gateway cloud: gateway: discovery: locator: enabled: true #开启从注册中心动态创建路由的功能,利用微服务名进行路由 routes: - id: payment_routh #路由的ID,没有固定规则但要求唯一,建议配合服务名 #uri: http://localhost:8001 #匹配后提供服务的路由地址 uri: lb://cloud-payment-service predicates: - Path=/payment/get/** #断言,路径相匹配的进行路由 - id: payment_routh2 #uri: http://localhost:8001 #匹配后提供服务的路由地址 uri: lb://cloud-payment-service predicates: - Path=/payment/lb/** #断言,路径相匹配的进行路由 eureka: instance: hostname: cloud-gateway-service client: service-url: register-with-eureka: true fetch-registry: true defaultZone: http://localhost:7001/eureka需要注意的是uri的协议为lb,表示启动Gateway的负载均衡功能。
lb://service//serviceName是spring cloud gateway在微服务中自动为我们创建的负载均衡uri。
-
此时访问http://localhost:9527/payment/lb
是8001/8002两个端口切换
Predicate
Spring Cloud Gateway将路由匹配作为Spring WebFlux HandlerMapper基础框架的一部分。
Spring Cloud Gateway包括许多内置的Route Predicate工厂。所有这些Predicate都和Http请求的不同属性匹配。多个RoutePredicate工厂可以进行组合。
Spring Cloud Gateway创建Route对象时,使用RoutePredicateFactory创建Predicate对象,Predicate对象可以赋值给Route。Spring Cloud Gateway包含许多内置的Route Predicate Factories。
所有这些为此都匹配Http请求的不同属性。多种谓词工厂可以组合,并且通过逻辑and。
常用的Route Predicate
-
After Route Predicate
如: - After=2020-03-08T10:59:34.102+08:00[Asia/Shanghai]
- After = 时间 -
Before Route Predicate
- Before = 时间 -
Between Route Predicate
- Between = 时间,时间 -
Cookie Route Predicate
- Cookie=username,name必须满足Cookie是username=name才能访问 -
Header Route Predicate
- Header=X-Request-Id, \d+请求头中要有X-Request-Id属性并且值为整数的正则表达式 -
Host Route Predicate
- Host=**.xx.xx -
Method Route Predicate
- Method=GET -
Path Route Predicate
-
Query Route Predicate
- Query=username, \d+要有参数名称并且是正整数才能路由
总结
其实,Predicate就是为了实现一组匹配规则,让请求过来找到对应的Route进行处理。
server:
port: 9527
spring:
application:
name: cloud-gateway
cloud:
gateway:
discovery:
locator:
enabled: true #开启从注册中心动态创建路由的功能,利用微服务名进行路由
routes:
- id: payment_routh #路由的ID,没有固定规则但要求唯一,建议配合服务名
#uri: http://localhost:8001 #匹配后提供服务的路由地址
uri: lb://cloud-payment-service
predicates:
- Path=/payment/get/** #断言,路径相匹配的进行路由
- id: payment_routh2
#uri: http://localhost:8001 #匹配后提供服务的路由地址
uri: lb://cloud-payment-service
predicates:
- Path=/payment/lb/** #断言,路径相匹配的进行路由
#- After=2020-03-08T10:59:34.102+08:00[Asia/Shanghai]
#- Cookie=username,zhangshuai #并且Cookie是username=zhangshuai才能访问
#- Header=X-Request-Id, \d+ #请求头中要有X-Request-Id属性并且值为整数的正则表达式
#- Host=**.dyy.com
#- Method=GET
#- Query=username, \d+ #要有参数名称并且是正整数才能路由
eureka:
instance:
hostname: cloud-gateway-service
client:
service-url:
register-with-eureka: true
fetch-registry: true
defaultZone: http://localhost:7001/eureka
Filter
路由过滤器可用于修改进入的HTTP请求和返回的Http先攻,路由过滤器只能指定路由进行使用。
SpringCloud Gateway内置了多种路由过滤器,他们都由GatewayFilter的工厂类来产生。
此处我们讨论Spring Cloud Gateway的Filter:
- 生命周期:只有两个(pre:业务逻辑之前;post:在业务逻辑之后)
- 种类:两种(GatewayFilter(有31种),GlobalFilter(10种))
常用的GatewayFilter:
AddRequestParameter
filters:
- AddRequestParameter=x-Request-Id,1024
自定义过滤器
-
自定义全局GlobalFilter,主要是同时实现两个接口GlobalFilter,Ordered
-
作用:全局日志记录,统一网关鉴权。。
import lombok.extern.slf4j.Slf4j; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.core.Ordered; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; import java.util.Date; @Component @Slf4j public class MyLogGateWayFilter implements GlobalFilter,Ordered { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { log.info("*********come in MyLogGateWayFilter: "+new Date()); String uname = exchange.getRequest().getQueryParams().getFirst("username"); if(StringUtils.isEmpty(username)){ log.info("*****用户名为Null 非法用户,(┬_┬)"); exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE); return exchange.getResponse().setComplete(); } return chain.filter(exchange); } @Override public int getOrder() { return 0; } }
链路跟踪:Sleuth
1.解决了什么问题
在微服务框架种,一个有客户端发起的请求在后端系统种会经过多个不同的服务节点调用来协同产生最后的请求结果,每一个前端请求都会形成一个复杂的分布式服务调用链路,链路中的任何一环出现高延时或者错误都会引起整个请求最后的失败。
2.是什么
Spring Cloud Sleuth提供了一套完整的服务跟踪的解决方案。
在分布式系统中提供追踪解决方案并且兼容支持了zipkin(负责展现)。
3.搭建链路监控步骤
Spring Cloud Sleuth和OpenZipkin(也称为Zipkin)集成。Zipkin是一个分布式跟踪平台,可用于跟踪跨多个服务调用的事务。Zipkin允许开发人员以图形方式查看事务占用的时间量,并分解在调用中涉及的每个微服务所有的时间。在微服务架构中。Zipkin是识别性能问题的宝贵工具。
建立Spring Cloud Sleuth和Zipkin涉及4项操作:
- 将Spring Cloud Sleuth和Zipkin JAR文件添加到捕获跟踪数据的服务中。
- 在每个服务中配置Spring属性以及执行收集跟踪数据的Zipkin服务器。
- 安装和配置Zipkin服务器以收集数据;
- 定义每个客户端所使用的采样策略,便于向Zipkin发送跟踪信息。
4.zipkin
curl -sSL https://zipkin.io/quickstart.sh | bash -s
java -jar zipkin.jar
访问http://your_host:9411/zipkin/运行控制台
完整的调用链路:
表示一请求链路,一条链路通过Trace Id唯一标识,Span 标识发起的请求信息,各span通过parent id关联起来
Trace:类似于树结构的Span集合,表示一条调用链路,存在唯一标识。
span:表示调用链路来源,通俗的理解span就是一次请求信息。
5.使用
服务提供者:cloud-provider-payment8001
<!--包含了sleuth+zipkin-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
yml
server:
port: 8001
spring:
application:
name: cloud-payment-service
zipkin:
base-url: http://localhost:9411
sleuth:
sampler:
#采样率值介于0~1之间,1表示全部采样
probability: 1
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/cloud2021?useUnicode=true&characterEncoding=utf-8&useSSL=false
username: root
password: 123456
mybatis:
mapperLocations: classpath:/mapper/*.xml
type-aliases-package: com.dyy.springcloud.entities
eureka:
client:
register-with-eureka: true
fetchRegistry: true
service-url:
defaultZone: http://localhost:7001/eureka
业务类:PaymentController
@GetMapping("/payment/zipkin")
public String paymentZipkin(){
return "hi ,i'am paymentzipkin server,welcome to dyy,O(∩_∩)O哈哈~";
}
消费者cloud-consumer-order80
<!--包含了sleuth+zipkin-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
yml
server:
port: 80
spring:
application:
name: cloud-order-service
zipkin:
base-url: http://localhost:9411
sleuth:
sampler:
probability: 1
eureka:
client:
#表示是否将自己注册进EurekaServer默认为true。
register-with-eureka: false
#是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
fetchRegistry: true
service-url:
#单机
defaultZone: http://localhost:7001/eureka
#集群
#defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka # 集群版
业务类PaymentController
//==> zipkin+sleuth
@GetMapping("/consumer/payment/zipkin")
public String paymentZipkin(){
String result = restTemplate.getForObject("http://localhost:8001"+"/payment/zipkin/", String.class);
return result;
}
依次启动eureka7001/8001/80,80调用8001几次测试下。然后打开浏览器访问 http://localhost:9411,就会出现zipkin管理界面,点击show,就可以查看依赖关系。
SpringCloud Alibaba入门简介
是因为Spring Cloud Netfix项目进入维护模式,将模块置于维护模式,意味着SpringCloud团队将不会在向模块中添加新功能。
是什么?
作用
- 服务限流降级:默认支持WebServlet、WebFlux,OpenFeign、RestTemplate、Spring Cloud Gateway,Zuul,Dubbo和RocketMq限流降级功能的接入,可以在运行时通过控制台实施修改限流降级规则,还支持查看限流降级Metrics监控。
- 服务注册和发现:适配Spring Cloud服务注册和发现标准,默认集成了Ribbon的支持
- 分配式配置管理:基于Spring Cloud Stream给微服务应用构建休息驱动能力。
- 分布式事务:使用@GlobalTransactional注解,高效并且对业务零侵入的解决分布式事务问题。
- 阿里云对象存储:阿里云提供的海量、安全、低成本、高可靠的云存储服务器。支持在任何应用、任何时间、任何地点存储和访问任意类型的数据。
- 分布式任务调度:提供秒级、精准、高可靠、高可用的定时(基于Cron表达式)任务调度服务。同时提供分布式的任务执行模型,如网格任务。网格任务支持海量子任务均匀分配到所有Worker上执行。
- 阿里云短信服务:覆盖全球的短信服务,友好、高效、只能的互联化通讯能力,帮助企业迅速搭建客户触达通道。
依赖
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.2.6.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
怎么用
一整套解决方案,简单理解就是替换Netflix那一套。
Sentinel:把流量作为切入点,从流量控制,熔断降级,系统负载保护等多个维度保护服务的稳定性。
Nacos:一个更易于构建云原生应用的的动态服务发现,配置管理和服务管理平台。
RocketMQ:一款开源的分布式消息系统,基于高可用分布式集群技术,提供低延时的、高可靠的消息发布和订阅服务。
Dubbo:Apache Dubbo是一款高性能Java RPC框架。
Seata:阿里巴巴开源产品,一个易于使用的高性能微服务分布式事务解决方案。
Alibaba Cloud ACM:一款在分布式架构环境中对应用配置进行集中管理和推送的应用配置中心产品。
Alibaba Cloud OSS:阿里云的对象存储服务,是阿里云提供的海量、安全、低成本、高可靠的云存储服务。可以在任何应用,任何时间,任何地点存储和访问任意类型的数据。
Alibaba Cloud SchedulerX:案例中间件团队开发的一款分布式任务调度产品,提供秒级、精准、高可靠、高可用的定时(基于Cron表达式)任务调度服务。
Alibaba Cloud SMS:覆盖全球的短信服务,友好、高效、智能的互联化通讯能力,帮助企业迅速搭建客户触达通道。
服务注册和配置中心:Spring Cloud Alibaba Nacos
1.简介
前四个字母分别为Naming和Configuration的前两个字母,最后的s为Service。
2.是什么
一个更易于构建云原生应用的动态服务发现,配置管理和服务管理中心。
Nacos就是注册中心+配置中心的组合
Nacos = Eureka + Config + Bus
3.作用
代替Eureka做服务注册中心
代替Config做服务配置中心
4.各种注册中心比较
| 服务注册与发现框架 | CAP模型 | 控制台管理 | 社区活跃度 |
|---|---|---|---|
| Eureka | AP | 支持 | 低(2.x版本闭源) |
| Zookeeper | CP | 不支持 | 中 |
| Consul | CP | 支持 | 高 |
| Nacos | AP | 支持 | 高 |
据说nacos在阿里巴巴内部有超过10万的实例运行,已经过了类似双十一等各种大型流量的考验
CAP原则
又称为CAP定理。
指的是在一个分布式系统中,一致性(Consistency)、可用性(Availability),分区容错性(Partition tolerance)。CAP原则指的是,这三个要素最多只能同时实现两点,不可能三者兼顾。
一致性(C):在分布式系统中的所有数据备份,在同一时刻是否同样的值(等同于所有节点访问同一份最新的数据富文本)
可用性(A):在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求(对数据更新具备高可用性)
分区容忍性(P):以实际效果而言,分区相当于对通信的实现要求,系统如果不能在时限内大曾数据一致性,就意味着发生了分区的情况,必须就当前操作在A和C之间做出选择。
CAP的精髓就是要么AP,要么CP,要么AC,但是不存在CAP
如果在某个分布式系统中数据没有副本,那么系统必然满足强一致性条件,因为只有独一数据,不会出现数据不一致的情况,此时C和P两要素具备,但是如果系统发生了网络分区状况或者宕机,必然会导致某些数据不可以访问,此时可用性条件就不能被满足,即使此情况下,获得了CP系统,但是CAP不可同时满足。
因此在进行分布式架构涉及时,必须做出取舍,当前一般是通过分布式缓存中各节点的最终一致性来提高系统的性能,通过使用多节点之间的数据异步复制技术来实现集群化的数据一致性。
5.作为服务注册中心使用
启动控制台
在官网下载Nacos后,解压安装包,直接运行bin/startup.cmd -m standalone(默认是MODE="cluster"集群方式启动,如果单击启动需要设置-m standalone参数,否则会启动失败),命令运行成功后直接访问http://localhost:8848/nacos。默认账号密码都是nacos
作为注册中心
可以在消费者服务者的父工程加上spring cloud alibaba依赖
<!--spring cloud alibaba 2.1.0.RELEASE-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.2.6.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
然后再消费者服务者工程中引用spring cloud alibaba里的nacos依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
服务提供者
server:
port: 9001
spring:
application:
name: nacos-payment-provider
cloud:
nacos:
discovery:
server-addr: localhost:8848 #配置Nacos地址
management:
endpoints:
web:
exposure:
include: '*' #默认只公开了/health和/info端点,要想暴露所有端点只需设置成星号
主启动类
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@EnableDiscoveryClient
@SpringBootApplication
public class PaymentMain9001 {
public static void main(String[] args) {
SpringApplication.run(PaymentMain9001.class,args);
}
}
业务类
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class PaymentController{
@Value("${server.port}")
private String serverPort;
@GetMapping(value = "/payment/nacos/{id}")
public String getPayment(@PathVariable("id") Long id) {
return "nacos registry, serverPort: "+ serverPort+"\t id"+id;
}
}
在nacos控制台的服务列表看到nacos-payment-provider就是注册成功了
如何快速创建多个提供者?
为了实现nacos的负载均衡,可以参照9001新建9011,那么可以直接拷贝虚拟端口映射。
服务消费者
server:
port: 83
spring:
application:
name: nacos-order-consumer
cloud:
nacos:
discovery:
server-addr: localhost:8848
#消费者将要去访问的微服务名称(注册成功进nacos的微服务提供者【可选】,注意:nacos-payment-provider含有IP和端口)
service-url:
nacos-user-service: http://nacos-payment-provider
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@EnableDiscoveryClient
@SpringBootApplication
public class OrderNacosMain83{
public static void main(String[] args){
SpringApplication.run(OrderNacosMain83.class,args);
}
}
业务类
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class ApplicationContextConfig{
@Bean
@LoadBalanced
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
}
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import javax.annotation.Resource;
@RestController
@Slf4j
public class OrderNacosController{
@Resource
private RestTemplate restTemplate;
@Value("${service-url.nacos-user-service}")
private String serverURL;
@GetMapping(value = "/consumer/payment/nacos/{id}")
public String paymentInfo(@PathVariable("id") Long id){
return restTemplate.getForObject(serverURL+"/payment/nacos/"+id,String.class);
}
}
controller
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import javax.annotation.Resource;
@RestController
@Slf4j
public class OrderNacosController{
@Resource
private RestTemplate restTemplate;
@Value("${service-url.nacos-user-service}")
private String serverURL;
@GetMapping(value = "/consumer/payment/nacos/{id}")
public String paymentInfo(@PathVariable("id") Long id){
return restTemplate.getForObject(serverURL+"/payment/nacos/"+id,String.class);
}
}
测试
http://localhost:83/consumer/payment/nacos/1
83访问9001/9002,轮询负载OK
为什么nacos支持负载均衡?
因为nacos内部集成了ribbon。
6.Nacos支持CP和AP模式的切换
curl -X PUT '$NACOS_SERVER:8848/nacos/v1/ns/operator/switches?entry=serverMode&value=CP'
7.作为服务配置中心使用
创建项目:
cloudalibaba-config-nacos-client3377
依赖
<dependencies>
<!--nacos-config-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!--nacos-discovery-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--web + actuator-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--一般基础配置-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
yml
Nacos和springcloud-config一样,在项目初始化时,要保证先从配置中心进行配置拉去,拉去配置之后,才能保证项目的正常启动。
springboot中配置文件的加载是存在优先级顺序的,bootstrap优先级高于application
bootstrap.yml
server:
port: 3377
spring:
application:
name: nacos-config-client
cloud:
nacos:
discovery:
server-addr: localhost:8848 #服务注册中心地址
config:
server-addr: localhost:8848 #配置中心地址
file-extension: yaml #指定yaml格式的配置(yml和yaml都可以)
#${spring.application.name}-${spring.profile.active}.${spring.cloud.nacos.config.file-extension}
#nacos-config-client-dev.yaml (一定要与file-extension值保持一致)
application.yml
spring:
profiles:
active: dev #表示开发环境
启动类
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@EnableDiscoveryClient
@SpringBootApplication
public class NacosConfigClientMain3377{
public static void main(String[] args) {
SpringApplication.run(NacosConfigClientMain3377.class, args);
}
}
控制器
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RefreshScope //通过SpringCould原生注解@RefreshScope实现配置自动更新
public class ConfigClientController{
@Value("${config.info}") //对应nacos配置:nacos-config-client-dev.yaml
private String configInfo;
@GetMapping("/config/info")
public String getConfigInfo() {
return configInfo;
}
8.在Nacos中添加配置信息
Nacos中的匹配规则
Nacos中的dataid的组成格式和SpringBoot配置文件中的匹配规则。
在Nacos Spring Cloud中,dataId的完整格式如下
${prefix}-${spring.profile.active}.${file-extension}
- prefix默认为spring.application.name的值,也可以通过配置项spring.cloud.nacos.cofig.prefix来配置
- spring.profile.active即当前环境对应的profile,当spring.profile.active为空时,对应的连接符也将不存在,dataId拼接规则变为
${prefix}.${file-extension} - file-exetension为配置内容的数据格式,可以通过配置项spring.cloud.nacos.config.file-extension来配置。目前只支持properties和yaml类型
最后公式:
${spring.application.name}-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
历史配置:
Nacos会记录配置文件的历史版本默认保留30天。
自带动态刷新:
修改Nacos中的yaml配置文件,查看配置已经刷新。
分类配置
1.问题
场景:多环境多项目管理
问题1:
实际开发中,通常一个系统会准备
- dev开发环境
- test测试环境
- prod生产环境
如何保证指定环境启动时服务能正确读取到Nacos上响应环境的配置文件呢?
问题2:
一个大型分布式微服务系统会有很多微服务子项目,每一个微服务项目又会有相应的开发环境、测试环境、预发环境、正式环境,那么怎么对这些微服务配置进行管理呢?
2.Nacos的图形化管理界面
配置管理
3.Namespace+Group+Data ID三者关系?
最外层的namespace是可以用于区分部署环境的,Group和DataID逻辑上区分两个目标对象。
默认情况:
Namespace=public,Group=DEFAULT_GROUP,默认Cluster是DEFAULT
- Nacos的Namespace主要是用来实现隔离的。
比如说我们现在有三个环境:开发、测试、生产环境、我们就可以创建三个Namespace,不同的Namespace之间是隔离的。
- Group可以把不同的微服务划分到同一个分组里面去。Service就是微服务;一个Service可以包含多个Cluster(集群),Nacos默认Cluster是DEFAULT,CLuster是对指定微服务的一个虚拟划分。
比如说为了容灾,将Service微服务分别部署在了杭州机房和广州机房,这是就可以给杭州机房的Service微服务起一个集群名称(HZ),给广州机房的Service微服务起一个集群名字(GZ),还可以尽量让同一个机房的微服务互相调用,以提升性能。
- Instance,就是微服务的实例。
4.解决方案
-
DataID方案
指定spring.profile.active和配置文件的DataID来使不同环境下读取不同的配置。
默认空间+默认分组+新建dev和test两个DataID。
那么通过spring.profile.active属性就能进行多环境下配置文件的读取。
-
Group方案
通过Group实现环境区分,新建Group,在nacos控制台上配置文件DataID。
我们分别设置bootstrap.yml和application.yml,在config下增加一条group的配置即可,可以配置为DEV_GROUP或者TEST_GROUP
-
Namespace方案
新建dev/test的Namespace。回到服务管理-服务列表查看(可以i同故宫切换不同的命名矿建查看微服务了)。
按照域名配置填写。
修改yml
bootstrap
# nacos配置 server: port: 3377 spring: application: name: nacos-config-client cloud: nacos: discovery: server-addr: localhost:8848 #Nacos服务注册中心地址 config: server-addr: localhost:8848 #Nacos作为配置中心地址 file-extension: yaml #指定yaml格式的配置 group: DEV_GROUP namespace: 7d8f0f5a-6a53-4785-9686-dd460158e5d4 # ${spring.application.name}-${spring.profile.active}.${spring.cloud.nacos.config.file-extension} # nacos-config-client-dev.yamlapplication
spring: profiles: active: dev # 表示开发环境 #active: test # 表示测试环境 #active: info
8.Nacos集群和持久化配置
推荐用户把所有服务放到一个vip下面,然后挂到一个域名下面。
http://ip1:port/openAPI 直连ip模式,机器挂则需要修改ip才可以使用。
http://SLB:port/openAPI 挂载SLB模式(内网SLB,不可暴露到公网,以免带来安全风险),直连SLB即可,下面挂server真实ip,可读性不好。
nacos.com:port/openAPI 域名 + SLB模式(内网SLB,不可暴露到公网,以免带来安全风险),可读性好,而且换ip方便,推荐模式
说明
默认Nacos使用嵌入式数据库实现数据的存储。所以,如果启动多个默认配置下的Nacos节点,数据存储是存在一致性问题的。
为了解决这个问题,Nacos采用了集中式存储的方式来支持集群化部署,目前只支持MySQL的存储
Nacos支持三种部署模式
- 单机模式-用于测试和单机试用
- 集群模式-用于生产环境,确保高可用
- 多集群模式-用于多数据中心场景。
Nacos持久化配置
-
Nacos默认自带的是嵌入式数据库derby
-
derby到mysql切换配置步骤
nacos-server-1.4.2\nacos\conf目录下找到sql脚本
nacos-mysql.sql
执行脚本
nacos-server-1.4.2\nacos\conf目录下找到application.properties
spring.datasource.platform=mysql db.num=1 db.url.0=jdbc:mysql://localhost:3306/nacos_devtest?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true db.user=root db.password=youdontknow修改ip,端口以及mysql账号密码。
-
启动nacos,可以看到是个全新的空记录界面,以前是记录进derby
-
测试:新建配置,发现配置信息写入mysql
9.Linux版Nacos+MySQL生产环境配置
结构
步骤:
- 测试通过nginx访问nacoshttps://192.168.137.150:1111/nacos
- 新建一个配置测试
- linux服务器的mysql插入一条记录
- 测试微服务从配置中心获取配置,微服务nacos-config-client启动注册进nacos集群。
- 访问url
熔断、降级、限流:Spring Cloud Alibaba Sentinel
1.介绍
Sentinel以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。
特征:
- 丰富的应用场景:秒杀、消息削峰填谷、集群流量控制、实施熔断下游不可用应用等。
- 完备的实时监控:提供实时的监控功能。
- 广泛的开源生态:开箱即用(引入依赖,填写配置)
- 完善的SPI扩展点:定制规则管理、适配动态数据源等。
Sentinel分为两个部分:
- 核心库(Java客户端)不依赖任何框架/库,能够运行于所有Java运行时环境,同时对Dubbo/Spring Cloud等框架也有较好的支持。
- 控制台基于Spring Boot开发,打包后可以直接运行,不需要额外的Tomcat等应用容器。
2.安装
下载到本地sentinel-dashboard-1.8.2.jar
运行命令
前提:需要有Java8的环境,8080端口不能被占用。
命令:
java -jar sentinel-dashboard-1.8.2.jar
访问Sentinel管理界面:
登录账号密码均为sentinel
初始化演示
-
启动Nacos8848成功。http://localhost:8848/nacos/#/login
-
创建cloudalibaba-sentinel-service8401工程
<dependencies> <dependency> <groupId>com.atguigu.springcloud</groupId> <artifactId>cloud-api-commons</artifactId> <version>${project.version}</version> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-datasource-nacos</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>4.6.3</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>server: port: 8401 spring: application: name: cloudalibaba-sentinel-service cloud: nacos: discovery: server-addr: localhost:8848 sentinel: transport: dashboard: localhost:8080 port: 8719 #默认8719,应用与Sentinel控制台交互的端口,应用本地会起一个该端口占用HttpServer management: endpoints: web: exposure: include: '*'配置控制台信息。application.yml
spring: cloud: sentinel: transport: port: 8719 dashboard: localhost:8080这里的
spring.cloud.sentinel.transport.port端口配置会在应用对应的机器上启动一个Http Server,该Server会和Sentinel控制台做交互。比如Sentinel控制台添加了一个限流规则,会把规则数据push给这个Http Server接收,Http Server接收,Http Server再将规则注册到Sentinel中。 -
主启动类
import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; @EnableDiscoveryClient @SpringBootApplication public class MainApp8401{ public static void main(String[] args) { SpringApplication.run(MainApp8401.class, args); } } -
业务类FlowLimitController
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController @Slf4j public class FlowLimitController{ @GetMapping("/testA") public String testA() { return "------testA"; } @GetMapping("/testB") public String testB() { return "------testB"; } } -
启动Sentinel8080
java -jar sentinel-dashboard-1.8.2.jar -
启动8401微服务
-
查看sentinel,发现啥也没有,这是因为Sentinel采用的是懒加载,执行一次访问即可。
-
访问http://localhost:8401/testA和http://localhost:8401/testB,可以看到:
结论:sentinel8080正在监控8401
3.流控规则
资源名:唯一名称,默认是请求路径
针对来源:Sentinel可以针对调用者进行限流,填写微服务名,默认default(不区分来源)
阈值类型/单机阈值:
- QPS:当调用该api的QPS达到阈值的时候,进限流
- 线程数:当调用该api的线程数达到阈值的时候,进行限流
是否集群:不需要集群
流控模式:
- 直接:api达到限流条件时,直接限流
- 关联:当关联的资源达到阈值时,就限流自己
- 链路:只记录指定链路上的流量(指定资源从入口资源进来的流量,如果达到阈值,就进行限流)【api级别的针对来源】
流控效果:
- 快速失败:直接失败,抛异常
- Warm Up:根据codeFactor(冷加载因子,默认3)的值,从阈值/codeFactor,经过预热时长,才达到设置的QPS阈值
- 排队等待:匀速排队,让请求匀速通过,阈值类型必须设置为QPS,否则无效
4.流控模式
1.直接(默认)
直接->快速失败(系统默认)
阈值类型=QPS:
单机阈值设置为1,表示1秒内查询1此就是OK。若超过次数1,就直接快速失败,报默认错误。
可以尝试快速点击访问 http://localhost:8401/testA,发现返回Blocked by Sentinel(flow limiting)错误。
阈值类型=线程数:
快速点击访问 http://localhost:8401/testA,发现并不会返回Blocked by Sentinel(flow limiting),是因为线程处理请求很快。但是再映射方法中添加sleep后,再尝试发现也会返回Blocked by Sentinel(flow limiting)错误。
我们可以尝试给一个fallback兜底方法,优化体验。
2.关联
当和A关联的资源B达到阈值后,就限流自己
测试数据(针对/testA写的流控规则)
流控模式:关联
关联资源:/testB
单机阈值:1
测试:
在postman里新建多线程集合组,将对B的请求保存到集合组。运行线程集合组。设置并发访问参数iterations:20,Delay:300ms,使20个线程每次间隔0.3秒访问一次。
访问testA,发现失效。
3.链路
多个请求调用同一个微服务。
5.流控效果
-
直接->快速失败(默认)
-
预热:当流量突然增加时,直接把系统升到高水位可能瞬间把系统压垮,预热可以通过流量的缓慢增加,在一定时间内逐渐增加到阈值上线,给冷系统一个预热时间,避免冷系统被压垮。
配置:默认clodFactor为3,即请求QPS从threshold/3开始,经过预热时长逐渐升到设定的QPS阈值。(系统初始化的阈值是10/3约等于3,即阈值刚开始为3,然后过了5秒后,阈值才慢慢升高,恢复到10),可以尝试多次点击http://localhost:8401/testB,发现 刚开始不行,后续慢慢OK
-
排队等待:匀速排队,阈值类型必须设置为QPS否则无效。超时时间为毫秒
6.降级规则
熔断策略:
- 慢调用比例:选择以慢调用比例作为阈值,需要设置允许的慢调用RT(即最大的相应时间),请求的响应时间大于该值则统计为慢调用。当单位统计时长内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断市场内请求将会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态,若接下来的一个请求响应时间设置的慢调用RT则结束熔断,若大于设置的慢调用RT则会再次被熔断。
- 异常比例:当单位统计时长内,请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会被自动熔断,经过熔断时长后,熔断器会进入探测恢复状态,若接下来的一个请求成功完成则结束熔断。否则再次被熔断,异常比率的阈值范围是[0.0,1.0],代表0%
- 异常数:当单位统计时长内的异常数目超过阈值之后会自动进行熔断,经过熔断时长后熔断器会进入探测恢复状态,若接下来的一个请求完成(没有错误)则结束熔断,否则将会再次被熔断。
1.慢调用
测试:
@GetMapping("/testA")
public String testA() {
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "------testA";
}
配置:
资源:/testA
熔断策略:慢调用比例
最大RT:200
比例阈值:0.5
熔断时长:10s
最小请求数:10
统计时长:5000ms
测试:
5秒内打进10个请求,由于每次请求都大于RT,并且比例阈值100%,所以,熔断器打开。
2.异常比例
代码
@GetMapping("/testB")
public String testB() {
int age = 10/0;
return "------testB";
}
配置
资源:/testB
熔断策略:异常比例
比例阈值:0.5
熔断时长:10s
最小请求数:10
统计时长:5000ms
访问测试:
5秒内打进10个请求,由于每次请求都抛异常,异常比例阈值100%超过50%,所以,熔断器打开,10s后半开。如果再次访问有异常,则继续熔断。
3.异常数
代码
@GetMapping("/testB")
public String testB(){
int age = 10/0;
return "------testB 测试异常数";
}
配置:
资源名:/testB
熔断策略:异常数
异常数:5
熔断时长:10s
最小请求数:10
统计时长:5000ms
测试:
5秒内打进10个请求,由于每次请求都抛异常,异常数超过5个,所以,熔断器打开,10s后半开。如果再次访问有异常,则继续熔断。
7.热点key限流
热点就是经常访问的数据。很多时候我们希望统计某个热点数据中访问频次最高的Top K数据,并且对他的访问进行限制,比如:
- 商品ID为参数,统计一段时间内最常购买的商品ID进行限制
- 用户ID为参数,针对一段时间内频繁访问的用户ID进行限制。
热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值和模式,对包含热点参数的资源调用进行限流。热点参数限流可以看作是一种特殊的流量控制,仅仅对包含热点参数的资源调用生效。
兜底方法:分为系统默认和客户自定义两种:
在之前的解决方案中,限流出问题之后,都是用sentinel系统默认的提示。
那么如何自定义?就类似于Hystrix,某个方法出现问题,就找对应的兜底降级方法?
Hystrix:@HystrixCommand
Sentinel:@SentinelResource
示例代码:
@GetMapping("/testHotKey") @SentinelResource(value = "testHotKey",blockHandler = "deal_testHotKey") public String testHotKey(@RequestParam(value = "p1",required = false) String p1, @RequestParam(value = "p2",required = false) String p2) { //int age = 10/0; return "------testHotKey"; } //兜底方法 public String deal_testHotKey (String p1, String p2, BlockException exception){ return "------deal_testHotKey,o(╥﹏╥)o"; }
配置热点规则:
资源名:testHotKey(默认)
限流模式:QPS模式
参数索引:0
单击阈值:1
统计窗口时长:1s
非集群。
默认是:@SentinelResource(value = "testHotKey") 异常打到了前台用户界面,不友好
我们可以自定义:
@SentinelResource(value="testHotKey",blockHandler="deal_testHotKey")
value值需要和资源名一致
方法testHostKey里面第一个参数只要QPS超过每秒1次,马上降级处理。
测试:
- error (1秒1下可以,但是,超过则降级,和p1参数有关)
http://localhost:8401/testHotKey?p1=abc
-
error(1秒1下可以,但是,超过则降级,和p1参数有关)
-
right(狂点不会触发降级,与p2参数无关)
参数例外项
上述案例演示了第一个参数p1,当QPS超过1秒1次点击后马上被限流。
场景:我们期望p1参数当它是某个特殊值时,它的限流值和平时不一样。(假设当p1的值等于5时,它的阈值可以达到200)
配置:
(限制索引为0的参数,每秒访问量是1次)
参数索引:0
单机阈值:1
统计窗口时长:1s
(索引为0的参数,参数值为5时,每秒访问量限制200次)
打开高级选项,在参数例外项中配置:
参数类型:java.lang.String(只能是简单类型)
参数值:5
限流阈值:200
点击添加。
测试
狂点测试
http://localhost:8401/testHotKey?p1=5 对
http://localhost:8401/testHotKey?p1=3 错
注意:
热点参数必须是基本类型或者String
8.其他
运行时异常不归@SentinelResource管。
@SentinelResource主管配置出错,运行出错走的是异常。
9.系统规则
系统保护规则是从应用级别的入口流量进行控制,从单台机器的load,cpu使用率。平均RT,入口QPS和并发线程数等几个维度监控应用指标,让系统尽可能的跑在最大吞吐量的同时保证系统整体的稳定性。
系统保护规则是应用整体维度的,而不是资源维度的,并且仅对入口流量生效,入口流量指的是进入应用的流量,比如Web服务或者Dubbo服务端接收的请求,都属于入口流量。
支持的模式:
- Load自适应:系统的load1作为启发指标,进行自适应系统的保护,当系统load1超过设定的启发值,并且系统当前的并发线程数超过估算的系统容量时才会触发系统保护,系统容量由系统的
maxQps*minRt估算得出,设定参考值一般是CPU cores *2.5 - CPU usage(1.5.0+版本):当系统CPU使用率超过阈值即触发系统保护(取值范围0.0-1.0),比较灵敏。
- 平均RT:当单台机器上所有入口流量的平均RT达到阈值即触发系统保护,单位是毫秒。
- 并发线程数:当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护
- 入口QPS:当单台机器上的所有入口流量的QPS达到阈值即触发系统保护。
10.@SentinelResource
流控规则阈值类型QPS,单机阈值设置为1
1.按资源名称限流+后续处理
修改·cloudalibaba-sentinel-service8401
pom
server:
port: 8401
spring:
application:
name: cloudalibaba-sentinel-service
cloud:
nacos:
discovery:
server-addr: localhost:8848
sentinel:
transport:
dashboard: localhost:8080
port: 8719 #默认8719,应用与Sentinel控制台交互的端口,应用本地会起一个该端口占用的HttpServer
management:
endpoints:
web:
exposure:
include: '*'
RateLimitController
@RestController
public class RateLimitController{
@GetMapping("/byResource")
@SentinelResource(value = "byResource",blockHandler = "handleException")
public CommonResult byResource(){
return new CommonResult(200,"按资源名称限流测试OK",new Payment(2020L,"serial001"));
}
public CommonResult handleException(BlockException exception){
return new CommonResult(444,exception.getClass().getCanonicalName()+"\t 服务不可用");
}
}
主启动
@EnableDiscoveryClient
@SpringBootApplication
public class MainApp8401{
public static void main(String[] args) {
SpringApplication.run(MainApp8401.class, args);
}
}
2.按照Url地址限流+后续处理
通过访问URL来限流,会返回Sentinel自带默认的限流处理信息。
业务类
@GetMapping("/rateLimit/byUrl")
@SentinelResource(value = "byUrl")
public CommonResult byUrl(){
return new CommonResult(200,"按url限流测试OK",new Payment(2020L,"serial002"));
}
· 疯狂点击http://localhost:8401/rateLimit/byUrl,会**返回Sentinel自带的限流处理结果**。
3.上面兜底方法面临的问题
按照现有条件,我们自定义的处理方法又和业务代码耦合在一起,不直观。每一个业务方法都增加一个兜底的,代码膨胀就会加剧。
全局同一的处理方法没有体现。
4.客户自定义限流处理逻辑
创建customerBlockHandler类用于自定义限流处理逻辑,方法必须是public static修饰的。
自定义限流处理类
package com.atguigu.springcloud.alibaba.myhandler;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.atguigu.springcloud.entities.CommonResult;
public class CustomerBlockHandler {
public static CommonResult handleException(BlockException exception){
return new CommonResult(2020,"自定义限流处理信息.... CustomerBlockHandler --- 1");
}
public static CommonResult handleException2(BlockException exception){
return new CommonResult(2020,"自定义限流处理信息.... CustomerBlockHandler --- 2");
}
}
控制器
@GetMapping("/rateLimit/customerBlockHandler")
@SentinelResource(value = "customerBlockHandler",
blockHandlerClass = CustomerBlockHandler.class, blockHandler = "handleException2")
public CommonResult customerBlockHandler(){
return new CommonResult(200,"按客戶自定义",new Payment(2020L,"serial003"));
}
启动微服务后先调用一次:
http://localhost:8401/rateLimit/customerBlockHandler,查看sentinel控制台配置,测试发现自定义的被触发了。
5.其他注解属性说明
需要特别注意的是
@SentinelResource注解方式埋点不支持private方法。
-
@SentinelResource 与 Hystrix 组件中的@HystrixCommand注解作用类似的。
-
value = "byResourceName" 用于设置资源名称,只有根据资源名称设置的规则,才能执行blockHandler所引用降级方法。
-
如果按照映射路径进行规则配置,返回默认降级消息:Blocked by Sentinel (flow limiting)
-
blockHandler 用于引用降级方法
-
blockHandlerClass 用于引用降级方法的处理器类。注意:降级方法必须是static的。否则,无法解析
-
blockHandler + blockHandlerClass 只处理配置违规,进行降级处理。代码出现异常,不执行的。
-
blockHandler + fallback 同时存在,配置违规,代码也有异常,这时,走blockHandler配置文件降级处理
-
exceptionsToIgnore 设置特定异常不需要降级处理。
@RequestMapping("/fallback/{id}")
@SentinelResource(value = "byFallbackName",blockHandler = "handleException3",
blockHandlerClass = RateLimitControllerHandler.class,
fallback = "handleException2",fallbackClass = RateLimitControllerHandler.class,
exceptionsToIgnore=IllegalArgumentException.class
)
public CommonResult<Payment> fallback(@PathVariable("id") Long id) {
if (id == 4) {
throw new IllegalArgumentException ("IllegalArgumentException,非法参数异常....");
}
if (id==-1) {
CommonResult<Payment> result = new CommonResult<>(444,"数据不存在",null);
throw new NullPointerException ("NullPointerException,该ID没有对应记录,空指针异常");
}
CommonResult<Payment> result = new CommonResult<>(200,"数据已经获取",new Payment(id,"test"+1));
return result;
}
11.熔断框架比较
| Sentinel | Hystrix | resilience4j | |
|---|---|---|---|
| 隔离策略 | 信号量隔离(并发线程数限流) | 线程池隔离/信号量隔离 | 信号量隔离 |
| 熔断降级策略 | 基于响应时间,异常比率,异常数 | 基于异常比率 | 基于异常比例,响应时长 |
| 实时统计实现 | 滑动窗口(LeapArray) | 滑动窗口(基于RxJava) | Ring Bit Buffer |
| 动态规则配置 | 支持多种数据源 | 支持多种数据源 | 有限支持 |
| 扩展性 | 多个扩展点 | 插件形式 | 接口形式 |
| 基于注解的支持 | 支持 | 支持 | 支持 |
| 限流 | 基于QPS,支持基于调用关系的限流 | 有限支持 | Rate Limiter |
| 流量整形 | 支持预热模式,匀速器模式,预热排队模式 | 不支持 | 简单的Rate Limiter模式 |
| 系统自适应保护 | 支持 | 不支持 | 不支持 |
| 控制台 | 提供开箱即用的控制台,可配置规则,查看秒级监控,机器发现等 | 简单的监控查看 | 不提供控制台,可对接其他监控系统 |
12.规则持久化
一旦重启应用,Sentinel规则将会消失,生产环境需要将配置规则进行持久化。
将限流配置规则持久化进Nacos保存,只要刷新8401微服务某个rest地址,sentinel控制台的流控规则就能看到,只要Nacos里面的配置不删除,针对8401上Sentinel上的流控规则持续有效。
步骤
修改cloudalibaba-sentinel-service8401
pom
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
yml(配置nacos对接的数据源)
server:
port: 8401
spring:
application:
name: cloudalibaba-sentinel-service
cloud:
nacos:
discovery:
server-addr: localhost:8848 #Nacos服务注册中心地址
sentinel:
transport:
dashboard: localhost:8080 #配置Sentinel dashboard地址
port: 8719
datasource:
ds1:
nacos:
server-addr: localhost:8848
dataId: cloudalibaba-sentinel-service
groupId: DEFAULT_GROUP
data-type: json
rule-type: flow
management:
endpoints:
web:
exposure:
include: '*'
添加Nacos业务规则配置(cloudalibaba-sentinel-service)
[
{
"resource": "/testA",
"limitApp": "default",
"grade": 1,
"count": 1,
"strategy": 0,
"controlBehavior": 0,
"clusterMode": false
}
]
resource:资源名,资源名是限流规则的作用对象
count:限流阈值
grade:限流阈值类型,QPS模式(1)或者并发线程数模式(0)(默认是QPS模式)
limitApp:流控针对的调用来源(默认default,代表不区分来源)
strategy:调用关系限流策略:直接、链路、关联(默认:直接拒绝)
clusterMode:是否集群限流(默认:否)
启动8401后刷新sentinel发现流控规则存在。
快速访问测试接口:http://localhost:8401/testA ,发现熔断正常
重启8401再看sentinel,可以稍等一会,然后多次调用http://localhost:8401/testA ,可以发现配置重新出现了,持久化验证通过。