1.服务容错保护Hystrix
1.1.背景
在微服务架构中consumer调用provider的时候,provider在响应的时候,有可能会慢,如果provider 10s响应,那么consumer也会至少10s才响应。如果这种情况频度很高,那么就会整体降低consumer端服务的性能。这种响应时间慢的症状,就会像一层一层波浪一样,从底层系统一直涌到最上层,造成整个链路的超时。此时若有大量的请求涌入,服务与服务之间的依赖性,故障会传播,会对整个微服务系统造成灾难性的严重后果,这就是服务故障的“雪崩”效应。
如下图所示:服务超时像滚雪球一样放大,雪崩效应就形成了。
1.2.如何解决雪崩效应
Hystrix “豪猪”,又称为熔断器,具有自我保护的能力。Hystrix是Netflix开源的一个限流熔断的项目、主要有以下功能:
降级:
整体资源不够用你了,忍痛将某些服务先关掉,待度过难关再开启。降级后可以配合降级接口返回托底数据。
熔断:
当失败率达到阀值自动触发降级,熔断器触发的降级会进行快速恢复。
隔离:(线程池隔离和信号量隔离)
限制调用分布式服务的资源使用,某一个调用的服务出现问题不会影响其他服务调用。
缓存:
提供了请求缓存。
请求合并:
提供请求合并。
1.3.Hystrix工作原理
以下三种情况将触发降级方法调用:
(1):熔断器开启
(2):线程池/信号量/错误率跑满
(3):业务逻辑异常
1.4.降级
服务器高并发下,压力剧增的时候,根据当业务情况以及流量,对一些服务和页面有策略的降级(整体资源不够用了,先将某些服务关掉,等度过难关再开启),以此缓解服务器资源的压力以保障核心任务的正常运行。
双十一期间,支付宝很多功能都会提示,[双十一期间,保障核心交易,某某服务数据延迟](降级后可以配合降级接口返回托底数据)。
1.4.1.创建feign接口工程
1.4.1.1.创建工程
springcloud_hystrix_feign
1.4.1.2.pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.6.RELEASE</version>
</parent>
<groupId>com.qf</groupId>
<artifactId>springcloud_hystrix_feign</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Greenwich.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!--Spring Cloud OpenFeign Starter -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>com.qf</groupId>
<artifactId>springcloud_common_pojo</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
</dependencies>
</project>
1.4.1.3.fallback
package com.qf.fallback;
import com.qf.feign.UserFeign;
import com.qf.pojo.User;
import org.springframework.stereotype.Component;
@Component
public class UserServiceFallback implements UserFeign {
@Override
public User getUser() {
return new User(1,"我是托底数据Fallback",0);
}
}
1.4.1.4.feign
package com.qf.feign;
import com.qf.fallback.UserServiceFallback;
import com.qf.pojo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
//获取eureka-provider的调用地址,且具有负载均衡的能力
@FeignClient(value="hystrix-provider",fallback= UserServiceFallback.class)
public interface UserFeign {
@RequestMapping("/provider/getUser")
public User getUser();
}
1.4.2.创建服务消费者
1.4.2.1.创建工程
springcloud_hystrix_consumer
1.4.2.2.pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.6.RELEASE</version>
</parent>
<groupId>com.qf</groupId>
<artifactId>springcloud_hystrix_consumer</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Greenwich.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- springBoot的启动器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--eureka-server客户端 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>com.qf</groupId>
<artifactId>springcloud_hystrix_feign</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
1.4.2.3.application.properties
#向注册中心注册的名字
spring.application.name=hystrix-consumer
#设置服务注册中心地址
eureka.client.serviceUrl.defaultZone=http://127.0.0.1:8761/eureka/
#开启hystrix
feign.hystrix.enabled=true
1.4.2.3.Controller
package com.qf.controller;
import com.qf.feign.UserFeign;
import com.qf.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class UserController {
@Autowired
private UserFeign userFeign;
@RequestMapping("/consumer/getUser")
public User getUser(){
return userFeign.getUser();
}
}
1.4.2.4.App
package com.qf;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.SpringCloudApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
/*@EnableCircuitBreaker //开启熔断器 断路器
@EnableEurekaClient
@SpringBootApplication*/
@EnableFeignClients
@SpringCloudApplication
public class ConsumerApp {
public static void main(String[] args) {
SpringApplication.run(ConsumerApp.class, args);
}
}
1.4.3.创建服务提供者
1.4.3.1.创建工程
springcloud_hystrix_provider
1.4.3.2.pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.6.RELEASE</version>
</parent>
<groupId>com.qf</groupId>
<artifactId>springcloud_hystrix_provider</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Greenwich.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- springBoot的启动器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--eureka-server客户端 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>com.qf</groupId>
<artifactId>springcloud_common_pojo</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
1.4.3.3.application.properties
#向注册中心注册的名字
spring.application.name=hystrix-provider
server.port=8777
#设置注册中心的地址
eureka.client.serviceUrl.defaultZone=http://127.0.0.1:8761/eureka/
1.4.3.3.service
package com.qf.service;
import com.qf.pojo.User;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl implements UserService {
@Override
public User getUser() {
return new User(1,"王粪堆",18);
}
}
1.4.3.3.Controller
package com.qf.controller;
import com.qf.pojo.User;
import com.qf.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ProviderController {
@Autowired
private UserService userService;
@RequestMapping("/provider/getUser")
public User getUser(){
return userService.getUser();
}
}
1.4.3.4.App
package com.qf;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
@EnableEurekaClient//允许向服务端注册服务
@SpringBootApplication
public class ProviderApp {
public static void main(String[] args) {
SpringApplication.run(ProviderApp.class, args);
}
}
1.4.4.测试
开启服务提供者:
关闭服务提供者:
1.4.5.降级后的异常记录
1.4.5.1.fallback
package com.qf.fallback;
import com.qf.feign.UserFeign;
import com.qf.pojo.User;
import feign.hystrix.FallbackFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
@Component
public class UserServiceFallback implements FallbackFactory<UserFeign> {
private Logger logger = LoggerFactory.getLogger(UserServiceFallback.class);
@Override
public UserFeign create(Throwable throwable) {
return new UserFeign() {
@Override
public User getUser() {
logger.warn("====Fallback Exception=====: ",throwable);
return new User(1,"我是托底数据Fallback",0);
}
};
}
}
1.4.5.2.feign
package com.qf.feign;
import com.qf.fallback.UserServiceFallback;
import com.qf.pojo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
//获取eureka-provider的调用地址,且具有负载均衡的能力
@FeignClient(value="eureka-provider",fallbackFactory= UserServiceFallback.class)
public interface UserFeign {
@RequestMapping(value="/user",method= RequestMethod.GET)
public User getUser();
}
1.4.5.3.测试
1.5熔断
防止直接调用那些很可能会调用失败的服务,当失败率或线程池、信号量达到阀值自动触发降级,熔断触发的降级会快速恢复。
熔断生效后,会在指定的时间后调用请求来测试依赖是否恢复,依赖的应用恢复后关闭熔断。
1.5.1.熔断参数
断路器属性控制HystrixCircuitBreaker的行为:
1.4.1.创建feign接口工程
1.4.1.1.创建工程
springcloud_hystrix_feign_breaker
1.4.1.2.pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.6.RELEASE</version>
</parent>
<groupId>com.qf</groupId>
<artifactId>springcloud_hystrix_feign_breaker</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Greenwich.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!--Spring Cloud OpenFeign Starter -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>com.qf</groupId>
<artifactId>springcloud_common_pojo</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
</dependencies>
</project>
1.4.1.4.feign
package com.qf.feign;
import com.qf.pojo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
//获取eureka-provider的调用地址,且具有负载均衡的能力
@FeignClient("eureka-provider")
public interface UserFeign {
@RequestMapping("/provider/user/{id}")
public User getUser(@PathVariable Integer id);
}
1.4.2.创建服务消费者
1.4.2.1.创建工程
springcloud_hystrix_consumer_breaker
1.4.2.2.pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.6.RELEASE</version>
</parent>
<groupId>com.qf</groupId>
<artifactId>springcloud_hystrix_consumer_breaker</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Greenwich.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- springBoot的启动器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--eureka-server客户端 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>com.qf</groupId>
<artifactId>springcloud_hystrix_feign_breaker</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
1.4.2.3.application.properties
#向注册中心注册的名字
spring.application.name=eureka-consumer
#设置服务注册中心地址
eureka.client.serviceUrl.defaultZone=http://127.0.0.1:8761/eureka/
#开启熔断
feign.hystrix.enabled=true
1.4.2.3.Controller
package com.qf.controller;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import com.netflix.hystrix.contrib.javanica.conf.HystrixPropertiesManager;
import com.qf.feign.UserFeign;
import com.qf.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class UserController {
@Autowired
private UserFeign userFeign;
@HystrixCommand(fallbackMethod = "fallback",
commandProperties = {
//错误百分比条件,达到熔断器最小请求数后错误率达到百分之多少后打开熔断器
@HystrixProperty(name = HystrixPropertiesManager.CIRCUIT_BREAKER_ERROR_THRESHOLD_PERCENTAGE, value = "5"),
//断容器最小请求数,达到这个值过后才开始计算是否打开熔断器
@HystrixProperty(name = HystrixPropertiesManager.CIRCUIT_BREAKER_REQUEST_VOLUME_THRESHOLD, value = "3"),
// 默认5秒; 熔断器打开后多少秒后 熔断状态变成半熔断状态(对该微服务进行一次请求尝试,不成功则状态改成熔断,成功则关闭熔断器
@HystrixProperty(name = HystrixPropertiesManager.CIRCUIT_BREAKER_SLEEP_WINDOW_IN_MILLISECONDS, value = "5000")
})
@RequestMapping("/consumer/getUser")
public User getUser(@PathVariable Integer id){
return userFeign.getUser(id);
}
// 返回托底数据的方法
public User fallback(Integer id){
return new User(0,"我是托底数据Fallback",0);
}
}
1.4.2.4.App
package com.qf;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.SpringCloudApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
/*@EnableCircuitBreaker //开启熔断器 断路器
@EnableEurekaClient
@SpringBootApplication*/
@EnableFeignClients
@SpringCloudApplication
public class ConsumerApp {
public static void main(String[] args) {
SpringApplication.run(ConsumerApp.class, args);
}
}
1.4.3.创建服务提供者
1.4.3.1.创建工程
springcloud_hystrix_provider_breaker
1.4.3.2.pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.6.RELEASE</version>
</parent>
<groupId>com.qf</groupId>
<artifactId>springcloud_hystrix_provider_breaker</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Greenwich.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- springBoot的启动器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--eureka-server客户端 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>com.qf</groupId>
<artifactId>springcloud_common_pojo</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
1.4.3.3.application.properties
#向注册中心注册的名字
spring.application.name=eureka-provider
server.port=8777
#设置注册中心的地址
eureka.client.serviceUrl.defaultZone=http://127.0.0.1:8761/eureka/
1.4.3.3.service
package com.qf.service;
import com.qf.pojo.User;
import org.springframework.stereotype.Service;
import java.util.Date;
@Service
public class UserServiceImpl implements UserService {
@Override
public User getUser(Integer id) {
System.out.println(id +"===>"+new Date());
if (id == 1) {
throw new RuntimeException("瞎胡搞!!!");
}
return new User(id,"王粪堆",18);
}
}
1.4.3.3.Controller
package com.qf.controller;
import com.qf.pojo.User;
import com.qf.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ProviderController {
@Autowired
private UserService userService;
@RequestMapping("/provider/user/{id}")
public User getUser(@PathVariable("id") Integer id){
return userService.getUser(id);
}
}
1.4.3.4.App
package com.qf;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
@EnableEurekaClient//允许向服务端注册服务
@SpringBootApplication
public class ProviderApp {
public static void main(String[] args) {
SpringApplication.run(ProviderApp.class, args);
}
}
1.5.5.测试
测试超过3个请求5%的错误率是否打开熔断器
1.5.6.配置文件方式配置熔断器
1.5.6.1.application.properties
修改消费者application.properties
#断容器最小请求数,达到这个值过后才开始计算是否打开熔断器
hystrix.command.default.circuitBreaker.requestVolumeThreshold=3
#错误百分比条件,达到熔断器最小请求数后错误率达到百分之多少后打开熔断器
hystrix.command.default.circuitBreaker.errorThresholdPercentage=5
#默认5秒; 熔断器打开后多少秒后 熔断状态变成半熔断状态(对该微服务进行一次请求尝试,不成功则状态改成熔断,成功则关闭熔断器
hystrix.command.default.circuitBreaker.sleepWindowInMilliseconds=5000
1.5.6.2.feign
添加fallback
@Component
public class UserServiceFallback implements FallbackFactory<UserFeign> {
private Logger logger = LoggerFactory.getLogger(UserServiceFallback.class);
@Override
public UserFeign create(Throwable throwable) {
return new UserFeign() {
@Override
public User getUser(Integer id) {
logger.warn("====Fallback Exception=====: ",throwable);
return new User(1,"我是托底数据Fallback",0);
}
};
}
}
修改feign接口
//获取eureka-provider的调用地址,且具有负载均衡的能力
@FeignClient(value="eureka-provider",fallbackFactory= UserServiceFallback.class)
public interface UserFeign {
... ... ... ... ... ...
}
1.5.6.3.controller
修改消费者controller
@RestController
public class UserController {
@Autowired
private UserFeign userFeign;
@RequestMapping("/consumer/user/{id}")
public User getUser(@PathVariable("id") Integer id){
return userFeign.getUser(id);
}
}
1.6.hystrix超时问题
#第一层 hystrix 超时时间设置
#默认情况下是线程池隔离,超时时间 1000ms
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=1000
#第二层 ribbon 超时时间设置:设置比第一层小
# 请求连接的超时时间
ribbon.ConnectTimeout=5000
# 请求处理的超时时间
ribbon.ReadTimeout=5000
2.服务网关Zuul
2.1.Zuul概述
Zuul 是netflix开源的一个开源的微服务API网关, 作用有:路由、限流、过滤。
Zuul做为网关层,自身也是一个微服务,跟其它服务Service-1,Service-2, … Service-N一样,都注册在eureka server上
2.2.创建Zuul工程
2.2.1.创建工程
springcloud_zuul
2.2.1.pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.6.RELEASE</version>
</parent>
<groupId>com.qf</groupId>
<artifactId>springcloud_zuul</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Greenwich.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
<!-- springBoot的启动器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--eureka-server客户端 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>
</project>
2.2.1.application.properties
#向注册中心注册的名字
spring.application.name=gateway
server.port=9527
#设置注册中心的地址
eureka.client.serviceUrl.defaultZone=http://127.0.0.1:8761/eureka/
#/gateway/开头的url请求,将转发到eureka-provider这个微服务上
zuul.routes.eureka-provider.path=/gateway/**
2.2.1.App
package com.qf;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
@EnableZuulProxy//开启网关
@EnableEurekaClient//允许向服务端注册服务
@SpringBootApplication
public class ZuulApp {
public static void main(String[] args) {
SpringApplication.run(ZuulApp.class, args);
}
}
2.3.路由
2.3.1.通过网关请求服务
2.3.1.1. 开启网关和服务
1、开启springcloud_hystrix_provider_breaker
2、开启springcloud_zuul
3、开启springcloud_eureka_server
2.3.1.1. 使用路由规则
http://127.0.0.1:9527/gateway/user/2
2.3.1.1. 不使用路由规则
http://127.0.0.1:9527/eureka-provider/user/2
问题:
现在不论是否使用路由规则都可以正常访问,如果需要隐藏真实的服务名称进行访问(只能使用代理路由进行访问)如何实现?
2.3.2. 路由规则
2.3.2.1.隐藏真实服务名
#向注册中心注册的名字
spring.application.name=gateway
server.port=9527
#设置注册中心的地址
eureka.client.serviceUrl.defaultZone=http://127.0.0.1:8761/eureka/
#/gateway/开头的url请求,将转发到eureka-provider这个微服务上
zuul.routes.eureka-provider.path=/gateway/**
#隐藏的服务,多个可以用*
#zuul.ignored-services=eureka-provider
zuul.ignored-services=*
2.3.1.2.使用路由规则
http://127.0.0.1:9527/gateway/user/2
2.3.1.3.不使用路由规则
http://127.0.0.1:9527/eureka-provider/user/2
2.3.3.设置统一公共前缀
#向注册中心注册的名字
spring.application.name=gateway
server.port=9527
#设置注册中心的地址
eureka.client.serviceUrl.defaultZone=http://127.0.0.1:8761/eureka/
#/gateway/开头的url请求,将转发到eureka-provider这个微服务上
zuul.routes.eureka-provider.path=/gateway/**
#排除的服务,多个可以用*
#zuul.ignored-services=eureka-provider
zuul.ignored-services=*
#/qf开头的url请求,才会转发到微服务上
zuul.prefix=/qf
2.3.3.1.使用路由规则
http://127.0.0.1:9527/qf/gateway/user/2
2.3.3.2.不使用路由规则
http://127.0.0.1:9527/gateway/user/2
2.4.过滤
2.4.1.过滤器
2.4.1.1.ZuulFilter抽象类
过滤器两个功能:
1、其中路由功能负责将外部请求转发到具体的微服务实例上,是实现外部访问统一入口的基础;
2、过滤器功能则负责对请求的处理过程进行预干预,是实现请求校验等功能的基础。
有4类可重写的方法来自定义过滤器,如下:
-
filterType:返回一个字符串代表过滤器的类型,在zuul中定义了四种不同生命周期的过滤器类型,具体如下:
- pre:在请求被路由之前调用,这种过滤器可以实现身份验证、记录调试信息等。
- routing:在路由请求时候被调,这种过滤器用于构建发送给微服务的请求
- post:在routing和error过滤器之后被调用,这种过滤器可用来为响应添加标准的HTTP Header、收集统计 信息和指标、将响应从微服务发送给客户端等。
- error:在其他阶段发送错误时被调用
-
filterOrder:通过int值来定义过滤器的执行顺序
-
shouldFilter:返回一个boolean类型来判断该过滤器是否要执行,所以通过此函数可实现过滤器的开关。
-
run:过滤器的具体逻辑。
2.4.1.1.生命周期
根据自己的需要在不同的生命周期中去实现不同类型的过滤器
2.4.2.网关过滤器实现身份验证
需求:在网关过滤器中通过Token 判断用户是否登录
2.4.2.1.创建过滤器
在服务网关中定义过滤器只需要继承ZuulFilter抽象类实现其定义的四个抽象函数就可对请求进行拦截与过滤。
package com.qf.filter;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
@Component
public class PreFilter extends ZuulFilter {
@Override
public Object run() {
// 获取请求上下文
RequestContext rc = RequestContext.getCurrentContext();
HttpServletRequest request = rc.getRequest();
// 获取表单中的 token
String token = request.getParameter("token");
// 对 token 做判断
if (token == null) {
System.out.println("token is null............");
rc.setSendZuulResponse(false);// 代表请求结束。不在继续向下请求
rc.setResponseStatusCode(401);// 添加一个响应的状态码
rc.setResponseBody("请登录后再访问!!!");// 响应内容
rc.getResponse().setContentType("text/html;charset=utf-8");// 响应类型
} else {
// 访问 redis 服务 进行验证
System.out.println("token is OK");
}
return null;
}
// 是否开启过滤器:默认为 false 不开启
@Override
public boolean shouldFilter() {
return true;
}
// 过滤器的执行顺序:通过整数表示顺序,数值越小,优先级越高
@Override
public int filterOrder() {
return 0;
}
// 过滤器类型:通过过滤器类型决定了过滤器执行的时机
@Override
public String filterType() {
return "pre";
}
}
2.4.2.2.测试
不加token:http://127.0.0.1:9527/qf/gateway/user/2
加token:http://127.0.0.1:9527/qf/gateway/user/2?token=12345
2.5.限流
2.5.1.令牌桶原理
2.5.2.使用令牌桶算法实现限流
package com.qf.filter;
import com.google.common.util.concurrent.RateLimiter;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletResponse;
/**
* 限流器
*/
@Component
public class RateLimitFilter extends ZuulFilter {
/**
* 创建令牌桶
* RateLimiter.create(1)1: 是每秒生成令牌的数量,数值越大代表处理请求量越多
*/
private static final RateLimiter RATE_LIMIT = RateLimiter.create(1);
@Override
public String filterType() {
return FilterConstants.PRE_TYPE;
}
/**
* 限流器的优先级应为最高
* DEBUG_FILTER_ORDER = 1;
* FORM_BODY_WRAPPER_FILTER_ORDER = -1;
* PRE_DECORATION_FILTER_ORDER = 5;
* RIBBON_ROUTING_FILTER_ORDER = 10;
* SEND_ERROR_FILTER_ORDER = 0;
* SEND_FORWARD_FILTER_ORDER = 500;
* SEND_RESPONSE_FILTER_ORDER = 1000;
* SIMPLE_HOST_ROUTING_FILTER_ORDER = 100;
* SERVLET_30_WRAPPER_FILTER_ORDER = -2;
* SERVLET_DETECTION_FILTER_ORDER = -3;
*/
@Override
public int filterOrder() {
return FilterConstants.SERVLET_DETECTION_FILTER_ORDER;
}
/**
/**
* 指定需要执行该Filter的规则
* 返回true则执行run()
* 返回false则不执行run()
*/
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
// 是否能从令牌桶中获取到令牌
if (!RATE_LIMIT.tryAcquire()) {
// 获取请求上下文
RequestContext rc = RequestContext.getCurrentContext();
rc.setSendZuulResponse(false);// 代表请求结束。不在继续向下请求
rc.setResponseStatusCode(401);// 添加一个响应的状态码
rc.setResponseBody("限流了,稍后再访问!!!");// 响应内容
rc.getResponse().setContentType("text/html;charset=utf-8");// 响应类型
}
return null;
}
}
2.5.3.测试
快速访问:http://127.0.0.1:9527/qf/gateway/user/2?token=12345
2.6.网关容错
2.6.1.什么是网关容错
当consumer调用provider时由于各种原因会出现无法调用的情况,此时可以使用hystrix进行provider降级。那么客户端通过zuul无法调用consumer时,zuul也可以对consumer进行降级。
2.6.2.zuul和hystrix无缝结合
在zuul的启动器中包含了hystrix的jar,所有我们无需在项目中添加hystrix的启动器。
2.6.3.在zuul中实现服务降级
package com.qf.fallback;
import org.springframework.cloud.netflix.zuul.filters.route.FallbackProvider;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.stereotype.Component;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
/**
* 降级类:设置zuul无法调用consumer时的降级逻辑
*/
@Component
public class UserProviderFallBack implements FallbackProvider {
/**
* 指定要降级的服务
* @return
*/
@Override
public String getRoute() {
return "*";//*:所有服务
}
/**
*
* @param route
* @param cause
* @return
*/
@Override
public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
return new ClientHttpResponse(){
@Override
public HttpHeaders getHeaders() {
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setContentType(MediaType.APPLICATION_JSON);
return httpHeaders;
}
/**
* 返回托底数据
* @return
* @throws IOException
*/
@Override
public InputStream getBody() throws IOException {
String msg = route+" 服务不可用,请联系管理员,联系电话:110";
return new ByteArrayInputStream(msg.getBytes());
}
/**
* 返回相应的状态码:
* zuul转请求失败了,但是客户端向zuul发送请求是成功的,所以不应该把404、500等问题抛给客
* 户端
* @return
* @throws IOException
*/
@Override
public HttpStatus getStatusCode() throws IOException {
return HttpStatus.OK;
}
@Override
public int getRawStatusCode() throws IOException {
return this.getStatusCode().value();
}
@Override
public String getStatusText() throws IOException {
return this.getStatusCode().getReasonPhrase();
}
@Override
public void close() {
}
};
}
}